第三步产生了一个中断告诉 CPU 并开始处理中断。重点的中断处理可分为两步:一是将中断信号屏蔽了,二是唤醒 NAPI 机制。
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 IRO_HANDLED; }上面的代码是 igb 网卡驱动中断处理函数做的事情。如果省略掉开始的变量声明和后面的返回,这个中断处理函数只有两行代码,非常短。需要关注的是第二个,在硬件中断处理函数中,只用激活外部 NIPA 软中断处理机制,无需做其他任何事情。因此这个中断处理函数会返回的非常快。
NIPI 激活
/* 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); }NIPI 的激活也很简单,主要为两步。内核网络系统在初始化的时每个 CPU 都会有一个结构体,它会把队列对应的信息插入到结构体的链表里。换句话说,每个网卡队列在收到数据的时候,需要把自己的队列信息告诉对应的 CPU,将这两个信息绑定起来,保证某个 CPU 处理某个队列。
除此之外,还要与触发硬中断一样,需要触发软中断。下图将很多步骤放到了一块,前面讲过的就不再赘述了。图中要关注的是软中断是怎么触发的。与硬中断差不多,软中断也有中断的向量表。每个中断号,都会对应一个处理函数,当需要处理某个中断,只需要在对应的中断向量表里找就好了,跟硬中断的处理是一模一样的。
数据接收-监控
说完了运作机制,再来看看有哪些地方可以做监控。在 proc 下面有很多东西,可以看到中断的处理情况。第一列就是中断号,每个设备都有独立的中断号,这是写死的。对网络来说只需要关注网卡对应的中断号,图中是 65、66、67、68 等。当然看实际的数字并没有意义,而是需要看它的分布情况,中断是不是被不同 CPU 在处理,如果所有的中断都是被一个 CPU 处理,那么就需要做些调整,把它分散开。
数据接收-调优
中断可以做的调整有两个:一是中断合并,二是中断亲和性。
自适应中断合并
rx-usecs: 数据帧到达后,延迟多长时间产生中断信号,单位微秒
rx-frames: 触发中断前积累数据帧的最大个数
rx-usecs-irq: 如果有中断处理正在执行,当前中断延迟多久送达 CPU
rx-frames-irq: 如果有中断处理正在执行,最多积累多少个数据帧
上面列的都是硬件网卡支持的功能。NAPI 本质上也是中断合并的机制,假如有很多包的到来,NAPI 就可以做到只产生一个中断,因此不需要硬件来帮助做中断合并,实际效果是跟 NAPI 是相同的,都是减少了总的中断数量。
中断亲和性
$ sudo bash -c ‘echo 1 > /proc/irq/8/smp_affinity’这个与网卡多队列是密切相关的。如果网卡有多个队列,就能手动来明确指定由哪个 CPU 来处理,均衡的把数据处理的负载分散到机器的可用 CPU 上。配置也比较简单,只需把数字写入到 /proc 对应的这个文件中就可以了。这是个位数组,转成二进制后就会有对应的 CPU 去处理。如果写个 1,可能就是 CPU0 来处理;如果写个 4,转化成二进制是 100,那么就会交给 CPU2 去处理。