因为要给几百万,几千万,甚至上亿的用户提供各种网络服务,所以在一线互联网公司面试推广后端开发学生的关键要求之一就是要能够支持高并发,了解性能开销,优化性能而且很多时候,如果你对Linux的底层没有深入的了解,当你遇到很多在线性能瓶颈的时候,你会觉得狗拿刺猬,无从下手
今天我们就以举例的方式来深入了解一下Linux下网络包的接收过程习惯上是借用最简单的代码开始思考
intmainintserverSocketFd = socket,bind,charbuff,intreadCount = recvfrom,buff= ' 0 ',printf,
上面的代码是udp服务器接收收据的一段逻辑从开发的角度来看,只要客户端发送相应的数据,服务器执行recv_from后就可以接收并打印出来我们现在想知道的是,当网络数据包到达网卡,直到我们的recvfrom接收到数据,中间发生了什么
友情提醒,这篇文章略长,可以马克后再看!
一,Linux网络包收集概述
在TCP/IP网络的分层模型中,整个协议栈分为物理层,链路层,网络层,传输层和应用层物理层对应的是网卡和网线,应用层对应的是我们常见的Nginx,FTP等应用Linux实现了三层:链路层,网络层和传输层
在Linux内核的实现中,链路层协议由网卡驱动实现,网络层和传输层由内核协议栈实现为内核上层的应用层提供socket接口,供用户进程访问我们从Linux的角度看到的TCP/IP网络的分层模型应该是这样的
图1从Linux角度看网络协议栈
在Linux的源代码中,网络设备驱动的对应逻辑位于driver/net/ethernet,intel系列网卡的驱动在driver/net/ethernet/intel目录下协议栈模块代码位于内核和网络目录中
内核和网络设备驱动程序是通过中断来处理的当数据到达设备时,将在CPU的相关引脚上触发电压变化,以通知CPU处理数据对于网络模块来说,由于处理过程复杂耗时,如果所有的处理都在中断函数中完成,那么中断处理函数会过度占用CPU,CPU将无法响应来自其他设备的消息,比如鼠标和键盘因此,Linux中断处理函数分为上半部分和下半部分前半段只做最简单的工作,然后快速释放CPU,然后CPU可以允许其他中断进来把剩下的大部分工作放在下半部分,就可以轻松了2.4以后的后半部分内核版本采用软中断,由ksoftirqd内核线程处理与硬中断不同,硬中断是通过向CPU的物理引脚施加电压变化,而软中断是通过向内存中的变量赋予二进制值来通知软中断处理程序
好了,在了解了网卡驱动,硬中断,软中断和ksoftirqd线程之后,我们基于这些概念给出一个内核包接收的路径指示:
图2 Linux内核网络数据包收集概述
当网卡接收到数据时,Linux中第一个工作的模块是网络驱动程序网络驱动程序将通过DMA将网卡上接收到的帧写入内存然后向CPU发送一个中断,通知CPU数据到达其次,当CPU收到中断请求时,会调用网络驱动注册的中断处理函数网卡的中断处理功能并没有做太多的工作它发出一个软中断请求,然后尽快释放CPUKsoftirqd检测到一个软中断请求的到来,调用poll开始轮询和接收数据包,收到后会交给各级协议栈处理对于UDP包,它们将被放在用户套接字的接收队列中
从上图中,我们从整体上把握了Linux中数据包的处理过程但是要了解更多网络模块的细节,还得往下看
二,Linux启动
Linux驱动,内核协议栈等模块在接收网卡数据包之前要做大量的准备工作比如提前创建ksoftirqd内核线程,注册每个协议对应的处理函数,提前初始化网络设备子系统,启动网卡只有这些都准备好了,才能真正开始接收数据包所以我们来看看现在这些准备工作是怎么做的
2.1创建ksoftirqd内核线程
Linux的软中断都是在特殊的内核线程中进行的,所以我们非常有必要看一看这些进程是如何初始化的,这样我们就可以在后面更准确的知道包的接收过程进程的数量不是一个,而是n个,其中n等于你机器的核心数量
在系统初始化的时候,内核/smpboot.c中调用smpboot_register_percpu_thread,这个函数将进一步执行spawn_ksoftirqd来创建softirqd进程。
图3创建ksoftirqd内核线程
相关代码如下:
//file:kernel/softirq . cstatistructsmp _ hot plug _ threadsoftirq _ threads =store = ampksoftirqd,thread _ should _ run = ksoftirqd _ should _ run,thread_fn=run_ksoftirqd,
ksoftirqd创建时,会进入自己的线程循环函数ksoftirqd_should_run和run_ksoftirqd不断判断是否有需要处理的软中断这里需要注意的是,不仅有网络软中断,还有其他类型的软中断
//file:include/Linux/interrupt . henumhi _ SOFTIRQ = 0,TIMER_SOFTIRQ,NET_TX_SOFTIRQ,NET_RX_SOFTIRQ,BLOCK_SOFTIRQ,BLOCK_IOPOLL_SOFTIRQ,TASKLET_SOFTIRQ,SCHED_SOFTIRQ,HRTIMER_SOFTIRQ,RCU_SOFTIRQ,,2.2网络子系统初始化
图4网络子系统初始化
Linux内核通过调用subsys_initcall来初始化每个子系统,您可以在源代码目录中grep出对这个函数的许多调用这里要讲的是网络子系统的初始化,会执行到net_dev_init函数
//file:net/core/dev . cstaticint _ _ initnet _ dev _ init for _ each _ possible _ CPU(I)structsoftnet _ data * SD = amp,per_cpu(softnet_data,I),memset(sd,0,sizeof(* SD)),skb _ queue _ head _ init(amp,SD—input _ PKT _ queue),skb _ queue _ head _ init(amp,SD—进程_队列),SD—完成_队列=空,初始化列表头(ampSD—poll _ list),open_softirq(NET_TX_SOFTIRQ,NET _ TX _ action),open_softirq(NET_RX_SOFTIRQ,NET _ RX _ action),subsys _ init call(net _ dev _ init),
在此功能中,将为每个CPU应用一个softnet_data数据结构这个数据结构中的poll_list正在等待驱动程序注册它的poll函数我们可以在后面网卡驱动初始化的时候看到这个过程
此外,open_softirq为每个软中断注册一个处理函数NET_TX_SOFTIRQ处理函数是net_tx_action,NET_RX_SOFTIRQ处理函数是net_rx_action跟踪open_softirq后,发现在softirq_vec变量中记录了注册方法当ksoftirqd线程接收到一个软中断时,它也会使用这个变量来查找每个软中断对应的处理函数
//file:kernel/softirq . cvoidoopen _ softirq(structsoftirq _ action *))softirq _ vec(NR)。行动=行动,2.3协议栈注册
内核实现网络层的ip协议,以及传输层的tcp协议和udp协议这些协议对应的实现函数分别是ip_rcv,tcp_v4_rcv和udp_rcv和我们平时写代码的方式不同,内核是通过注册来实现的Linux内核中的Fs_initcall类似于subsys_initcall,也是初始化模块的入口fs_initcall调用inet_init并启动网络协议栈注册通过inet_init,这些函数被注册在inet_protos和ptype_base数据结构中
图5 AF_INET协议栈注册
相关代码如下
在上面的代码中,我们可以看到udp_protocol结构中的handler是udp_rcv,tcp_protocol结构中的handler是tcp_v4_rcv,由inet_add_protocol初始化。
intinet_add_protocolif(!prot—netns _ ok)pr _ err( " Protocol % uisnotnamespace aware,cannotregistern ",协议),return—EINVAL,回归!cmpxchg((const struct net _ protocol * *)amp,inet_protos(协议),NULL,prot)
inet_add_protocol函数在inet_protos数组中注册tcp和udp对应的处理函数看dev _ add _ pack在这一行中,ip_packet_type结构中的type是协议名,func是ip_rcv函数,它将被注册在dev_add_pack中的ptype_base哈希表中
//file:net/core/dev . cvoiddev _ add _ packstructlist _ head * head = ptype _ head(pt),staticinlinestructlist _ head * ptype _ head(conststructpacket _ type * pt)if(pt—typehtons(ETH _ P _ ALL))return amp,ptype _ allelsereturnampptype_base(ntohs(pt型)ampPTYPE _ HASH _ MASK),
这里我们需要记住inet_protos记录了udp和tcp处理函数的地址,ptype_base存储了ip_rcv函数的地址后面我们会看到在软中断中通过ptype_base找到ip_rcv函数地址,然后ip包会被正确的发送到ip_rcv执行在ip_rcv中,会通过inet_protos找到tcp或udp的处理函数,然后将数据包转发给udp_rcv或tcp_v4_rcv函数
推而广之,看看ip_rcv和udp_rcv函数的代码,就能看出很多协议的处理例如,netfilter和iptables过滤将在ip_rcv中处理如果你有很多或者很复杂的netfilter或者iptables规则,这些规则都是在软中断的上下文中执行的,会增加网络延迟例如,udp_rcv将确定套接字接收队列是否已满对应的内核参数是net.core.rmem_max和net.core.rmem_default如果有兴趣的话,建议你看看inet_init的代码
2.4网卡驱动程序初始化
每个驱动程序将使用module_init向内核注册一个初始化函数,内核将在驱动程序加载时调用这个函数。比如igb网卡驱动的代码就位于drivers/net/Ethernet/Intel/IGB/IGB _ main . c
//file:drivers/net/Ethernet/Intel/igb/igb _ main . cstaticstructpci _ drive rigb _ driver =name=igb_driver_name,id_table=igb_pci_tbl,probe=igb_probe,
驱动程序的pci_register_driver调用完成后,Linux内核就知道了驱动程序的相关信息,比如igb网卡驱动的igb _ driver _ name和igb_probe函数地址等等当网卡设备被识别后,内核会调用它驱动的probe方法驱动探针法的目的是使设备准备就绪
图6网卡驱动程序初始化
第五步我们看到网卡驱动实现了ethtool需要的接口,也在这里注册了函数地址当ethtool发起系统调用时,内核会找到相应操作的回调函数对于igb网卡来说,它的实现函数都在drivers/net/Ethernet/Intel/IGB/IGB _ ethtool . c下,相信你这次能彻底理解ethtool的工作原理了吧这个命令之所以能检查网卡收发包的统计,修改网卡的适配方式,调整RX队列的数量和大小,是因为ethtool命令最终调用了网卡驱动的相应方法,而不是ethtool本身就有这个超能力
第6步注册的igb_netdev_ops包含igb_open等函数,网卡启动时会调用这些函数。
//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_xmit_frame,ndo _ get _ stats 64 = igb _ get _ stats 64,.ndo _ set _ rx _ mode = igb _ set _ rx _ mode,ndo _ set _ MAC _ address = igb _ set _ MAC,ndo_change_mtu=igb_change_mtu,
在第7步中,在igb_probe的初始化过程中,igb_alloc_q_vector也被调用他注册了一个NAPI机制所必需的投票功能对于igb NIC驱动程序,这个函数是igb_poll,如下面的代码所示
staticintigb _ alloc _ q _ vector/* initializeNAPI */netif _皮娜_add(adapter—netdev,ampq _ vector—皮娜,igb_poll,64),2.5启动网卡
以上初始化完成后,就可以启动网卡了回想一下网卡驱动初始化的时候,我们提到过驱动向内核注册了结构net_device_ops变量,这个变量包含了网卡激活,包传递,mac地址设置等回调函数当网卡启用时(比如通过ifconfig eth0 up),会调用net_device_ops中的igb_open方法
图7启动网卡
//file:drivers/net/Ethernet/Intel/igb/igb _ main . cstaticint _ _ igb _ open/* allocatensmitdescriptors */err = igb _ setup _ all _ tx _ resources(适配器),/* allocatereceivedescriptors */err = igb _ setup _ all _ rx _ resources(适配器),/*注册中断处理函数*/err=igb_request_irq(适配器),if(err)goto err _ req _ IRQ,/*启用NAPI */for(I = 0,ilt适配器—gt,数量_数量_矢量,i++)皮娜_使能(amp(适配器—gt,q _ vector(I)—gt,皮娜)),
上面__igb_open函数调用igb_setup_all_tx_resources和igb_setup_all_rx_resources在igb_setup_all_Rx_resources的操作中,分配了RingBuffer,建立了内存和Rx队列的映射关系
staticintigb _ request _ irqstructigb _ adapter * adapter)if(adapter—msix _ entries)err = igb _ request _ msix(adapter),如果(!err)goto request _ done,(I = 0,的staticintigb _ request _ msix(struct igb _ adapter * adapter,ilt适配器数量_ q _ vectorsi++)err = request _ IRQ adapter—msix _ entries(vector)。向量,igb_msix_ring,0,q_vector—name,
跟踪上面代码中的函数调用,_ _ igb _ open = gtigb _ request _ irq = gtIgb_request_msix我们在igb_request_msix中可以看到,对于有多个队列的网卡,每个队列都注册了中断,对应的中断处理函数是igb_msix_ring我们还可以看到,在msix模式下,每个RX队列都有一个独立的MSI—X中断从网卡的硬件中断级别,可以设置接收到的数据包由不同的CPU处理(可以通过irqbalance或者修改/proc/IRQ/IRQ _ number/SMP _ affinity来修改与CPU的绑定行为)
以上准备工作做好之后,就可以开门迎客了!
三,迎接数据的到来3.1硬中断处理
第一,当数据帧从网线到达网卡时,第一站就是网卡的接收队列在网卡分配的环形缓冲区中寻找可用的内存位置找到后,DMA引擎会将数据DMA到之前与网卡关联的内存中这时候CPU就不灵敏了当DMA操作完成后,网卡会像CPU一样发起硬中断,通知CPU数据已经到达
图8网卡数据硬中断的处理过程
注意:当环形缓冲区已满时,新的数据包将被丢弃当ifconfig查看网卡时,它可能会溢出,这表明循环队列中充满了丢弃的数据包如果发现数据包丢失,可能需要通过ethtool命令增加循环队列的长度
在启动网卡这一节,我们说了网卡硬中断注册的处理函数是igb_msix_ring。
//file:drivers/net/Ethernet/Intel/igb/igb _ main . cstaticirqreturn _ tigb _ msix _ ringstructigb _ q _ vector * q _ vector = data,/* WritetheITRvaluecalculatedfromthepreviousinterrupt。*/igb _ write _ itr(q _ vector),皮娜_时间表(ampq _矢量—皮娜),returnIRQ _ HANDLED
IG _ WRITE _ ITR只是记录硬件中断频率。全程跟随皮娜_日程通话,_ _皮娜_日程= gt_ _ _ _皮娜_时间表
/* calledthirqdisabled */staticinlinevoid _ _ _ _ _皮娜_ schedule list _ add _ tail(amp,皮娜民意调查公司ampSD—poll _ list),_ _ raise _ SOFTIRQ _ irqoff(NET _ RX _ SOFTIRQ),
这里我们可以看到list_add_tail修改了CPU变量softnet_data中的poll_list,并添加了驱动程序皮娜_struct发送的poll_listsoftnet_data中的Poll_list是一个双向列表,其中所有设备都有输入帧等待处理然后__raise_softirq_irqoff触发了一个软中断NET_RX_SOFTIRQ这个所谓的触发过程只对一个变量执行了OR运算
void _ _ raise _ softirq _ irqoff(local _ softirq _ pending)
我们说过,Linux只在硬中断中完成简单且必要的工作,剩下的大部分处理都转移到软中断中从上面的代码可以看出,硬中断处理过程真的很短刚记录了一个寄存器,修改了CPU的poll_list,然后发出软中断就这么简单硬中断工作完成
3.2 ksoftirqd内核线程处理软中断
图9 ksoftirqd内核线程
内核初始化时,我们在ksoftirqd中引入两个线程函数ksoftirqd_should_run和run_ksoftirqd。其中ksoftirqd_should_run代码如下:
staticintksoftirqd _ should _ runreturnlocal _ softirq _ pending(),# definelocal _ softirq _ pending() _ _ IRQ _ STAT(SMP _ processor _ id(),__softirq_pending)
这里可以看到,在硬中断中调用了同一个函数local_softirq_pending不同的是,硬中断位置是用来写标记的,而这里是只读的如果在硬中断中设置了NET_RX_SOFTIRQ,则可以在这里自然地读取它
staticvoidrun _ ksoftirqdlocal _ IRQ _ disable(),if(local _ softirq _ pending())_ _ do _ softirq(),rcu _ note _ context _ switch(CPU),local _ IRQ _ enable(),cond _ resched(),返回,local _ IRQ _ enable(),
在__do_softirq中,根据其软中断类型判断当前CPU注册的动作方法被调用。
asmlinkagevoid _ _ do _ softirqdoif(pending amp,1)unsignedintvec _ NR = h—softirq _ vec,int prev _ count = preempt _ count(),trace _ softirq _ entry(vec _ NR),h—行动(h),trace _ softirq _ exit(vec _ NR),h++,pendinggt=1,while(待定),
在网络子系统初始化部分,我们看到我们为NET_RX_SOFTIRQ注册了处理函数net_rx_action因此,将执行net_rx_action函数
这里,我们需要注意一个细节硬中断中设置软中断标志,ksoftirq判断是否有软中断都是基于smp_processor_id这意味着只要硬中断在哪个CPU上被响应,软中断也在这个CPU上被处理所以,如果你发现你的Linux软中断CPU消耗集中在一个核上,你就要调整硬中断的CPU亲和度,把硬中断分散到不同的CPU核上
让我们来关注一下这个核心函数,net_rx_action。
staticvoidnet _ rx _ actionstructsoftnet _ data * SD = amp,_ _ get _ CPU _ var(softnet _ data),unsignedlongtime _ limit = jiffies+2,intbudget = netdev _ budgetvoid * havelocal _ IRQ _ disable(),而(!list _ empty(amp,SD—poll _ list))n = list _ first _ entry(amp,sd—poll_list,structnapi _ struct,poll _ list),工作= 0,if(test_bit(NAPI_STATE_SCHED,ampn—state))work=n—poll(n,权重),trace _皮娜_ poll(n),预算—=工作,
函数开头的time_limit和budget用于控制net_rx_action函数主动退出,以保证网络包不占用CPU等到网卡下一次硬中断到来,再处理剩余的接收包预算可以通过内核参数进行调整这个函数中剩下的核心逻辑是获取当前CPU变量softnet_data,遍历它的poll_list,然后执行NIC驱动程序注册的poll函数对于igb网卡,是igb驱动力的igb _ Poll函数
staticintigb _ pollif(q _ vector—tx . ring)clean _ complete = igb _ clean _ tx _ IRQ(q _ vector),if(q _ vector—rx . ring)clean _ complete amp,=igb_clean_rx_irq(q_vector,budget),
在读操作中,igb_poll的关键工作是对igb_clean_rx_irq的调用。
staticbooligb_clean_rx_irq...do/* retrieveabufferfromthering */skb = igb _ fetch _ rx _ buffer(rx _ ring,rx_desc,skb),/* fetchnextbufferinframeifnon */if(igb _ is _ non _ eop(rx _ ring,rx_desc))继续,/* verifythepacktlayoutiscorrect */if(igb _ clean up _ headers(rx _ ring,rx_desc,skb))skb = NULL,继续,/*populatechecksum,timestamp,VLAN和protocol */igb _ process _ skb _ fields(rx _ ring,rx_desc,skb),皮娜_格罗_接收(ampq _ vector—gt,皮娜,skb),
igb_fetch_rx_buffer和igb_is_non_eop的功能是从环形缓冲区中取出数据帧为什么需要两个功能因为该帧可能会占用不止一个环形缓冲区,所以在循环中获取该帧,直到该帧结束获取的数据帧由sk_buff表示收到数据后,对其进行检查,然后设置sbk变量的时间戳,VLAN id,协议等字段
//file:net/core/dev . cgro _ result _ tnapi _ gro _ receiveskb _ gro _ reset _ offset(skb),return napi _ skb _ finish(dev _ gro _ receive(皮娜,skb),skb),
函数dev_GRO_receive代表了网卡的GRO特性,可以简单理解为将相关的包组合成一个大的包目的是减少传输到网络堆栈的数据包数量,这有助于减少CPU的使用我们暂且忽略它,直接看皮娜_skb_finish这个函数主要调用netif_receive_skb
//file:net/core/dev . cstaticgro _ result _ tnapi _ skb _ finish switch(ret)caseGRO _ NORMAL:if(netif _ receive _ skb(skb))ret = GRO _ DROP,打破,
在netif_receive_skb中,数据包将被发送到协议栈声明下面的3.3,3.4,3.5都属于软中断处理过程,但是因为篇幅较长,所以单独拿出来分成几节
3.3网络协议栈处理
如果是udp数据包,netif_receive_skb函数会将数据包发送到IP _ RCV和udp _ RCV协议处理函数,以便根据数据包的协议进行处理。
图10网络协议栈处理
//file:net/core/dev . cint netif _ receive _ skb//RPS处理逻辑,先忽略return _ _ netif _ receive _ skb(skb),static int _ _ netif _ receive _ sk bret = _ _ netif _ receive _ skb _ core(skb,false),static int _ _ netif _ receive _ skb _ core(struct sk _ buff * skb,boolpfmemalloc)//PCAP逻辑,其中数据将被发送到数据包捕获点。Tcpdump是list _ for _ each _ entry _ rcu (ptype,ampptype_all,list)if(!ptype—gt,devptype—gt,dev skb—gt,dev)if(pt_prev)ret = deliver _ skb(skb,pt _ prev,orig _ dev),pt _ prev = ptypelist_for_each_entry_rcu(ptype,ampptype _ base(ntohs(type)amp,PTYPE_HASH_MASK),list)if(PTYPE—gt,typetypeampamp(ptype—gt,devnull _ or _ devptype—gt,dev skb—gt,devptype—gt,devo rig _ dev))if(pt_prev)ret = deliver _ skb(skb,pt _ prev,orig _ dev),pt _ prev = ptype
在__netif_receive_skb_core中,看着tcpdump的抓取点,使用频率很高,非常激动看来花在看一次源代码上的时间真的没有浪费然后__netif_receive_skb_core拿出协议它将从数据包中取出协议信息,然后遍历在该协议上注册的回调函数列表Ptype_base是一个哈希表,我们在协议注册部分提到过ip_rcv函数的地址存储在这个哈希表中
//file:net/core/dev . cstaticinlineintdeliver _ skbreturnpt_prev—func(skb,skb—dev,pt _ prev,orig _ dev),
pt _ prev—gt,func行调用协议层注册的处理函数对于ip包,它会进入ip_rcv
3.4 IP协议层处理
我们先来大致了解一下linux在ip协议层做了什么,数据包是如何进一步发送到udp或者tcp协议处理函数的。
//file:net/IP v4/IP _ input . cintip _ rcvreturnNF _ HOOK(NF proto _ IP v4,NF_INET_PRE_ROUTING,skb,dev,NULL,IP _ rcv _ finish),
这里NF_HOOK是一个钩子函数注册的钩子执行时,会执行到最后一个参数指向的函数ip_rcv_finish
staticintip_rcv_finishif(!skb _ dst(skb))interr = IP _ route _ input _ noref(skb,iph—daddr,iph—saddr,iph—tos,skb—dev),returndst _ input(skb),
跟踪ip_route_input_noref后,看到它又调用了ip_route_input_mc。在ip_route_input_mc中,函数ip_local_deliver被分配给dst.input,如下所示:
//file:net/IP v4/route . cstaticintip _ route _ input _ mcif(our)rth—dst . input = IP _ local _ deliver,rth—rt_flags
所以回去在ip_rcv_finish中返回dst _ input。
/* inputpacketfromnetnetworktotransport。*/staticinlineintdst _ inputreturnskb _ dst(skb)—input(skb),
skb _ dst—gt,被调用的输入法是路由子系统分配的ip_local_deliver。
//file:net/IP v4/IP _ input . CIN tip _ local _ deliver/* * recommer IP fragments。*/if(IP _ is _ fragment(IP _ HDR(skb))if(IP _ DEFRAG(skb,IP _ DEFRAG _ LOCAL _ DELIVER))return 0,returnNF_HOOK(NFPROTO_IPV4,NF_INET_LOCAL_IN,skb,sk b—gt,dev,NULL,IP _ local _ deliver _ finish),静态启动_本地_交付_完成......int protocol = IP _ HDR(skb)—gt,协议,const struct net _ protocol * IP prot,IP prot = rcu _ de reference(inet _ protos(协议)),如果(ipprot!= NULL)ret = IP prot—gt,handler(skb),
正如在协议注册部分看到的,inet_protos保存tcp_rcv和udp_rcv的函数地址在这里,它将根据包中的协议类型进行分发,其中skb包将进一步发送到更高级别的协议,udp和tcp
3.5 UDP协议层处理
我们在协议注册部分说过,udp协议的处理函数是udp_rcv。
//file:net/IP v4/UDP . cintudp _ rcv return _ _ UDP 4 _ lib _ rcv(skb,ampudp_table,IP proto _ UDP),int _ _ UDP 4 _ lib _ rcv(struct sk _ buff * skb,structudp_table*udptable,int proto)sk = _ _ UDP 4 _ lib _ lookup _ skbskb,uh—source,uh—dest,UDP table),如果(sk!= NULL)intret = UDP _ queue _ rcv _ skbsk,skbicmp_send(skb,ICMP_DEST_UNREACH,ICMP_PORT_UNREACH,0),
__udp4_lib_lookup_skb是根据skb找到对应的socket,找到后将数据包放入socket的缓存队列中否则,发送目的地不可达的icmp数据包
//file:net/IP v4/UDP . cintudp _ queue _ rcv _ sk BIF(sk _ rcv queues _ full(sk,skb,sk—sk _ rcvbuf))goto drop,RC = 0,IP v4 _ pktinfo _ prepare(skb),BH _ lock _ sock(sk),如果(!sock _ owned _ by _ user(sk))RC = _ _ UDP _ queue _ rcv _ skb(sk,skb),elseif(sk_add_backlog(sk,skb,sk—sk _ rcvbuf))BH _ unlock _ sock(sk),gotodropBH _ unlock _ sock(sk),returnrc
Sock_owned_by_user判断用户是否在这个socker上进行系统调用如果没有,可以直接放在socket的接收队列中如果是,通过sk _ add _ queue将数据包添加到队列中当用户释放套接字时,内核会检查队列,如果有数据,会将其移动到接收队列中
如果sk_rcvqueues_full接收队列已满,将直接丢弃该数据包接收队列大小受内核参数net.core.rmem_max和net.core.rmem_default的影响
四。recvfrom系统调用
花开两朵,每桌一朵我们已经在整个Linux内核中完成了接收和处理数据包的过程,最后将数据包放入socket的接收队列中然后我们再回头看看调用recvfrom的用户进程之后发生了什么我们在代码中调用的recvfrom是glibc的一个库函数函数执行后,用户会被困在内核状态,进入Linux实现的系统调用sys_recvfrom在理解Linux与sys_revvfrom的关系之前,让我们简单地看一下核心数据结构socket
图11套接字内核数据机制
套接字数据结构中的const struct proto_ops对应于协议的方法集每个协议将实现一组不同的方法对于IPv4互联网协议族,每个协议都有相应的处理方法,如下Udp由inet_dgram_ops定义,其中注册了inet_recvmsg方法
//file:net/IP v4/af _ inet . cconstructproto _ op sinet _ stream _ ops =recvmsg=inet_recvmsg,mmap=sock_no_mmap,conststructproto _ op sinet _ dgram _ ops =sendmsg=inet_sendmsg,
sock *sk结构是socket数据结构中的另一种数据结构,它是一个非常大而且非常重要的子结构其中sk_prot定义了一个二级处理函数对于udp协议,将设置为UDP协议实现的方法集udp_prot
//file:net/IP v4/UDP . cstructprotoudp _ prot =name="UDP ",owner=THIS_MODULE,close=udp_lib_close,connect=ip4_datagram_connect,sendmsg=udp_sendmsg,recvmsg=udp_recvmsg,
看完socket变量,我们再来看看sys_revvfrom的实现过程。
图12 recvfrom函数的内部实现过程
在inet_recvmsg中调用了sk—gt,sk _ prot—gt,recvmsg .
//file:net/IP v4/af _ inet . cint inet _ recvmsgerr = sk—sk _ prot—recvmsg(iocb,sk,msg,size,flagsampMSG_DONTWAIT,flagsamp~MSG_DONTWAIT,ampaddr _ len),if(err = 0)msg—msg _ name len = addr _ len,returnerr
我们上面说过,这个sk_prot就是net/ipv4/udp.c下的struct proto udp _ PROT,用于udp协议的套接字因此,我们找到了udp_recvmsg方法
//file:net/core/datagram . c:EXPORT _ SYMBOL,struct sk _ buff * _ _ skb _ recv _ datagram(struct sock * sk,unsignedintflags,int*peeked,int*off,int*err)......do structsk _ buff _ head * queue = amp,sk—gt,接收队列,skb_queue_walk(队列,skb)....../* user not ' wanttowait */error =—EAGAIN,如果(!timeo)goto no _ packet,而(!等待更多数据包(sk,err,amptimeo,last)),
终于找到了想看的重点在上面,我们看到了所谓的读取过程,也就是访问sk—gt,接收队列.如果没有数据,并且允许用户等待,将调用wait_for_more_packets来执行等待操作,它的加入将使用户进程进入睡眠状态
动词 (verb的缩写)摘要
网络模块是Linux内核中最复杂的模块看起来一个简单的包接收过程涉及到很多内核组件之间的交互,比如网卡驱动,协议栈,内核ksoftirqd线程等看起来很复杂本文试图通过举例的方式,用通俗易懂的方式把内核包收集的过程解释清楚现在让我们把整个包裹接收过程串起来
当用户执行recvfrom调用时,用户进程通过系统调用进入内核状态如果接收队列中没有数据,进程将进入睡眠状态,并被操作系统挂起这一块比较简单,剩下的大部分场景都是由Linux内核的其他模块来完成的
首先,在开始接收包之前,Linux要做大量的准备工作:
1.创建ksoftirqd线程,为其设置自己的线程函数,然后指望它处理软中断。
2.协议栈注册linux要实现很多协议,比如arp,icmp,ip,udp,tcp每个协议都会注册自己的处理函数,方便快速找到对应的处理函数
3.初始化网卡驱动程序每个驱动都有一个初始化函数,内核会让驱动也初始化它在这个初始化过程中,准备好你的DMA并告诉内核NAPI的轮询函数地址
4.启动网卡,分配RX和TX队列,注册中断对应的处理功能。
以上是内核准备接收包之前的重要工作以上都准备好了,就可以打开硬中断,等待数据包的到来
数据到了,首先迎接它的是网卡:
1.网卡将数据帧DMA到内存中的环形缓冲区,然后向CPU发送中断通知。
2.CPU响应中断请求,调用网卡启动时注册的中断处理函数。
3.中断处理程序几乎什么都没做,就发起了软中断请求。
4.内核线程ksoftirqd线程发现一个软中断请求到来,首先关闭硬中断。
5.ksoftirqd线程开始调用驱动轮询函数来收集数据包。
6.poll函数将收到的数据包发送给协议栈中注册的ip_rcv函数。
7.IP _ RCV函数然后被发送到udp_rcv函数。
现在我们可以回到开始的问题,我们在用户层看到的一行简单的recvfromLinux内核要为我们做这么多工作,才能顺利接收数据还是一个简单的UDP如果是TCP,内核就有更多的工作要做我不禁感叹,内核开发者真是用心良苦
了解了整个包接收过程之后,我们就可以清楚的知道Linux接收一个包的CPU开销了第一个是用户进程调用系统调用进入内核状态的开销第二块是CPU响应包硬中断的CPU开销第三个块由ksoftirqd内核线程的软中断上下文使用后面我们会专门贴一篇文章来实际观察这些开销
此外,网络收发器中还有很多细节我们没有讨论,比如没有NAPI,GRO,RPS等。因为我觉得我说的太对了会影响大家对整个过程的把握,所以尽量只保留主要框架,少即是多!
。