再回头看一下硬中断触发软中断的源码。
//file: drivers/net/ethernet/intel/igb/igb_main.c
static inline void ____napi_schedule(...){
list_add_tail(&napi->poll_list, &sd->poll_list);
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
这里有个很有意思的细节,无论硬中断是因为是有数据要接收,还是说发送完成通知,从硬中断触发的软中断都是 NET_RX_SOFTIRQ。这个我们在第一节说过了,这是软中断统计中 RX 要高于 TX 的一个原因。
好我们接着进入软中断的回调函数 igb_poll。在这个函数里,我们注意到有一行 igb_clean_tx_irq,参见源码:
//file: drivers/net/ethernet/intel/igb/igb_main.c
static int igb_poll(struct napi_struct *napi, int budget)
{
//performs the transmit completion operations
if (q_vector->tx.ring)
clean_complete = igb_clean_tx_irq(q_vector);
...
}
我们来看看当传输完成的时候,igb_clean_tx_irq 都干啥了。
//file: drivers/net/ethernet/intel/igb/igb_main.c
static bool igb_clean_tx_irq(struct igb_q_vector *q_vector)
{
//free the skb
dev_kfree_skb_any(tx_buffer->skb);
//clear tx_buffer data
tx_buffer->skb = NULL;
dma_unmap_len_set(tx_buffer, len, 0);
// clear last DMA location and unmap remaining buffers */
while (tx_desc != eop_desc) {
}
}
无非就是清理了 skb,解除了 DMA 映射等等。到了这一步,传输才算是基本完成了。
为啥我说是基本完成,而不是全部完成了呢?因为传输层需要保证可靠性,所以 skb 其实还没有删除。它得等收到对方的 ACK 之后才会真正删除,那个时候才算是彻底的发送完毕。
最后
用一张图总结一下整个发送过程
![](http://imgq8.q578.com/ef/0602/91ed9d33b9ba9cd1.jpg)
了解了整个发送过程以后,我们回头再来回顾开篇提到的几个问题。
1.我们在监控内核发送数据消耗的 CPU 时,是应该看 sy 还是 si ?
在网络包的发送过程中,用户进程(在内核态)完成了绝大部分的工作,甚至连调用驱动的事情都干了。只有当内核态进程被切走前才会发起软中断。发送过程中,绝大部分(90%)以上的开销都是在用户进程内核态消耗掉的。只
有一少部分情况下才会触发软中断(NET_TX 类型),由软中断 ksoftirqd 内核进程来发送。
所以,在监控网络 IO 对服务器造成的 CPU 开销的时候,不能仅仅只看 si,而是应该把 si、sy 都考虑进来。
2. 在服务器上查看 /proc/softirqs,为什么 NET_RX 要比 NET_TX 大的多的多?
之前我认为 NET_RX 是读取,NET_TX 是传输。对于一个既收取用户请求,又给用户返回的 Server 来说。这两块的数字应该差不多才对,至少不会有数量级的差异。但事实上,飞哥手头的一台服务器是这样的:
![](http://imgq8.q578.com/ef/0602/41525ce7947f635c.jpg)
经过今天的源码分析,发现这个问题的原因有两个。
第一个原因是当数据发送完成以后,通过硬中断的方式来通知驱动发送完毕。但是硬中断无论是有数据接收,还是对于发送完毕,触发的软中断都是 NET_RX_SOFTIRQ,而并不是 NET_TX_SOFTIRQ。
第二个原因是对于读来说,都是要经过 NET_RX 软中断的,都走 ksoftirqd 内核进程。而对于发送来说,绝大部分工作都是在用户进程内核态处理了,只有系统态配额用尽才会发出 NET_TX,让软中断上。
综上两个原因,那么在机器上查看 NET_RX 比 NET_TX 大的多就不难理解了。
3.发送网络数据的时候都涉及到哪些内存拷贝操作?
这里的内存拷贝,我们只特指待发送数据的内存拷贝。