此文来源于微信公众平台:开发设计内功修炼 (ID:kfngxl),创作者:张彦飞 allen
由于会对上百万、一定、甚至过亿的客户提供各种各样互联网服务,因此在一线互联网公司里面试和升职后端工程师同学们的其中一个关键规定就是为了能支撑点分布式系统,要清楚特性花销,将进行性能调优。而有些时候,如果对 Linux 最底层的了解较浅得话,遇到几个网上性能瓶颈你就会觉得狗拿仓鼠,找不到方向。
今天我们用详解的形式,来深层理解一下在 Linux 下网络包的传输操作过程。或是遵照惯例来使用一段最简单的代码开始考虑。以便简易考虑,我们要用 udp 来说吧,如下所示:
int main(){
int serverSocketFd=socket(AF_INET,SOCK_DGRAM,0);
bind(serverSocketFd,);
char buff[BUFFSIZE];
int readCount=recvfrom(serverSocketFd,buff,BUFFSIZE,0,);
buff[readCount]=\”\\0\”;
printf(\”Receive from client:%s\\n\”,buff);
}
上边编码是一段 udp server 接受收条的思路。当在研发角度看的时候,要是手机客户端有相对应的数据信息推送来,服务端实行 recv_from 后就可以接到它,并将其直接打印。现在我们想要知道的是,当网络包做到网卡,直至我们自己的 recvfrom 接到数据信息,这其中,到底都发生过什么?
根据文中,我们将深刻理解 Linux 应用系统内部结构是怎样达到的,及每个部位中间怎样互动。坚信这会对你工作可能有着非常大的协助。文中根据 Linux 3.10,源码参照 https://mirrors.edge.kernel.org/ pub / linux / kernel / v3.x/,网卡推动选用 Intel 的 igb 网卡举例说明。
温馨提示,文中偏长,可以直接 Mark 后看!
一、Linux 互联网收包一览
在 TCP / IP 互联网分层模型里,全部协议栈被划分成mac层、链路层、传输层,网络层和应用层。mac层相匹配的是网卡和网络线,网络层相匹配的是比较常见的 Nginx,FTP 等等一系列运用。Linux 完成的是链路层、传输层和网络层这三层。
在 Linux 内核完成中,链路层协议书靠网卡推动来达到,内核协议栈来达到传输层和网络层。内核对更最上层网络层给予 socket 插口来供客户过程浏览。我们要用 Linux 的角度来看见的 TCP / IP 互联网分层模型该是下边这个样子。
在 Linux 的源码中,互联网设备驱动相对应的逻辑性坐落于 driver / net / ethernet, 在其中 intel 系列产品网卡的驱动程序在 driver / net / ethernet / intel 目录下。协议栈控制模块编码坐落于 kernel 和 net 文件目录。
内核和互联网设备驱动是由中断的方式去处理。当设备中有数据到达时,能给 CPU 的有关管脚上开启一个电压波动,以通告 CPU 来建立模型。针对网络接口而言,因为处理方式较为复杂和用时,若是在中断函数中进行每一个解决,将也会导致中断处理函数(优先太高)将过多占有 CPU,可能导致 CPU 没法回应其他机器设备,比如键盘和鼠标消息。因而 Linux 中断处理函数也是分上半部和下半边的。上半部是只开展简单的工作中,快速解决随后释放出来 CPU,然后 CPU 就能容许其他中断进去。剩余将绝大多数的工作也放进下半边中,再慢慢坦然解决。2.4 往后的内核版本号所采用的下半边控制方式是软中断,由 ksoftirqd 内核进程全权处理。和硬中断不一样的是,硬中断是由给 CPU 物理学管脚增加电压波动,而软中断是由给运行内存中的一个自变量的二进制值以通告软中断程序处理。
好啦,大致了解了网卡推动、硬中断、软中断和 ksoftirqd 进程以后,让我们在这里好多个定义的前提下给出一个内核收包的路线提示:
当网卡上接到数据信息之后,Linux 中第一个的工作控制模块是网络驱动。网络驱动便以 DMA 的形式把网卡上收到帧提到运行内存里。向 CPU 进行一个中断,以通告 CPU 有数据到达。第二,当 CPU 接到中断要求后,想去启用网络驱动登记注册的中断处理函数。网卡的中断处理函数并不是进行任何工作中,传出软中断要求,随后尽早释放出来 CPU。ksoftirqd 检验过有软中断要求抵达,启用 poll 逐渐轮循收包,到手后交给各个协议栈解决。针对 UDP 包而言,能被放进客户 socket 的接受序列中。
大家从上述这张图片里已经从宏观上掌握到 Linux 对数据文件的处理方式。但是得想要了解更多网络接口工作中的小细节,我们还需要看下去。
二、Linux 运行
Linux 推动,内核协议栈这些板块在具有接受网卡数据文件以前,需做许多准备工作才可以。例如最好提前建立好 ksoftirqd 内核进程,要申请注册好每个协议书相对应的处理函数,计算机设备分系统最好提前复位好,网卡要运行好。仅有这都 Ready 以后,大家才能做到真正逐渐接受数据文件。那样现在我们来看看这个前期准备工作都是怎么做成的。
2.1 建立 ksoftirqd 内核进程
Linux 的软中断都在专门内核进程(ksoftirqd)中所进行的,所以我们很有必要看一下这种进程是怎么初始化的,那样才能真正的后边最准确了解收包全过程。该过程总数并不是 1 个,反而是 N 个,在其中 N 相当于你设备的核数。
初始设置时在 kernel / smpboot.c 中启用了 smpboot_register_percpu_thread,该函数公式进一步会实行到 spawn_ksoftirqd(坐落于 kernel / softirq.c)来构建出 softirqd 过程。
有关编码如下所示:
//file:kernel/softirq.c
static struct smp_hotplug_thread softirq_threads={
.store=&ksoftirqd,
.thread_should_run=ksoftirqd_should_run,
.thread_fn=run_ksoftirqd,
.thread_comm=\”ksoftirqd/%u\”,};
static__init int spawn_ksoftirqd(void){
register_cpu_notifier(&cpu_nfb);
BUG_ON(smpboot_register_percpu_thread(&softirq_threads));
return 0;
}
early_initcall(spawn_ksoftirqd);
当 ksoftirqd 被建立出来之后,它也会进到自已的进程循环系统函数公式 ksoftirqd_should_run 和 run_ksoftirqd 了。不断地分辨是否有软中断需要被解决。这儿需要注意的问题一点是,软中断不仅仅有互联网软中断,还有其他种类。
//file:include/linux/interrupt.h
enum{
HI_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 互联网子初始设置
linux 内核根据启用 subsys_initcall 来复位每个分系统,在源码文件目录里你能 grep 出很多对于这个函数的调用。接下来我们说起的是互联网分系统的复位,会实行到 net_dev_init 函数公式。
//file:net/core/dev.c
static int__init net_dev_init(void){
for_each_possible_cpu(i){
struct softnet_data *sd=&per_cpu(softnet_data,i);
memset(sd,0,sizeof(*sd));
skb_queue_head_init(&sd-input_pkt_queue);
skb_queue_head_init(&sd-process_queue);
sd-completion_queue=NULL;
INIT_LIST_HEAD(&sd-poll_list);
}
open_softirq(NET_TX_SOFTIRQ,net_tx_action);
open_softirq(NET_RX_SOFTIRQ,net_rx_action);
}
subsys_initcall(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.c
void open_softirq(int nr,void(*action)(struct softirq_action *)){
softirq_vec[nr].action=action;
}
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 算法设计中奖了。如下图所示:
有关编码如下所示
//file:net/ipv4/af_inet.c
static struct packet_type ip_packet_type__read_mostly={
.type=cpu_to_be16(ETH_P_IP),
.func=ip_rcv,};static const struct net_protocol udp_protocol={
.handler=udp_rcv,
.err_handler=udp_err,
.no_policy=1,
.netns_ok=1,};static const struct net_protocol tcp_protocol={
.early_demux=tcp_v4_early_demux,
.handler=tcp_v4_rcv,
.err_handler=tcp_v4_err,
.no_policy=1,
.netns_ok=1,
};
static int__init inet_init(void){
……
if(inet_add_protocol(&icmp_protocol,IPPROTO_ICMP)<0)
pr_crit("%s:Cannot add ICMP protocol\\n",__func__);
if(inet_add_protocol(&udp_protocol,IPPROTO_UDP)<0)
pr_crit("%s:Cannot add UDP protocol\\n",__func__);
if(inet_add_protocol(&tcp_protocol,IPPROTO_TCP)<0)
pr_crit("%s:Cannot add TCP protocol\\n",__func__);
……
dev_add_pack(&ip_packet_type);
}
上边的编码中大家可以看到,udp_protocol 建筑结构里的 handler 是 udp_rcv,tcp_protocol 建筑结构里的 handler 是 tcp_v4_rcv,根据 inet_add_protocol 被复位了进去。
int inet_add_protocol(const struct net_protocol *prot,unsigned char protocol){
if(!prot-netns_ok){
pr_err(\”Protocol%u is not namespace aware,cannot register.\\n\”,
protocol);
return-EINVAL;
}
return!cmpxchg((const struct net_protocol **)&inet_protos[protocol],
NULL,prot)?0:-1;
}
inet_add_protocol 函数公式将 tcp 和 udp 相对应的处理函数都注册到 inet_protos 二维数组中奖了。再看一遍 dev_add_pack (&ip_packet_type); 这一行,ip_packet_type 建筑结构里的 type 是协议名,func 是 ip_rcv 函数公式,在 dev_add_pack 时会被注册到 ptype_base 哈希表中。
//file:net/core/dev.c
void dev_add_pack(struct packet_type *pt){
struct list_head *head=ptype_head(pt);
}
static inline struct list_head *ptype_head(const struct packet_type *pt){
if(pt-type==htons(ETH_P_ALL))
return&ptype_all;
else
return&ptype_base[ntohs(pt-type)&PTYPE_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 等函数的编码能看到许多协议的处理方式。比如,ip_rcv 时会解决 netfilter 和 iptable 过虑,假如你有许多或者很繁杂的 netfilter 或 iptables 标准,这种标准都在软中断的前后文中实施的,也会增加网络延时。再比如,udp_rcv 时会分辨 socket 接受序列是不是快满了。相对应的有关内核主要参数是 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.c
static struct pci_driver igb_driver={
.name=igb_driver_name,
.id_table=igb_pci_tbl,
.probe=igb_probe,
.remove=igb_remove,
};
static int__init igb_init_module(void){
ret=pci_register_driver(&igb_driver);
return ret;
}
驱动 pci_register_driver 启用结束后,Linux 内核就明白了该推动相关信息,例如 igb 无线驱动的 igb_driver_name 和 igb_probe 函数公式详细地址这些。当网口机器设备被鉴别之后,内核会获取其驱动 probe 方式(igb_driver 的 probe 方法是什么 igb_probe)。推动 probe 方式实行的目的就是为了让机器设备 ready,针对 igb 网口,其 igb_probe 坐落于 drivers / net / ethernet / intel / igb / igb_main.c 下。关键实行操作如下所示:
第 5 步中你看到的,无线驱动完成了 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.c
static const struct net_device_ops igb_netdev_ops=
.ndo_open=igb_open,
.ndo_stop=igb_close,
.ndo_start_xmit=igb_xmit_frame,
.ndo_get_stats64=igb_get_stats64,
.ndo_set_rx_mode=igb_set_rx_mode,
.ndo_set_mac_address=igb_set_mac,
.ndo_change_mtu=igb_change_mtu,
.ndo_do_ioctl=igb_ioctl,
第 7 步中,在 igb_probe 初始化环节中,还调用到了 igb_alloc_q_vector。他申请了一个 NAPI 体制所必需的 poll 函数,针对 igb 网卡驱动而言,这一函数便是 igb_poll, 如下所示编码所示。
static int igb_alloc_q_vector(struct igb_adapter *adapter,
int v_count,int v_idx,
int txr_count,int txr_idx,
int rxr_count,int rxr_idx){
/* initialize NAPI */
netif_napi_add(adapter-netdev,&q_vector-napi,
igb_poll,64);
}
2.5 运行网口
当上边的初始化都结束之后,就能运行网卡了。追忆前边网卡驱动初始化时,大家提到了推动向核心申请了 structure net_device_ops 自变量,它蕴含着网口开启、分包、设定 mac 详细地址等调整函数(函数表针)。当开启一个网口时(比如,根据 ifconfig eth0 up),net_device_ops 里的 igb_open 方式能被调用。它一般会做下列事儿:
//file:drivers/net/ethernet/intel/igb/igb_main.c
static int__igb_open(struct net_device *netdev,bool resuming){
/* allocate transmit descriptors */
err=igb_setup_all_tx_resources(adapter);
/* allocate receive descriptors */
err=igb_setup_all_rx_resources(adapter);
/* 申请注册中断处理函数 */
err=igb_request_irq(adapter);
if(err)
goto err_req_irq;
/* 开启 NAPI */
for(i=0;i num_q_vectors;i )
napi_enable(&(adapter->q_vector[i]->napi));
}
在墙上__igb_open 函数调用了 igb_setup_all_tx_resources, 和 igb_setup_all_rx_resources。在 igb_setup_all_rx_resources 这一步操作过程中,分派了 RingBuffer,并制定运行内存和 Rx 序列的映射关系。(Rx Tx 序列的总数和尺寸能通过 ethtool 开展配备)。咱们就继续看终断函数申请注册 igb_request_irq:
static int igb_request_irqstruct igb_adapter *adapter)
if(adapter-msix_entries)
err=igb_request_msix(adapter);
if(!err)
goto request_done;
}
}
static int igb_request_msix(struct igb_adapter *adapter)
for(i=0;i<adapter-num_q_vectors;i )
err=request_irqadapter-msix_entries[vector].vector,
igb_msix_ring,0,q_vector-name,
}
在后面的编码中追踪函数调用,__igb_open=>igb_request_irq=>igb_request_msix, 在 igb_request_msix 中我们看到了,针对多序列的网口,为每一个序列都申请了终断,其相对应的中断处理函数是 igb_msix_ring(该函数还在 drivers / net / ethernet / intel / igb / igb_main.c 下)。大家也能看到,msix 方法下,每一个 RX 序列具有独立的 MSI-X 终断,从网口硬件中断层面上就能设让收到抱被不同类型的 CPU 解决。(能通过 irqbalance ,或是改动 /proc/ irq / IRQ_NUMBER / smp_affinity 能更改和 CPU 的关联个人行为)。
当搞好之上前期准备工作之后,就能隆重开业(数据文件)了!
三、迎来数据信息的来临
3.1 硬中断处理
最先当数据帧从网络线抵达网口里的情况下,第一站是网口的接受序列。网口在分派给自己 RingBuffer 中找到可利用的内存位置,寻找后 DMA 模块能把数据信息 DMA 到网口以前关联运行内存里,这时候 CPU 全是无感觉的。当 DMA 使用结束之后,网口也会像 CPU 进行一个硬终断,通告 CPU 有数据到达。
留意:当 RingBuffer 满的情况下,新来数据文件将会对丢掉。ifconfig 查看网卡时,能够里面有一个 overruns,表明由于环状序列满被丢掉的包。一旦发现有网络丢包,可能还需要根据 ethtool 指令来增加环状序列长度。
在运行网口一节,大家说到了网口的硬终断登记注册的解决函数是 igb_msix_ring。
//file:drivers/net/ethernet/intel/igb/igb_main.c
static irqreturn_t igb_msix_ring(int irq,void *data){
struct igb_q_vector *q_vector=data;
/* Write the ITR value calculated from the previous interrupt.*/
igb_write_itr(q_vector);
napi_schedule(&q_vector-napi);
return IRQ_HANDLED;
}
igb_write_itr 仅仅记录一下硬件中断工作频率(听说目的是为了在尽可能减少 CPU 的终断工作频率时使用)。沿着 napi_schedule 调用一路追踪下来,__napi_schedule=>____napi_schedule
/* Called with irq disabled */
static inline void____napi_schedule(struct softnet_data *sd,
struct napi_struct *napi){
list_add_tail(&napi-poll_list,&sd-poll_list);
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
这儿你看到的,list_add_tail 更改了 CPU 自变量 softnet_data 中的 poll_list,将推动 napi_struct 传出去的 poll_list 加了进去。在其中 softnet_data 里的 poll_list 是一个双重目录,在其中的机器都自带键入帧等待被解决。随后__raise_softirq_irqoff 触动了一个软中断 NET_RX_SOFTIRQ,这一所谓开启全过程只对一个自变量进行了一次或运算罢了。
void__raise_softirq_irqoff(unsigned int nr
trace_softirq_raise(nr;
or_softirq_pending1UL<nr;
}
//file:include/linux/irq_cpustat.h
#define or_softirq_pendingx)(local_softirq_pending)|=(x))
大家说过,Linux 在硬终断里只进行简易必须的工作中,剩下来的绝大部分的解决全是转交到软中断的。根据上边编码能够看见,硬中断处理全过程真的是很短。仅仅展示了一个存储器,更改了一下下 CPU 的 poll_list,随后传出个软中断。如此简单,硬终断工作中即便是实现了。
3.2 ksoftirqd 内核线程解决软中断
内核线程初始化时,大家阐述了 ksoftirqd 中2个进程函数 ksoftirqd_should_run 和 run_ksoftirqd。在其中 ksoftirqd_should_run 编码如下所示:
static int ksoftirqd_should_run(unsigned int cpu){
return local_softirq_pending();
}
#define local_softirq_pending()\\__IRQ_STAT(smp_processor_id(),__softirq_pending)
这儿见到和硬终断中调用了同一个函数 local_softirq_pending。使用步骤不一样的是硬中断部位就是为了写入标记,这儿只不过是读取。假如硬中断中设置权限 NET_RX_SOFTIRQ, 这儿自然就能读取的到。未来会真真正正进到线程函数中 run_ksoftirqd 解决:
static void run_ksoftirqd(unsigned int cpu){
local_irq_disable();
if(local_softirq_pending()){
__do_softirq();
rcu_note_context_switch(cpu);
local_irq_enable();
cond_resched();
return;
}
local_irq_enable();
}
在__do_softirq 中,分辨依据现阶段 CPU 的软中断种类,启用其登记注册的 action 方式。
asmlinkage void__do_softirq(void){
do{
if(pending&1){
unsigned int vec_nr=h-softirq_vec;
int prev_count=preempt_count();
trace_softirq_entry(vec_nr);
h-action(h);
trace_softirq_exit(vec_nr);
}
h ;
pending>=1;
}while(pending);
}
在互联网子初始设置小标题,你看到的我们为 NET_RX_SOFTIRQ 申请了处理函数 net_rx_action。因此 net_rx_action 函数公式便会被实行到。
这儿应注意一个关键点,硬中断中设定软中断标记,和 ksoftirq 的确定是否有软中断抵达,都是围绕 smp_processor_id () 的。这就意味着只需硬中断在哪些 CPU 上被回应,那么软中断也就是在这一 CPU 上处理。所以,如果你感觉你 Linux 软中断 CPU 耗费集中在一个核上得话,作法就是要把调节硬中断的 CPU 两亲性,把硬中断打撒到不同类型的 CPU 核上来。
我们再把注意力集中到这些关键函数公式 net_rx_action 上去。
static void net_rx_action(struct softirq_action *h){
struct softnet_data *sd=&__get_cpu_var(softnet_data);
unsigned long time_limit=jiffies 2;
int budget=netdev_budget;
void *have;
local_irq_disable();
while(!list_empty(&sd-poll_list)){
n=list_first_entry(&sd-poll_list,struct napi_struct,poll_list);
work=0;
if(test_bit(NAPI_STATE_SCHED,&n-state)){
work=n-poll(n,weight);
trace_napi_poll(n);
}
budget-=work;
}
}
函数公式开头 time_limit 和 budget 就是用来操纵 net_rx_action 函数公式积极退出的,目的是确保网络包的接受不占据 CPU 没放。等下次网口还有硬中断来的时候才解决剩下来的接受数据文件。在其中 budget 能通过核心主要参数调节。这一函数中剩下来的关键思路是掌握到现阶段 CPU 自变量 softnet_data,并对 poll_list 开展赋值,随后实行到无线驱动申请注册过的 poll 函数公式。针对 igb 网口而言,便是 igb 推动力的 igb_poll 函数公式了。
static int igb_poll(struct napi_struct *napi,int budget){
if(q_vector-tx.ring)
clean_complete=igb_clean_tx_irq(q_vector);
if(q_vector-rx.ring)
clean_complete&=igb_clean_rx_irq(q_vector,budget);
}
在读取操作过程中,igb_poll 的核心工作就是对 igb_clean_rx_irq 的启用。
static bool igb_clean_rx_irq(struct igb_q_vector *q_vector,const int budget){
…
do{
/* retrieve a buffer from the ring */
skb=igb_fetch_rx_buffer(rx_ring,rx_desc,skb);
/* fetch next buffer in frame if non-eop */
if(igb_is_non_eop(rx_ring,rx_desc))
continue;
}
/* verify the packet layout is correct */
if(igb_cleanup_headers(rx_ring,rx_desc,skb)){
skb=NULL;
continue;
}
/* populate checksum,timestamp,VLAN,and protocol */
igb_process_skb_fields(rx_ring,rx_desc,skb);
napi_gro_receive(&q_vector->napi,skb);
}
igb_fetch_rx_buffer 和 igb_is_non_eop 的作用是把数据帧从 RingBuffer 上拿下来。为何必须2个函数公式呢?由于有很有可能帧要占多好几个 RingBuffer,所以也是在一个循环中获得的,直至帧尾端。获得出来的一个数据帧用一个 sk_buff 来描述。扣除完数据信息之后,对它进行一些校检,然后就设定 sbk 自变量的 timestamp, VLAN id, protocol 等字段。下面进入 napi_gro_receive 中:
//file:net/core/dev.c
gro_result_t napi_gro_receive(struct napi_struct *napi,struct sk_buff *skb){
skb_gro_reset_offset(skb);
return napi_skb_finish(dev_gro_receive(napi,skb),skb);
}
dev_gro_receive 这一函数公式意味着的是网口 GRO 特点,可以将其理解为把有关的小包包合拼成一个大包包就可以了,目的是降低传输给互联网栈的包数,这有利于降低 CPU 的用量。咱们姑且忽视,直接看 napi_skb_finish, 这一函数公式主要是启用了 netif_receive_skb。
//file:net/core/dev.c
static gro_result_t napi_skb_finish(gro_result_t ret,struct sk_buff *skb)
switch(ret){
case GRO_NORMAL:
if(netif_receive_skb(skb))
ret=GRO_DROP;
break;
}
在 netif_receive_skb 中,数据文件将送往tcp协议中。申明,以内的 3.3, 3.4, 3.5 也都是属于软中断的处理方式,只不过是因为篇数过长,独立取出来成小标题。
3.3 网络协议栈解决
netif_receive_skb 函数公式会依据包的协议书,如果是 udp 包,会把包先后送至 ip_rcv (),udp_rcv () 协议书处理函数中进行修复。
图 10 网络协议栈解决
//file:net/core/dev.c
int netif_receive_skb(struct sk_buff *skb)
//RPS 解决逻辑性,先忽视
return__netif_receive_skb(skb);
}
static int__netif_receive_skb(struct sk_buff *skb)
ret=__netif_receive_skb_core(skb,false);}static int__netif_receive_skb_core(struct sk_buff *skb,bool pfmemalloc){
//pcap 逻辑性,这儿会将数据送进抓包软件点。tcpdump 也是从这一通道获得包的 list_for_each_entry_rcu(ptype,&ptype_all,list){
if(!ptype->dev ptype->dev==skb->dev){
if(pt_prev)
ret=deliver_skb(skb,pt_prev,orig_dev);
pt_prev=ptype;
}
}
list_for_each_entry_rcu(ptype,
&ptype_base[ntohs(type)&PTYPE_HASH_MASK],list){
if(ptype->type==type&&
(ptype->dev==null_or_dev ptype->dev==skb->dev
ptype->dev==orig_dev)){
if(pt_prev)
ret=deliver_skb(skb,pt_prev,orig_dev);
pt_prev=ptype;
}
}
}
在__netif_receive_skb_core 中,看着原先常用的一种 tcpdump 的抓包软件点,甚是兴奋,来看读一遍源码时长确实没白消耗。然后__netif_receive_skb_core 取下 protocol,它是从数据文件中取下协议信息,随后赋值申请注册在这个协议里的调用函数目录。ptype_base 是一个 hash table,在协议申请注册小节大家讲过。ip_rcv 函数公式详细地址便是存在这个 hash table 里的。
//file:net/core/dev.c
static inline int deliver_skb(struct sk_buff *skb,
struct packet_type *pt_prev,
struct net_device *orig_dev){
return pt_prev-func(skb,skb-dev,pt_prev,orig_dev);
}
pt_prev->func 这一行就启用到协议层登记注册的处理函数了。针对 ip 包而言,便会进入 ip_rcv(假如是 arp 包得话,会进入 arp_rcv)。
3.4 IP 协议层解决
我们再大概看一下 linux 在 ip 协议层都干了什么,包又是怎样进一步送往 udp 或 tcp 协议处理函数里的。
//file:net/ipv4/ip_input.c
int ip_rcv(struct sk_buff *skb,struct net_device *dev,struct packet_type *pt,struct net_device *orig_dev){
return NF_HOOK(NFPROTO_IPV4,NF_INET_PRE_ROUTING,skb,dev,NULL,
ip_rcv_finish);
}
这儿 NF_HOOK 是一个钩子函数,当执行完登记注册的勾子之后就会实行到后来一个主要参数偏向的函数公式 ip_rcv_finish。
static int ip_rcv_finish(struct sk_buff *skb){
if(!skb_dst(skb)){
int err=ip_route_input_noref(skb,iph-daddr,iph-saddr,
iph-tos,skb-dev);
}
return dst_input(skb);
}
追踪 ip_route_input_noref 后看见它又获取了 ip_route_input_mc。在 ip_route_input_mc 中,函数公式 ip_local_deliver 被取值给 dst.input, 如下所示:
//file:net/ipv4/route.c
static int ip_route_input_mc(struct sk_buff *skb,__be32 daddr,__be32 saddr,u8 tos,struct net_device *dev,int our){
if(our){
rth-dst.input=ip_local_deliver;
rth-rt_flags |=RTCF_LOCAL;
}
}
因此返回 ip_rcv_finish 里的 return dst_input (skb);。
/* Input packet from network to transport.*/
static inline int dst_input(struct sk_buff *skb){
return skb_dst(skb)-input(skb);
}
skb_dst (skb)->input 启用的 input 办法就是路由器分系统赋的 ip_local_deliver。
//file:net/ipv4/ip_input.c
int ip_local_deliver(struct sk_buff *skb){
/* * Reassemble IP fragments.*/
if(ip_is_fragment(ip_hdr(skb))){
if(ip_defrag(skb,IP_DEFRAG_LOCAL_DELIVER))
return 0;
}
return NF_HOOK(NFPROTO_IPV4,NF_INET_LOCAL_IN,skb,skb->dev,NULL,
ip_local_deliver_finish);
}
static int ip_local_deliver_finish(struct sk_buff *skb){
……
int protocol=ip_hdr(skb)->protocol;
const struct net_protocol *ipprot;
ipprot=rcu_dereference(inet_protos[protocol]);
if(ipprot!=NULL){
ret=ipprot->handler(skb);
}
}
如协议申请注册小节见到 inet_protos 中保存着 tcp_rcv () 和 udp_rcv () 的函数公式详细地址。这儿将会依据库中的协议种类挑选开展派发,在这儿 skb 包将会进一步被派送至更最上层协议中,udp 和 tcp。
3.5 UDP 协议层解决
在协议申请注册小节的时候曾经说过,udp 协议的处理函数是 udp_rcv。
//file:net/ipv4/udp.c
int udp_rcv(struct sk_buff *skb){
return__udp4_lib_rcv(skb,&udp_table,IPPROTO_UDP);
}
int__udp4_lib_rcv(struct sk_buff *skb,struct udp_table *udptable,
int proto)
sk=__udp4_lib_lookup_skbskb,uh-source,uh-dest,udptable);
if(sk!=NULL)
int ret=udp_queue_rcv_skbsk,skb
}
icmp_send(skb,ICMP_DEST_UNREACH,ICMP_PORT_UNREACH,0);
}
__udp4_lib_lookup_skb 是依据 skb 来获取相对应的 socket,当寻找之后将数据文件放进 socket 的缓存文件序列里。假如找不到,则推送一个目标不能达的 icmp 包。
//file:net/ipv4/udp.c
int udp_queue_rcv_skb(struct sock *sk,struct sk_buff *skb){
if(sk_rcvqueues_full(sk,skb,sk-sk_rcvbuf))
goto drop;
rc=0;
ipv4_pktinfo_prepare(skb);
bh_lock_sock(sk);
if(!sock_owned_by_user(sk))
rc=__udp_queue_rcv_skb(sk,skb);
else if(sk_add_backlog(sk,skb,sk-sk_rcvbuf)){
bh_unlock_sock(sk);
goto drop;
}
bh_unlock_sock(sk);
return rc;
}
sock_owned_by_user 判断的是消费者是否正在这个 socker 中进行系统进程(socket 被占有),要是没有,那就能直接放进 socket 的接受序列中。若是有,那就可以通过 sk_add_backlog 把数据文件导入到 backlog 序列。当客户释放出来的 socket 时,核心会查验 backlog 序列,若是有数据信息再移到接受序列中。
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 socket 核心数据信息组织
socket 算法设计里的 const struct proto_ops 对应着协议的办法结合。每一个协议都是会完成不同类型的方式集,针对 IPv4 Internet 协议族而言,每一种协议都是有相匹配的处理方式,如下所示。针对 udp 而言,是由 inet_dgram_ops 来衡量的,在其中申请了 inet_recvmsg 方式。
//file:net/ipv4/af_inet.c
const struct proto_ops inet_stream_ops={
.recvmsg=inet_recvmsg,
.mmap=sock_no_mmap,
}
const struct proto_ops inet_dgram_ops={
.sendmsg=inet_sendmsg,
.recvmsg=inet_recvmsg,
}
socket 数据结构中的另一个数据结构 struct sock *sk 是一个特别大,至关重要的子结构体。这其中的 sk_prot 又界定了二级处理函数。针对 UDP 协议而言,能被设成 UDP 协议完成的办法集 udp_prot。
//file:net/ipv4/udp.c
struct proto udp_prot={
.name=\”UDP\”,
.owner=THIS_MODULE,
.close=udp_lib_close,
.connect=ip4_datagram_connect,
.sendmsg=udp_sendmsg,
.recvmsg=udp_recvmsg,
.sendpage=udp_sendpage,
}
看完了 socket 自变量以后,大家再来看看 sys_revvfrom 的完成过程。
在 inet_recvmsg 启用了 sk->sk_prot->recvmsg。
//file:net/ipv4/af_inet.c
int inet_recvmsg(struct kiocb *iocb,struct socket *sock,struct msghdr *msg,size_t size,int flags){
err=sk-sk_prot-recvmsg(iocb,sk,msg,size,flags&MSG_DONTWAIT,
flags&~MSG_DONTWAIT,&addr_len);
if(err=0)
msg-msg_namelen=addr_len;
return err;
}
上大家说过这个对于 udp 协议的 socket 而言,这一 sk_prot 便是 net / ipv4 / udp.c 中的 struct proto udp_prot。从而大家找到 udp_recvmsg 方式。
//file:net/core/datagram.c:EXPORT_SYMBOL(__skb_recv_datagram);
struct sk_buff *__skb_recv_datagram(struct sock *sk,unsigned int flags,int *peeked,int *off,int *err){
……
do{
struct sk_buff_head *queue=&sk->sk_receive_queue;
skb_queue_walk(queue,skb){
……
}
/* User doesn\”t want to wait */
error=-EAGAIN;
if(!timeo)
goto no_packet;
}while(!wait_for_more_packets(sk,err,&timeo,last));
}
终于我们找到我们要想看的英语关键,在墙上我看到了所谓载入过程,便是浏览 sk->sk_receive_queue。假如没有数据,且客户也容许等候,则把启用 wait_for_more_packets () 实行等候实际操作,它添加会让消费者过程进到休眠状态。
五、汇总
网络接口是 Linux 核心中最复杂的板块了,看上去一个很简单的收包过程就涉及很多核心部件间的互动,如无线驱动、协议栈,核心 ksoftirqd 进程等。看上去很繁杂,文中想要通过图例的形式,最好以容易接受的方式去将核心收包过程讲明白。目前使我们再串一串全部收包过程。
当客户执行完 recvfrom 启用后,客户过程就可以通过系统进程进行到内核态上班了。假如接受序列没有数据,过程就进入了休眠状态被电脑操作系统挂起来。这方面相对性非常简单,剩余绝大多数的戏份均是由 Linux 核心其他控制模块来表演了。
最先在进行收包以前,Linux 需做很多准备工作:
1. 建立 ksoftirqd 进程,给它设定好它自已的进程函数公式,后边指望着它去处理软中断呢
2. 协议栈申请注册,linux 想要实现很多协议,例如 arp,icmp,ip,udp,tcp,每一个协议都会把自已的处理函数申请注册一下,便捷包来啦快速寻找相对应的处理函数
3. 无线驱动复位,每一个推动都有一个复位函数公式,核心会使推动也复位一下。在这样一个复位过程中,将自己的 DMA 备好,把 NAPI 的 poll 函数公式详细地址告知核心
4. 运行网口,分派 RX,TX 序列,申请注册终断相对应的处理函数
以上就是核心提前准备收包以前的重要任务,当上边都 ready 以后,就能打开硬终断,等候数据文件的来临了。
当数据信息来临了之后,第一个迎来它是网口(我,这个不是空话么):
1. 网口将数据帧 DMA 到内存条的 RingBuffer 中,随后向 CPU 进行终断通告
2. CPU 回应中断请求,启用网卡启动时登记注册的终断处理函数
3. 终断处理函数基本上没干啥,就发起软中断请求
4. 内核线程 ksoftirqd 进程发现软中断请求来临,先关掉硬终断
5. ksoftirqd 进程逐渐启用驱动 poll 函数公式收包
6. poll 函数公式将收到包送至协议栈登记注册的 ip_rcv 函数中
7. ip_rcv 函数公式再说包送至 udp_rcv 函数中(针对 tcp 包就送至 tcp_rcv)
我们现在能够返回开场问题了,大家在用户层见到简单地一行 recvfrom,Linux 核心要替我们做如此之多工作,才能让自己成功接到数据信息。这已经是很简单的 UDP,假如是 TCP,核心要做的事情大量,不由自主感慨核心的开发商们可以说是良苦用心。
明白了全部收包过程之后,我们就可以确立了解 Linux 收一个包的 CPU 花销了。最先第一块是消费者过程启用系统进程深陷内核态的花销。第二块是 CPU 回应包的硬终断的 CPU 花销。第三块是 ksoftirqd 内核线程的软中断前后文投入的。后边咱们就专业发一篇文章具体观察一下这种花销。
此外互联网接收中有许多末支关键点我们并没进行了说,例如 no NAPI,GRO,RPS 等。因为我知道说的太对了反倒会危害大家对于整个过程的掌握,所以最好只留主架构了,少就是多!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。