总览
Linux实现的是链路层、网络层和传输层这三层。
在Linux内核实现中,链路层协议靠网卡驱动来实现
内核协议栈来实现网络层和传输层
在Linux的源代码中,网络设备驱动对应的逻辑位于driver/net/ethernet
intel系列网卡的驱动在driver/net/ethernet/intel目录下。协议栈模块代码位于kernel和net目录。
内核和网络设备驱动是通过中断的方式来处理的。当设备上有数据到达的时候,会给CPU的相关引脚上触发一个电压变化,以通知CPU来处理数据
对于网络模块来说,由于处理过程比较复杂和耗时,如果在中断函数中完成所有的处理,将会导致中断处理函数(优先级过高)将过度占据CPU,将导致CPU无法响应其它设备,例如鼠标和键盘的消息。
因此Linux中断处理函数是分上半部和下半部的。上半部是只进行最简单的工作,快速处理然后释放CPU,接着CPU就可以允许其它中断进来。剩下将绝大部分的工作都放到下半部中,可以慢慢从容处理
2.4以后的内核版本采用的下半部实现方式是软中断,由ksoftirqd内核线程全权处理。和硬中断不同的是,硬中断是通过给CPU物理引脚施加电压变化,而软中断是通过给内存中的一个变量的二进制值以通知软中断处理程序。
当网卡上收到数据以后
Linux中第一个工作的模块是网络驱动
网络驱动会以DMA的方式把网卡上收到的帧写到内存里。再向CPU发起一个中断,以通知CPU有数据到达。
第二,当CPU收到中断请求后会去调用网络驱动注册的中断处理函数,网卡的中断处理函数并不做过多工作,发出软中断请求,然后尽快释放CPU。ksoftirqd检测到有软中断请求到达,调用poll开始轮询收包,收到后交由各级协议栈处理。对于UDP包来说,会被放到用户socket的接收队列中。
更多网络模块工作的细节
Linux驱动,内核协议栈等等模块在具备接收网卡数据包之前,要做很多的准备工作才行。比如要
提前创建好ksoftirqd内核线程,
要注册好各个协议对应的处理函数,
网络设备子系统要提前初始化好,
网卡要启动好。
只有这些都Ready之后,我们才能真正开始接收数据包。那么我们现在来看看这些准备工作都是怎么做的。
创建ksoftirqd内核线程
Linux的软中断都是在专门的内核线程(ksoftirqd)中进行的,因此我们非常有必要看一下这些进程是怎么初始化的,这样我们才能在后面更准确地了解收包过程。该进程数量不是1个,而是N个,其中N等于你的机器的核数。
系统初始化的时候在kernel/smpboot.c中调用了smpboot_register_percpu_thread, 该函数进一步会执行到spawn_ksoftirqd(位于kernel/softirq.c)来创建出softirqd进程。
//Linux softirq全分析 https://blog.csdn.net/matrix2live/article/details/21296161
//软中断(本校学长博客)https://blog.csdn.net/qq_34258344/article/details/108202096
//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
分析 https://blog.csdn.net/dijuan6625/article/details/102004224
软中断不仅仅只有网络软中断,还有其它类型。
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,};
网络子系统初始化
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);//申请softnet_data数据结构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);//等待驱动程序将poll函数注册进来......}......open_softirq(NET_TX_SOFTIRQ, net_tx_action);//每一个软中断都注册一个处理函数open_softirq(NET_RX_SOFTIRQ, net_rx_action);}subsys_initcall(net_dev_init);
另外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;}
协议栈注册
内核实现了网络层的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.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 = {//udp.handler = udp_rcv, //udp.handler.err_handler = udp_err,.no_policy = 1,.netns_ok = 1,};static const struct net_protocol tcp_protocol = {//tcp.early_demux = tcp_v4_early_demux, //tcp.handler.handler = tcp_v4_rcv, //tcp.handler.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;}
