目录

1 NAPI 机制

1.1 NAPI 缺陷

1.2 使用 NAPI 先决条件

1.3 非NAPI帧的接收

1.3.1 netif_rx - 将网卡中收到的数据包放到系统中的接收队列中

1.3.2 enqueue_to_backlog

1.3.3 ____napi_schedule

1.4 NAPI方式

1.4.1 NAPI帧的接收

1.5 NAPI接口

1.5.1 struct napi_struct结构(内核处理软中断的入口)

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

1.5.3 __napi_schedule函数(网卡硬件中断用来触发软中断)

1.5.4 napi_schedule_prep(对napi_struct进行检查)

1.5.5 napi_poll(用于调用收包poll函数)

1.5.6 napi_gro_receive(poll函数用来将网卡上的数据包发给协议栈处理)

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

2.1 e1000_probe(E1000网卡的初始化函数)

2.2 e1000_open(E1000网卡驱动程序的open方法)

2.3 e1000_setup_rx_resources(为其环形缓冲区分配资源)

2.4 request_irq(向系统申请irq中断号,中断处理函数 e1000_intr)

2.5 net_rx_action函数(软中断处理函数)

2.5.1 激活软中断(ixgbe 网卡调用栈)

2.5.2 执行软中断(ixgbe 网卡调用栈)

2.6 poll 函数

2.6.1 process_backlog(非NAPI)

2.6.2 e1000_clean(e1000网卡 NAPI)

2.6.3 e1000_clean_rx_irq(处理e1000网卡中断收到的数据包,核心)


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

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

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

1 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特性的添加不会影响内核的向后兼容性。

1.1 NAPI 缺陷

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

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

1.2 使用 NAPI 先决条件

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

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

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

我们都知道中断分为中断上半部和下半部,上半部完成的任务很是简单,仅仅负责把数据保存下来;而下半部负责具体的处理。为了处理下半部,每个CPU有维护一个softnet_data结构(定义在netdevice.h文件中,下文将进行讲解)。我们不对此结构做详细介绍,仅仅描述和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_RPSstruct softnet_data    *rps_ipi_list;
#endif
#ifdef CONFIG_NET_FLOW_LIMITstruct sd_flow_limit __rcu *flow_limit;
#endifstruct Qdisc      *output_queue;struct Qdisc      **output_queue_tailp;struct sk_buff     *completion_queue;
#ifdef CONFIG_XFRM_OFFLOADstruct 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;
#endifunsigned int      dropped;struct sk_buff_head input_pkt_queue;struct napi_struct  backlog;};

1.3 非NAPI帧的接收

我们将讨论内核在接收一个数据帧后的大致处理流程,不会详细叙述所有细节。我们认为有必要先了解一下传统的数据包处理流程以便更好的理解NAPI和传统收包方式的区别。

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

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

1.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;
}

1.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_RPSstruct 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;
}

1.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队列中传递给了网络协议栈,现在数据包可以被处理了。

1.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 条件检查当前设备是否 可被调度,主要检查两个方面:

  • 是否已经在调度
  • 是否禁止了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每次都是通过中断通知。

1.4.1 NAPI帧的接收

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

图 - NAPI frame reception

因此网卡支持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 来配置)

3. 轮询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 函数传递到协议栈中去。

抢占策略是依赖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会超过quantum,此时会导致一个轮询周期的时间变长。

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

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模型中数据包接收处理过程的时序图:

图 - 非NAPI模型的时序图

                                     图 - NAPI模型的时序图

1.5 NAPI接口

1.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_NETPOLLint            poll_owner;
#endifstruct 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;
};

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

1.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_NETPOLLnapi->poll_owner = -1;
#endifset_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);

1.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); //设置了软中断接收标志位
}

1.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的接口函数吧。

1.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;
}

1.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。

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

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

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

2.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,
};

2.2 e1000_open(E1000网卡驱动程序的open方法)

在分析网卡接收数据包的过程中,设备的 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 缓冲区的接收状态。

2.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;
}

2.4 request_irq(向系统申请irq中断号,中断处理函数 e1000_intr)

在 e1000_open() 函数中,调用e1000_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;
}

2.5 net_rx_action函数(软中断处理函数)

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

static void net_rx_action(struct softirq_action *h)
{//获取percpu的sdstruct 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需要减去workbudget -= 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_RPSstruct 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
#endiflocal_irq_enable();
}

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

2.5.1 激活软中断(ixgbe 网卡调用栈)

//硬件中断到来时调用中断处理函数 ixgbe_msix_clean_rings
ixgbe_msix_clean_ringsnapi_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);

2.5.2 执行软中断(ixgbe 网卡调用栈)

__do_softirqnet_rx_actionn = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);work = n->poll(n, weight); //即调用 ixgbe_pollixgbe_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_SCHEDnapi_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;}}
#endifret = __netif_receive_skb(skb);rcu_read_unlock();return ret;
}

2.6 poll 函数

2.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);}
#endifnapi->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_queueskb_queue_splice_tail_init(&sd->input_pkt_queue,&sd->process_queue);rps_unlock(sd);}local_irq_enable();return work;
}

函数还是比较简单的,需要注意的每次处理都携带一个配额,即本次只能处理quota个数据包,如果超额了,即使没处理完也要返回,这是为了保证处理器的公平使用。处理在一个while循环中完成,循环条件正是work < quota,首先会从process_queue中取出skb,调用__netif_receive_skb上传给协议栈,然后增加work。当work即将大于quota时,即++work >= quota时,就要返回。当work还有剩余额度,但是 process_queue 中数据处理完了,就需要检查input_pkt_queue,因为在具体处理期间是开中断的,那么期间就有可能有新的数据包到来,如果input_pkt_queue不为空,则调用s kb_queue_splice_tail_init 函数把数据包迁移到 process_queue。如果剩余额度足够处理完这些数据包,那么就把虚拟设备移除轮询队列。这里有些疑惑就是最后为何要增加额度,剩下的额度已经足够处理这些数据了呀?根据此流程不难发现,其实执行的是在两个队列之间移动数据包,然后再做处理。

2.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_irqadapter->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;
}

2.6.3 e1000_clean_rx_irq(处理e1000网卡中断收到的数据包,核心)

设备轮询接收机制中最重要的函数就是下面这个函数,当然它同时也可以为中断接收机制所用,只不过处理过程有一定的差别。上面看到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_bufferscleaned_count = 0;}/* use prefetched values */rx_desc = next_rxd;buffer_info = next_buffer;}rx_ring->next_to_clean = i;...return cleaned;
}

linux 内核协议栈 NAPI机制与处理流程分析(图解)相关推荐

  1. Linux网络协议栈:NAPI机制与处理流程分析(图解)

    Table of Contents NAPI机制 NAPI缺陷 使用 NAPI 先决条件 非NAPI帧的接收 netif_rx - 将网卡中收到的数据包放到系统中的接收队列中 enqueue_to_b ...

  2. linux内核协议栈 邻居协议之 arp 数据包收发处理流程

    目录 前言 1 arp数据包文接收 arp_rcv() 1.1 处理arp请求 arp_process()[核心] 2 arp数据包发送 arp_send() 2.1 arp 数据包构造 arp_cr ...

  3. Linux内核IP Queue机制的分析(一)

    将会通过包括本文在内的三篇文章,对IP Queue机制从用户态的应用到内核态的模块程序设计进行分析.三篇文章的题目分别是: Linux内核IP Queue机制的分析(一)--用户态接收数据包 Linu ...

  4. linux收发包内核进程名称,Linux内核IP Queue机制的分析(一)——用户态接收数据包...

    序 笔者将会通过包括本文在内的三篇文章,对IP Queue机制从用户态的应用到内核态的模块程序设计进行分析.三篇文章的题目分别是: Linux内核IP Queue机制的分析(一)­--用户态接收数据包 ...

  5. Linux内核延迟写机制学习

    Linux内核延迟写机制 Linux内核延迟写的特点,是指在Linux通过write的场景下写入数据之后,会将数据直接标记为dirty,然后通过延迟读写的方式最后将数据回写到磁盘上.在本文的Linux ...

  6. Linux内核中断处理“下半部”机制(超详细~)

    Linux内核中断处理"下半部"机制(超详细~) ///插播一条:我自己在今年年初录制了一套还比较系统的入门单片机教程,想要的同学找我拿就行了免費的,私信我就可以哦~点我头像黑色字 ...

  7. linux内核协议栈 TCP层数据发送之TSO/GSO

    目录 1 基本概念 2 TCP延迟分段判定 2.1 客户端初始化 2.2 服务器端初始化 2.3 sk_setup_caps() 3 整体结构 4. TCP发送路径TSO处理 4.1 tcp_send ...

  8. Linux内核中锁机制之完成量、互斥量

    在上一篇博文中笔者分析了关于信号量.读写信号量的使用及源码实现,接下来本篇博文将讨论有关完成量和互斥量的使用和一些经典问题. 八.完成量 下面讨论完成量的内容,首先需明确完成量表示为一个执行单元需要等 ...

  9. Linux内核抢占实现机制分析【转】

    Linux内核抢占实现机制分析 转自:http://blog.chinaunix.net/uid-24227137-id-3050754.html [摘要]本文详解了Linux内核抢占实现机制.首先介 ...

最新文章

  1. ContentProvider是如何实现数据共享的
  2. HDU.3177Crixalis's Equipment(贪心)
  3. 关于MYSQL中like 检索汉字问题。
  4. mysql proxy 延迟严重_使用MySQL Proxy解决MySQL主从同步延迟
  5. 关于.NET5在IIS中部署的几个问题总结
  6. oracle 9i 只读模式,我的oracle 9i学习日志(6)--Starting Up and shutting down a Database
  7. SQL老司机,居然是这样智能挖掘异常日志
  8. SQL 全文索引 CONTAINS
  9. css 背景重复渐变_CSS3重复渐变[CSS3提示]
  10. Carboxyrhodamine 110-PEG4-DBCO,羧罗丹明110-PEG4-DBCO是一种荧光标记染料
  11. 机械硬盘低级格式化软件_西数硬盘专用修复工具_WD HDD Repair Tool|西部数据硬盘修复工具 V3.6 中文版 - 偶要下载站...
  12. C++工作笔记-32位和64位程序的区别
  13. 根据自定义类属性导出Excel
  14. js实现图片粘贴功能
  15. C语言代码程序运行不出
  16. C#接口定义,索引器的定义
  17. 飞思卡尔XS128的基本模板程序
  18. 湖南大学计算机科学课表,计算机科学志技术专业课程表
  19. zblog php伪静态,zblog php 伪静态设置详解
  20. alfresco 介绍 docker安装

热门文章

  1. 网络OSI(七层模型)
  2. 【IoT开发】D3引擎升级啦!速速来体验机智云新版智能场景推送
  3. VMware中ubuntu设置成中文
  4. Python3断网离线安装依赖包
  5. NAACL'22 Findings | 社交媒体上的抱怨强度分析
  6. 中文字符编码之GBK,UTF-16和UTF-8
  7. 计算机专业座谈会问题,我院计算机专业开展专业抽检主题座谈会
  8. Android开发之CardView卡片布局
  9. javascript构造函数
  10. Navicat for MySQL 连接 MySQL 报2005错误