napi —— linux 网卡驱动收包机制

linux 操作系统一般指 linux 内核。在 linux 上开发应用的时候,可以使用 linux 提供的系统调用。linux 内核管理着机器上的硬件资源:内存,磁盘,网卡等。开发应用的时候不能直接操作这些硬件,而只能通过系统调用来使用这些资源。linux 系统调用可以说是 linux 内核提供给上层的一些接口。这种内核和用户隔离的机制保证了安全性的同时,也简化了应用的开发。

linux 内核除了给上层应用提供了接口之外,给底层的硬件驱动也提供了框架和机制。在 linux 中开发驱动的时候,硬件驱动也可以看做是 linux 内核的应用层,不过和用户态应用相比,硬件驱动工作在内核态。

linux 内核的用户有两个部分,一个是用户态的程序,一个是底层的硬件驱动。

1 基础收包方式

在讨论 io 模型时,经常讨论阻塞,非阻塞,同步,异步的 io 模型。在网卡收包方面,有两种基础的收包方式,分别是中断和轮询。中断和轮询的工作机制与 io 模型是有些类似的,中断是异步的,轮询是同步非阻塞的的。

1.1 中断

中断方式是软件和硬件交互的基本方式。在 linux 中,中断的优先级是最高的,高于线程也高于软中断。

中断的优点是实时性好(谁都可以打断),适用于网络流量不是很大的场景。如果网络流量很大的话,那么 cpu 就会一直陷入网络报文的处理中,其它中断和其它任务得不到机会运行。

当内核线程被中断打断时,调用中断服务例程之前还要保存当前线程的寄存器和栈信息。如果中断太多的话会造成频繁的线程上下文和中断之间的切换,上下文切换也会造成 cpu 的浪费。

1.2 轮询

中断方式是硬件主动通知软件,轮询方式是软件主动查询硬件。轮询收包的方式,一般会有一个线程,这个线程中是一个 while(1) 死循环,在死循环中会通过读网卡的寄存器来判断当前接收队列中有没有包,如果有包的话便会从接收队列中接收报文。

使用轮询方式收包,不会有线程和中断之间的上下文切换。轮询方式适用于网络流量比较大的场景,因为轮询方式会占满一个 cpu,如果网络流量很小的话,那么线程会有很多空转的情况,造成 cpu 资源的浪费。一些网络专用设备,比如路由器,核心工作就是转发报文,不像服务器上会跑很多业务,路由器的功能比较单一, 所以在路由器上使用轮询的方式,即使一直占着 cpu,也不会影响其它应用,因为路由器上其它的应用很少。DPDK 中使用了轮询的方式来收包。

2 napi

napi 的名字全称是 new api,一个新的 api,这样的名字并不是很直观。在软件开发中起名字是很困难的事情,从 napi 也可以看出来,即使在 linux 中,也有这样不是很直观的名字存在。

从上边的分析中也可以看出来,中断方式收包和轮询方式收包,各有优缺点。中断方式收包的优点是响应快,缺点是只能适用于流量较小的场景,如果在流量较大的场景下使用中断方式收包,网络中断会影响系统其它任务的执行。轮询方式收包的优点是不会有线程和中断之间的上下文切换,缺点是当流量较小的时候,会造成 cpu 资源的浪费。

所以中断收包适用于流量较小的场景,轮询适用于流量较大的场景。

napi 收包方式中既有中断,也有轮询,集成了中断和轮询的优点。

本文中以 ixgbe 网卡为例进行记录。

2.1 napi 框架

软中断收包:

使用 napi 来接收报文是在软中断中处理的。硬中断服务例程中只做很少量的工作,做完之后硬中断立即返回,报文后续的处理在软中断重处理。

(1)硬中断会调用函数 napi_schedule_irqoff(),最终会调用到函数 ____napi_schedule(),在该函数中做两件事

cpp 复制代码
/* 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);
}

① 将 napi 加到数据结构 struct softnet_data 中,struct softnet_data 是一个全局的数据结构,是 per cpu 类型的。在软中断中处理的时候便会从 struct softnet_data 中找到挂接的 napi,从 napi 找到处理函数,然后进行收包。

② 触发 NET_RX_SOFTIRQ 软中断

struct napi_struct:

cpp 复制代码
/*
 * Structure for NAPI scheduling similar to tasklet but with weighting
 */
struct napi_struct {
	/* The poll_list must only be managed by the entity which
	 * changes the state of the NAPI_STATE_SCHED bit.  This means
	 * whoever atomically sets that bit can add this napi_struct
	 * to the per-CPU poll_list, and whoever clears that bit
	 * can remove from the list right before clearing the bit.
	 */
	struct list_head	poll_list;

	unsigned long		state;
    ...
    int			(*poll)(struct napi_struct *, int); NAPI_STATE_SCHED
	int			weight;
	...
};

struct napi_struct 中重要的成员有 4 个。

|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| weight | napi 一次可以处理的报文个数,在软中断中收包是轮询的方式。如果网络流量很大,那么会一直轮询收包吗 ? 不会的,为了网络不影响系统中其它任务的执行,napi 每次接收报文的数量是有上限的,当接收的的报文数量达到上限时,即使网卡接收队列中有报文,也不再继续处理了,而是让出 cpu,等下次软中断处理时再进程接收。 保证了收包任务和系统其它任务的公平性。 |
| poll | 函数指针,使用 napi 的网卡将网卡的收包函数挂到这个指针上。 |
| state | napi 的状态, NAPI_STATE_SCHED 状态说明 napi 是可以工作的。 |
| poll_list | 上边也说了,napi 通过 poll_list 与 struct softnet_data 进行关联。 |

netif_napi_add():

网卡驱动中会调用这个函数对网卡使用 napi 进行初始化。包括挂接 poll 函数,初始化 weight。

cpp 复制代码
void netif_napi_add(struct net_device *dev, struct napi_struct *napi,

            int (*poll)(struct napi_struct *, int), int weight)

2.2 网卡驱动使用 napi

struct ixgbe_q_vector 可以看作网卡的一个接收队列。在 struct ixgbe_q_vector 中有一个成员 struct napi_struct napi。

cpp 复制代码
struct ixgbe_q_vector {
    ...
	struct napi_struct napi;
    ...
};

在初始化中断时,中断的参数是 struct ixgbe_q_vector * 指针类型,所以在中断处理函数中能通过 q_vector 找到 napi,从而可以将 napi 挂接到 struct softnet_data 中,进而被软中断处理。

cpp 复制代码
static int ixgbe_request_msix_irqs(struct ixgbe_adapter *adapter)
{
    ...
    err = request_irq(entry->vector, &ixgbe_msix_clean_rings, 0,
                q_vector->name, q_vector);
    ...
    return err;
}

从下边的代码中可以看出来,ixgbe 网卡的 napi 收包函数是 ixgbe_poll(),weight 是 64。

cpp 复制代码
static int ixgbe_alloc_q_vector(struct ixgbe_adapter *adapter,
				int v_count, int v_idx,
				int txr_count, int txr_idx,
				int xdp_count, int xdp_idx,
				int rxr_count, int rxr_idx)
{
    ...
	/* initialize NAPI */
	netif_napi_add(adapter->netdev, &q_vector->napi,
		       ixgbe_poll, 64);
    ...
}

2.3 网卡收包过程分析

网卡收包过程分为两个阶段,第一阶段是硬中断处理,第二阶段是软中断处理。

2.3.1 硬中断

ixgbe_msix_clean_rings() 是 ixgbe 网卡的中断服务例程。从注释来看,调用这个函数的时候,中断是关闭的,所以调用函数 napi_schedule_irqoff() 来调度 napi。

cpp 复制代码
static irqreturn_t ixgbe_msix_clean_rings(int irq, void *data)
{
    struct ixgbe_q_vector *q_vector = data;

    /* EIAM disabled interrupts (on this vector) for us */
    if (q_vector->rx.ring || q_vector->tx.ring)
        napi_schedule_irqoff(&q_vector->napi);

    return IRQ_HANDLED;
}

对于一个 napi 来说,将这个 napi 调度,就是将这个 napi 设置状态 NAPI_STATE_SCHED,只有设置了这个状态,软中断处理流程中才会处理这个 napi,否则不处理。

对于一个 napi,在同一时刻只允许调度一次,不允许多次调度。函数 napi_schedule_prep() 就是判断 napi 是不是已经被调度,如果是的话返回 false,否则返回 true。

cpp 复制代码
/**
 *	napi_schedule_irqoff - schedule NAPI poll
 *	@n: NAPI context
 *
 * Variant of napi_schedule(), assuming hard irqs are masked.
 */
static inline void napi_schedule_irqoff(struct napi_struct *n)
{
	if (napi_schedule_prep(n))
		__napi_schedule_irqoff(n);
}

在硬中断服务例程中,最终调用到函数 ____napi_schedule(),在该函数中做两件事:将 napi 挂接到 struct softnet_data 中,触发软中断。

cpp 复制代码
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);
}

2.3.2 软中断

函数 net_rx_action() 是收包软中断的入口函数。

cpp 复制代码
static __latent_entropy void net_rx_action(struct softirq_action *h)
{
    // softnet_data 是全局的数据结构,是 per cpu 类型的
    // 在硬中断中将 napi 挂在了 softnet_data 中
    // 在这里将 napi 取出来进行处理
	struct softnet_data *sd = this_cpu_ptr(&softnet_data);

    // 软中断处理的最长时间
	unsigned long time_limit = jiffies +
		usecs_to_jiffies(READ_ONCE(netdev_budget_usecs));

    // 软中断处理的最大报文个数
	int budget = READ_ONCE(netdev_budget);
	LIST_HEAD(list);
	LIST_HEAD(repoll);

	local_irq_disable();
	list_splice_init(&sd->poll_list, &list);
	local_irq_enable();

	for (;;) {
		struct napi_struct *n;

		if (list_empty(&list)) {
			if (!sd_has_rps_ipi_waiting(sd) && list_empty(&repoll))
				goto out;
			break;
		}

		n = list_first_entry(&list, struct napi_struct, poll_list);
        // 调用网卡的 poll 函数进行收包
		budget -= napi_poll(n, &repoll);

		/* If softirq window is exhausted then punt.
		 * Allow this to run for 2 jiffies since which will allow
		 * an average latency of 1.5/HZ.
		 */
        // 当处理的报文数量超过 budget 或者处理的时间超过限制的时候
        // 停止处理,保证内核任务的公平性
		if (unlikely(budget <= 0 ||
			     time_after_eq(jiffies, time_limit))) {
			sd->time_squeeze++;
			break;
		}
	}

	local_irq_disable();

	list_splice_tail_init(&sd->poll_list, &list);
	list_splice_tail(&repoll, &list);
	list_splice(&list, &sd->poll_list);
    // 如果 softnet_data 还没处理完,那么再次触发软中断
	if (!list_empty(&sd->poll_list))
		__raise_softirq_irqoff(NET_RX_SOFTIRQ);

	net_rps_action_and_irq_enable(sd);
out:
	__kfree_skb_flush();
}

函数 napi_poll() 中调用 napi 中的 poll 函数指针来收包。对于 ixgbe 网卡来说,对应的函数是 ixgbe_poll()。

cpp 复制代码
static int napi_poll(struct napi_struct *n, struct list_head *repoll)
{
    ...
	if (test_bit(NAPI_STATE_SCHED, &n->state)) {
		work = n->poll(n, weight);
		trace_napi_poll(n, work, weight);
	}
    ...
}
相关推荐
川贝枇杷膏cbppg7 分钟前
DmServiceDMSERVER.log是干嘛的
java·服务器·数据库
fufu03117 分钟前
Linux环境下的C语言编程(四十二)
linux·c语言·算法
Trouvaille ~10 分钟前
【Linux】进程调度与环境变量:Linux内核的智慧
linux·运维·服务器·操作系统·进程·环境变量·调度算法
HalvmånEver11 分钟前
Linux : 基础IO(三)
linux·运维·算法
oushaojun213 分钟前
linux中backtrace实战
linux·运维·算法·backtrace
soft200152517 分钟前
MySQL 8.0.39 Rocky Linux 一键安装脚本(完整可直接运行)
linux·mysql·adb
Nerd Nirvana19 分钟前
WSL——Windows Subsystem for Linux流程一览
linux·运维·服务器·windows·嵌入式·wsl·wsl2
CS创新实验室1 小时前
计算机考研408【操作系统】核心知识点总结
java·linux·考研·计算机·操作系统·408
bulucc1 小时前
vim 快捷操作
linux·编辑器·vim
萧技电创EIIA1 小时前
威纶通触摸屏入门指南(从安装到入门使用)
运维·服务器·网络