注: 内核代码是 4.9 版本
协议栈从报文接收说起,报文接收从网卡驱动说起。
两种方式,NAPI 和 非NAPI。
NAPI(New API) 是Linux内核针对网络数据传输做出的一个优化措施。
其目的是在大量数据传输时, 在收到硬件中断后,通过poll方式将传输过来的数据包统一处理, 通过禁止网络设备中断以减少硬件中断数量((Interrupt Mitigation),从而实现更高的数据传输。
其中要点:
1、硬件中断后开始处理报文。中断处理函数只是触发软中断准备接收报文;
2、软中断中通过pool方式处理报文。通过轮训的方式一次性处理多个报文;
3、禁止网络设备中断以减少硬件中断数量。同上,在软中断处理函数中,将禁止中断,处理完成后,开启中断,这样一次中断处理多个报文。
##先从NAPI方式说起
以 Inter 的 e1000 的驱动为例,e1000在加载驱动并做设备初始化时会调用 e1000_probe 函数,完成设备的部分初始化工作,重要的是设备的napi结构 和 poll函数是在这个函数中设置的:
static int e1000_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
struct net_device *netdev;
struct e1000_adapter *adapter;
struct e1000_hw *hw;
......
// 为网卡创建网络设备对象 net_device 结构,并完成组册
netdev = alloc_etherdev(sizeof(struct e1000_adapter));
if (!netdev)
goto err_alloc_etherdev;
SET_NETDEV_DEV(netdev, &pdev->dev);
// 设置网卡私有数据
pci_set_drvdata(pdev, netdev);
adapter = netdev_priv(netdev);
adapter->netdev = netdev;
adapter->pdev = pdev;
adapter->msg_enable = netif_msg_init(debug, DEFAULT_MSG_ENABLE);
adapter->bars = bars;
adapter->need_ioport = need_ioport;
hw = &adapter->hw;
hw->back = adapter;
err = -EIO;
// 映射寄存器IO区域(后面拷贝报文)
hw->hw_addr = pci_ioremap_bar(pdev, BAR_0);
if (!hw->hw_addr)
goto err_ioremap;
......
// 挂载网络设备操作接口
netdev->netdev_ops = &e1000_netdev_ops;
e1000_set_ethtool_ops(netdev);
netdev->watchdog_timeo = 5 * HZ;
// 初始化并挂载设备的NAPI接口,e1000_clean 是其poll函数,软中断中调用处理报文
// adapter->napi 挂到 netdev->napi_list
netif_napi_add(netdev, &adapter->napi, e1000_clean, 64);
strncpy(netdev->name, pci_name(pdev), sizeof(netdev->name) - 1);
......
}
还有个重要的函数是e1000的网卡中断处理函数,其是在上面 e1000_netdev_ops 中的ndo_open函数(网卡UP后会调用)中,调用的 e1000_request_irq注册的 e1000_intr 函数。
然后是NAPI的接收流程。
在网卡中断之前,数据包到达网卡之后,就通过DMA直接将数据从网卡拷贝到内存的环形缓冲区了,成为 ring buffer,和非NAPI不同。
中断处理函数,如果没有在运行的NAPI任务,调度一个新的NAPI任务,会调用通用的NAPI处理函数,__napi_schedule,将设备的napi 挂载到当前CPU的 softnet_data 的待轮训设备列表poll_list中,并触发软中断。
napi_schedule_prep 中会检查并设置 napi_struct的 NAPI_STATE_SCHED位,并在流程结束后(软中断处理完报文)调用 __napi_complete,清楚状态位。
static irqreturn_t e1000_intr(int irq, void *data)
{
struct net_device *netdev = data;
struct e1000_adapter *adapter = netdev_priv(netdev);
struct e1000_hw *hw = &adapter->hw;
......
// 如果没有在运行的NAPI任务,调度一个新的NAPI任务
if (likely(napi_schedule_prep(&adapter->napi))) {
adapter->total_tx_bytes = 0;
adapter->total_tx_packets = 0;
adapter->total_rx_bytes = 0;
adapter->total_rx_packets = 0;
// 调用通用的NAPI处理函数
__napi_schedule(&adapter->napi);
} else {
/* this really should not happen! if it does it is basically a
* bug, but not a hard error, so enable ints and continue
*/
if (!test_bit(__E1000_DOWN, &adapter->flags))
e1000_irq_enable(adapter);
}
return IRQ_HANDLED;
}
void __napi_schedule(struct napi_struct *n)
{
unsigned long flags;
local_irq_save(flags);
____napi_schedule(this_cpu_ptr(&softnet_data), n);
local_irq_restore(flags);
}
static inline void ____napi_schedule(struct softnet_data *sd,
struct napi_struct *napi)
{
// 将设备的napi 挂载带了 sd 的poll_list中。
list_add_tail(&napi->poll_list, &sd->poll_list);
// 触发软中断
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
软中断接收处理函数是net_rx_action,在网络设备模块初始化时注册。
net_dev_init:
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
内核ksoftirqd%d(%d对应CPU的ID)内核线程用于处理CPU上的软中断。内核在初始化的时候,会在每个CPU上启动一个这样的内核线程用来处理每个CPU上的软中断。
最终会调用到上面注册的收发报的软中断处理函数。
// 注册
static struct smp_hotplug_thread softirq_threads = {
.store = &ksoftirqd,
.thread_should_run = ksoftirqd_should_run,
.thread_fn = run_ksoftirqd,
.thread_comm = "ksoftirqd/%u&