在开始今天的文章之前,大家先思考几个小问题。 问1:我们在查看内核发送数据消耗的CPU时,是应该看sy还是si? 问2:为什么你服务器上的/proc/softirqs里NET_RX要比NET_TX大的多的多? 问:发送网络数据的时候都涉及到哪些内存拷贝操作? 这些问题虽然在线上经常看到,但我们似乎很少去深究。如果真的能透彻地把这些问题理解到位,我们对性能的掌控能力将会变得更强。 带着这三个问题,我们开始今天对Linux内核网络发送过程的深度剖析。还是按照我们之前的传统,先从一段简单的代码作为切入。如下代码是一个典型服务器程序的典型的缩微代码: intmain(){fd=sockt(AF_INET,SOCK_STREAM,0);bind(fd,...);listn(fd,...);cfd=accpt(fd,...);//接收用户请求rad(cfd,...);//用户请求处理dosomting();//给用户返回结果snd(cfd,buf,sizof(buf),0);} 今天我们来讨论上述代码中,调用snd之后内核是怎么样把数据包发送出去的。本文基于Linux.10,网卡驱动采用Intl的igb网卡举例。 Linux网络发送过程总览 看Linux源码最重要的是得有整体上的把握,而不是一开始就陷入各种细节。 这里先给大家准备了一个总的流程图,简单阐述下snd发送了的数据是如何一步一步被发送到网卡的。 在这幅图中,我们看到用户数据被拷贝到内核态,然后经过协议栈处理后进入到了RingBuffr中。随后网卡驱动真正将数据发送了出去。当发送完成的时候,是通过硬中断来通知CPU,然后清理RingBuffr。 因为文章后面要进入源码,所以我们再从源码的角度给出一个流程图。 虽然数据这时已经发送完毕,但是其实还有一件重要的事情没有做,那就是释放缓存队列等内存。 那内核是如何知道什么时候才能释放内存的呢,当然是等网络发送完毕之后。网卡在发送完毕的时候,会给CPU发送一个硬中断来通知CPU。更完整的流程看图: 注意,我们今天的主题虽然是发送数据,但是硬中断最终触发的软中断却是NET_RX_SOFTIRQ,而并不是NET_TX_SOFTIRQ!!!(T是transmit的缩写,R表示rciv) 意不意外,惊不惊喜??? 所以这就是开篇问题1的一部分的原因(注意,这只是一部分原因)。 问1:在服务器上查看/proc/softirqs,为什么NET_RX要比NET_TX大的多的多? 传输完成最终会触发NET_RX,而不是NET_TX。所以自然你观测/proc/softirqs也就能看到NET_RX更多了。 好,现在你已经对内核是怎么发送网络包的有一个全局上的把握了。不要得意,我们需要了解的细节才是更有价值的地方,让我们继续!! 网卡启动准备 现在的服务器上的网卡一般都是支持多队列的。每一个队列上都是由一个RingBuffr表示的,开启了多队列以后的的网卡就会对应有多个RingBuffr。 网卡在启动时最重要的任务之一就是分配和初始化RingBuffr,理解了RingBuffr将会非常有助于后面我们掌握发送。因为今天的主题是发送,所以就以传输队列为例,我们来看下网卡启动时分配RingBuffr的实际过程。 在网卡启动的时候,会调用到__igb_opn函数,RingBuffr就是在这里分配的。 //fil:drivrs/nt/thrnt/intl/igb/igb_main.cstaticint__igb_opn(structnt_dvic*ntdv,boolrsuming){structigb_adaptr*adaptr=ntdv_priv(ntdv);//分配传输描述符数组rr=igb_stup_all_tx_rsourcs(adaptr);//分配接收描述符数组rr=igb_stup_all_rx_rsourcs(adaptr);//开启全部队列ntif_tx_start_all_quus(ntdv);} 在上面__igb_opn函数调用igb_stup_all_tx_rsourcs分配所有的传输RingBuffr,调用igb_stup_all_rx_rsourcs创建所有的接收RingBuffr。 //fil:drivrs/nt/thrnt/intl/igb/igb_main.cstaticintigb_stup_all_tx_rsourcs(structigb_adaptr*adaptr){//有几个队列就构造几个RingBuffrfor(i=0;iadaptr-num_tx_quus;i++){igb_stup_tx_rsourcs(adaptr-tx_ring[i]);}} 真正的RingBuffr构造过程是在igb_stup_tx_rsourcs中完成的。 //fil:drivrs/nt/thrnt/intl/igb/igb_main.cintigb_stup_tx_rsourcs(structigb_ring*tx_ring){//1.申请igb_tx_buffr数组内存siz=sizof(structigb_tx_buffr)*tx_ring-count;tx_ring-tx_buffr_info=vzalloc(siz);//2.申请_adv_tx_dscDMA数组内存tx_ring-siz=tx_ring-count*sizof(union_adv_tx_dsc);tx_ring-siz=ALIGN(tx_ring-siz,);tx_ring-dsc=dma_alloc_cohrnt(dv,tx_ring-siz,tx_ring-dma,GFP_KERNEL);//.初始化队列成员tx_ring-nxt_to_us=0;tx_ring-nxt_to_clan=0;} 从上述源码可以看到,实际上一个RingBuffr的内部不仅仅是一个环形队列数组,而是有两个。 1)igb_tx_buffr数组:这个数组是内核使用的,通过vzalloc申请的。2)_adv_tx_dsc数组:这个数组是网卡硬件使用的,硬件是可以通过DMA直接访问这块内存,通过dma_alloc_cohrnt分配。 这个时候它们之间还没有啥联系。将来在发送的时候,这两个环形数组中相同位置的指针将都将指向同一个skb。这样,内核和硬件就能共同访问同样的数据了,内核往skb里写数据,网卡硬件负责发送。 最后调用ntif_tx_start_all_quus开启队列。另外,对于硬中断的处理函数igb_msix_ring其实也是在__igb_opn中注册的。 accpt创建sockt 在发送数据之前,我们往往还需要一个已经建立好连接的sockt。 我们就以开篇服务器缩微源代码中提到的accpt为例,当accpt之后,进程会创建一个新的sockt出来,然后把它放到当前进程的打开文件列表中,专门用于和对应的客户端通信。 假设服务器进程通过accpt和客户端建立了两条连接,我们来简单看一下这两条连接和进程的关联关系。 其中代表一条连接的sockt内核对象更为具体一点的结构图如下。 为了避免喧宾夺主,accpt详细的源码过程这里就不介绍了,感兴趣请参考《图解 深入揭秘poll是如何实现IO多路复用的!》。一文中的第一部分。 今天我们还是把重点放到数据发送过程上。 发送数据真正开始 4.1snd系统调用实现 snd系统调用的源码位于文件nt/sockt.c中。在这个系统调用里,内部其实真正使用的是sndto系统调用。整个调用链条虽然不短,但其实主要只干了两件简单的事情, 第一是在内核中把真正的sockt找出来,在这个对象里记录着各种协议栈的函数地址。 第二是构造一个structmsghdr对象,把用户传入的数据,比如buffr地址、数据长度啥的,统统都装进去. 剩下的事情就交给下一层,协议栈里的函数int_sndmsg了,其中int_sndmsg函数的地址是通过sockt内核对象里的ops成员找到的。大致流程如图。 有了上面的了解,我们再看起源码就要容易许多了。源码如下: //fil:nt/sockt.cSYSCALL_DEFINE4(snd,int,fd,void__usr*,buff,siz_t,ln,unsigndint,flags){rturnsys_sndto(fd,buff,ln,flags,NULL,0);}SYSCALL_DEFINE6(......){//1.根据fd查找到socktsock=sockfd_lookup_light(fd,rr,fput_ndd);//2.构造msghdrstructmsghdrmsg;structiovciov;iov.iov_bas=buff;iov.iov_ln=ln;msg.msg_iovln=1;msg.msg_iov=iov;msg.msg_flags=flags;......//.发送数据sock_sndmsg(sock,msg,ln);} 从源码可以看到,我们在用户态使用的snd函数和sndto函数其实都是sndto系统调用实现的。snd只是为了方便,封装出来的一个更易于调用的方式而已。 在sndto系统调用里,首先根据用户传进来的sockt句柄号来查找真正的sockt内核对象。接着把用户请求的buff、ln、flag等参数都统统打包到一个structmsghdr对象中。 接着调用了sock_sndmsg=__sock_sndmsg==__sock_sndmsg_nosc。在__sock_sndmsg_nosc中,调用将会由系统调用进入到协议栈,我们来看它的源码。 //fil:nt/sockt.cstaticinlinint__sock_sndmsg_nosc(...){......rturnsock-ops-sndmsg(iocb,sock,msg,siz);} 通过第三节里的sockt内核对象结构图,我们可以看到,这里调用的是sock-ops-sndmsg实际执行的是int_sndmsg。这个函数是AF_INET协议族提供的通用发送函数。 由于本技术干货文章篇幅过长,小编分成两次分享给大家,这一篇记得先收藏哟,免得到时候找不到呀~ 转载请注明原文网址:http://www.13801256026.com/pgyy/pgyy/2782.html |