图解Linux网络包接收过程( 二 )


2.1 创建ksoftirqd内核线程Linux的软中断都是在专门的内核线程(ksoftirqd)中进行的,因此我们非常有必要看一下这些进程是怎么初始化的,这样我们才能在后面更准确地了解收包过程 。该进程数量不是1个,而是N个,其中N等于你的机器的核数 。
系统初始化的时候在kernel/smpboot.c中调用了smpboot_register_percpu_thread,该函数进一步会执行到spawn_ksoftirqd(位于kernel/softirq.c)来创建出softirqd进程 。

图解Linux网络包接收过程

文章插图
 
图3 创建ksoftirqd内核线程
相关代码如下:
//file: kernel/softirq.cstatic 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【图解Linux网络包接收过程】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网络包接收过程

文章插图
 
图4 网络子系统初始化
linux内核通过调用subsys_initcall来初始化各个子系统,在源代码目录里你可以grep出许多对这个函数的调用 。这里我们要说的是网络子系统的初始化,会执行到net_dev_init函数 。
//file: net/core/dev.cstatic 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.cvoid 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数据结构中了 。如下图:
图解Linux网络包接收过程

文章插图
 
图5 AF_INET协议栈注册
相关代码如下
//file: net/ipv4/af_inet.cstatic 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 protocoln", __func__);    if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)        pr_crit("%s: Cannot add UDP protocoln", __func__);    if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)        pr_crit("%s: Cannot add TCP protocoln", __func__);    ......    dev_add_pack(&ip_packet_type);}


推荐阅读