linux之网络子系统-NAPI机制的分析

一、背景

随着网络带宽的发展,网速越来越快,之前的中断收包模式已经无法适应目前千兆,万兆的带宽了。如果每个数据包大小等于MTU大小1460字节。当驱动以千兆网速收包时,CPU将每秒被中断91829次。在以MTU收包的情况下都会出现每秒被中断10万次的情况。过多的中断会引起一个问题,CPU一直陷入硬中断而没有时间来处理别的事情了。为了解决这个问题,内核在2.6中引入了NAPI机制。

NAPI就是混合中断和轮询的方式来收包,当有中断来了,驱动关闭中断,通知内核收包,内核软中断轮询当前网卡,在规定时间尽可能多的收包。时间用尽或者没有数据可收,内核再次开启中断,准备下一次收包。

本文将介绍Linux内核中的NAPI:Linux网络设备驱动程序中的一种支持新一代网络适配器的架构。

二、NAPI机制

New API(NAPI)用于支持高速网卡处理网络数据包的一种机制 - 例如在Linux 2.6内核版本中引入的千兆以太网卡,后来又被移植到了2.4.x版本中。

NAPI 是 Linux 上采用的一种提高网络处理效率的技术,它的核心概念就是不采用中断的方式读取数据,而代之以首先采用中断唤醒数据接收的服务程序,然后 POLL 的方法来轮询数据。随着网络的接收速度的增加,NIC 触发的中断能做到不断减少,目前 NAPI 技术已经在网卡驱动层和网络层得到了广泛的应用,驱动层次上已经有 E1000 系列网卡,RTL8139 系列网卡,3c50X 系列等主流的网络适配器都采用了这个技术,而在网络层次上,NAPI 技术已经完全被应用到了著名的netif_rx 函数中间,并且提供了专门的 POLL 方法--process_backlog 来处理轮询的方法;根据实验数据表明采用NAPI技术可以大大改善短长度数据包接收的效率,减少中断触发的时间。

NAPI 对数据包到达的事件的处理采用轮询方法,在数据包达到的时候,NAPI 就会强制执行dev->poll方法。而和不像以前的驱动那样为了减少包到达时间的处理延迟,通常采用中断的方法来进行。

以前的网络设备驱动程序架构已经不能适用于每秒产生数千个中断的高速网络设备,并且它可能导致整个系统处于饥饿状态(译者注:饥饿状态的意思是系统忙于处理中断程序,没有时间执行其他程序)。有些网络设备具有中断合并,或者将多个数据包组合在一起来减少中断请求这种高级功能。

在内核没有使用NAPI来支持这些高级特性之前,这些功能只能全部在设备驱动程序中结合抢占机制(例如基于定时器中断),甚至中断程序范围之外的轮询程序(例如:内核线程,tasklet等)中实现。

正如我们看到的,网络子系统中加入的这个新特性是用于更好的支持中断缓解和数据包限制等功能,更重要的是它允许内核通过round-robin策略(轮询即Round Robin,一种负载均衡策略)将负载分发到不同网络设备上。

NAPI特性的添加不会影响内核的向后兼容性。

2.1 NAPI缺陷
NAPI 存在一些比较严重的缺陷:

1. 对于上层的应用程序而言,系统不能在每个数据包接收到的时候都可以及时地去处理它,而且随着传输速度增加,累计的数据包将会耗费大量的内存,经过实验表明在 Linux 平台上这个问题会比在 FreeBSD 上要严重一些;
2. 另外一个问题是对于大的数据包处理比较困难,原因是大的数据包传送到网络层上的时候耗费的时间比短数据包长很多(即使是采用 DMA 方式),所以正如前面所说的那样,NAPI 技术适用于对高速率的短长度数据包的处理。


2.2 使用 NAPI 先决条件

驱动可以继续使用老的 2.4 内核的网络驱动程序接口,NAPI 的加入并不会导致向前兼容性的丧失,但是 NAPI 的使用至少要得到下面的保证:

1. 设备需要有足够的缓冲区,保存多个数据分组。要使用 DMA 的环形输入队列(也就是 ring_dma,这个在 2.4 驱动中关于 Ethernet 的部分有详细的介绍),或者是有足够的内存空间缓存驱动获得的包。
2. 可以禁用当前设备中断,然而不影响其他的操作。在发送/接收数据包产生中断的时候有能力关断 NIC 中断的事件处理,并且在关断 NIC 以后,并不影响数据包接收到网络设备的环形缓冲区(以下简称 rx-ring)处理队列中。

当前大部分的设备都支持NAPI,但是为了对之前的保持兼容,内核还是对之前中断方式提供了兼容。我们先看下NAPI具体的处理方式。

我们都知道中断分为中断上半部和下半部,上半部完成的任务很是简单,仅仅负责把数据保存下来;而下半部负责具体的处理。为了处理下半部,每个CPU有维护一个softnet_data结构(下文将进行讲解)。我们不对此结构做详细介绍,仅仅描述和NAPI相关的部分。结构中有一个poll_list字段,连接所有的轮询设备。还 维护了两个队列input_pkt_queue和process_queue。这两个用户传统不支持NAPI方式的处理。前者由中断上半部的处理函数把数据包入队,在具体的处理时,使用后者做中转,相当于前者负责接收,后者负责处理。最后是一个napi_struct的backlog,代表一个虚拟设备供轮询使用。在支持NAPI的设备下,每个设备具备一个缓冲队列,存放到来数据。每个设备对应一个napi_struct结构,该结构代表该设备存放在poll_list中被轮询。而设备还需要提供一个poll函数,在设备被轮询到后,会调用poll函数对数据进行处理。基本逻辑就是这样,下文将给出具体流程。

/*
 * Incoming packets are placed on per-CPU queues
 */
struct softnet_data {
	struct list_head	poll_list;
	struct sk_buff_head	process_queue;
 
	/* stats */
	unsigned int		processed;
	unsigned int		time_squeeze;
	unsigned int		received_rps;
#ifdef CONFIG_RPS
	struct softnet_data	*rps_ipi_list;
#endif
#ifdef CONFIG_NET_FLOW_LIMIT
	struct sd_flow_limit __rcu *flow_limit;
#endif
	struct Qdisc		*output_queue;
	struct Qdisc		**output_queue_tailp;
	struct sk_buff		*completion_queue;
#ifdef CONFIG_XFRM_OFFLOAD
	struct sk_buff_head	xfrm_backlog;
#endif
	/* written and read only by owning cpu: */
	struct {
		u16 recursion;
		u8  more;
	} xmit;
#ifdef CONFIG_RPS
	/* input_queue_head should be written by cpu owning this struct,
	 * and only read by other cpus. Worth using a cache line.
	 */
	unsigned int		input_queue_head ____cacheline_aligned_in_smp;
 
	/* Elements below can be accessed between CPUs for RPS/RFS */
	call_single_data_t	csd ____cacheline_aligned_in_smp;
	struct softnet_data	*rps_ipi_next;
	unsigned int		cpu;
	unsigned int		input_queue_tail;
#endif
	unsigned int		dropped;
	struct sk_buff_head	input_pkt_queue;
	struct napi_struct	backlog;
 
};
 2.3 非NAPI帧的接收

我们将讨论内核在接收一个数据帧后的大致处理流程,不会详细叙述所有细节。

我们认为有必要先了解一下传统的数据包处理流程以便更好的理解NAPI和传统收包方式的区别。

在传统的收包方式中(如下图)数据帧向网络协议栈中传递发生在中断上下文(在接收数据帧时)中调用netif_rx的函数中。 这个函数还有一个变体netif_rx_ni,他被用于中断上下文之外。

 

 netif_rx函数将网卡中收到的数据包(包装在一个socket buffer中)放到系统中的接收队列中(input_pkt_queue),前提是这个接收队列的长度没有大于netdev_max_backlog。这个参数和另外一些参数可以在/proc文件系统中看到(/proc/sys/net/core文件中,可以手动调整这个数值)

netif_rx 函数在中断函数中被调用。

2.3.1 netif_rx - 将网卡中收到的数据包放到系统中的接收队列中
int netif_rx(struct sk_buff *skb)
{
	int ret;
...
	ret = netif_rx_internal(skb);
...
	return ret;
}
static int netif_rx_internal(struct sk_buff *skb)
{
	int ret;
 
	net_timestamp_check(netdev_tstamp_prequeue, skb);
	trace_netif_rx(skb);
 
#ifdef CONFIG_RPS
...
#endif
	{
		unsigned int qtail;
 
		ret = enqueue_to_backlog(skb, get_cpu_light(), &qtail);
		put_cpu_light();
	}
	return ret;
}
2.3.2 enqueue_to_backlog

 中间RPS暂时不关心,这里直接调用enqueue_to_backlog放入CPU的全局队列 input_pkt_queue

static int enqueue_to_backlog(struct sk_buff *skb, int cpu,
			      unsigned int *qtail)
{
	struct softnet_data *sd;
	unsigned long flags;
	unsigned int qlen;
   /*获取cpu相关的softnet_data变量*/
	sd = &per_cpu(softnet_data, cpu);
   /*关中断*/
	local_irq_save(flags);
 
	rps_lock(sd);
	if (!netif_running(skb->dev))
		goto drop;
	qlen = skb_queue_len(&sd->input_pkt_queue);
	/*如果input_pkt_queue的长度小于最大限制,则符合条件*/
	if (qlen <= netdev_max_backlog && !skb_flow_limit(skb, qlen)) {
		/*如果input_pkt_queue不为空,说明虚拟设备已经得到调度,此时仅仅把数据加入
          input_pkt_queue队列即可 */
		if (qlen) {
enqueue:
			__skb_queue_tail(&sd->input_pkt_queue, skb);
			input_queue_tail_incr_save(sd, qtail);
			rps_unlock(sd);
			local_irq_restore(flags);
			return NET_RX_SUCCESS;
		}
 
		/* Schedule NAPI for backlog device
		 * We can use non atomic operation since we own the queue lock
		 */
		/*队列为空时,即skb是第一个入队元素,则将state设置为 NAPI_STATE_SCHED(软中断处理函数
          rx_net_action会检查此标志),表示软中断可以处理此 backlog */
		if (!__test_and_set_bit(NAPI_STATE_SCHED, &sd->backlog.state)) {
		/* if返回0的情况下,需要将sd->backlog挂到sd->poll_list上,并激活软中断。
			rps_ipi_queued看下面的分析 */
			if (!rps_ipi_queued(sd))
				____napi_schedule(sd, &sd->backlog);
		}
		goto enqueue;
	}
 
drop:
	sd->dropped++;
	rps_unlock(sd);
 
	local_irq_restore(flags);
	preempt_check_resched_rt();
 
	atomic_long_inc(&skb->dev->rx_dropped);
	kfree_skb(skb);
	return NET_RX_DROP;
}
 
/*
 * Check if this softnet_data structure is another cpu one
 * If yes, queue it to our IPI list and return 1
 * If no, return 0
 */ 
/*上面注释说的很清楚,在配置RPS情况下,检查sd是当前cpu的还是其他cpu的,
如果是其他cpu的,将sd放在当前cpu的mysd->rps_ipi_list上,并激活当前cpu的软中断,返回1. 
在软中断处理函数net_rx_action中,通过ipi中断通知其他cpu来处理放在其他
cpu队列上的skb如果是当前cpu,或者没有配置RPS,则返回0,
在外层函数激活软中断,并将当前cpu的backlog放入sd->poll_list上*/
static int rps_ipi_queued(struct softnet_data *sd)
{
#ifdef CONFIG_RPS
    struct softnet_data *mysd = this_cpu_ptr(&softnet_data);
 
    if (sd != mysd) {
        sd->rps_ipi_next = mysd->rps_ipi_list;
        mysd->rps_ipi_list = sd;
 
        __raise_softirq_irqoff(NET_RX_SOFTIRQ);
        return 1;
    }
#endif /* CONFIG_RPS */
    return 0;
}
2.3.3 ____napi_schedule函数

该函数逻辑也比较简单,主要注意的是设备必须先添加调度然后才能接受数据,添加调度调用了____napi_schedule函数,该函数把设备对应的napi_struct结构插入到softnet_data的poll_list链表尾部,然后唤醒软中断,这样在下次软中断得到处理时,中断下半部就会得到处理。不妨看下源码:

/* 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);
}

input_pkt_queue是softnet_data结构体中的一个成员,定义在netdevice.h文件中。

如果接收到的数据包没有因为input_pkt_queue队列已满而被丢弃,它会被netif_rx_schedule函数调度给软中断NET_RX_SOFTIRQ处理,netif_rx_schedule函数在netif_rx函数内部被调用。

软中断NET_RX_SOFTIRQ的处理逻辑在net_rx_action函数中实现。

此时,我们可以说此函数将数据包从input_pkt_queue队列中传递给了网络协议栈,现在数据包可以被处理了。

2.4 NAPI方式

NAPI的方式相对于非NAPI要简单许多,看下e100网卡的中断处理函数e100_intr,核心部分:

static irqreturn_t e100_intr(int irq, void *dev_id)
{
	struct net_device *netdev = dev_id;
	struct nic *nic = netdev_priv(netdev);
    ...
	if (likely(napi_schedule_prep(&nic->napi))) {
		e100_disable_irq(nic);//屏蔽当前中断
		__napi_schedule(&nic->napi);//把设备加入到轮训队列
	}
 
	return IRQ_HANDLED;
}

if条件检查当前设备是否 可被调度,主要检查两个方面:

1、是否已经在调度
2、是否禁止了napi pending.

如果符合条件,就关闭当前设备的中断,调用__napi_schedule函数把设备假如到轮训列表,从而开启轮询模式。

总结:结合上面两种方式,还是可以发现两种方式的异同。其中softnet_data作为主导结构,在NAPI的处理方式下,主要维护轮询链表。NAPI设备均对应一个napi_struct结构,添加到链表中;非NAPI没有对应的napi_struct结构,为了使用NAPI的处理流程,使用了softnet_data结构中的back_log作为一个虚拟设备添加到轮询链表同时由于非NAPI设备没有各自的接收队列,所以利用了softnet_data结构的input_pkt_queue作为全局的接收队列。这样就处理而言,可以和NAPI的设备进行兼容。但是还有一个重要区别,在NAPI的方式下,首次数据包的接收使用中断的方式,而后续的数据包就会使用轮询处理了;而非NAPI每次都是通过中断通知。

2.4.1 NAPI帧的接收

在NAPI架构中(如下图),当接收到数据包产生中断时,驱动程序会通知网络子系统有新的数据包到来(而不是立即处理数据包),这样就可以在ISR(Interrupt Service Routines - 中断服务程序)上下文之外使用轮询的方式来一次性接收多个数据包(VPP了解一下)

因此网卡支持NAPI必须满足几个条件:

  • 驱动程序不再使用数据包接收队列
  • 网卡本身需要维护一个缓冲区来保存接收到数据包,并且可以禁止中断。


这种方法减少了中断的产生并且在突发情况下减少了丢包的可能性,避免了接收队列的饱和。

从NAPI实现的角度来看,与传统收包方式的不同地方在中断程序和轮询函数上(在net_device结构体中定义),定义如下:

int (*poll)(struct net_device *dev, int *budget);


除此之外,net_device结构体中还有另外两个属性quota(配额)和weight(权重),他们用于在一个轮询周期中实现抢占机制(译者注:意思是通过这两个参数来控制一个轮询周期的运行时间,恩,是的)我们将在后面详细讨论。

NAPI模型中的中断函数将数据帧传送到协议栈的任务交给poll函数执行。 换句话说中断函数的工作被简化为禁用网络设备中断(再此期间设备可以继续接收数据帧),和确认中断然后调度(通过netif_rx_schedule函数调度)软中断NET_RX_SOFTIRQ关联的net_rx_action函数。

等待被轮询的设备通过netif_rx_schedule函数将net_device结构体实例的指针加入到poll_list链表中。 在调用net_rx_action函数执行软中断NET_RX_SOFTIRQ时会遍历poll_list链表,然后调用每个设备的poll()函数将数据帧存放在socket buffers中并通知上层协议栈。

net_rx_action函数的执行步骤如下:

1、回收当前处理器的poll_list链表的引用。
2、将jiffies的值保存在start_time变量中。
3、设置轮询的budget(预算,可处理的数据包数量)为netdev_budget变量的初始值(这个值可以通过 /proc/sys/net/core/netdev_budget来配置)
4、轮询poll_list链表中的每个设备,直到你的budget用完,当你的运行时间还没有超过一个jiffies时:

  •  如果quantum(配额)为正值则调用设备的poll()函数,否则将weight的值加到quantum中,将设备放回poll_list链表;
  • 如果poll()函数返回一个非零值,将weight的值设置到quantum中然后将设备放回poll_list链表;
  • 如果poll()函数返回零值,说明设备已经被移除poll_list链表(不再处于轮询状态)。

budget的值和net_device结构体的指针会传递到poll()函数中。poll()函数应该根据数据帧的处理数量来减小budget的值。数据帧从网络设备的缓冲区中复制出来包装在socket buffers中,然后通过netif_receive_skb函数传递到协议栈中去。poll 函数实现的功能就是 把数据帧memcpy出来,封装到new_skb中。

抢占策略是依赖budget变量的配额机制实现的:poll()函数必须根据分配给设备的最大配额来决定可以传递多少个数据包给内核。 当配额使用完就不允许在传递数据包给内核了,应该轮询poll_list链表中的下一个设备了。因此poll()必须和减小budget的值一样根据数据帧的处理数量来减小quota的值。

如果驱动在用完了所有的quota之后还没有传递完队列中所有的数据包,poll()函数必须停止运行并返回一个非NULL值。
如果所有数据包都传递到了协议栈,驱动程序必须再次使能设备的中断并停止轮询,然后调用netif_rx_complete函数(它会将设备从poll_list链表去除),最后停止运行并返回零值给调用者(net_rx_action函数)。

net_device结构体中的另一个重要成员weight,它用于每次调用poll()函数时重置quota的值。 很明显weight的值必须被初始化为一个固定的正值。通常对于高速网卡这个值一般在16和32之间,对于千兆网卡来说这个值会大一点(通常时64)。
从net_rx_action函数的实现中我们可以看到当weight的值设置太大时,驱动使用的budget会超过quota,此时会导致一个轮询周期的时间变长。

下面我们给出了设备驱动程序接收中断并执行轮询函数的伪代码:

static irqreturn_t sample_netdev_intr(int irq, void *dev)
{
    struct net_device *netdev = dev;
    struct nic *nic = netdev_priv(netdev);
 
    if (! nic->irq_pending())
        return IRQ_NONE;
 
    /* Ack interrupt(s) */
    nic->ack_irq();
 
    nic->disable_irq();  
 
    netif_rx_schedule(netdev);
 
    return IRQ_HANDLED;
}
 
 
static int sample_netdev_poll(struct net_device *netdev, int *budget)
{
    struct nic *nic = netdev_priv(netdev);
 
    unsigned int work_to_do = min(netdev->quota, *budget);
    unsigned int work_done = 0;
 
    nic->announce(&work_done, work_to_do);
 
    /* If no Rx announce was done, exit polling state. */
 
    if(work_done == 0) || !netif_running(netdev)) {
 
    netif_rx_complete(netdev);
    nic->enable_irq();  
 
    return 0;
    }
 
    *budget -= work_done;
    netdev->quota -= work_done;
 
    return 1;
}

下图分别展示了非NAPI和NAPI模型中数据包接收处理过程的时序图:

2.5  NAPI接口
2.5.1 struct napi_struct结构  - 内核处理软中断的入口

struct napi_struct 是内核处理软中断的入口,每个net_device都对应一个napi_struct,驱动在硬中断中将自己的napi_struct挂载到CPU的收包队列softnet_data。内核在软中断中轮询该队列,并执行napi_sturct中的回调函数int(*poll)(struct napi_struct *, int);

在poll函数中,驱动将网卡数据转换成skb_buff形式,最终发往协议栈。也就是说,协议栈对数据包的处理,使用的是软中断的时间片。如果协议栈处理耗费了过多的CPU时间的化,会直接影响到设备的网络性能。

/*
 * 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			weight;//每次轮询最大处理数据包数量
	int			defer_hard_irqs_count;
	unsigned long		gro_bitmask;
	int			(*poll)(struct napi_struct *, int);//轮询设备的回调函数
#ifdef CONFIG_NETPOLL
	int			poll_owner;
#endif
	struct net_device	*dev;
	struct gro_list		gro_hash[GRO_HASH_BUCKETS];
	struct sk_buff		*skb;
	struct list_head	rx_list; /* Pending GRO_NORMAL skbs */
	int			rx_count; /* length of rx_list */
	struct hrtimer		timer;
	struct list_head	dev_list;
	struct hlist_node	napi_hash_node;
	unsigned int		napi_id;
};

 有了保存数据的结构体,让我们在看看为它配套提供的接口函数吧:

2.5.2  netif_napi_add函数 - 驱动初始时向内核注册软软中断处理回调poll函数

驱动在初始化net_device时通过这函数将通过这个函数绑定一个napi_struct结构。驱动需要在这里注册软中断中用于轮询的网卡的poll函数。

void netif_napi_add(struct net_device *dev, struct napi_struct *napi,
		    int (*poll)(struct napi_struct *, int), int weight)
{
	if (WARN_ON(test_and_set_bit(NAPI_STATE_LISTED, &napi->state)))
		return;
 
	INIT_LIST_HEAD(&napi->poll_list);
	INIT_HLIST_NODE(&napi->napi_hash_node);
    ...
	napi->poll = poll;
	if (weight > NAPI_POLL_WEIGHT)
		netdev_err_once(dev, "%s() called with weight %d\n", __func__,
				weight);
	napi->weight = weight;
	napi->dev = dev;
#ifdef CONFIG_NETPOLL
	napi->poll_owner = -1;
#endif
	set_bit(NAPI_STATE_SCHED, &napi->state);
	set_bit(NAPI_STATE_NPSVC, &napi->state);
	list_add_rcu(&napi->dev_list, &dev->napi_list);
	napi_hash_add(napi);
}
EXPORT_SYMBOL(netif_napi_add);
2.5.3  __napi_schedule函数 - 网卡硬件中断用来触发软中断

_napi_schedule函数,为驱动硬件中断提供的接口,驱动在硬件中断中,将自己的napi_struct挂载到当前CPU的softnet_data上

/**
 * __napi_schedule - schedule for receive
 * @n: entry to schedule
 *
 * The entry's receive function will be scheduled to run.
 * Consider using __napi_schedule_irqoff() if hard irqs are masked.
 */
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);
}
 
/* 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); //设置了软中断接收标志位
}
2.5.4 napi_schedule_prep函数 - 对napi_struct进行检查

napi_schedule_prep函数是上面__napi_schedule的配套函数,用于__napi_schedule调用前对napi_struct进行检查。前面博文e1000网卡的中断函数就是这样调用的。

/**
 * e1000_intr - Interrupt Handler
 * @irq: interrupt number
 * @data: pointer to a network interface device structure
 **/
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;
	u32 icr = er32(ICR);
    ...
	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_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;
}

判断NAPI是否可以调度。如果NAPI没有被禁止,且不存在已被调度的NAPI,则允许调度NAPI,因为同一时刻只允许有一个NAPI poll instance。测试napi.state字段,只有当其不是NAPI_STATE_SCHED时,返回真,并设置为NAPI_STATE_SCHED.

/**
 *	napi_schedule_prep - check if napi can be scheduled
 *	@n: napi context
 *
 * Test if NAPI routine is already running, and if not mark
 * it as running.  This is used as a condition variable to
 * insure only one NAPI poll instance runs.  We also make
 * sure there is no pending NAPI disable.
 */
bool napi_schedule_prep(struct napi_struct *n)
{
	unsigned long val, new;
 
	do {
		val = READ_ONCE(n->state);
		if (unlikely(val & NAPIF_STATE_DISABLE))
			return false;
		new = val | NAPIF_STATE_SCHED;
 
		/* Sets STATE_MISSED bit if STATE_SCHED was already set
		 * This was suggested by Alexander Duyck, as compiler
		 * emits better code than :
		 * if (val & NAPIF_STATE_SCHED)
		 *     new |= NAPIF_STATE_MISSED;
		 */
		new |= (val & NAPIF_STATE_SCHED) / NAPIF_STATE_SCHED *
						   NAPIF_STATE_MISSED;
	} while (cmpxchg(&n->state, val, new) != val);
 
	return !(val & NAPIF_STATE_SCHED);
}
EXPORT_SYMBOL(napi_schedule_prep);

 上面的三个函数netif_napi_add,__napi_schedule,napi_schedule_prep是驱动使用NAPI收包机制的接口,下面再看看内核软中断使用NAPI的接口函数吧。

2.5.5 napi_poll函数 - 用于调用收包poll函数

这函数是被软中断处理函数net_rx_action调用的。这个函数将在napi_struct.weight规定的时间内,被net_rx_action循环调用,直到时间片用尽或者网卡当前DMA中所有缓存的数据包被处理完。如果是由于时间片用尽而退出的的话,napi_struct会重新挂载到softnet_data上,而如果是所有数据包处理完退出的,napi_struct会从softnet_data上移除并重新打开网卡硬件中断.

 

static int napi_poll(struct napi_struct *n, struct list_head *repoll)
{
	void *have;
	int work, weight;
 
	list_del_init(&n->poll_list);
 
	have = netpoll_poll_lock(n);
 
	weight = n->weight;
 
	/* This NAPI_STATE_SCHED test is for avoiding a race
	 * with netpoll's poll_napi().  Only the entity which
	 * obtains the lock and sees NAPI_STATE_SCHED set will
	 * actually make the ->poll() call.  Therefore we avoid
	 * accidentally calling ->poll() when NAPI is not scheduled.
	 */
	work = 0;
	if (test_bit(NAPI_STATE_SCHED, &n->state)) {
		work = n->poll(n, weight);
		trace_napi_poll(n, work, weight);
	}
 
	if (unlikely(work > weight))
		pr_err_once("NAPI poll function %pS returned %d, exceeding its budget of %d.\n",
			    n->poll, work, weight);
 
	if (likely(work < weight))
		goto out_unlock;
 
	/* Drivers must not modify the NAPI state if they
	 * consume the entire weight.  In such cases this code
	 * still "owns" the NAPI instance and therefore can
	 * move the instance around on the list at-will.
	 */
	if (unlikely(napi_disable_pending(n))) {
		napi_complete(n);
		goto out_unlock;
	}
 
	if (n->gro_bitmask) {
		/* flush too old packets
		 * If HZ < 1000, flush all packets.
		 */
		napi_gro_flush(n, HZ >= 1000);
	}
 
	gro_normal_list(n);
 
	/* Some drivers may have called napi_schedule
	 * prior to exhausting their budget.
	 */
	if (unlikely(!list_empty(&n->poll_list))) {
		pr_warn_once("%s: Budget exhausted after napi rescheduled\n",
			     n->dev ? n->dev->name : "backlog");
		goto out_unlock;
	}
 
	list_add_tail(&n->poll_list, repoll);
 
out_unlock:
	netpoll_poll_unlock(have);
 
	return work;
}
2.5.6 napi_gro_receive函数 - poll函数用来将网卡上的数据包发给协议栈处理

准确来说napi_gro_receive函数是驱动通过poll注册,内核调用的函数。通过这函数的的调用,skb将会传给协议栈的入口函数__netif_receive_skb。dev_gro_receive函数用于对数据包的合并,他将合并napi_struct.gro_list链表上的skb。GRO是一个网络子系统的另一套机制,以后再看。

gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
	gro_result_t ret;
 
	skb_mark_napi_id(skb, napi);
	trace_napi_gro_receive_entry(skb);
 
	skb_gro_reset_offset(skb, 0);
 
	ret = napi_skb_finish(napi, skb, dev_gro_receive(napi, skb));
	trace_napi_gro_receive_exit(ret);
 
	return ret;
}
EXPORT_SYMBOL(napi_gro_receive);
小结:
  • netif_napi_add:驱动初始时向内核注册软软中断处理回调poll函数
  • napi_schedule_prep函数是上面__napi_schedule的配套函数,用于__napi_schedule调用前对napi_struct进行检查
    __napi_schedule:网卡硬件中断用来触发软中断
  • napi_poll:软中断处理函数net_rx_action用来回调上面驱动初始化是通过netif_napi_add注册的回调收包poll函数
  • napi_gro_receive:poll函数用来将网卡上的数据包发给协议栈处理。

到这,NAPI机制下的收包处理流程就很清晰了。

IRQ
    ->__napi_schedule
        ->进入软中断
            ->net_rx_action
                ->napi_poll
                    ->驱动注册的poll
                        ->napi_gro_receive。

三、 E1000网卡驱动程序对NAPI的支持

上面已经介绍过了,使用NAPI需要在编译内核的时候选择打开相应网卡设备的NAPI支持选项,对于E1000网卡来说就是CONFIG_E1000_NAPI宏。

3.1 e1000_probe函数 - E1000网卡的初始化函数

E1000网卡的初始化函数,也就是通常所说的probe方法,定义为e1000_probe():

/**
 * e1000_probe - Device Initialization Routine
 * @pdev: PCI device information struct
 * @ent: entry in e1000_pci_tbl
 *
 * Returns 0 on success, negative on failure
 *
 * e1000_probe initializes an adapter identified by a pci_dev structure.
 * The OS initialization, configuring of the adapter private structure,
 * and a hardware reset occur.
 **/
static int e1000_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
	struct net_device *netdev;
	struct e1000_adapter *adapter = NULL;
	struct e1000_hw *hw;
 
	static int cards_found;
	static int global_quad_port_a; /* global ksp3 port a indication */
	int i, err, pci_using_dac;
	u16 eeprom_data = 0;
	u16 tmp = 0;
	u16 eeprom_apme_mask = E1000_EEPROM_APME;
	int bars, need_ioport;
	bool disable_dev = false;
 
	/* do not allocate ioport bars when not needed */
	need_ioport = e1000_is_need_ioport(pdev);
	if (need_ioport) {
		bars = pci_select_bars(pdev, IORESOURCE_MEM | IORESOURCE_IO);
		err = pci_enable_device(pdev);
	} else {
		bars = pci_select_bars(pdev, IORESOURCE_MEM);
		err = pci_enable_device_mem(pdev);
	}
	if (err)
		return err;
 
	err = pci_request_selected_regions(pdev, bars, e1000_driver_name);
	if (err)
		goto err_pci_reg;
 
	pci_set_master(pdev);
	err = pci_save_state(pdev);
	if (err)
		goto err_alloc_etherdev;
 
	err = -ENOMEM;
	//为e1000网卡对应的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;
	hw->hw_addr = pci_ioremap_bar(pdev, BAR_0);
	if (!hw->hw_addr)
		goto err_ioremap;
	
	...
	
	/* make ready for any if (hw->...) below */
	err = e1000_init_hw_struct(adapter, hw);
	if (err)
		goto err_sw_init;
 
	/* there is a workaround being applied below that limits
	 * 64-bit DMA addresses to 64-bit hardware.  There are some
	 * 32-bit adapters that Tx hang when given 64-bit DMA addresses
	 */
	pci_using_dac = 0;
	if ((hw->bus_type == e1000_bus_type_pcix) &&
	    !dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64))) {
		pci_using_dac = 1;
	} else {
		err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
		if (err) {
			pr_err("No usable DMA config, aborting\n");
			goto err_dma;
		}
	}
	/*将e1000网卡驱动程序的相应函数注册到net_device结构的成员函数上。这里值得注意的
    是如果定义了设备的CONFIG_E1000_NAPI宏,则设备对应的poll方法被注册为e1000_clean。
    在网络设备初始化时(net_dev_init()函数)将所有的设备的poll方法注册为系统默认
    函数process_backlog(),该函数的处理方法就是从CPU相关队列softnet_data的输入
    数据包队列中读取skb,然后调用netif_receive_skb()函数提交给上层协议继续处
    理。设备的poll方法是在软中断处理函数中调用的。*/
	netdev->netdev_ops = &e1000_netdev_ops;
	e1000_set_ethtool_ops(netdev);
	netdev->watchdog_timeo = 5 * HZ;
	netif_napi_add(netdev, &adapter->napi, e1000_clean, 64);
 
	strncpy(netdev->name, pci_name(pdev), sizeof(netdev->name) - 1);
 
	adapter->bd_number = cards_found;
 
	/* setup the private structure */
 
	err = e1000_sw_init(adapter);
	if (err)
		goto err_sw_init;
	...
	/* reset the hardware with the new settings */
	e1000_reset(adapter);
 
	strcpy(netdev->name, "eth%d");
	err = register_netdev(netdev);
	if (err)
		goto err_register;
 
	e1000_vlan_filter_on_off(adapter, false);
	...
	return err;
}
 
static const struct net_device_ops e1000_netdev_ops = {
	.ndo_open		= e1000_open,
	.ndo_stop		= e1000_close,
	.ndo_start_xmit		= e1000_xmit_frame,
	.ndo_set_rx_mode	= e1000_set_rx_mode,
	.ndo_set_mac_address	= e1000_set_mac,
	.ndo_tx_timeout		= e1000_tx_timeout,
	.ndo_change_mtu		= e1000_change_mtu,
	.ndo_do_ioctl		= e1000_ioctl,
	.ndo_validate_addr	= eth_validate_addr,
	.ndo_vlan_rx_add_vid	= e1000_vlan_rx_add_vid,
	.ndo_vlan_rx_kill_vid	= e1000_vlan_rx_kill_vid,
#ifdef CONFIG_NET_POLL_CONTROLLER
	.ndo_poll_controller	= e1000_netpoll,
#endif
	.ndo_fix_features	= e1000_fix_features,
	.ndo_set_features	= e1000_set_features,
};
3.2 e1000_open函数 - E1000网卡驱动程序的open方法

在分析网卡接收数据包的过程中,设备的open方法是值得注意的,因为在这里对网卡设备的各种数据结构进行了初始化,特别是环形缓冲区队列和中断。E1000网卡驱动程序的open方法注册为e1000_open():

int e1000_open(struct net_device *netdev)
{
	struct e1000_adapter *adapter = netdev_priv(netdev);
	struct e1000_hw *hw = &adapter->hw;
	int err;
 
	/* disallow open during test */
	if (test_bit(__E1000_TESTING, &adapter->flags))
		return -EBUSY;
 
	netif_carrier_off(netdev);
 
	/* allocate transmit descriptors */
	err = e1000_setup_all_tx_resources(adapter);
	if (err)
		goto err_setup_tx;
 
	/* allocate receive descriptors */
	err = e1000_setup_all_rx_resources(adapter);
	if (err)
		goto err_setup_rx;
 
	e1000_power_up_phy(adapter);
 
	adapter->mng_vlan_id = E1000_MNG_VLAN_NONE;
	if ((hw->mng_cookie.status &
			  E1000_MNG_DHCP_COOKIE_STATUS_VLAN_SUPPORT)) {
		e1000_update_mng_vlan(adapter);
	}
 
	/* before we allocate an interrupt, we must be ready to handle it.
	 * Setting DEBUG_SHIRQ in the kernel makes it fire an interrupt
	 * as soon as we call pci_request_irq, so we have to setup our
	 * clean_rx handler before we do so.
	 */
	e1000_configure(adapter);
 
	err = e1000_request_irq(adapter);
	if (err)
		goto err_req_irq;
 
	/* From here on the code is the same as e1000_up() */
	clear_bit(__E1000_DOWN, &adapter->flags);
 
	napi_enable(&adapter->napi);
 
	e1000_irq_enable(adapter);
 
	netif_start_queue(netdev);
 
	/* fire a link status change interrupt to start the watchdog */
	ew32(ICS, E1000_ICS_LSC);
 
	return E1000_SUCCESS;
 
err_req_irq:
	e1000_power_down_phy(adapter);
	e1000_free_all_rx_resources(adapter);
err_setup_rx:
	e1000_free_all_tx_resources(adapter);
err_setup_tx:
	e1000_reset(adapter);
 
	return err;
}

事实上e1000_open() 函数调用了e1000_setup_rx_resources()函数为其环形缓冲区分配资源。

e1000设备的接收方式是一种缓冲方式,能显著的降低 CPU接收数据造成的花费,接收数据之前,软件需要预先分配一个 DMA 缓冲区,一般对于传输而言,缓冲区最大为 8Kbyte 并且把物理地址链接在描述符的 DMA 地址描述单元,另外还有两个双字的单元表示对应的 DMA 缓冲区的接收状态。

3.3 e1000_setup_rx_resources函数 - 为其环形缓冲区分配资源

在 /driver/net/e1000/e1000/e1000.h 中对于环形缓冲队列描述符的数据单元如下表示:

struct e1000_rx_ring {
    void *desc; /* 指向描述符环状缓冲区的指针。*/
    dma_addr_t dma; /* 描述符环状缓冲区物理地址,也就是DMA缓冲区地址*/
    unsigned int size; /* 描述符环状缓冲区的长度(用字节表示)*/
    unsigned int count; /* 缓冲区内描述符的数量,这个是系统初始化时规定好的,它
    决定该环形缓冲区有多少描述符(或者说缓冲区)可用*/
    unsigned int next_to_use; /* 下一个要使用的描述符。*/
    unsigned int next_to_clean; /* 下一个待删除描述符。*/
    struct e1000_buffer *buffer_info; /* 缓冲区信息结构数组。*/
	struct sk_buff *rx_skb_top;
 
	/* cpu for rx queue */
	int cpu;
 
	u16 rdh;
	u16 rdt;
};
 
static int e1000_setup_rx_resources(struct e1000_adapter *adapter,
				    struct e1000_rx_ring *rxdr)
{
	struct pci_dev *pdev = adapter->pdev;
	int size, desc_len;
 
	size = sizeof(struct e1000_rx_buffer) * rxdr->count;
	rxdr->buffer_info = vzalloc(size);
	if (!rxdr->buffer_info)
		return -ENOMEM;
 
	desc_len = sizeof(struct e1000_rx_desc);
 
	/* Round up to nearest 4K */
 
	rxdr->size = rxdr->count * desc_len;
	rxdr->size = ALIGN(rxdr->size, 4096);
 
	rxdr->desc = dma_alloc_coherent(&pdev->dev, rxdr->size, &rxdr->dma,
					GFP_KERNEL);
	if (!rxdr->desc) {
setup_rx_desc_die:
		vfree(rxdr->buffer_info);
		return -ENOMEM;
	}
 
	/* Fix for errata 23, can't cross 64kB boundary */
	if (!e1000_check_64k_bound(adapter, rxdr->desc, rxdr->size)) {
		void *olddesc = rxdr->desc;
		dma_addr_t olddma = rxdr->dma;
		e_err(rx_err, "rxdr align check failed: %u bytes at %p\n",
		      rxdr->size, rxdr->desc);
		/* Try again, without freeing the previous */
		rxdr->desc = dma_alloc_coherent(&pdev->dev, rxdr->size,
						&rxdr->dma, GFP_KERNEL);
		/* Failed allocation, critical failure */
		if (!rxdr->desc) {
			dma_free_coherent(&pdev->dev, rxdr->size, olddesc,
					  olddma);
			goto setup_rx_desc_die;
		}
	...
	memset(rxdr->desc, 0, rxdr->size);
 
	rxdr->next_to_clean = 0;
	rxdr->next_to_use = 0;
	rxdr->rx_skb_top = NULL;
 
	return 0;
}
3.4 request_irq函数 - 向系统申请irq中断号

在e1000_open()函数中,调用request_irq()向系统申请irq中断号,然后将e1000_intr()中断处理函数注册到系统当中,系统有一个中断向量表irq_desc[]。然后使能网卡的中断。接 下来就是网卡处于响应中断的模式,这里重要的函数是 e1000_intr()中断处理函数

/**
 * e1000_intr - Interrupt Handler
 * @irq: interrupt number
 * @data: pointer to a network interface device structure
 **/
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;
	u32 icr = er32(ICR);
 
	if (unlikely((!icr)))
		return IRQ_NONE;  /* Not our interrupt */
	...
	/* disable interrupts, without the synchronize_irq bit */
	ew32(IMC, ~0);
	E1000_WRITE_FLUSH();
 
	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_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;
}
3.5  net_rx_action函数 - 软中断处理函数

下半部的处理函数,之前提到,网络数据包的接发对应两个不同的软中断,接收软中断NET_RX_SOFTIRQ的处理函数对应net_rx_action。

static void net_rx_action(struct softirq_action *h)
{
    //获取percpu的sd
    struct softnet_data *sd = this_cpu_ptr(&softnet_data);
    unsigned long time_limit = jiffies + 2;
 
    //netdev_budget默认值300,可通过sysctl修改
    int budget = netdev_budget;
    void *have;
 
    local_irq_disable();
    //如果sd->poll_list不为空,说明有数据需要处理
    while (!list_empty(&sd->poll_list)) {
        struct napi_struct *n;
        int work, weight;
 
        /* If softirq window is exhuasted then punt.
         * Allow this to run for 2 jiffies since which will allow
         * an average latency of 1.5/HZ.
         */
        //如果budget用完了,或者经过了两个时间片,说明数据
        //包压力过大,还没处理完就需要跳出循环,在
        //softnet_break会再次激活软中断(因为执行软中断时已
        //经把所有的pending清空了)
        if (unlikely(budget <= 0 || time_after_eq(jiffies, time_limit)))
            goto softnet_break;
 
        local_irq_enable();
 
        /* Even though interrupts have been re-enabled, this
         * access is safe because interrupts can only add new
         * entries to the tail of this list, and only ->poll()
         * calls can remove this head entry from the list.
         */
        n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);
 
        have = netpoll_poll_lock(n);
 
        weight = n->weight;
 
        /* This NAPI_STATE_SCHED test is for avoiding a race
         * with netpoll's poll_napi().  Only the entity which
         * obtains the lock and sees NAPI_STATE_SCHED set will
         * actually make the ->poll() call.  Therefore we avoid
         * accidentally calling ->poll() when NAPI is not scheduled.
         */
        work = 0;
        //只有state为NAPI_STATE_SCHED才会执行poll函数。
        //对于non-napi来说,poll函数为process_backlog,处理
        //percpu的input queue上的数据包。
        //对于napi来说,poll函数为网卡驱动提供的poll函数,比
        //如ixgbe_poll,分配skb,将skb上送协议栈
        //如果poll处理后的结果work小于weight说明没有更多数
        //据需要处理,poll函数中会把napi从链表sd->poll_list删
        //除。如果work等于weight说明还有更多数据需要处理,
        //不会删除napi,只是将napi移动到链表尾部
        if (test_bit(NAPI_STATE_SCHED, &n->state)) {
            work = n->poll(n, weight);
            trace_napi_poll(n);
        }
 
        WARN_ON_ONCE(work > weight);
        //work为poll实际处理的数据个数,budget需要减去work
        budget -= work;
 
        local_irq_disable();
 
        /* Drivers must not modify the NAPI state if they
         * consume the entire weight.  In such cases this code
         * still "owns" the NAPI instance and therefore can
         * move the instance around on the list at-will.
         */
        //如果work等于weight说明还有更多数据需要处理
        if (unlikely(work == weight)) {
            if (unlikely(napi_disable_pending(n))) {
                local_irq_enable();
                napi_complete(n);
                local_irq_disable();
            } else {
                if (n->gro_list) {
                    /* flush too old packets
                     * If HZ < 1000, flush all packets.
                     */
                    local_irq_enable();
                    napi_gro_flush(n, HZ >= 1000);
                    local_irq_disable();
                }
                //将napi移动到链表尾部
                list_move_tail(&n->poll_list, &sd->poll_list);
            }
        }
 
        netpoll_poll_unlock(have);
    }
out:
    net_rps_action_and_irq_enable(sd);
 
    return;
 
softnet_break:
    sd->time_squeeze++;
    __raise_softirq_irqoff(NET_RX_SOFTIRQ);
    goto out;
}
 
/*
 * net_rps_action_and_irq_enable sends any pending IPI's for rps.
 * Note: called with local irq disabled, but exits with local irq enabled.
 */
//如果链表 sd->rps_ipi_list不为空,说明在rps下,将skb放在其他
//cpu上的percpu队列上了,所以需要通过ipi中断通知其他cpu,通
//过smp_call_function_single_async远程激活其他cpu的软中断,
//使其他cpu处理数据包
static void net_rps_action_and_irq_enable(struct softnet_data *sd)
{
#ifdef CONFIG_RPS
    struct softnet_data *remsd = sd->rps_ipi_list;
 
    if (remsd) {
        sd->rps_ipi_list = NULL;
 
        local_irq_enable();
 
        /* Send pending IPI's to kick RPS processing on remote cpus. */
        while (remsd) {
            struct softnet_data *next = remsd->rps_ipi_next;
 
            if (cpu_online(remsd->cpu))
                smp_call_function_single_async(remsd->cpu,
                               &remsd->csd);
            remsd = next;
        }
    } else
#endif
        local_irq_enable();
}

这里有处理方式比较直观,直接遍历poll_list链表,处理之前设置了两个限制:budget和time_limit。前者限制本次处理数据包的总量,后者限制本次处理总时间。只有二者均有剩余的情况下,才会继续处理。处理期间同样是开中断的,每次总是从链表表头取设备进行处理,如果设备被调度,其实就是检查NAPI_STATE_SCHED位,则调用 napi_struct的poll函数,处理结束如果没有处理完,则把设备移动到链表尾部,否则从链表删除。NAPI设备对应的poll函数会同样会调用__netif_receive_skb函数上传协议栈.

3.5.1 激活软中断(ixgbe 网卡调用栈)
//硬件中断到来时调用中断处理函数 ixgbe_msix_clean_rings
ixgbe_msix_clean_rings
    napi_schedule(&q_vector->napi);
____napi_schedule(this_cpu_ptr(&softnet_data), n);
            //将napi添加到per cpu的softnet_data->poll_list中
            list_add_tail(&napi->poll_list, &sd->poll_list);
            //将接收软中断置位
            __raise_softirq_irqoff(NET_RX_SOFTIRQ);
3.5.2 执行软中断(ixgbe 网卡调用栈)
__do_softirq
    net_rx_action
        n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);
        work = n->poll(n, weight); //即调用 ixgbe_poll
            ixgbe_clean_rx_irq(q_vector, ring)
                skb = ixgbe_fetch_rx_buffer(rx_ring, rx_desc);
                ixgbe_rx_skb(q_vector, skb);
                    napi_gro_receive(&q_vector->napi, skb);
                        //上送协议栈,但如果开启了RPS就走non-NAPI的路径了
                        netif_receive_skb_internal
            /* all work done, exit the polling mode */
            //如果处理的skb小于配额,说明工作已经完成,将napi从poll_list删除
            //清除标志位 NAPI_STATE_SCHED
            napi_complete(napi);
                list_del(&n->poll_list);
                clear_bit(NAPI_STATE_SCHED, &n->state);

 如果没有开启RPS,则直接调用__netif_receive_skb 送协议栈了。如果开启RPS,则调用get_rps_cpu获取合适的cpu(有可能是本地cpu, 也可能是其他cpu),再调用enqueue_to_backlog将skb 放在percpu的队列中,激活相应cpu的软中断。

static int netif_receive_skb_internal(struct sk_buff *skb)
{
    int ret;
 
    net_timestamp_check(netdev_tstamp_prequeue, skb);
 
    if (skb_defer_rx_timestamp(skb))
        return NET_RX_SUCCESS;
 
    rcu_read_lock();
 
#ifdef CONFIG_RPS
    //注意使用的是static_key_false进行判断,意思是分支预测为false概率很大
    if (static_key_false(&rps_needed)) {
        struct rps_dev_flow voidflow, *rflow = &voidflow;
        int cpu = get_rps_cpu(skb->dev, skb, &rflow);
 
        if (cpu >= 0) {
            ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail);
            rcu_read_unlock();
            return ret;
        }
    }
#endif
    ret = __netif_receive_skb(skb);
    rcu_read_unlock();
    return ret;
}
 3.6 poll 函数
3.6.1 process_backlog ( 非NAPI)
static int process_backlog(struct napi_struct *napi, int quota)
{
    int work = 0;
    struct softnet_data *sd = container_of(napi, struct softnet_data, backlog);
 
#ifdef CONFIG_RPS
    /* Check if we have pending ipi, its better to send them now,
     * not waiting net_rx_action() end.
     */
    //激活其他cpu上的软中断
    if (sd->rps_ipi_list) {
        local_irq_disable();
        net_rps_action_and_irq_enable(sd);
    }
#endif
    napi->weight = weight_p;
    local_irq_disable();
    while (1) {
        struct sk_buff *skb;
 
        while ((skb = __skb_dequeue(&sd->process_queue))) {
            rcu_read_lock();
            local_irq_enable();
            //将skb上送协议栈
            __netif_receive_skb(skb);
            rcu_read_unlock();
            local_irq_disable();
            input_queue_head_incr(sd);
            //处理skb的个数达到quota了,说明还有更多数据
            //包需要处理
            if (++work >= quota) {
                local_irq_enable();
                return work;
            }
        }
 
        rps_lock(sd);
        if (skb_queue_empty(&sd->input_pkt_queue)) {
            /*
             * Inline a custom version of __napi_complete().
             * only current cpu owns and manipulates this napi,
             * and NAPI_STATE_SCHED is the only possible flag set
             * on backlog.
             * We can use a plain write instead of clear_bit(),
             * and we dont need an smp_mb() memory barrier.
             */
            //如果input_pkt_queue队列为空,将napi从链表
            //poll_list删除
            list_del(&napi->poll_list);
            napi->state = 0;
            rps_unlock(sd);
 
            break;
        }
        //将input_pkt_queue队列中的skb挂到process_queue
        //上,并清空input_pkt_queue
        skb_queue_splice_tail_init(&sd->input_pkt_queue,
                       &sd->process_queue);
        rps_unlock(sd);
    }
    local_irq_enable();
 
    return work;
}

这里有处理方式比较直观,直接遍历poll_list链表,处理之前设置了两个限制:budget和time_limit。前者限制本次处理数据包的总量,后者限制本次处理总时间。只有二者均有剩余的情况下,才会继续处理。处理期间同样是开中断的,每次总是从链表表头取设备进行处理,如果设备被调度,其实就是检查NAPI_STATE_SCHED位,则调用 napi_struct的poll函数,处理结束如果没有处理完,则把设备移动到链表尾部,否则从链表删除。NAPI设备对应的poll函数会同样会调用__netif_receive_skb函数上传协议栈。

3.6.2 e1000_clean函数 (e1000 网卡NAPI)

下面介绍一下e1000网卡的轮询poll处理函数e1000_clean(),这个函数只有定义了NAPI宏的情况下才有效:

/**
 * e1000_clean - NAPI Rx polling callback
 * @napi: napi struct containing references to driver info
 * @budget: budget given to driver for receive packets
 **/
static int e1000_clean(struct napi_struct *napi, int budget)
{
	struct e1000_adapter *adapter = container_of(napi, struct e1000_adapter,
						     napi);
	int tx_clean_complete = 0, work_done = 0;
 
	tx_clean_complete = e1000_clean_tx_irq(adapter, &adapter->tx_ring[0]);
	//处理网卡中断收到的数据包即 e1000_clean_rx_irq
	adapter->clean_rx(adapter, &adapter->rx_ring[0], &work_done, budget);
 
	if (!tx_clean_complete || work_done == budget)
		return budget;
 
	/* Exit the polling mode, but don't re-enable interrupts if stack might
	 * poll us due to busy-polling
	 */
	if (likely(napi_complete_done(napi, work_done))) {
		if (likely(adapter->itr_setting & 3))
			e1000_set_itr(adapter);
		if (!test_bit(__E1000_DOWN, &adapter->flags))
			e1000_irq_enable(adapter);
	}
 
	return work_done;
}
3.6.3  e1000_clean_rx_irq函数 - 处理网卡中断收到的数据包,核心函数

设备轮询接收机制中最重要的函数就是下面这个函数,当然它同时也可以为中断接收机制所用,只不过处理过程有一定的差别。

上面看到budget -= napi_poll(n, &repoll);他会去调用我们驱动初始化时注册的poll函数,在e1000网卡中就是e1000_clean函数。

/**
 * e1000_clean_rx_irq - Send received data up the network stack; legacy
 * @adapter: board private structure
 * @rx_ring: ring to clean
 * @work_done: amount of napi work completed this call
 * @work_to_do: max amount of work allowed for this call to do
 */
static bool e1000_clean_rx_irq(struct e1000_adapter *adapter,
			       struct e1000_rx_ring *rx_ring,
			       int *work_done, int work_to_do)
{
	struct net_device *netdev = adapter->netdev;
	struct pci_dev *pdev = adapter->pdev;
	struct e1000_rx_desc *rx_desc, *next_rxd;
	struct e1000_rx_buffer *buffer_info, *next_buffer;
	u32 length;
	unsigned int i;
	int cleaned_count = 0;
	bool cleaned = false;
	unsigned int total_rx_bytes = 0, total_rx_packets = 0;
	
	/*把i置为下一个要清除的描述符索引,因为在环形缓冲区队列当中,我们即使已经处理
     *完一个缓冲区描述符,也不是将其删除,而是标记为已经处理,这样如果有新的数据需要
     *使用缓冲区,只是将已经处理的缓冲区覆盖而已。*/
	i = rx_ring->next_to_clean;
	rx_desc = E1000_RX_DESC(*rx_ring, i);
	buffer_info = &rx_ring->buffer_info[i];
	
    /*如果i对应的描述符状态是已经删除,则将这个缓冲区取出来给新的数据使用*/
	while (rx_desc->status & E1000_RXD_STAT_DD) {
		struct sk_buff *skb;
		u8 *data;
		u8 status;
		
		/*在配置了NAPI的情况下,判断是否已经完成的工作?,因为是轮询机制,所以我
        *们必须自己计算我们已经处理了多少数据。*/
		if (*work_done >= work_to_do)
			break;
		(*work_done)++;
		dma_rmb(); /* read descriptor and rx_buffer_info after status DD */
 
		status = rx_desc->status;
		length = le16_to_cpu(rx_desc->length);
 
		data = buffer_info->rxbuf.data;
		prefetch(data);
		/*调用__alloc_skb创建一个sk_buff,
		*dma_sync_single_for_cpu读取DMA数据最新,
		并将data数据拷贝进skb中*/
		skb = e1000_copybreak(adapter, buffer_info, length, data);
		if (!skb) {
			unsigned int frag_len = e1000_frag_len(adapter);
 
			skb = build_skb(data - E1000_HEADROOM, frag_len);
			if (!skb) {
				adapter->alloc_rx_buff_failed++;
				break;
			}
 
			skb_reserve(skb, E1000_HEADROOM);
			/*这个是DMA函数,目的是解除与DMA缓冲区的映射关系,这样我们就可以访问这个
			 *缓冲区,获取通过DMA传输过来的数据包(skb)。驱动程序在分配环形缓冲区的时
			 *候就将缓冲区与DMA进行了映射。*/
			dma_unmap_single(&pdev->dev, buffer_info->dma,
					 adapter->rx_buffer_len,
					 DMA_FROM_DEVICE);
			buffer_info->dma = 0;
			buffer_info->rxbuf.data = NULL;
		}
		...
	
process_skb:
		total_rx_bytes += (length - 4); /* don't count FCS */
		total_rx_packets++;
 
		if (likely(!(netdev->features & NETIF_F_RXFCS)))
			/* adjust length to remove Ethernet CRC, this must be
			 * done after the TBI_ACCEPT workaround above
			 */
			length -= 4;
		/*对接收的数据包检查一下正确性。确认是一个正确的数据包以后,将skb的数据
         *指针进行偏移。*/
		if (buffer_info->rxbuf.data == NULL)
			skb_put(skb, length);
		else /* copybreak skb */
			skb_trim(skb, length);
 
		/* Receive Checksum Offload */
		e1000_rx_checksum(adapter,
				  (u32)(status) |
				  ((u32)(rx_desc->errors) << 24),
				  le16_to_cpu(rx_desc->csum), skb);
 
		e1000_receive_skb(adapter, status, rx_desc->special, skb);
 
next_desc:
		rx_desc->status = 0;
 
		/* return some buffers to hardware, one at a time is too slow */
		if (unlikely(cleaned_count >= E1000_RX_BUFFER_WRITE)) {
			adapter->alloc_rx_buf(adapter, rx_ring, cleaned_count);//e1000_alloc_rx_buffers
			cleaned_count = 0;
		}
 
		/* use prefetched values */
		rx_desc = next_rxd;
		buffer_info = next_buffer;
	}
	rx_ring->next_to_clean = i;
	...
	return cleaned;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值