要求:
- 在深入理解Linux内核任务调度(中断处理、softirg、tasklet、wq、内核线程等)机制的基础上,分析梳理send和recv过程中TCP/IP协议栈相关的运行任务实体及相互协作的时序分析。
- 编译、部署、运行、测评、原理、源代码分析、跟踪调试等
- 应该包括时序图目录
TCP/IP协议族简述
?TCP/IP 协议采用4层结构,分别是应用层、传输层、网络层和链路层,每一层都呼叫它的下一层所提供的协议来完成自己的需求。
Lunux核心网络架构
系统调用接口
?在内核空间中:顶部是系统调用接口,它是用户空间的应用程序正常访问内核的唯一途径。
协议无关接口
?接下来是一个协议无关接口,由socket来实现的,其提供一组通用功能,以支持各种不同的协议。
?socket层不仅支持典型的TCP和UDP协议,还支持IP,原始以太网和其他传输协议,如流控制传输协议(SCTP)。
?网络栈使用socket通信。Linux中的socket结构struct sock
是在linux/include/net/sock.h
中定义的。该大型结构包含特定socket的所有必需状态,包括socket使用的特定协议以及可能在其上执行的操作。
?网络子系统通过定义了其功能的特殊结构(即proto)来了解各个可用协议。每个协议维护一个名为proto
(在linux/include/net/sock.h
中找到)的结构。该结构定义了可以从socket层到传输层执行的特定socket操作(例如,如何创建socket,如何与socket建立连接,如何关闭socket等)。
?socket的数据移动使用核心结构socket缓冲区(sk_buff)来进行。一个sk_buff 包含包数据(package data),和状态数据(state data, 覆盖协议栈的多个层)。每个发送或接收的数据包都用一个sk_buff来表示。该sk_buff 结构是在linux/include/linux/skbuff.h
中定义的,如下图所示。
?由于sk_buff 是socket数据管理的核心,因此kernel已经创建了许多支撑函数来管理它们,包括sk_buff的创建和销毁,克隆和队列管理等函数。
?总的来说,内核socket缓冲器设计思路是,某一的socket的sk_buff串链接在一起,并且sk_buff包括许多信息,包括到协议头的指针,时间戳(发送或接收数据包的时间)以及与数据包相关的网络设备。
网络协议
?接下来是实际的协议,在Linux中包括TCP,UDP和IP的内置协议。
?这些协议都是在linux/net/ipv4/af_inet.c
中的inet_init
函数的开头进行初始化的(因为TCP和UDP是协议inet族的一部分)。inet_init
函数调用proto_register
注册每个内置协议。proto_register
在linux/net/core/sock.c
中定义,除了将协议添加到活动协议列表之外,还可以根据需要分配一个或多个slab高速缓存。
?这些协议的proto结构体都按照类型和协议映射到inetsw_array
,将内部协议映射到对应的操作。结构体inetsw_array
及其关系如下图所示。该数组中的每个协议都在初始化inetsw
时,通过在inet_init
调用inet_register_protosw
来初始化。函数inet_init
还初始化各种inet模块,如ARP,ICMP,IP模块,TCP和UDP模块。
设备无关接口
?接下来是设备无关接口,提供一组通用函数供底层网络设备驱动程序使用。
?首先,设备驱动程序可以通过调用register_netdevice
/unregister_netdevice
将自己注册到内核。调用者首先填写net_device
结构(可以在linux/include/linux/netdevice.h
中找到),然后将其传入register_netdevice
进行注册。内核调用其init功能(如果有定义),执行许多健全检查,创建一个 sysfs条目,然后将新设备添加到设备列表(在内核中Active设备的链表)。各个函数在linux/net/core/dev.c
中实现。
?使用dev_queue_xmit
函数(linux/net/core/dev.c
中)将sk_buff从协议层发送到网络设备。dev_queue_xmit
函数会将sk_buff添加到底层网络设备驱动程序最终要传输的队列中(网络设备在net_device
或者sk_buff->dev
中定义)。dev
结构包含函数hard_start_xmit
,保存用于启动sk_buff传输的驱动程序功能的方法。
?通常使用netif_rx
(linux/net/core/dev.c
中)接收报文数据。当下级设备驱动程序接收到一个包(包含在新分配的sk_buff)时,内核通过调用netif_rx
将sk_buff传递给网络层。然后,netif_rx
通过调用netif_rx_schedule
将sk_buff排队到上层协议的队列以进行进一步处理。
设备驱动程序
?最后是设备驱动程序。
?在初始化时,设备驱动程序分配一个net_device
结构,然后用其必需的例程进行初始化。dev->hard_start_xmit
就是其中一个例程,它定义了上层如何排队sk_buff用以传输。这个程序需要一个sk_buff。此功能的操作取决于底层硬件,但通常将sk_buff中的数据包移动到硬件环或队列。如设备无关层所述,帧接收使用该netif_rx
接口或符合NAPI的网络驱动程序的netif_receive_skb
。NAPI驱动程序对底层硬件的功能提出了约束。
?在设备驱动程序配置其结构中的dev
接口后,调用register_netdevice
以后驱动就可以使用了(网络设备专用的驱动程序可以在linux/drivers/net
中找到)。
send和recv过程时序分析
应用层
?send和recv的前提是客户端与服务器端已经建立连接:client端使用tcp_v4_connect
发出连接请求;此函数中,调用tcp_set_state
函数实现发送SYN;进入tcp_set_state
函数,其调用tcp_connect(sk)
函数,构造SYN的TCP头部发送出去,并维护一个timer计时器;
server端使用inet_csk_accept
响应请求。此函数中维护socket请求队列,若队列空,则connet进入循环等待请求。取出1个请求完成3次握手,完成连接的建立。
发送端
?进程想要发送消息,必须进行系统调用进入内核空间插口层,发送方进入的是BSD sendmsg interface,即
?__sys_sendmsg
是Linux sendmmsg interface,在这个函数里面调用了sock_sendmsg
?这里调用了sock-ops->sendmsg
.看一下socket结构:
struct socket {
socket_state state;
short type;
unsigned long flags;
struct socket_wq *wq;
struct file *file;
struct sock *sk;
const struct proto_ops *ops;
};
struct proto_ops
里面有许多成员,其中就包括:
int (*sendmsg) (struct socket *sock, struct msghdr *m, size_t total_len);
int (*recvmsg) (struct socket *sock, struct msghdr *m, size_t total_len, int flags);
?sock-ops->sendmsg
的意思就是根据不同的协议调用不同的sendmsg函数,对于 TCP ,调用tcp_sendmsg
函数。
接收端
?对于接收端来说也是类似的操作,先系统调用进入内核插口层,调用BSD recvmsg interface,在里面调用Linux recvmmsg interface:___sys_recvmsg
函数
?___sys_recvmsg
函数里面调用了sock_recvmsg_nosec
函数,再根据不同的协议类型调用不同的recvmsg函数,tcp调用的是 tcp_recvmsg。
传输层
发送端
?要发送的数据到达传输层时,调用tcp_sendmsg
函数,这个函数首先对传输控制块上锁(在发送和接收TCP数据前都要对传输控制块上锁,以免应用程序主动发送接收和传输控制块被动接收而导致 控制块中的发送或接收队列混乱)。然后调用tcp_sendsmg_locked
函数,再释放锁。
?tcp_sendmsg_locked
函数中代码很多,主要步骤是:
获取阻塞标识,并且做出相应动作,然后获取阻塞超时时间。
?接下来就是检查已经建立的 TCP connection 的状态,开始 segement 发送流程。TCP只在ESTABLISHED或CLOSE_WAIT这两种状态下,接收窗口是打开的,才能接收数据。因此如果不处于这两种状态,则调用sk_stream_wait_connect()
等待建立起连接,一旦超时则跳转到out_err
处做出错处理。
?接下来构造 TCP 段的 playload:首先获取连接的MSS,并且清零copied(copied是已从用户数据块复制到SKB的字节数。)
?在开始分段前,先初始化错误码为-EPIPE,然后判断此时套接字是否存在错误,以及该套接字是否允许发送数据,如果有错或不允许发送数据,则跳转到do_error
处作处理。
?随后就是将所有的用户数据拷贝到skb,并组织好sk发送队列。(代码太多,到检查push前全都是,不上了)
?最后就是检查是否必须立即发送,即检查自上次发送后产生的数据是否已超过对方曾经通告过的最大通告窗口值的一半.如果必须立即发送,则设置PUSH标志后调用__tcp_push_pending_frames()
将在发送队列上的SKB从sk_send_head开始发送出去。如果没有必要立即发送,且发送队列上只存在这个段,则调用tcp_push_one()
只发送当前段.
?__tcp_push_pending_frames()
只是在判断是否有段需要发送时简单地调用tcp_write_xmit()
发送段,如果发送失败,再调用tcp_check_probe_timer()
复位持续探测定时器.tcp_push_one
也是调用的tcp_write_xmit()
函数。
?tcp_write_xmit
代码也很多(在/net/ipv4/tcp_output.c
里面),mss_now:当前有效的MSS,nonagle: 标识是否启用nonagle算法。push_one表示是发送队列上的一个SKB还是把全部SKB一起发送出去。这个函数返回值为0表示发送成功。
?这个函数的大致流程:
? 1)检测当前状态是否是TCP_CLOSE
? 2)检测拥塞窗口的大小
? 3)检测当前段是否完全处在发送窗口内
? 4)检测段是否使用nagle算法进行发送
? 5)通过以上检测后将该SKB发送出去,最终调用的是tcp_transmit_skb
?tcp_transmit_skb
函数主要就是为待发送的段构造TCP首部,然后调用网络层接口到IP层,最终抵达网络设备。由于在成功发送到网络设备后会释放该 SKB,而TCP必须要接到对应的ACK后才能真正释放数据,因此在发送前会根据参数确定是克隆还是复制一份SKB用于发送。最终的tcp发送都会调用这个 clone_it表示发送发送队列的第一个SKB的时候,采用克隆skb还是直接使用skb,如果是发送应用层数据则使用克隆的,等待对方应答ack回来才把数据删除。如果是回送ack信息,则无需克隆。
?调用网络层接口代码如下:调用发送接口queue_xmit
发送报文,如果失败则返回错误码。在TCP中该接口实现函数为ip_queue_xmit()
。
接收端
?传输层 TCP 处理入口在 tcp_v4_rcv
函数,该函数首先对tcp头部以及校验和进行一番检测以及查找该 package 的 open socket,稍有不对就丢弃或者转到出错处理。
?然后就是和发送方一样,检测连接状态,最后调用 tcp_v4_do_rcv
。
?若连接建立,tcp_v4_do_rcv
会调用tcp_rcv_established
函数。
?tcp_rcv_established
函数对头部进行一系列检测和相应操作,一切正常后会调用tcp_queue_rcv
函数。
?tcp_queue_rcv
函数将收到的数据挂到sk接收队列末尾。而后然socket 会被唤醒,调用 system call,并最终调用 tcp_recvmsg
函数去从 socket recieve queue 中获取 segment。
网络层
发送端
?主要任务:路由选择,构造ip头部,获取下一跳的 MAC 地址,设置链路层报文头,然后转入链路层处理。
?发送端调用接口ip_queue_xmit()
进入到网络层,ip_queue_xmit
实际上调用的是__ip_queue_xmit
函数
?__ip_queue_xmit
函数首先是查找路由缓存,如果找到则直接跳转到packet_routed
处作处理。
?如果输出该数据包的传输控制块中缓存了输出路由缓存项,则需检测该路由缓存项是否过期。如果过期,重新通过输出网络设备、目的地址、源地址等信息查找输出路由缓存项。如果查找到对应的路由缓存项,则将其缓存到传输控制块中,否则丢弃该数据包。如果未过期,则直接使用缓存在传输控制块中的路由缓存项。
?也就是说通过ip_route_output_ports
查找路由的,这个函数会调用ip_route_output_flow
函数,而后调用__ip_route_output_key
函数。这个函数调用ip_route_output_key_hash
函数进行查找,然后调用 ip_route_output_key_hash_rcu
函数。再调用fib_lookup
函数查找表最后执行fib_table_lookup
函数然后通过fib_get_table
找到路由项。
?packet_routed
代码段首先先进行严格源路由选项的处理。如果存在严格源路由选项,并且数据包的下一跳地址和网关地址不 一致,则丢弃该数据包(goto no_route
)。如果没问题,就进行ip头部设置,设置完成后调用ip_local_out
函数。
?ip_local_out
函数调用的是__ip_local_out
函数,而__ip_local_out
函数最终调用的是nf_hook
函数并在里面调用了dst_output
函数。
?dst_output
函数实际上调用ip_output
函数,而ip_output
函数调用了ip_finish_output
函数。这个函数实际上调用的是__ip_finish_output
函数。
?ip_finish_output
函数会检查数据包长度,如果数据包长度大于MTU,则调用ip_fragment()
对IP数据包进行分片。否则走ip_finish_output2
。
?ip_finish_output2
函数会检测skb的前部空间是否还能存储链路层首部。如果不够,则重新分配更大存储区的skb,并释放原skb。最终会调用邻居子系统的输出函数neigh_output
进行输出。
?如果缓存了链路层的首部,则调用neigh_hh_output()
输出数据包。否则通过邻居项的输出方法输出数据包。不管执行哪个函数,最终都会调用 dev_queue_xmit
将数据包传入数据链路层。
接收端
?接收端IP 层的入口函数是 ip_rcv
函数。首先会做包括 package checksum 在内的各种检查,如果需要的话会做 IP defragment(将多个分片合并),然后 packet 调用已经注册的 Pre-routing netfilter hook ,完成后最终到达 ip_rcv_finish
函数。
?ip_rcv_finish
最终会调用dst_input
函数,这个函数调用的是ip_input
函数,当缓存查找没有匹配路由时将调用ip_route_input_slow()
,决定该 package 将会被发到本机还是会被转发还是丢弃。如果是发到本机的将会执行ip_local_deliver
函数。
?如果分片,就调用ip_defrag
函数,没有则调用ip_local_deliver_finish
函数
?接着调用ip_protocol_deliver_rcu
函数
?ip_protocol_deliver_rcu
将输入数据包从网络层传递到传输层。过程如下:
?1)首先,在数据包传递给传输层之前,去掉IP首部
?2)接着,如果是RAW套接字接收数据包,则需要复制一份副本,输入到接收该数据包的套接字。
?3)最后,通过传输层的接收例程,将数据包传递到传输层,由传输层进行处理(ret = ipprot->handler(skb)
这句代码将数据包传送到上层)。
数据链路层和物理层
发送端
?发送端通过dev_queue_xmit
进入数据链路层。这个函数调用的是__dev_queue_xmit
函数。
?__dev_queue_xmit
函数对不同类型的数据包进行不同的处理。HARD_TX_LOCK/HARD_TX_UNLOCK是一对操作, 在这两个操作之间不能再次调用dev_queue_xmi
t接口。因此如果正在用该网络设备发送数据包的CPU又调用dev_queue_xmit()
输出数据包,则 说明代码有bug,需输出警告信息。否则,首先需加锁,以防止其他CPU的并发操作,然后在网络设备处于开启状态时,调用dev_hard_start_xmit()
输出数据包到设备。
?dev_hard_start_xmit
函数会循环调用xmit_one
函数,直到将待输出的数据包提交给网络设备的输出接口,完成数据包的输出。
?xmit_one
函数会调用netdev_start_xmti
函数,而netdev_start_xmit
会调用_netdev_start_xmit
函数。
?物理层在收到发送请求之后,通过 DMA 将该主存中的数据拷贝至内部RAM(buffer)之中。在数据拷贝中,同时加入符合以太网协议的相关header,IFG、前导符和CRC。对于以太网网络,物理层发送采用CSMA/CD,即在发送过程中侦听链路冲突。
?一旦网卡完成报文发送,将产生中断通知CPU,然后驱动层中的中断处理程序就可以删除保存的 skb 了。
接收端
?接收端物理网络适配器接收到数据帧时,会触发一个中断,并将通过 DMA 传送到位于 linux kernel 内存中的 rx_ring。当底层设备驱动程序接收一个报文时,就会通过调用netif_rx
将报文的SKB上传至网络层。
在netif_rx
函数中会调用netif_rx_schedule
, 然后该函数又会去调用__netif_rx_schedule
,在函数__netif_rx_schedule
中会去触发软中断NET_RX_SOFTIRQ, 也即是去调用net_rx_action
.然后在net_rx_action
函数中会去调用设备的poll函数, 它是设备自己注册的.
在设备的poll函数中, 会去调用netif_receive_skb
函数。
?neti_receive_skb
调用netif_reveive_skb_internal
函数,这个函数又调用了__netif_receive_skb
函数。又是一连串的调用,最终执行到_netif_recieve_skb_one_core
,在里面有代码 pt_prev->func
, 此处的func为一个函数指针, 在之前的注册中设置为ip_rcv
.
?因此, 就完成了从链路层上传到网络层的这一个过程了.