TCP/IP 协议栈在Linux内核中的运行时序分析
调研要求
-
-
编译、部署、运行、测评、原理、源代码分析、跟踪调试等
-
应该包括时序图
一、TCP/IP协议介绍
TCP/IP协议简单来说就是网络中所有相关协议簇的简称,它是众多网络协议的集合,这一类协议簇是有非常多的协议,如常见的:ARP/TCP/UDP/IP/ICMP/IGMP/HTTP/DNS/DHCP/TFP/MQTT等等

TCP/IP模型是一个抽象的分层模型,这个模型中,所有的TCP/IP系列网络协议都被归类到4个抽象的"层"中。每一抽象层创建在低一层提供的服务上,并且为高一层提供服务。 完成一些特定的任务需要众多的协议协同工作,这些协议分布在参考模型的不同层中的,因此有时称它们为一个协议栈。
网络协议通常分不同层次进行开发,每一层分别负责不同的通信功能。一个协议族,比如TCP/IP,是一组不同层次上的多个协议的组合。TCP/IP通常被认为是一个四层协议系统,其分层模型如下:
每一层都负责不同的功能。
链路层:也称作数据链路层或网络接口层,通常包括操作系统中的设备驱动程序和计算机中对应的网络接口卡。它们一起处理与电缆(或其他任何传输媒介)的物理接口细节。
网络层:有时也称作互联网层,处理数据报在网络中的活动,例如数据报路由。其中网络层协议包括IP协议(网际协议), ICMP协议(互联网控制报文协议),以及IGMP协议(英特网组管理协议)。
运输层:运输层主要为两台主机上的应用程序提供端到端的通信。在TCP/IP协议族中,有两个传输层协议: TCP(传输控制协议)和UDP(用户数据报协议)。TCP为两台主机提供高可靠性的数据通信。它所做的工作包括把应用程序交给它的数据分成合适的小块交给下面的网络层,同时还要确认接收到的数据是正确的,并且将其组装成有序的数据递交到应用层,同时还要处理超时重传、流量控制等。由于运输层提供了高可靠性的端到端的通信,因此应用层可以更加方便来处理数据。
而另一方面,UDP则为应用层提供一种非常简单的服务。它只是把称作数据报的分组从一台主机发送到另一台主机,但并不保证该数据报能到达另一端,因此数据的可靠性必须由应用层来提供,这就导致应用层处理程序的困难,但是对于数据要求不可靠的传输通常使用UDP协议,如视频的播放等。
应用层:应用层就是用户程序,不同的应用会有不一样的操作。
二、sock介绍
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部。

Socket起源于Unix,而Unix/Linux 基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式 来操作。Socket就是该模式的一个实现,Socket即是一种特殊的文件,一些Socket函数就是对其进行的操作(读/写IO、打开、关闭)
Socket保证了不同计算机之间的通信,也就是网络通信。对于网站,通信模型是服务器与客户端之间的通信。两端都建立了一个Socket对象,然后通过Socket对象对数据进行传输。通常服务器处于一个无限循环,等待客户端的连接。

常见函数及作用:
-
int socket(int domain, int type, int protocol):创建一个新的套接字,返回套接字描述符 -
int bind(int sockfd,struct sockaddr * my_addr,int addrlen):为套接字指明一个本地端点地址TCP/IP协议使用sockaddr_in结构,包含IP地址和端口号,服务器使用它来指明熟知的端口号,然后等待连接 -
int listen(int sockfd,int input_queue_size):面向连接的服务器使用它将一个套接字置为被动模式,并准备接收传入连接。用于服务器,指明某个套接字连接是被动的 -
int accept(int sockfd, struct sockaddr *addr, int *addrlen):获取传入连接请求,返回新的连接的套接字描述符 -
int connect(int sockfd,struct sockaddr *server_addr,int sockaddr_len):同远程服务器建立主动连接,成功时返回0,若连接失败返回-1。 -
int send(int sockfd, const void * data, int data_len, unsigned int flags):在TCP连接上发送数据,返回成功传送数据的长度,出错时返回-1 -
int recv(int sockfd, void *buf, int buf_len,unsigned int flags):从TCP接收数据,返回实际接收的数据长度 -
close(int sockfd):撤销套接字
三、调试环境介绍
调试版本:linux-5.4.34
虚拟机环境:ubuntu-20.04.1 64位
服务端代码:server.c
1 #include <stdio.h> /* perror */ 2 #include <stdlib.h> /* exit */ 3 #include <sys/types.h> /* WNOHANG */ 4 #include <sys/wait.h> /* waitpid */ 5 #include <string.h> /* memset */ 6 #include <sys/time.h> 7 #include <sys/types.h> 8 #include <unistd.h> 9 #include <fcntl.h> 10 #include <sys/socket.h> 11 #include <errno.h> 12 #include <arpa/inet.h> 13 #include <netdb.h> /* gethostbyname */ 14 15 #define true 1 16 #define false 0 17 18 19 //设置socke连接配置参数 20 #define MYPORT 3490 /* 监听的端口 */ 21 #define BACKLOG 10 /* listen的请求接收队列长度 */ 22 #define MAXDATASIZE 100 /* 一次可以读的最大字节数 */ 23 24 25 26 //接收消息 27 void Recv(int sockfd, int buf_len) 28 { 29 char buf[MAXDATASIZE]; 30 int numbytes; 31 if ((numbytes = recv(sockfd, buf, buf_len, 0)) == -1) 32 { 33 34 perror("recv"); 35 exit(1); 36 } 37 38 buf[numbytes] = '\0'; 39 printf("Received: %s", buf); 40 41 } 42 43 44 45 //发送消息 46 void Send(int sockfd, const void * data, int data_len) 47 { 48 if (send(sockfd,data, data_len, 0) == -1) 49 perror("send"); 50 } 51 52 53 //获取socket 54 int Socket(){ 55 return socket(PF_INET, SOCK_STREAM, 0); 56 } 57 58 //绑定 59 void Bind(int sockfd) 60 { 61 struct sockaddr_in sa; /* 自身的地址信息 */ 62 sa.sin_family = AF_INET; 63 sa.sin_port = htons(MYPORT); /* 网络字节顺序 */ 64 sa.sin_addr.s_addr = INADDR_ANY; /* 自动填本机IP */ 65 memset(&(sa.sin_zero), 0, 8); /* 其余部分置0 */ 66 67 if (bind(sockfd, (struct sockaddr *)&sa, sizeof(sa)) == -1) 68 { 69 perror("bind"); 70 exit(1); 71 } 72 } 73 74 //监听 75 void Listen(int sockfd) 76 { 77 if (listen(sockfd, BACKLOG) == -1) 78 { 79 perror("listen"); 80 exit(1); 81 } 82 83 } 84 85 //获取请求 86 int Accept(int sockfd, struct sockaddr *addr, int *addrlen) 87 { 88 return accept(sockfd,addr,addrlen); 89 } 90 91 92 //建立连接并监听连接请求 93 void StartSocket() 94 { 95 int sockfd, new_fd; /* 监听端口,数据端口 */ 96 97 struct sockaddr_in their_addr; /* 连接对方的地址信息 */ 98 unsigned int sin_size; 99 100 if ((sockfd = Socket()) == -1) 101 { 102 perror("socket"); 103 exit(1); 104 } 105 106 107 Bind(sockfd); 108 109 Listen(sockfd); 110 111 /* 主循环 */ 112 while (1) 113 { 114 sin_size = sizeof(struct sockaddr_in); 115 new_fd = Accept(sockfd, 116 (struct sockaddr *)&their_addr, &sin_size); 117 if (new_fd == -1) 118 { 119 perror("accept"); 120 continue; 121 } 122 123 printf("Got connection from %s\n", 124 inet_ntoa(their_addr.sin_addr)); 125 /* 子进程 */ 126 if (fork() == 0) 127 { 128 129 //接收消息 130 Recv(new_fd, MAXDATASIZE); 131 132 133 134 //发送消息 135 Send(new_fd, "Hello, world!\n", 14); 136 137 138 close(new_fd); 139 exit(0); 140 } 141 142 close(new_fd); 143 144 /*清除所有子进程 */ 145 while (waitpid(-1, NULL, WNOHANG) > 0) 146 ; 147 } 148 close(sockfd); 149 } 150 151 152 153 int main() 154 { 155 StartSocket(); 156 return true; 157 }
客户端代码:client.c
1 #include <stdio.h> /* perror */ 2 #include <stdlib.h> /* exit */ 3 #include <sys/types.h> /* WNOHANG */ 4 #include <sys/wait.h> /* waitpid */ 5 #include <string.h> /* memset */ 6 #include <sys/time.h> 7 #include <sys/types.h> 8 #include <unistd.h> 9 #include <fcntl.h> 10 #include <sys/socket.h> 11 #include <errno.h> 12 #include <arpa/inet.h> 13 #include <netdb.h> /* gethostbyname */ 14 15 #define true 1 16 #define false 0 17 18 //设置socke连接配置参数 19 #define PORT 3490 /* Server的端口 */ 20 #define MAXDATASIZE 100 /* 一次可以读的最大字节数 */ 21 22 23 //接收消息 24 void Recv(int sockfd, int buf_len) 25 { 26 char buf[MAXDATASIZE]; 27 int numbytes; 28 if ((numbytes = recv(sockfd, buf, buf_len,0)) == -1) 29 { 30 31 perror("recv"); 32 exit(1); 33 } 34 35 buf[numbytes] = '\0'; 36 printf("Received: %s", buf); 37 38 } 39 40 41 //发送消息 42 void Send(int sockfd, const void * data, int data_len) 43 { 44 if (send(sockfd,data, data_len, 0) == -1) 45 perror("send"); 46 } 47 48 49 //建立连接 50 void Connect(int sockfd,struct hostent *he,struct sockaddr_in server_addr) 51 { 52 server_addr.sin_family = AF_INET; 53 server_addr.sin_port = htons(PORT); /* short, NBO */ 54 server_addr.sin_addr = *((struct in_addr *)he->h_addr_list[0]); 55 memset(&(server_addr.sin_zero), 0, 8); /* 其余部分设成0 */ 56 57 if (connect(sockfd, (struct sockaddr *)&server_addr, 58 sizeof(struct sockaddr)) == -1) 59 { 60 perror("connect"); 61 exit(1); 62 } 63 } 64 65 66 //获得主机名 67 struct hostent * GetHost(const char * hostname) 68 { 69 return gethostbyname(hostname); 70 } 71 72 73 //获取socket 74 int Socket(){ 75 return socket(PF_INET, SOCK_STREAM, 0); 76 } 77 78 79 80 //获取socket并连接 81 void StartSocket(int argc, char *argv[]) 82 { 83 int sockfd; 84 struct hostent *he; /* 主机信息 */ 85 struct sockaddr_in server_addr; /* 对方地址信息 */ 86 87 if (argc != 2) 88 { 89 fprintf(stderr, "usage: client hostname\n"); 90 exit(1); 91 } 92 93 94 /* get the host info */ 95 if ((he = GetHost(argv[1])) == NULL) 96 { 97 /* 注意:获取DNS信息时,显示出错需要用herror而不是perror */ 98 /* herror 在新的版本中会出现警告,已经建议不要使用了 */ 99 perror("gethostbyname"); 100 exit(1); 101 } 102 103 if ((sockfd = Socket()) == -1) 104 { 105 perror("socket"); 106 exit(1); 107 } 108 109 Connect(sockfd,he,server_addr); 110 111 Send(sockfd, "Hi, world!\n", 14); 112 113 Recv(sockfd, MAXDATASIZE); 114 115 close(sockfd); 116 117 } 118 119 120 int main(int argc, char *argv[]) 121 { 122 StartSocket(argc,argv); 123 return true; 124 }
调试代码过程如下图所示:

四、send过程分析
1) 应用层
流程:
1.网络应用调用Socket API socket (int family, int type, int protocol) 创建一个 socket,该调用最终会调用 Linux system call socket() ,并最终调用 Linux Kernel 的 sock_create() 方法。该方法返回被创建好了的那个 socket 的 file descriptor。对于每一个 userspace 网络应用创建的 socket,在内核中都有一个对应的 struct socket和 struct sock。其中,struct sock 有三个队列(queue),分别是 rx , tx 和 err,在 sock 结构被初始化的时候,这些缓冲队列也被初始化完成;在收据收发过程中,每个 queue 中保存要发送或者接受的每个 packet 对应的 Linux 网络栈 sk_buffer 数据结构的实例 skb。
2.对于TCP socket 来说,应用调用 connect()API ,使得客户端和服务器端通过该 socket 建立一个虚拟连接。在此过程中,TCP 协议栈通过三次握手会建立 TCP 连接。默认地,该 API 会等到 TCP 握手完成连接建立后才返回。在建立连接的过程中的一个重要步骤是,确定双方使用的 Maxium Segemet Size (MSS)。因为 UDP 是面向无连接的协议,因此它是不需要该步骤的。
3.应用调用 Linux Socket 的 send 或者 write API 来发出一个 message 给接收端。
4.sock_sendmsg 被调用,它使用 socket descriptor 获取 sock struct,创建 message header 和 socket control message。
5._sock_sendmsg 被调用,根据 socket 的协议类型,调用相应协议的发送函数。
6.对于TCP ,调用 tcp_sendmsg 函数。
7.对于UDP 来说,userspace 应用可以调用 send()/sendto()/sendmsg() 三个 system call 中的任意一个来发送 UDP message,它们最终都会调用内核中的 udp_sendmsg() 函数。
源码分析:
当调用send()函数时,内核封装send()为sendto(),然后发起系统调用。其实也很好理解,send()就是sendto()的一种特殊情况,而sendto()在内核的系统调用服务程序为sys_sendto。

这里定义了一个struct msghdr msg,他是用来表示要发送的数据的一些属性。

__sys_sendto函数其实做了3件事:
1.通过fd获取了对应的struct socket
2.创建了用来描述要发送的数据的结构体struct msghdr。
3.调用了sock_sendmsg来执行实际的发送。
sys_sendto构建完这些后,调用sock_sendmsg继续执行发送流程,传入参数为struct msghdr和数据的长度。忽略中间的一些不重要的细节,sock_sendmsg继续调用sock_sendmsg(),sock_sendmsg()最后调用struct socket->ops->sendmsg,即对应套接字类型的sendmsg()函数,所有的套接字类型的sendmsg()函数都是 sock_sendmsg,该函数首先检查本地端口是否已绑定,无绑定则执行自动绑定,而后调用具体协议的sendmsg函数。


继续追踪这个函数,会看到最终调用的是inet_sendmsg

这里间接调用了tcp_sendmsg即传送到传输层。
gdb调试验证:

2)传输层
流程:
1.tcp_sendmsg 函数会首先检查已经建立的 TCP connection 的状态,然后获取该连接的 MSS,开始 segement 发送流程。
2.构造TCP 段的 playload:它在内核空间中创建该 packet 的 sk_buffer 数据结构的实例 skb,从 userspace buffer 中拷贝 packet 的数据到 skb 的 buffer。
3.构造TCP header。
4.计算TCP 校验和(checksum)和 顺序号 (sequence number):TCP校验和是一个端到端的校验和,由发送端计算,然后由接收端验证。其目的是为了发现TCP首部和数据在发送端到接收端之间发生的任何改动。如果接收方检测到校验和有差错,则TCP段会被直接丢弃。TCP校验和覆盖 TCP 首部和 TCP 数据;TCP的校验和是必需的。
5.发到 IP 层处理:调用 IP handler 句柄 ip_queue_xmit,将 skb 传入 IP 处理流程。
源码分析:
入口函数是tcp_sendmsg

tcp_sendmsg实际上调用的是tcp_sendmsg_locked函数

在tcp_sendmsg_locked中,完成的是将所有的数据组织成发送队列,这个发送队列是struct sock结构中的一个域sk_write_queue,这个队列的每一个元素是一个skb,里面存放的就是待发送的数据。然后调用了tcp_push()函数。
在tcp协议的头部有几个标志字段:URG、ACK、RSH、RST、SYN、FIN,tcp_push中会判断这个skb的元素是否需要push,如果需要就将tcp头部字段的push置一,置一的过程如下:

然后,tcp_push调用了__tcp_push_pending_frames(sk, mss_now, nonagle);函数发送数据:

随后又调用了tcp_write_xmit来发送数据:

tcp_write_xmit位于tcpoutput.c中,它实现了tcp的拥塞控制,然后调用了tcp_transmit_skb(sk, skb, 1, gfp)传输数据,实际上调用的是__tcp_transmit_skb

tcp_transmit_skb是tcp发送数据位于传输层的最后一步,这里首先对TCP数据段的头部进行了处理,然后调用了网络层提供的发送接口icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);实现了数据的发送,自此,数据离开了传输层,传输层的任务也就结束了。
gdb调试验证:

3) 网络层
流程:
1.首先,ip_queue_xmit(skb)会检查skb->dst路由信息。如果没有,比如套接字的第一个包,就使用ip_route_output()选择一个路由。
2.接着,填充IP包的各个字段,比如版本、包头长度、TOS等。
3.中间的一些分片等,可参阅相关文档。基本思想是,当报文的长度大于mtu,gso的长度不为0就会调用 ip_fragment 进行分片,否则就会调用ip_finish_output2把数据发送出去。ip_fragment 函数中,会检查 IP_DF 标志位,如果待分片IP数据包禁止分片,则调用 icmp_send()向发送方发送一个原因为需要分片而设置了不分片标志的目的不可达ICMP报文,并丢弃报文,即设置IP状态为分片失败,释放skb,返回消息过长错误码。
4.接下来就用 ip_finish_ouput2 设置链路层报文头了。如果,链路层报头缓存有(即hh不为空),那就拷贝到skb里。如果没,那么就调用neigh_resolve_output,使用 ARP 获取。
源码分析:
入口函数是ip_queue_xmit,ip_queue_xmit是 ip 层提供给 tcp 层发送回调函数。ip_queue_xmit()完成面向连接套接字的包输出,当套接字处于连接状态时,所有从套接字发出的包都具有确定的路由, 无需为每一个输出包查询它的目的入口,可将套接字直接绑定到路由入口上, 这由套接字的目的缓冲指针(dst_cache)来完成。ip_queue_xmit()首先为输入包建立IP包头, 经过本地包过滤器后,再将IP包分片输出(ip_fragment)。

Ip_queue_xmit实际上是调用__ip_queue_xmit

其中Skb_rtable(skb)获取 skb 中的路由缓存,然后判断是否有缓存,如果有缓存就直接进行packet_routed函数,否则就 执行ip_route_output_ports查找路由缓存。最后调用ip_local_out发送数据包。

同函数ip_queue_xmit一样,ip_local_out函数内部调用__ip_local_out。

返回一个nf_hook函数,里面调用了dst_output,这个函数实质上是调用ip_finish__output函数,ip_finish__output函数内部在调用__ip_finish_output函数

如果需要分片就调用ip_fragment,否则直接调用ip_finish_output2。


在构造好 ip 头,检查完分片之后,会调用邻居子系统的输出函数 neigh_output进行输出。

输出分为有二层头缓存和没有两种情况,有缓存时调用neigh_hh_output进行快速输出,没有缓存时,则调用邻居子系统的输出回调函数进行慢速输出。

最后调用dev_queue_xmit函数进行向链路层发送包。
gdb调试验证:

4)链路层和物理层
流程:
1.数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。这一层数据的单位称为帧(frame)。从dev_queue_xmit函数开始,位于net/core/dev.c文件中。上层调用dev_queue_xmit,进而调用 __dev_queue_xmit,再调用dev_hard_start_xmit函数获取skb。
2.在xmit_one中调用__net_dev_start_xmit函数。进而调用netdev_start_xmit,实际上是调用__netdev_start_xmit函数。
3.调用各网络设备实现的ndo_start_xmit回调函数指针,从而把数据发送给网卡,物理层在收到发送请求之后,通过 DMA 将该主存中的数据拷贝至内部RAM(buffer)之中。在数据拷贝中,同时加入符合以太网协议的相关header,IFG、前导符和CRC。对于以太网网络,物理层发送采用CSMA/CD,即在发送过程中侦听链路冲突。一旦网卡完成报文发送,将产生中断通知CPU,然后驱动层中的中断处理程序就可以删除保存的 skb 了。
源码分析:
上层调用dev_queue_xmit进入链路层的处理流程,实际上调用的是__dev_queue_xmit。

__dev_queue_xmit会调用dev_hard_start_xmit函数获取skb。

然后在xmit_one中调用__net_dev_start_xmit函数。

调用netdev_start_xmit,实际上是调用__netdev_start_xmit。

调用各网络设备实现的ndo_start_xmit回调函数指针,其为数据结构struct net_device,从而把数据发送给网卡,物理层在收到发送请求之后,通过 DMA 将该主存中的数据拷贝至内部RAM(buffer)之中。在数据拷贝中,同时加入符合以太网协议的相关header,IFG、前导符和CRC。对于以太网网络,物理层发送采用CSMA/CD,即在发送过程中侦听链路冲突。
一旦网卡完成报文发送,将产生中断通知CPU,然后驱动层中的中断处理程序就可以删除保存的 skb 了。
gdb调试验证:

五、recv过程分析
1)链路层和物理层
流程:
1.包到达机器的物理网卡时候触发一个中断,并将通过DMA传送到位于 linux kernel 内存中的rx_ring。中断处理程序分配 skb_buff 数据结构,并将接收到的数据帧从网络适配器I/O端口拷贝到skb_buff 缓冲区中,并设置 skb_buff 相应的参数,这些参数将被上层的网络协议使用,例如skb->protocol;
2.然后发出一个软中断(NET_RX_SOFTIRQ,该变量定义在include/linux/interrupt.h 文件中),通知内核接收到新的数据帧。进入软中断处理流程,调用 net_rx_action 函数。包从 rx_ring 中被删除,进入 netif _receive_skb 处理流程。
3.netif_receive_skb根据注册在全局数组 ptype_all 和 ptype_base 里的网络层数据报类型,把数据报递交给不同的网络层协议的接收函数(INET域中主要是ip_rcv和arp_rcv)。
源码分析:
接受数据的入口函数是net_rx_action。

net_rx_action调用网卡驱动里的napi_poll函数来一个一个的处理数据包。在poll函数中,驱动会一个接一个的读取网卡写到内存中的数据包,内存中数据包的格式只有驱动知道。驱动程序将内存中的数据包转换成内核网络模块能识别的skb格式,然后调用napi_gro_receive函数。

然后会直接调用netif_receive_skb_core函数。

netif_receive_skb_core调用 __netif_receive_skb_one_core,将数据包交给上层ip_rcv进行处理。

待内存中的所有数据包被处理完成后(即poll函数执行完成),启用网卡的硬中断,这样下次网卡再收到数据的时候就会通知CPU。
gdb调试验证:

2)网络层
流程:
1.IP层的入口函数在 ip_rcv 函数。该函数首先会做包括 package checksum 在内的各种检查,如果需要的话会做 IP defragment(将多个分片合并),然后 packet 调用已经注册的 Pre-routing netfilter hook ,完成后最终到达 ip_rcv_finish 函数。
2.ip_rcv_finish 函数会调用 ip_router_input 函数,进入路由处理环节。它首先会调用 ip_route_input 来更新路由,然后查找 route,决定该 package 将会被发到本机还是会被转发还是丢弃: (1)如果是发到本机的话,调用 ip_local_deliver 函数,可能会做 de-fragment(合并多个 IP packet),然后调用 ip_local_deliver 函数。该函数根据 package 的下一个处理层的 protocal number,调用下一层接口,包括 tcp_v4_rcv (TCP), udp_rcv (UDP),icmp_rcv (ICMP),igmp_rcv(IGMP)。对于 TCP 来说,函数 tcp_v4_rcv 函数会被调用,从而处理流程进入 TCP 栈。(2)如果需要转发 (forward),则进入转发流程。该流程需要处理 TTL,再调用 dst_input 函数。该函数会 <1>处理 Netfilter Hook<2>执行 IP fragmentation<3>调用 dev_queue_xmit,进入链路层处理流程。
源码分析:
IP 层的入口函数在 ip_rcv 函数。

ip_rcv 函数内部会调用 ip_rcv_finish 函数。

如果是发到本机就调用dst_input,进一步调用ip_local_deliver函数。

判断是否分片,如果有分片就ip_defrag()进行合并多个数据包的操作。

没有分片就调用ip_local_deliver_finish()。

进一步调用ip_protocol_deliver_rcu,该函数根据 package 的下一个处理层的 protocal number,调用下一层接口,包括 tcp_v4_rcv (TCP), udp_rcv (UDP)。对于 TCP 来说,函数 tcp_v4_rcv 函数会被调用,从而处理流程进入 TCP 栈。

gdb调试验证:

3)传输层
流程:
1.传输层TCP 处理入口在 tcp_v4_rcv 函数(位于 linux/net/ipv4/tcp ipv4.c 文件中),它会做 TCP header 检查等处理。
2.调用 _tcp_v4_lookup,查找该package的open socket。如果找不到,该package会被丢弃。接下来检查 socket 和 connection 的状态。
3.如果socket 和 connection 一切正常,调用 tcp_prequeue 使 package 从内核进入 user space,放进 socket 的 receive queue。然后 socket 会被唤醒,调用 system call,并最终调用 tcp_recvmsg 函数去从 socket recieve queue 中获取 segment。
源码分析:
tcp_v4_rcv函数为TCP的总入口,数据包从IP层传递上来,进入该函数;其协议操作函数结构如下所示,其中handler即为IP层向TCP传递数据包的回调函数,设置为tcp_v4_rcv;
1 static struct net_protocol tcp_protocol = { 2 .early_demux = tcp_v4_early_demux, 3 .early_demux_handler = tcp_v4_early_demux, 4 .handler = tcp_v4_rcv, 5 .err_handler = tcp_v4_err, 6 .no_policy = 1, 7 .netns_ok = 1, 8 .icmp_strict_tag_validation = 1, 9 };
在IP层处理本地数据包时,会获取到上述结构的实例,并且调用实例的handler回调,也就是调用了tcp_v4_rcv;
tcp_v4_rcv函数只要做以下几个工作:(1) 设置TCP_CB (2) 查找控制块 (3)根据控制块状态做不同处理,包括TCP_TIME_WAIT状态处理,TCP_NEW_SYN_RECV状态处理,TCP_LISTEN状态处理 (4) 接收TCP段;
之后,调用的也就是__sys_recvfrom,整个函数的调用路径与send非常类似。整个函数实际调用的是sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags),同样,根据tcp_prot结构的初始化,调用的其实是tcp_rcvmsg .接受函数比发送函数要复杂得多,因为数据接收不仅仅只是接收,tcp的三次握手也是在接收函数实现的,所以收到数据后要判断当前的状态,是否正在建立连接等,根据发来的信息考虑状态是否要改变,在这里,我们仅仅考虑在连接建立后数据的接收。

这里共维护了三个队列:prequeue、backlog、receive_queue,分别为预处理队列,后备队列和接收队列,在连接建立后,若没有数据到来,接收队列为空,进程会在sk_busy_loop函数内循环等待,知道接收队列不为空,并调用函数数skb_copy_datagram_msg将接收到的数据拷贝到用户态,实际调用的是__skb_datagram_iter,这里同样用了struct msghdr *msg来实现。
即skb_copy_datagram_msg → skb_copy_datagram_iter → __skb_datagram_iter



gdb调试验证:

4)应用层
流程:
1.每当用户应用调用 read 或者 recvfrom 时,该调用会被映射为/net/socket.c 中的 sys_recv 系统调用,并被转化为 sys_recvfrom 调用,然后调用 sock_recgmsg 函数。
2.对于 INET 类型的 socket,/net/ipv4/af inet.c 中的 inet_recvmsg 方法会被调用,它会调用相关协议的数据接收方法。
3.对TCP 来说,调用 tcp_recvmsg。该函数从 socket buffer 中拷贝数据到 user buffer。
4.对UDP 来说,从 user space 中可以调用三个 system call recv()/recvfrom()/recvmsg() 中的任意一个来接收 UDP package,这些系统调用最终都会调用内核中的 udp_recvmsg 方法。
源码分析:
对于recv函数,也是recvfrom的特殊情况,调用的也就是__sys_recvfrom,整个函数的调用路径与send在应用层的情况非常类似:



sock->ops->recvmsg即inet_recvmsg,最后在inet_recvmsg中调用的是tcp_recvmsg。

gdb调试验证:

六、时序图

浙公网安备 33010602011771号