25张图,一万字,拆解Linux网络包发送过程
大家好,我是飞哥!
在开始今天的文章之前,让我请你思考几个小问题。
问:看内核发送数据消耗的CPU应该看sy还是si。
问:为什么你服务器上/proc/ softirqs中的NET_RX比NET_TX大很多。
问:发送网络数据时涉及哪些内存复制操作。
虽然这些问题经常在网上看到,但我们似乎很少去深究如果真的能把这些问题理解透彻,我们驾驭性能的能力会变得更强
带着这三个问题,我们开始今天对Linux内核网络发送过程的深入分析根据我们以前的传统,从一段简单的代码开始
intmainfd=socket,bind,听,cfd =接受,//接收用户请求读取,//用户请求处理剂量测量,//将结果send,0)返回给用户,
今天我们来讨论一下上面代码中调用send后内核是如何发送数据包的基于Linux 3.10,本文以Intel的igb网卡为例
警告:本文一万多字,25张图。写的时候要小心!
1.Linux网络发送过程概述
我觉得看Linux源代码最重要的是有一个整体的把握,而不是一开始就纠结于各种细节。
在这里,我给大家准备了一个通用的流程图,简要说明send发送的数据是如何一步步发送到网卡的。
在这个图中,我们可以看到用户数据被复制到内核状态,然后被协议栈处理并进入环形缓冲区然后网卡驱动真的把数据发出去了当传输完成时,通过硬中断通知CPU,然后清除环形缓冲区
由于文章后面会输入源代码,所以我们从源代码的角度给出一个流程图。
虽然此时数据已经发出,但是还有一件重要的事情没有做,就是释放缓存队列等内存。
那内核怎么知道什么时候释放内存呢当然是网络发了之后网卡发送时会向CPU发送硬中断通知CPU
注意,虽然我们今天的主题是发送数据,但是硬中断触发的软中断是NET_RX_SOFTIRQ,而不是NET_TX_SOFTIRQ!!!
没有惊喜,没有惊喜
所以这就是开题1的部分原因。
1:在服务器上检查/proc/ softirqs为什么NET_RX比NET_TX大很多
传输完成将最终触发NET_RX而不是NET_TX所以很自然的,当你观察/proc/ softirqs的时候,你可以看到更多的NET_RX
好了,现在你对内核如何发送网络数据包有了一个全局的了解不要自满我们需要了解的细节才是更有价值的地方
二,网卡启动准备
现在服务器上的网卡一般都支持多队列每个队列由一个环形缓冲区表示,当打开多个队列时,网卡将有多个环形缓冲区
网卡启动时最重要的任务之一就是分配和初始化环形缓冲区了解RingBuffer对我们以后掌握发送很有帮助因为今天的话题是发送,所以我们以传输队列为例我们来看看网卡启动时分配RingBuffer的实际过程
网卡启动时会调用__igb_open函数,这里分配了RingBuffer。
//file:drivers/net/Ethernet/Intel/igb/igb _ main . cstaticint _ _ igb _ openstructigb _ adapter * adapter = netdev _ priv(netdev),//分配传输描述符数组err = igb _ setup _ all _ tx _ resources(adapter),//分配接收描述符数组err = igb _ setup _ all _ rx _ resources(adapter),//打开所有队列netif _ tx _ start _ all _ queues(netdev),
在上面__igb_open函数中,调用igb_setup_all_tx_resources分配所有发送环形缓冲区,调用igb_setup_all_rx_resources创建所有接收环形缓冲区。
//文件:drivers/net/Ethernet/Intel/IGB/IGB _ main。cstatcitigb _ setup _ all _ TX _ resources//有几个队列为(I = 0,ilt适配器—gt,数量_发送_队列,i++)igb _ setup _ tx _ resources(adapter—gt,tx _ ring(I)),
真正的RingBuffer构造过程是在igb_setup_tx_resources中完成的。
//文件:drivers/net/Ethernet/Intel/igb/igb _ main . cint igb _ setup _ tx _ resources//1。申请igb_tx_buffer数组内存大小= sizeof(struct igb _ tx _ buffer)* tx _ ring—gt,数数,tx _ ring—gt,tx _ buffer _ info = vzalloc(size),//2.申请e1000_adv_tx_descDMA阵列存储器tx _ ring—gt,size = tx _ ring—gt,count * sizeof(union 1000 _ adv _ tx _ desc),tx _ ring—gt,size = ALIGN(tx _ ring—gt,尺寸,4096),tx _ ring—gt,desc=dma_alloc_coherent(dev,tx _ ring—gt,尺寸,安培,tx _ ring—gt,dma,GFP _ KERNEL),//3.初始化队列成员tx _ ring—gt,next _ to _ use = 0,tx _ ring—gt,next _ to _ clean = 0,
从上面的源代码可以看出,其实一个RingBuffer的内部并不只是一个循环队列数组,而是两个。
1)igb_tx_buffer数组:该数组由内核使用,通过vzalloc应用。
2)e1000_adv_tx_desc数组:这个数组是网卡硬件使用的,可以通过DMA直接访问这个内存,通过dma_alloc_coherent分配。
此时它们之间没有联系以后发送时,这两个循环数组中相同位置的指针都会指向同一个skb这样,内核和硬件可以一起访问相同的数据内核将数据写入skb,网卡硬件负责发送
最后调用netif_tx_start_all_queues打开队列此外,硬中断的处理函数igb_msix_ring实际上注册在__igb_open中
第三,accept创建一个新的套接字
在发送数据之前,我们经常需要一个已经建立连接的套接字。
让我们以开放服务器微电影源代码中提到的accept为例接受后,进程会创建一个新的套接字,然后放在当前进程的打开文件列表中,专门用来和对应的客户端进行通信
假设服务器进程已经通过accept与客户机建立了两个连接,让我们简单看一下这两个连接和进程之间的关系。
表示一个连接的套接字内核对象的更具体的结构图如下。
为了避免篡夺主机的角色,这里就不介绍accept的详细源代码过程了今天,我们仍然关注数据发送过程
四,真正开始发送数据4.1实现发送系统调用
send系统调用的源代码位于文件net/socket.c中在这个系统调用中,实际上sendto系统调用实际上是内部使用的。虽然整个调用链并不短,但它实际上只做了两件简单的事情,
首先是找到内核中真正的socket,里面记录了各种协议栈的函数地址。
二是构造一个struct msghdr对象,用户输入的所有数据,比如缓冲区地址,数据长度等等,都加载在这个对象中。
剩下的就交给下一层了,协议栈中的函数inet_sendmsg,inet_sendmsg函数的地址是通过socket内核对象中的ops成员找到的一般流程如图所示
有了以上的认识,我们再看源代码就容易多了。源代码如下:
//file:net/socket . csys call _ define 4 return sys _ send to(FD,buff,len,flags,NULL,0),SYSCALL_DEFINE6()//1查找socket sock = sockfd _ lookup _ light(FD,amp呃,ampfput _ needed),//2.构造msghdrstructmsghdrmsg结构向量,iov.iov _ base = buffiov.iov _ len = lenmsg . msg _ iov len = 1,msg.msg _ iov = ampiovmsg.msg _ flags = flags//3.发送数据sock_sendmsg(sock,ampmsg,len),
从源代码中可以看到,我们在用户模式下使用的send函数和sendto函数,实际上都是通过sendto系统调用来实现的Send只是一种更简单的调用方式,为了方便起见进行了封装
在sendto系统调用中,首先根据用户传入的套接字句柄号找到真正的套接字内核对象然后将用户请求的buff,len,flag等所有参数打包成一个struct msghdr对象
然后叫sock _ sendmsg = gt_ _ sock _ sendmsg gt__sock_sendmsg_nosec .在__sock_sendmsg_nosec中,调用将由系统调用进入协议栈我们来看看它的源代码
//file:net/socket . cstaticinlineint _ _ sock _ send msg _ nosecreturnsock—ops—send msg(iocb,sock,msg,size),
通过第三节的socket内核对象结构图,我们可以看到这里调用的是sock—gt,ops—gt,Sendmsg实际执行的是inet_sendmsg该函数是AF_INET协议族提供的通用发送函数
4.2传输层处理
1)传输层拷贝
进入协议栈inet_sendmsg后,内核会在socket上找到具体的协议发送函数对于TCP协议,这是tcp_sendmsg
在这个函数中,内核将申请一个内核skb内存,并复制用户要发送的数据请注意,此时发送可能不会真正开始如果不满足发送条件,很可能这个调用会直接返回
我们来看inet_sendmsg函数的源代码。
//file:net/IP v4/af _ inet . cintinet _ sendmsgreturnsk—sk _ prot—sendmsg(iocb,sk,msg,size),
在这个函数中,将调用特定协议的发送函数参考第三节的socket内核对象结构图,我们可以看到,对于TCP协议下的socket,sk—gt,sk _ prot—gt,Sendmsg指向tcp_sendmsg
Tcp_sendmsg是一个长函数让我们多看几遍先看这一段
//file:net/IP v4/TCP . CIN TCP _ sendmsgwelewhile//获取发送队列skb = TCP _ write _ queue _ tail(sk),//申请skb,复制......
//file:include/net/TCP . hstaticinlinestructsk _ buff * TCP _ write _ queue _ tailreturnskb _ peek _ tail(amp,sk—sk _ write _ queue),
理解调用tcp_write_queue_tail到socket是理解发送的前提如上图,这个函数是获取socket发送队列中的最后一个skbSk是struct sk_buff对象的缩写,用户的发送队列就是这个对象的链表
让我们继续讨论tcp_sendmsg的其他部分。
//file:net/IP v4/TCP . CIN TCP _ send msg//获取用户传递的数据和标志iov = msg—gt,msg _ iov//用户数据地址iov len = msg—gt,msg _ iovlen//数据块数为1 flags = msg—gt,消息_标志,//各种标志//遍历数据块,同时(—iovlengt,=0)//要发送的数据块的地址unsignedchar _ _ user * from = iov—gt,iov _ basewhile(seglent,0)//你需要申请一个新的sk BIF(copy lt,=0)//申请skb,添加到发送队列末尾SKB = sk _ stream _ alloc _ skb (sk,select _ size (sk,SG),sk—gt,sk _ allocation),//将skb挂在socket skb _ retain(sk,skb)的发送队列上,//skb中有足够的空间if(skb _ avail room(skb)gt,0)//将用户空间的数据复制到内核空间,同时计算校验和
这个函数比较长,但其实逻辑并不复杂其中msg—gt,Msg_iov将待发送数据的缓冲区存储在用户模式存储器中接下来在内核态申请内核内存,比如skb,将用户内存中的数据复制到内核态内存中这将涉及一个或多个内存副本的开销
至于内核什么时候会真正把skb发出去有些判断会在tcp_sendmsg中进行
//file:net/IP v4/TCP . CIN TCP _ sendmsgwelewhile//申请内核内存并复制//发送判断if(forced _ push(TP))TCP _ mark _ push(TP,SKB),__tcp_push_pending_frames(sk,mss_now,TCP _ NAGLE _ PUSH),else if(skbtcp _ send _ head(sk))TCP _ push _ one(sk,MSS _ now),继续,
只有当forced_push或skb tcp_send_head (sk)满足时,内核才会真正开始发送数据包其中forced_push判断未发送的数据是否已经超过最大窗口的一半
如果不满足条件,用户这次要发送的数据只是复制到内核,就搞定了!
2)传输层发送
假设已经满足内核发送条件,让我们跟踪实际的发送过程对于上一节的函数,当真正的发送条件满足时,无论调用__tcp_push_pending_frames还是tcp_push_one,实际都会执行到tcp_write_xmit
所以我们直接看tcp_write_xmit该功能处理传输层中拥塞控制和滑动窗口相关的工作满足窗口要求时,设置TCP头,然后将skb转移到较低的网络层进行处理
我们来看看tcp_write_xmit的源代码。
//file:net/IP v4/TCP _ output . cstaticboolcop _ write _ xmit//循环获取要发送的skb while((skb = TCP _ send _ head(SK))//滑动窗口相关cwnd _ quota = TCP _ cwnd _ test (TP,skb),tcp_snd_wnd_test(tp,skb,MSS _ now),TCP _ MSS _ split _ point(),tso_fragment(sk,skb,),//真正开始发送tcp_transmit_skb(sk,skb,1,GFP),
大家可以看到,我们之前在网络协议中学习的滑动窗口和拥塞控制都是在这个函数中完成的,所以这部分就不展开太多了有兴趣的同学可以找这个源码自己看今天只看发送的主要过程,然后来tcp_transmit_skb
//file:net/IP v4/TCP _ output . cstaticitcp _ transmit _ skb//1克隆新skb if (easy (clone _ it)) SKB = SKB _克隆(SKB,GFP _ mask),//2.封装TCP头th = TCP _ HDR(skb),th—source = inet—inet _ sport,th—dest = inet—inet _ dport,th—window =,th—urg =,//3.调用网络层发送接口err = icsk—gt,icsk _ af _ ops—gt,queue_xmit(skb,ampinet—cork . fl),
第一件事就是先克隆一个新的skb在这里,为什么要复制一个skb
因为skb随后调用网络层,最后到达网卡发送,所以这个skb会被释放而且我们知道TCP协议支持丢失重传,在收到对方的ACK之前,这个skb是不能删除的所以每次内核调用网卡发送,其实都是发送一份skb等到收到ACK后,再实际删除它
第二件事是修改skb中的TCP头,根据实际情况设置TCP头这里有一个小技巧skb实际上包含了网络协议中的所有报头设置TCP头时,只要将指针指向skb的适当位置即可后面设置IP头的时候,只需要移动指针就可以避免频繁的内存申请和复制,效率非常高
Tcp_transmit_skb是传输层发送数据的最后一步,然后你就可以进入网络层进行下一层的操作了。调用网络层提供的发送接口icsk—gt,icsk _ af _ ops—gt,queue_xmit .
在下面的源代码中,我们确实知道queue_xmit实际上指向了ip_queue_xmit函数。
//file:net/IP v4/TCP _ IP v4 . cconstructinet _ connection _ sock _ af _ opsipv4 _ specific =queue_xmit=ip_queue_xmit,
此后,传输层的工作已经完成离开传输层后,数据会进入内核在网络层的实现
4.3网络层发送处理
Linux内核网络层传输的实现位于文件net/ipv4/ip_output.c传输层调用的ip_queue_xmit也在这里。
在网络层,主要处理路由项搜索,IP头设置,netfilter过滤,skb分段等这些任务完成后,会交给下级邻居子系统处理
我们来看网络层入口函数ip_queue_xmit的源代码:
//file:net/IP v4/IP _ output . CIN tip _ queue _ xmit//检查套接字中是否有缓存的路由表RT =(structr table *)_ _ sk _ dst _ Check(sk,0),If(rtNULL)//如果没有缓存,则扩展搜索//如果没有缓存,找到路由项,缓存在套接字中
Ip_queue_xmit已到达网络层在这个函数中,我们可以找到与网络层相关的函数路由项,如果找到,就设置为skb
在Linux上,可以通过route命令看到自己机器的路由配置。
在路由表中,您可以找到目的网络应该通过哪个接口和网关发送找到后缓存在socket上,下次发送数据就不用检查了
然后把路由表地址放到skb里。
//file:include/Linux/skbuff . hstructsk _ buff//保存部分路由相关信息unsignedlong _ skb _ refdst
下一步是在skb中定位IP头,然后根据协议规范设置IP头。
然后,ip_local_out进入下一步。
//file:net/IP v4/IP _ output . cintiplocal _ out//执行netfilter过滤err = _ _ IP _ local _ out(skb),//开始发送数据if(easy(err 1))err = dst _ output(skb),......
At ip _ local _ out = gt_ _ ip _ local _ out = gtnf_hook执行netfilter过滤如果你使用iptables来配置一些规则,那么这里会检查这些规则是否命中如果你设置了一个非常复杂的netfilter规则,在这个函数中你的进程的CPU开销会大大增加
先不展开,继续说发送相关的进程dst_output。
//file:include/net/dst . hstaticinlineintdst _ outputreturnskb _ dst(skb)—output(skb),
这个函数找到这个skb的路由表,然后调用路由表的output方法这是另一个函数指针,指向ip_output方法
//file:net/IP v4/IP _ output.cintipoutput//statistics...//再次给netfilter,回调IP _ finish _ outputreturnnf _ hook _ cond(nf proto _ IP v4,nf _ inet _ post _ routing,skb,null,dev,。(IPCB(skb)—gt,flagsampIPSKB _ re routed)),
在ip_output中做一些简单的统计工作,再次执行netfilter过滤过滤后,回调ip_finish_output
//file:net/IP v4/IP _ output . cstaticinip _ finish _ output//如果大于mtu,则碎片化If(sk b—gt,长度,IP _ skb _ dst _ MTU(skb)amp,amp!skb _ is _ GSO(skb))return IP _ fragment(skb,IP _ finish _ output 2),elsereturnip _ finish _ output 2(skb),
在ip_finish_output中我们可以看到,如果数据大于MTU,就会进行分片。
在ip_finish_output2中,发送过程最终将进入下一层,即邻居子系统。
//file:net/IP v4/IP _ output . cstatichinlineintip _ finish _ output 2//根据下一跳IP地址找到邻居项,创建一个nexthop =(_ _ force 32)RT _ nexthop(RT,IP _ HDR(SKB)—gt,daddr),neigh = _ _ IP v4 _ neigh _ lookup _ noref(dev,next hop),如果(不太可能(!neigh))neigh = _ _ neigh _ create(amp,arp_tbl,ampnexthop,dev,false),//继续向下层传递intros = dst _ neighbor _ output (dst,neighbor,skb),4.4邻居子系统
邻居子系统是位于网络层和数据链路层之间的系统它的作用是给网络层提供一个封装,让网络层不必关心下层的地址信息,由下层决定发送到哪个MAC地址
并且这个邻居子系统不在协议栈net/ipv4/目录中,而是在net/core/neighbor . c中,ipv4和IPv6都需要这个模块。
在邻居子系统中,主要是查找或创建邻居项创建邻居项目时,可能会发出实际的arp请求然后封装MAC头,将发送过程转移到下层网络设备子系统一般流程如图所示
了解了大致流程后,我们再回头看看源代码_ _ IPv4 _ neighbor _ lookup _ noref是在上一节的ip_finish_output2源代码中调用的它在arp缓存中查找,它的第二个参数是路由下一跳IP信息
//file:include/net/ARP . hexternstructnewn _ table ARP _ TBL,staticinlinestructneighbor * _ _ IP v4 _ neigh _ lookup _ norefstructneigh _ hash _ table * nht = rcu _ de reference _ BH(ARP _ TBL . nht),//计算哈希值加快hash_val=arp_hashfn()的搜索速度,for(n = rcu _ de reference _ BH(nht—gt,hash _ bucket(hash _ val)),n!= NULLn = rcu _ de reference _ BH(n—gt,接下来))if(n—gt,devdevampamp*(u32 *)n—gt,primary _ key key)returnn,
如果找不到,调用_ _ neighbor _ create来创建邻居。
//file:net/core/neighbor . cstructneighbor * _ _ neighbor _ create//申请邻居表项structneighbor * n1,* rc,* n = neighbor _ alloc (TBL,dev),//构造赋值memcpy(n—gt,primary_key,pkey,key _ len),n—gt,dev = devn—gt,parms—gt,neff _ setup(n),//最后添加到rcu _ assign _ pointer(nht—gt,hash_buckets(hash_val),n),
有了邻居条目,此时仍然不具备发送IP报文的能力,因为还没有获得目的MAC地址调用dst _ neighbor _ output继续传递skb
//file:include/net/dst . hstaticinlineintdst _ neigh _ outputreturnn—output(n,skb),
Output被调用,实际指向neighbor _ resolve _ output在这个函数中可以发出arp网络请求
//file:net/core/neighbor . cint high _ resolve _ output//注意:arp请求if(!邻居事件发送(邻居,skb))//邻居—gt,ha MAC地址是dev _ hard _ header (skb,dev,ntohs(sk b—gt,协议),neigh—gt,哈,NULL,sk b—gt,len),//发送dev _ queue _ xmit(skb),
当获得硬件MAC地址时,可以封装skb的MAC报头最后调用dev_queue_xmit将skb转移到Linux网络设备子系统
4.5网络设备子系统
邻居通过dev_queue_xmit进入网络设备子系统。
//file:net/core/dev . cint dev _ queue _ xmit//选择发送队列txq=netdev_pick_tx(dev,skb),//获取排队规则q = rcu _ de reference _ BH(txq—gt,q disc),//如果有队列,调用__dev_xmit_skb继续处理数据If(q—gt,入队)rc=__dev_xmit_skb(skb,q,dev,txq),gotoout//有没有队列的环回设备和隧道设备。......
在开篇第2节,我们说网卡有多个发送队列前面对netdev_pick_tx函数的调用是为了选择要发送的队列
netdev_pick_tx发送队列的选择受XPS等配置的影响,还有缓存,也是一套小而复杂的逻辑这里我们只关注两个逻辑首先我们会得到用户的XPS配置,否则会自动计算
//file:net/core/flow _ distributor . cu16 _ _ net dev _ pick _ tx//Get XPS configuration int new _ index = Get _ XPS _ queue(dev,skb),//自动计算队列if(new _ index lt,0)new_index=skb_tx_hash(dev,skb),
然后获取与该队列相关联的qdisc在linux上,可以通过tc命令看到qdisc类型,比如我的一台多队列网卡机上的mq disc
#tcqdiscqdiscmq0:deveth0root
大多数设备都有队列,所以现在我们去__dev_xmit_skb。
//file:net/core/dev . cstaticinlineint _ _ dev _ xmit _ skb//1如果可以绕过排队系统If(q—gt,flagsampTCQ _ F _ CAN _ BYPASS)amp,amp!q disc _ qlen(q)amp,ampIsc qd _ run _ begin (q))//2。正常排队else//入队q—gt,Enqueue(skb,q)//开始发送_ _ qdisc _ run(q),
上面的代码有两种情况,一种是绕过排队系统,一种是正常排队我们只看第二种情况
先调用q—gt,Enqueue将skb添加到队列中然后调用__qdisc_run开始发送
//file:net/sched/sch _ generic . cv oid _ _ qdisc _ runintquota = weight _ p,//松散地从队列中取出一个skb,发送while(qdisc_restart(q))//如果出现下列情况之一,则处理延迟://1 .配额用尽//2 .该进程需要CPU if(—quota,= 0 need _ resch ed())//NET _ TX _ SOFTIRQ类型softirq__netif_schedule(q)将被触发一次,打破,
在上面的代码中,我们看到while循环不断地从队列中取出skb并发送它注意,这个时间实际上占用了用户进程的系统状态时间只有当配额耗尽或其他进程需要CPU时,才触发软中断发送
所以这是/proc/ softirqs在一般服务器上查看的第二个原因,一般NET_RX比NET_TX大很多对于读取,需要经过NET_RX软中断,而对于发送,只有当系统状态配额耗尽时才允许软中断
我们重点看一下qdisc_restart,继续看发送过程。
staticinlineintqdisc _ restart///从qdisc中取出要发送的skbskb = dequeue _ skb(q),returnsch_direct_xmit(skb,q,dev,txq,root _ lock),
Qdisc_restart从队列中取出一个skb,调用sch_direct_xmit继续发送。
//file:net/sched/sch _ generic . cint sch _ direct _ xmit//调用驱动发送数据ret = dev _ hard _ start _ xmit (SKB,dev,txq),4.6软中断调度
在4.5中我们可以看到,如果系统CPU发送的网络包不够,就会调用__netif_schedule触发软中断该函数将转到_ _ netif _ reschedule,它将实际发出一个NET_TX_SOFTIRQ类型的软中断
软中断由内核线程运行,内核线程会进入net_tx_action函数,在这个函数中可以获取发送队列,最后调用驱动中的入口函数dev_hard_start_xmit。
//file:net/core/dev . cstaticinlinevoid _ _ netif _ re scheduled = amp,_ _ get _ CPU _ var(softnet _ data),q—next _ sched = NULL,* SD—output _ queue _ tailp = q,SD—output _ queue _ tailp = amp,q—next _ sched,raise _ SOFTIRQ _ irqoff(NET _ TX _ SOFTIRQ),
在该函数中,要发送的数据队列设置在可由软中断访问的softnet_data中,并添加到output_queue中然后触发NET_TX_SOFTIRQ类型的软中断
这里就不深挖软中断的入门代码了有兴趣的同学可以参考文章《举例说明Linux接收网络包的过程》中的3.2节——ksoftirqd内核线程处理软中断
先说NET_TX_SOFTIRQ softirq注册的回调函数net_tx_action在用户进程触发软中断后,软中断内核线程将执行net_tx_action
记住,之后发送数据消耗的CPU会在这里用si显示,不会消耗用户进程的系统时间。
//file:net/core/dev . cstaticviddnet _ tx _ action//通过softnet_data获取发送队列structsoftnet _ data * sd = amp_ _ get _ CPU _ var(softnet _ data),//如果有qdiscif(SD—gt,Output_queue)//将头指向第一个qdishead = SD—gt,输出_队列,//在(head)structQdisc*q=head时遍历qdsics列表,head = head—gt,next _ sched//发送数据qdisc _ run(q),
这里的软中断会得到softnet_data我们前面看到,进程的内核状态在调用_ _ _ netif _ reschedule时,将发送队列写入了softnet_data的output_queue软中断循环通过SD—gt,Output_queue发送数据帧
看看qdisc_run,它会像进程用户状态一样调用__qdisc_run。
//file:include/net/PKT _ sched . hstaticinlinevoidqdisc _ runif(qdisc _ run _ begin(q))_ _ qdisc _ run(q),
那么同样是输入qdisc _ restart = gtSch_direct_xmit直到驱动函数dev_hard_start_xmit。
4.7 igb NIC驱动程序发送
正如我们前面看到的,网络设备子系统中的dev_hard_start_xmit函数将为用户进程的内核状态和软中断上下文调用在这个函数中,会调用驱动中的发送函数igb_xmit_frame
在驱动函数中,skb会挂在RingBuffer上,调用驱动后,数据包会从网卡发出。
让我们来看看实际的源代码:
//file:net/core/dev . cint dev _ hard _ start _ xmit//获取设备的回调函数集ops construct net _ device _ ops * ops = dev—gt,netdev _ ops//获取设备功能支持的函数列表= netif _ skb _ features(skb),//调用驱动ops中的发送回调函数ndo_start_xmit将数据包发送到网卡设备skb _ len = sk b—gt,lenRC = ops—gt,ndo_start_xmit(skb,dev),
其中ndo_start_xmit是网卡驱动要实现的功能,在net_device_ops中定义。
//file:include/Linux/net device . hstructnet _ device _ opsnetdev _ tx _ t(struct sk _ buff * skb,struct net _ device * dev),
在igb网卡驱动源码中,我们找到了。
//file:drivers/net/Ethernet/Intel/igb/igb _ main . cstaticconstructnet _ device _ op sigb _ net dev _ ops =ndo_open=igb_open,ndo_stop=igb_close,
即对于网络设备层定义的ndo_start_xmit,igb的实现函数是igb_xmit_frame该功能在网卡驱动程序初始化时分配具体的初始化过程请参见图中Linux网络包接收过程中的2.4节网卡驱动初始化
因此,在上面网络设备层调用ops—gt,Nd _ start _ xmit实际上会进入igb_xmit_frame函数让我们进入这个函数,看看驱动程序是如何工作的
//file:drivers/net/Ethernet/Intel/igb/igb _ main . cstaticnetdev _ tx _ tigb _ xmit _ framereturnigb _ xmit _ frame _ ring(skb,igb_tx_queue_ming(adapter,skb)),netdev _ tx _ tigb _ xmit _ frame _ ring(struct sk _ buff * skb,struct GB _ ring * tx _ ring)//获取TXQueue first=amp中的下一个可用缓冲区信息,tx _ ring—gt,tx _ buffer _ info(tx _ ring—gt,next _ to _ use),first—gt,skb = skbfirst—gt,bytecount = sk b—gt,lenfirst—gt,GSO _ segs = 1,//IGB发送映射函数准备发送给设备的数据。igb_tx_map(tx_ring,first,HDR _ len),
这里从网卡的发送队列的RingBuffer中取一个元素,在元素上挂skb。
IG _ TX _ MAP函数用于将skb数据映射到网卡可访问的存储器DMA区域。
//file:drivers/net/Ethernet/Intel/IGB/IGB _ main . cstaticvoidigb _ TX _ map//获取下一个可用的描述符指针tx_desc=IGB_TX_DESC(tx_ring,I),//是sk b—gt,构造一个数据存储器映射,允许设备通过DMA从RAM读取数据DMA = DMA _ map _ single(tx _ ring—gt,dev,sk b—gt,数据,大小,DMA _ TO _ DEVICE),//遍历数据包的所有片段,为skb的每个片段生成一个有效映射(frag = ampskb _ shinfo(skb)—gt,frags(0)frag++)tx _ desc—gt,read . buffer _ addr = CPU _ to _ le64(DMA),tx _ desc—gt,read . cmd _ type _ len =,tx _ desc—gt,read . oli info _ status = 0,//设置最后一个descriptorcmd_type
当建立了所有需要的描述符并且skb的所有数据都被映射到DMA地址时,驱动程序将进入它的最后一步并触发真正的发送。
4.8发送完整的硬中断
当数据发送出去的时候,工作还没有完成因为内存还没有清理传输完成后,网卡设备会触发硬中断来释放内存
在3.1节和3.2节的Linux网络数据包接收过程中,我们详细描述了硬中断和软中断的处理过程。
在发送完成的硬中断中,RingBuffer的内存会被清空,如图所示。
回头看看硬中断触发软中断的源代码。
//file:drivers/net/Ethernet/Intel/igb/igb _ main . cstaticinlinevoid _ _ _ _ _皮娜_ schedule list _ add _ tail(amp,皮娜民意调查公司ampSD—poll _ list),_ _ raise _ SOFTIRQ _ irqoff(NET _ RX _ SOFTIRQ),
这里有一个有趣的细节无论硬中断是由于要接收数据还是发送完成通知,硬中断触发的软中断都是NET_RX_SOFTIRQ正如我们在第一节中所说,这是软中断统计中RX高于TX的一个原因
好了,我们来看软中断的回调函数igb_poll在这个函数中,我们注意到有一行igb_clean_tx_irq
//file:drivers/net/Ethernet/Intel/igb/igb _ main . cstaticintigb _ poll//performsthetransmitcompletionoperations if(q _ vector—tx . ring)clean _ complete = igb _ clean _ tx _ IRQ(q _ vector),
让我们看看传输完成时igb_clean_tx_irq做了什么。
//file:drivers/net/Ethernet/Intel/igb/igb _ main . cstaticbooligb _ clean _ tx _ IRQ//freetheskbdev _ kfree _ skb _ any(tx _ buffer—skb),//cleartx _ buffer datax _ buffer—skb = NULL,dma_unmap_len_set(tx_buffer,len,0),//clearlastDMAlocationandunmapremainingbuffers */while(tx _ desc!=eop_desc)
无非就是清除skb,去掉DMA映射等等至此,传输基本完成
为什么我说基本完成了,不是全部完成了因为传输层需要保证可靠性,所以skb实际上并没有被删除直到收到对方的ACK才会真正删除那时,它将被认为是完全传输
最后
用一张图总结整个发送过程。
了解了整个发送过程之后,我们再回头复习一下开头提到的一些问题。
1.我们在监控内核发送数据所消耗的CPU时,应该看sy还是si。
在发送网络数据包的过程中,用户进程已经完成了大部分工作,甚至连调用驱动的事情都已经完成了只有当内核态进程被切断时,才会启动软中断在发送过程中,大部分开销(90%以上)消耗在用户进程的内核状态
只有少数情况会触发软中断,它是由软中断ksoftirqd的内核进程发送的。
所以在监控网络IO对服务器造成的CPU开销时,不能只看si,si和sy都要考虑。
2.在服务器上检查/proc/ softirqs为什么NET_RX比NET_TX大很多
之前我以为NET_RX是读,NET_TX是传对于接收用户请求并将其返回给用户的服务器这两个数字应该差不多,至少不会有数量级的差异
经过今天的源代码分析,这个问题有两个原因。
第一个原因是当数据传输完成时,通过硬中断通知驱动器传输完成可是,无论是接收还是发送数据,硬中断触发的软中断都是NET_RX_SOFTIRQ,而不是NET_TX_SOFTIRQ
第二个原因是,对于读取,都要经过NET_RX软中断,都要经过ksoftirqd内核进程至于发送,大部分工作是在用户进程中的内核模式下完成的只有当系统模式的配额耗尽时,才会发出NET_TX,并给出软中断
综上所述,不难理解在机器上NET_RX比NET_TX大很多。
3.发送网络数据时涉及哪些内存复制操作。
这里,我们仅指要发送的数据的内存副本。
第一个复制操作是内核申请skb后,用户传递的缓冲区中的数据会全部复制到skb中如果要发送的数据量很大,这种复制操作的成本也不小
当第二次复制操作从传输层进入网络层时,将克隆每个skb的新副本而网络层的驱动,软中断等后续组件在传输完成后会删除这个副本传输层保存原始skb,在对方网络方没有ack的情况下可以重新发送,实现TCP中要求的可靠传输
第三个副本不是必须的,只有当IP层发现skb大于MTU时才需要会申请额外的skb,把原来的skb复制成几个小skb
这里说个题外话,我们在网络性能优化中经常听到零拷贝,我觉得有点夸张为了保证TCP的可靠性,没有办法保存第二份副本如果包大于MTU,碎片中的复制是无法避免的
看到这里,相信内核发包对你来说不再是一个完全无法理解的黑匣子即使你只理解了这篇文章的十分之一,你也已经掌握了打开这个黑匣子的方法以后优化网络性能的时候就知道从哪里入手了
声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多企业信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。投资有风险,需谨慎。