高性能网络框架笔记一(网络包接收与发送流程)

 一、网络收包流程

1、当网络数据帧通过网络传输到达网卡时,网卡会将网络数据帧通过DMA的方式放到缓冲区RingBuffer中。

RingBuffer时网卡启动的时候分配和初始化的环形缓冲队列。当RingBuffer满的时候,新来的数据包就会被丢弃。我们可通过ifconfig命令查看网卡收发数据包情况。其中overruns数据项表示当RingBuffer满时被丢弃的数据包。如果发现出现丢包情况,可以通过ethtool命令来增大RingBuffer长度。

2、当DMA操作完成时,网卡向CPU发起一个硬中断,告诉CPU有网络数据到达。CPU调用网卡驱动注册的硬中断响应程序。网卡硬中断响应程序会为网卡数据帧创建内核数据结构sk_buffer,并将网络数据帧拷贝到sk_buffer中。然后发起软中断请求,通知内核有新的网络数据帧到达。

sk_buffer缓冲区,是一个维护网络帧结构的双向链表,链表中的每一个元素都是一个网络帧。虽然TCP/IP协议栈分了好几层,但上下不同层之间的传递,实际上只需要操作这个数据结构中的指针,而无需进行数据复制。

3、内核线程ksoftirqd发现有软中断请求到来,随后调用网卡驱动注册的poll函数将sk_buffer中的网络数据包送到内核协议栈中注册的ip_rcv函数中。

每个CPU会绑定一个ksoftirqd内核线程专门来处理软中断响应。2个CPU时,就会有ksoftirqd/0和ksoftirqd/1这两个内核线程。

网卡接收到数据后,当DMA拷贝完成时,向CPU发出硬中断,这时哪个CPU上相遇了这个硬中断,那么网卡硬中断响应程序中发出的软中断请求也会在这个CPU绑定的ksoftirqd线程中响应。所以如果发现Linux软中断,CPU消耗都集中在一个核上的话,那么就需要调整硬中断的CPU亲和性,来将硬中断打散到不同的CPU核上去。

4、在ip_rcv函数中,也就是网络层,取出数据包的IP头,判断该数据包下一跳的走向,如果数据包是发送给本机的,则取出传输层的协议类型(TCP或UDP),并去掉数据包的IP头,将数据包交给传输层处理。

 传输层的处理函数:TCP协议对应内核协议中的注册函数的tcp_rcv函数,UDP协议对应内核协议栈中注册的udp_rcv函数。

5、当我们采用的是TCP协议时,数据包到达传输层时,会在内核协议栈中的tcp_rcv函数处理,在tcp_rcv函数中去掉TCP头,根据四元组(源IP、源端口、目的IP、目的端口)查找队友的Socket,如果找到对应的Socket则将网络数据包中传输数据拷贝到Socket中的接收缓冲区中。如果没有找到,则发送一个目标不可达的icmp包。

6、内核在接收网络数据包时所做的工作已经介绍完毕,现在我们把视角放到应用层,当我们程序通过系统调用read读取Socket接收缓冲区中的数据时,如果接收缓冲区中没有数据,那么应用程序就会在系统调用上阻塞,直到Socket缓冲区有数据,然后CPU将内核空间(Socket接收缓冲区)的数据拷贝到用户空间,最后系统调用read返回,应用程序读取数据。

7、性能开销

从内核处理网络数据包接收到整个过程来看,内核帮我们做了非常多的工作,最终我们的应用程序才能读到网络数据。随之而来的也带来了很多性能开销,结合前面介绍的网络数据包接收过程,我们来看下网络数据包接收的过程有哪些性能开销:

  • 应用成通过系统调用从用户态转为内核态的开销以及系统调用返回时从内核态转为用户态的开销。
  • 网络数据从内核空间通过CPU拷贝到用户空间的开销。
  • 内核线程ksoftirqd响应软中断的开销。
  • DMA拷贝网络数据包到内存中的开销。

二、网络发包流程

1、当我们在应用程序中调用send系统调用发送数据时,由于是系统调用所以线程会发生一次用户态到内核态到转换,在内核中首先根据fd将真正的Socket找出,这个Socket对象中记着各种协议栈的函数地址,然后构造struct msghdr对象,将用户需要发送的数据全部封装在这个struct msghdr结构体中。

2、调用内核协议函数inet_sendmsg,发送流程进入内核协议栈处理。在进入到内核协议栈之后,内核会找到Socket上的具体协议发送函数。

比如:我们使用的是TCP协议,对应的TCP协议发送函数是tcp_sendmsg,如果是UDP协议的话,对应的发送函数为udp_sendmsg。

3、在TCP协议的发送函数tcp_sendmsg中,创建内核数据结构sk_buffer,将struct msghdr结构体中的发送数据拷贝到sk_buffer中。调用tcp_write_queue_tail函数获取Socket发送队列中对尾元素,将新创建的sk_buffer添加到Socket发送队列的尾部。

Socket的发送队列是由sk_buffer组成的一个双向链表。

发送流程走到这里,用户要发送的数据总算是从用户空间拷贝到内核中,这时虽然发送数据已经拷贝到了内核Socket中的发送队列中,但并不代表内核会开始发送,因为TCP协议的流量控制和拥塞控制,用户要发送的数据包并不一定会立马发送出去,而是需要符合TCP协议的发送条件。如果没有达到发送条件,那么本次send系统调用就会直接返回。

4、如果符合发送条件,则开始调用tcp_write_xmit内核函数。在这个函数中,会循环获取Socket发送队列中待发送的sk_buffer,然后进行拥塞控制以及滑动窗口的管理。

5、将从Socket发送队列中获取到的sk_buffer重新拷贝一份,设置sk_buffer副本中TCP HEADER。

sk_buffer内部其实包含了网络协议中所有的header。在设置TCP HEADER的时候,只是把指针指向sk_buffer的合适位置。后面再设置IP HEADER的时候,再把指针移动一下就行,避免频繁的内存申请和拷贝,效率很高。

为什么不直接使用Socket发送队列中的sk_buffer而是需要拷贝一份呢?因为TCP协议是支持丢包重传的,在没有收到对端ACK之前,这个sk_buffer是不能删除的。内核每次调用网卡发送数据的时候,实际上传递的是sk_buffer的拷贝副本,当网卡把数据发送出去后,sk_buffer拷贝副本就会被释放。当收到对端的ACK之后,Socket发送队列中的sk_buffer才会被真正删除。

6、当设置完TCP头后,内核协议栈传输层的事情就做完了,下面通过调用ip_queue_xmit内核函数,正式来到内核协议栈网络层的处理。

通过route命令可以查看本机路由配置。

如果使用iptables配置了一些规则,那么这将检测是否命中规则。如果你设置了非常复杂的netfilter规则,在这个函数里讲导致你的线程CPU开销会极大增加。

将sk_buffer中的指针移动到IP头位置上,设置IP头。

执行netfilters过滤。过滤通过之后,如果数据对于MTU的话,则执行分片。

检查Socket中是否有缓存路由表,如果没有的话,则查找路由项,并缓存到Socket中。接着再把路由表设置到sk_buffer中。

7、内核协议栈网络层的事情处理完后,现在发送流进入到了邻居子系统,邻居子系统位于内核协议栈中的网络和网络接口层之间,用于发送ARP请求获取MAC地址,然后将sk_buffer中的指针移动到MAC头位置,填充MAC头。

8、经过邻居子系统的处理,现在sk_buffer中已经封装了一个完整的数据帧,随后内核将sk_buffer交给网络设备子系统进行处理。网络设备子系统主要做以下几项事情:

  • 选择发送队列(RingBuffer)。因为网卡拥有多个发送队列,所以在发送之前需要选择一个发送队列。
  • 将sk_buffer添加中发送队列中。
  • 循环从发送队列(RingBuffer)取出sk_buffer,调用内核函数sch_direct_xmil发送数据,其中会调用网卡驱动程序来发送数据。

以上过程全部是用户线程的内核态在执行,占用的CPU时间是系统态时间(sy),当分配给用户线程的CPU quota用完的时候,会触发NET_TX_SOFTIRQ类型的软中断,内核线程ksoftirqd会响应这个软中断,并执行NET_TX_SOFTIRQ类型的软中断注册的回调函数net_tx_action,在毁掉函数中会执行到驱动程序函数de_hard_stat_xmit来发送数据。

注意:当触发NET_TX_SOFTIRQ软中断来发送数据时,后面消耗的CPU就显示在si这里来,不会消耗用户进程的系统态时间(sy)了。

从这里可以看到网络包的发送过程和接收过程是不同的,在介绍网络包的接收过程时,我们提到是通过出发NET_RX_SOFTIRQ类型的软中断在内核线程ksoftirqd中执行内核网络协议栈接受数据。而在网络数据包的发送过程中是用户线程的内核态在执行内核网络协议栈,只有当线程的CPU quota用尽时,才触发NET_TX_SOFTIRQ软中断来发送数据。

在整个网络包的发送和接收过程中,NET_TX_SOFTIRQ类型的软中断只会在发送网络包时且当用户线程的CPU quota用尽时,才会触发。剩下的接收过程中触发的软中断类型以及发送完整数据触发的软中断类型均为NET_RX_SOFTIRQ。所以这就是你在服务器上查看/proc/softirqs,一般NET_RX都要比NET_TX大很多的原因。

9、现在发送流程终于到了网卡真是发送数据的阶段,前面我们讲到无论时用户线程的内核态还是触发NET_TX_SOFTIRQ类型的软中断在发送数据的时候最终会调用到网卡的驱动程序函数dev_hard_start_xmit来发送数据。在网卡驱动程序函数dev_hard_xmit中会将sk_buffer映射到网卡可以访问的内存DMA区域,最终网卡驱动程序通过DMA的方式将数据帧通过物理网卡发送出去。

10、当数据发送完毕后,还以最后一项重要的工作,就是清理工作。数据发送完毕后,网卡设备会向CPU发送一个硬中断,CPU调用网卡驱动程序注册的硬中断响应程序,在硬中断响应中触发NET_RX_SOFTIRQ类型的软中断,在软中断的回调函数igb_poll中清理是否sk_buffer,清理网卡发送队列(RingBuffer),解除DMA映射。

无论硬中断是因为有数据要接收,还是说发送完成通知,从硬中断触发的软中断都是NET_RX_SOFTIRQ。

这里释放清理的只是sk_buffer的副本,真正的sk_buffer现在还是存放在Socket的发送队列中。前面在传输层处理的时候我们提到过,因为传输层需要保证可靠性,所以sk_buffer其实还没有删除。它得等到收到对方的ACK之后才会真正删除。

11、性能开销:

前面我们提到了网络包接收过程中涉及的性能开销,现在介绍完了网络包的发送过程,我们来看下在数据包发送过程中的性能开销:

  • 和接收数据一样,应用程序在调用系统调用send的时候会从用户态转为内核态以及发送完数据后,系统调用返回时从内核态转为用户态的开销。
  • 用户线程内核态CPU quota用尽时触发NET_TX_SOFTIRQ类型软中断,内核响应软中断的开销。
  • 网卡发送完数据,向CPU发送硬中断,CPU响应硬中断的开销。以及在硬中断中发送NET_RX_SOFTIRQ软中断执行具体的内存清理工作。内核响应软中断的开销。
  • 内存拷贝的开销。我们来回顾下在数据包发送的过程中都发生了哪些内存拷贝:
    • 在内核协议栈的传输层中,TCP协议对应的发送函数tcp_sendmsg会申请sk_buffer将用户要发送的数据拷贝到sk_buffer中。
    • 在发送流程从传输层到网络层的时候,会拷贝一个sk_buffer副本出来,将这个sk_buffer副本向下传递。原始sk_buffer保留在Socket发送队列中,等待网络对端ACK,对端ACK后删除Socket发送队列中的sk_buffer。对端没有发送ACK,则重新从Socket发送队列中发送,实现TCP协议的可靠传输。
    • 在网络层,如果发现要发送的数据大于MTU,则会进行分片操作,申请额外的sk_buffer,并将原来的sk_buffer拷贝到多个小的sk_buffer中。
posted @ 2022-03-06 13:02  钟齐峰  阅读(733)  评论(0编辑  收藏  举报