TCP/IP协议栈在Linux内核中的运行时序分析
本文将要分析梳理send和recv过程中TCP/IP协议栈相关的运行任务实体及相互协作的时序分析
一. 基础概念简介
1.OSI
OSI(Open Systems Interconnection,开放系统互连)模型是ISO(International Organization for Standardization,国际标准化组织)设计的一种参考模型,它定义了组成网络的各个层。该模型由7层组成,自底向上分别为物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。每一层均可与其紧邻的上层和下层进行交互,并且它们都有自己的一套功能集。顶层的应用层通过应用软件能够直接与用户进行交互。在该模型中,每个分层都接受由它下一层所提供的特定服务,并且负责为自己的上一层提供特定的服务。如下图所示:

2.TCP/IP五层模型
TCP/IP五层协议系统,自底而上分别是物理层、数据链路层、网络层、传输层和应用层。与OSI七层模型类似,它的每一层通过若干协议来实现不同的功能,且上层协议使用下层协议提供的服务。其中数据链路层实现了网卡接口的网络驱动程序,以处理数据在物理媒介(比如以太网、令牌环等)上的传输,网络层实现数据包的选路和转发,传输层为两台主机上的应用程序提供端到端(end to end)的通信。与网络层使用的逐跳通信方式不同,传输层只关心通信的起始端和目的端,而不在乎数据包的中转过程,应用层则负责处理应用程序的逻辑。它与OSI参考模型各层的对应关系如下图所示:

3.Linux 内核网络架构
Linux网络体系结构由五个部分组成,分别是系统调用接口、协议无关接口、网络协议、设备无关接口和设备驱动程序。系统调用接口是用户空间的应用程序正常访问内核的唯一合法途径(终端和陷入也可访问内核);协议无关接口是由socket来实现的,它提供了一组通用函数来支持各种不同协议; 设备无关接口是由net_device实现的,任何设备和上层通信都是通过net_device设备无关接口;网络栈底部则是负责管理物理网络设备的设备驱动程序。结构分层如下图所示:

二.send和recv过程分析
调用send和recv的前提是客户端与服务器端建立连接。连接过程如下:
首先,客户端使用tcp_v4_connect发出连接请求,在该函数中,调用tcp_set_state函数实现发送SYN。进入tcp_set_state函数后,其调用tcp_connect(sk)函数,构造SYN的TCP头部发送出去,并维护一个timer计时器,server端使用inet_csk_accept响应请求。此函数中维护socket请求队列,若队列空,则connet进入循环等待请求。取出1个请求完成3次握手,完成连接的建立。
上面的tcp_connect(sk)中参数sk的类型是sock结构体,该结构的定义在linux/include/net/sock.h文件下,该结构将近两百行的定义中包含了所有特定socket的必需状态,包括socket使用的特定协议以及可能在其上执行的操作。
当调用send()函数时,内核封装send()为sendto(),然后发起系统调用。函数调用栈如图所示:

查看具体的函数定义如下:

由此可知,send()其实是sendto()的一种特殊情况,而二者其实都是调用系统调用服务__sys_sendto()函数,该函数的参数解释如下:
fd socket文件描述符
buff 指向需要发送的数据
len 需要发送的数据的长度
flags 标志位
addr 数据报文要发送的对方端点的地址信息
addr_len 地址信息的长度
当send()函数被调用时,参数addr被置为NULL,addr_len为0。查看__sys_sendto()函数的定义,如图所示:

__sys_sendto()首先根据传入的描述符fd,找到对应的struct socket结构体。然后构建内核的消息结构struct msghdr,该结构定义如下:

msg_name和msg_namelen就是数据报文要发向的对端的地址信息(即sendto系统调用中的addr和addr_len)。当使用send时,它们的值为NULL和0。
msg_iov表示存放待发送数据的一个缓冲区,它的定义如下:

iov_base是缓冲区的起始地址,指向message, iov_len是缓冲区的长度,指向length。msg_iovlen是缓冲区的数量,对于sendto和send来讲,msg_iovlen都是1。
__sys_sendto()构建完这些后,调用sock_sendmsg()继续执行发送流程,传入参数为socket和struct msghdr。sock_sendmsg()定义如下:

先执行安全检查,之后调用sock_sendmsg_nosec()函数,定义如下:

在这里我们可以看到sock->ops->sendmsg这个系统调用,它对应套接字类型的sendmsg()函数,所有的套接字类型的sendmsg()函数都是inet_sendmsg(),该函数首先检查本地端口是否已绑定,无绑定则执行自动绑定,而后调用具体协议的sendmsg函数。
到这里,就进入到传输层了,在这里由于我的测试代码使用TCP连接,所以调用tcp_sendmsg(),该函数定义如下:

这里首先对sk进行加锁,目的是让接收和发送队列能够有序进行相关的工作,然后主要的发送函数即为tcp_sendmsg_locked()。这个函数代码非常多,涉及到了tcp协议中具体的几个处理过程。首先做一些前期的工作,例如检查连接状态和检查数据是否分段等等。到发送这一步,可以看到tcp_push()函数,

该函数在判断了是否需要设置PUSH标记位之后,会调用__tcp_push_pending_frames(),该函数调用tcp_write_xmit()完成发送。tcp_write_xmit()函数是TCP发送新数据的核心函数,包括发送窗口判断、拥塞控制判断等核心操作都是在该函数中完成。大致流程是首先检测当前状态是否是TCP_CLOSE,然后检测拥塞窗口的大小、检测当前段是否完全处在发送窗口内,再检测段是否使用nagle算法进行发送。通过以上检测后将SKB发送出去,最终调用的是tcp_transmit_skb(),

该函数内部调用了__tcp_transmit_skb()函数,该函数的注释如下,
/* This routine actually transmits TCP packets queued in by
* tcp_do_sendmsg(). This is used by both the initial
* transmission and possible later retransmissions.
* All SKB's seen here are completely headerless. It is our
* job to build the TCP header, and pass the packet down to
* IP so it can do the same plus pass the packet off to the
* device.
*
* We are working here with either a clone of the original
* SKB, or a fresh unique copy made by the retransmit engine.
*/
可知该函数的工作是建立TCP报头,并将数据包传递给IP层,在该函数的最后,我们看到如下代码,

可以看到icsk->icsk_af_ops这个指针。通过查阅相关资料,这个指针在tcp协议栈中,会被初始化为ip_queue_xmit,所以知道了这个函数queue_xmit()在这里进入了网络层。所以到这个函数为止,传输层发送过程追踪完毕。当前函数调用栈如下所示:

接下来进入IP层的分析,这一层的主要任务有路由处理、添加IP头部、IP校验和、IP分片和转发进链路层等。
当前函数是ip_queue_xmit(),与之前若干函数类似,该函数实际上调用__ip_queue_xmit()函数,该函数进行具体的信息处理,

首先skb_rtable()函数检测skb的中的rtable是否为空,不为空说明已经指定了路由,跳到packet_routed继续执行,为空则添加,之后为输入包建立IP包头, 经过本地包过滤器后,再将IP包分片输出(ip_fragment)。

检查完标志位和路由之后,一般会调用ip_finish_output2发送数据报,最终调用dev_queue_xmit(skb)将数据包拷贝到链路层skb,交由下一层处理。
以上则是send过程中,各层函数的调用过程,接下来我们看recv函数的调用过程。
与send函数类似,如图:

首先,recv()函数是recvfrom()函数的特殊情况,二者实际上都调用了__sys_recvfrom()函数,recv()将后两个参数置为NULL,__sys_recvfrom()函数里面调用了sock_recvmsg()函数,

之后调用sock_recvmsg_nosec()函数,再根据不同的协议类型调用不同的recvmsg函数,tcp调用的是 tcp_recvmsg。

之后我们关注一下传输层, 传输层 TCP 处理入口在tcp_v4_rcv函数,数据包从IP层传递上来,进入该函数,查看tcp_protocol结构,

该结构是在af_inet.c中的inet_init()被添加的,

它的handler即为IP层向TCP传递数据包的回调函数,设置为tcp_v4_rcv。
tcp_v4_rcv函数做了以下几个工作:(1) 设置TCP_CB (2) 查找控制块 (3)根据控制块状态做不同处理,包括TCP_TIME_WAIT状态处理,TCP_NEW_SYN_RECV状态处理,TCP_LISTEN状态处理 (4) 接收TCP段。该函数最后调用具体的接收处理函数tcp_v4_do_rcv(),

查看源码可知,建立连接后使用tcp_rcv_established()进行数据的接收,函数对头部进行一系列检测和相应操作,一切正常后会调用tcp_queue_rcv()函数。

tcp_queue_rcv函数将收到的数据挂到sk接收队列末尾。而后然socket会被唤醒,调用system call,并最终调用 tcp_recvmsg 函数去从 socket recieve queue 中获取 segment。


在网络层,接收端函数的入口地址是ip_rcv,

ip_rcv完成基本的校验和处理工作后,经过PRE_ROUTING钩子点,经过PRE_ROUTING钩子点之后,调用ip_rcv_finish完成数据包接收,包括选项处理,路由查询,并且根据路由决定数据包是发往本机还是转发。

ip_rcv_finish()函数会调用dst_input()函数, 在dst_input()中,最终在ip层即生成ip_input(),根据路由选择调用ip_router_input()函数,进入路由处理环节。它首先会调用 ip_route_input 来更新路由,然后查找 route,决定该 package 将会被发到本机还是会被转发还是丢弃。数据发向上层的时,会调用 ip_local_deliver 函数,然后调用 ip_local_deliver 函数。该函数根据 package 的下一个处理层的协议号,调用下一层的包括 tcp_v4_rcv等的接口。这样的话就可以和我们刚刚追踪的传输层的函数连接起来了。

三.时序图

浙公网安备 33010602011771号