那么该怎么做呢?我们可以通过前面提到的工具,特意把 80 端口 tcp 数据流量导向到对应 CPU2 处理的网卡队列。这么做的效果是数据包从到达网卡开始,到内核处理完再到送达应用层,都是同一个 CPU。这样最大的好处就是缓存,CPU 的 cache 始终是热的,如此整体下来,它的延迟、效果也会非常好。当然这个例子并不实际,主要是为了说明能做到的一个效果。
内核网络子系统说完了整个网卡驱动部分,接下来是讲解内核子系统部分,这块会分为软中断与网络子系统初始化两部分来分享。
软中断
上图的 NETDEV 是 linux 网络子系统每年都会开的一个分会,其中比较有意思的点是每年大会举办的届数会以一个特殊字符来表示。图中是办到了 0X15 届,想必也都发现这是 16 进制的数字,0X15 刚好就是 21 年,也是比较极客范。对网络子系统感兴趣的可以去关注一下。
言归正传,内核延时任务有多种机制,而软中断只是其中一种。上图是 linux 的基本结构,上层是用户态,中间是内核,下层是硬件,很抽象的一个分层。用户态和内核态之间会有两种交互的方式:通过系统调用,或者通过异常可以陷入到内核态里面。那底层的硬件跟内核又是怎么交互的呢?答案是中断,硬件跟内核交互的时候必须通过中断,处理任何事件都需要产生一个中断信号来告知 CPU 与内核。
不过这样的机制一般情况下也许没有问题,但是对网络数据来说,一个数据报一个中断,这样会有很明显的两个问题。
问题一:中断在处理期间,会屏蔽之前的中断信号。当一个中断处理的时间很长,在处理期间收到的中断信号都会丢掉。 如果处理一个包用了十秒,在这十秒期间又收到了五个数据包,但因为中断信号丢了,即便前面的处理完了,后面的数据包也不会再处理了。对应到 tcp 这边,假如客户端给服务端发了一个数据包,几秒后处理完了,但在处理期间客户端又发了后续的三个包,但是服务端后面并不知道,以为只收到了一个包,这时客户端又在等待服务端的回包,如此会导致两边都卡住了,也说明了信号丢失是一个极其严重的问题。
问题二:一个数据包触发一次中断处理的话,当有大量的数据包到来后,就会产生非常大量的中断。 如果达到了 10 万、50 万、甚至百万的 pps,那 CPU 就需要处理大量的网络中断,也就不用干其他事情了。
而针对以上两点问题的解决方法就是让中断处理尽可能的短。 具体来说,不能在中断处理函数,只能把它揪出来,交到软中断机制里。这样之后的实际结果是硬件的中断处理做的事情就很少了,将接收数据等一些必须的事情交到软中断去完成,这也是软中断存在的意义。
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) { regiter_cpu_notifier(&cpu_nfb); BUG_ON(smpboot_register_percpu_thread(&softirq_threads)); return 0; } early_initcall(spawn_ksoftirqd);软中断机制是通过内核的线程来实现的。图中是对应的一个内核线程。服务器 CPU 都会有一个 ksoftirqd 这样的内核线程,多 CPU 的机器会相对应的有多个线程。图中结构体最后一个成员 ksoftirqd/,如果有三个 CPU 对应就会有 /0/1/2 三个内核线程。
软中断机制的信息在 softirqs 下面可以看到。软中断并不多只有几种,其中需要关注的,跟网络相关的就是 NET-TX 和 NET-RX,网络数据收发的两种场景。
内核初始化
铺垫完软中断之后,下面来看内核初始化的流程。主要为两步:
针对每个 CPU,创建一个数据结构,这上面挂了非常多的成员,与后面的处理密切相关;
注册一个软中断处理函数,对应上面看到的 NET-TX 和 NET-RX 这两个软中断的处理函数。
上图是手绘的一个数据包的处理流程:
第一步网卡收到了数据包;
第二步把数据包通过 DMA 拷到了内存里面;