Bootstrap

深入理解Linux网络(一):内核如何接收网络包

一、网络收包总览

在这里插入图片描述
网卡本身就是一个单片机装载了射频收发模块,任务就是不停地收发无线电波并解析为基带数据传输到总线,内核有通知就会去接收。

⽹络驱动会以 DMA 的⽅式把⽹卡上收到的帧写到内存⾥。再向 CPU 发起⼀个中断,以通知 CPU 有数据到达。当 CPU 收到中断请求后,会去调⽤⽹络驱动注册的中断处理函数。
⽹卡的中断处理函数发出软中断请求,并释放 CPU。
ksoftirqd 检测到有软中断请求到达,调⽤ poll 开始轮询收包,收到后交由各级协议栈处理。对于 udp 包来说,会被放到⽤户socket 的接收队列中。
在这里插入图片描述

二、Linux启动

内核在接收⽹卡数据包之前,需要提前创建好ksoftirqd内核线程,注册好各个协议对应的处理函数,初始化⽹卡设备⼦系统。

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, /* Preferable RCU should always be the last softirq */
 NR_SOFTIRQS
};

2、网络子系统初始化

在这里插入图片描述
linux 内核通过调⽤ subsys_initcall 来初始化各个⼦系统,其中就有⽹络⼦系统
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 函数注册进来。
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;
}

3、协议栈注册

IP协议的实现函数是 ip_rcv()
TCP协议的实现函数是 tcp_rcv()
UDP协议的实现函数是 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 这个函数的代码

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 下。主要执⾏的操作如下:
在这里插入图片描述
igb ⽹卡驱动实现了 ethtool 所需要的接⼝,也在这⾥注册完成函数地址的注册。
当 ethtool 发起⼀个系统调⽤之后,内核会找到对应操作的回调函数。对于 igb ⽹卡来说,其实现函数都在 drivers/net/ethernet/intel/igb/igb_ethtool.c 下。网卡驱动中提供了能让 ethtool 查看⽹卡收发包统计、能修改⽹卡⾃适应模式、能调整 RX 队列的数量和⼤⼩等功能的接口。

第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,......

NAPI

第 7 步中,在 igb_probe 初始化过程中,还调⽤到了 igb_alloc_q_vector 。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);
}

5、启动网卡

之前⾯⽹卡驱动初始化时,我们提到了驱动向内核注册了 structure net_device_ops 变量,它包含着⽹卡启⽤、发包、设置 mac 地址等回调函数(函数指针)。当启⽤⼀个⽹卡时(例如,通过 ifconfig eth0 up),net_device_ops 中的 igb_open ⽅法会被调⽤。它通常会做以下事情:
在这里插入图片描述
NAPI 的机制非常重要!!!

//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 < adapter->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_irq(struct 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_irq(adapter->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 的绑定⾏为)。
当做好以上准备⼯作后金额可以接收数据包了!

三、接收数据

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_pending(1UL << nr);
}

Linux 在硬中断⾥只完成简单必要的⼯作,剩下的⼤部分的处理都是转交给软中断的。通过上⾯代码可以看到,硬中断处理过程真的是⾮常短。只是记录了⼀个寄存器,修改了⼀下下 CPU 的 poll_list,然后发出个软中断。就这么简单,硬中断⼯作就算是完成了。

2、ksoftirqd 内核线程处理软中断

在这里插入图片描述
内核线程初始化的时候,我们介绍了 ksoftirqd 中两个线程函数 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);
}

这⾥需要注意⼀个细节,硬中断中设置软中断标记,和 ksoftirq 的判断是否有软中断到达,都是基于 smp_processor_id() 的。这意味着只要硬中断在哪个 CPU 上被响应,那么软中断也是在这个 CPU 上处理的。所以说,如果你发现你的 Linux 软中断 CPU 消耗都集中在⼀个核上的话,做法是要把调整硬中断的 CPU 亲和性,来将硬中断打散到不同的 CPU 核上去。
我们再来把精⼒集中到这个核⼼函数 net_rx_action 上来。

//file:net/core/dev.c
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;net_rx_action
 }
}

注意:在硬中断中添加设备到 poll_list,会不会重复添加呢?答案是不会的,在软中断处理函数 net_rx_action 这⾥⼀进来就调⽤ local_irq_disable 把所有的硬中断都给关了,不会让硬中断重复添加 poll_list 的机会。在硬中断的处理函数中本身也有类似的判断机制,打磨了⼏⼗年的内核考虑在细节考虑上还是很完善的。

函数开头的 time_limit 和 budget 是⽤来控制 net_rx_action 函数主动退出的,⽬的是保证⽹络包的接收不霸占 CPU 不放。 等下次⽹卡再有硬中断过来的时候再处理剩下的接收数据包。其中 budget 可以通过内核参数调整。 这个函数中剩下的核⼼逻辑是获取到当前 CPU 变量 softnet_data,对其 poll_list 进⾏遍历, 然后执⾏到⽹卡驱动注册到的 poll 函数。对于 igb ⽹卡来说,就是 igb 驱动⾥的 igb_poll 函数了。

/**
* igb_poll - NAPI Rx polling callback
* @napi: napi polling structure
* @budget: count of how many packets we should handle
**/
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 上取下来。
为什么需要两个函数呢?因为有可能帧要占多个 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 中,数据包将被送到协议栈中。

3、网络协议栈处理

netif_receive_skb 函数会根据包的协议,假如是 udp 包,会将包依次送到 ip_rcv(),udp_rcv() 协议处理函数中进⾏处理。
在这里插入图片描述

//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)。

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_v4_rcv() 和 udp_rcv() 的函数地址。这⾥将会根据包中的协议类型选择进⾏分发,在这⾥ skb 包将会进⼀步被派送到更上层的协议中,udp 和 tcp。

整理回顾:

  1. ⽹卡将数据帧 DMA 到内存的 RingBuffer 中,然后向 CPU 发起中断通知
  2. CPU 响应中断请求,调⽤⽹卡启动时注册的中断处理函数
  3. 中断处理函数⼏乎没⼲啥,就发起了软中断请求
  4. 内核线程 ksoftirqd 线程发现有软中断请求到来,先关闭硬中断
  5. ksoftirqd 线程开始调⽤驱动的 poll 函数收包
  6. poll 函数将收到的包送到协议栈注册的 ip_rcv 函数中
  7. ip_rcv 函数再将包送到 udp_rcv 函数中(对于 tcp 包就送到 tcp_rcv )

最后,再次推荐一下飞哥的《深入理解Linux网络–修炼底层内功》,希望大家有所收获。

;