前言
上一篇博客我们分析了普通的中断方式接收数据包的流程。并且在分析流程和代码的过程中看到napi的入口函数(napi_schedule)。当时提到了该函数,大家应该还有印象。我们这里再次把代码截取一份,来回顾一下调用的地方。
__IRAM_GEN static inline void rtl_rx_interrupt_process(unsigned int status, struct dev_priv *cp)
{
#if defined(REINIT_SWITCH_CORE)
if(rtl865x_duringReInitSwtichCore==1) {
return;
}
#endif
#ifdef CONFIG_RTL_8197F
if (status & (RX_DONE_IP_ALL | PKTHDR_DESC_RUNOUT_IP_ALL))
#else
//if (status & (RX_DONE_IP_ALL | PKTHDR_DESC_RUNOUT_IP_ALL))
#endif
{
#if defined(CONFIG_RTL_ETH_NAPI_SUPPORT)
if ((rtl_rx_tasklet_running==0)&&(cp->napi.poll)) {
rtl_rx_tasklet_running=1;
REG32(CPUIIMR) &= ~(RX_DONE_IE_ALL | PKTHDR_DESC_RUNOUT_IE_ALL);
rtl_rxSetTxDone(FALSE);
napi_schedule(&cp->napi);
}
#else
#if defined(RX_TASKLET)
if (rtl_rx_tasklet_running==0) {
rtl_rx_tasklet_running=1;
REG32(CPUIIMR) &= ~(RX_DONE_IE_ALL | PKTHDR_DESC_RUNOUT_IE_ALL);
rtl_rxSetTxDone(FALSE);
tasklet_hi_schedule(&cp->rx_dsr_tasklet);
}
#else
interrupt_dsr_rx((unsigned long)cp);
#endif
#endif
}
}
数据包在中断下半部即tasklet中有两个分支。分别是普通接收方式和NAPI接收方式。我们来看看连一个分支,即napi_schedule函数的处理
根据上一篇博客,我们可以在这里来做一个总结。非NAPI的内核接口为netif_rx(),NAPI的内核接口为napi_schedule()。只不过在这里netif_rx被再次封装了。
NAPI流程分析
napi主要涉及到3个函数:
- netif_napi_add(NAPI初始化函数,一般在网卡初始化的时候会调用)
- napi_schedule(该函数是napi的调度函数,一般在接受网卡数据包的时候调用-我们在上一篇博客中已经看到了该函数在接收中断下半部被调用)
- napi_complete(该函数是一个状态机。当数据包不是很多的时候,用来告诉内核,后续采用纯中断的方式进行接收)
下面我们就来结合实际的驱动程序和接收程序来分析这三个函数以及具体的流程。
这里涉及到了一个结构体,即struct napi_struct(该结构是NAPI的重要数据结构,对于理解NAPI的流程很有帮助)。
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;
unsigned int gro_count;
int (*poll)(struct napi_struct *, int);
#ifdef CONFIG_NETPOLL
spinlock_t poll_lock;
int poll_owner;
#endif
struct net_device *dev;
struct sk_buff *gro_list;
struct sk_buff *skb;
struct list_head dev_list;
};
在这里我们来对该结构体一些重要的值进行说明。
- poll_list: 这是设备列表,其中的设备就是在入口队列中有新的帧等待被处理的设备。这些设备就是所谓的处于轮训状态。次列表的头问softnet_data->poll_list。在此列表中的设备都处于中断功能关闭的状态,而内核当前正在轮训。
2)weight:该字段代表每次调用poll函数的时候分配的最大的skb的数量。或者可以理解为从DMA中每次可以获取的最大的skb的数量。对于NAPI程序而言,默认的值为64。但是也有16和32。它可以通过sysfs来调整。 - poll:该函数指针代表的是轮训的具体方法。
netif_napi_add函数
该函数是为了初始化struct napi_struct结构体。因为在后续的接收程序中会调用napi的接收程序,即napi_schedule。napi_schedule的参数就是struct napi_struct。
#if defined(CONFIG_RTL_ETH_NAPI_SUPPORT)
netif_napi_add(dev, &cp->napi, rtl865x_poll, RTL865X_NAPI_WEIGHT);
napi_enable(&cp->napi);
#else
#if defined(RX_TASKLET)
tasklet_init(&cp->rx_dsr_tasklet, (void *)interrupt_dsr_rx, (unsigned long)cp);
#endif
从上面的这段代码中我们可以看到如果是支持NAPI的方式即调用netif_napi_add函数和napi_enable函数。说明:该代码是网卡open函数中的代码,正如上文所说,该函数一般在网卡初始化的时候调用。所以大家以后查找该函数就在网卡初始化代码中查找即可
netif_napi_add(dev, &cp->napi, rtl865x_poll, RTL865X_NAPI_WEIGHT);
void netif_napi_add(struct net_device *dev, struct napi_struct *napi,
int (*poll)(struct napi_struct *, int), int weight)
{
INIT_LIST_HEAD(&napi->poll_list);
napi->gro_count = 0;
napi->gro_list = NULL;
napi->skb = NULL;
napi->poll = poll;
if (weight > NAPI_POLL_WEIGHT)
pr_err_once("netif_napi_add() called with weight %d on device %s\n",
weight, dev->name);
napi->weight = weight;
list_add(&napi->dev_list, &dev->napi_list);
napi->dev = dev;
#ifdef CONFIG_NETPOLL
spin_lock_init(&napi->poll_lock);
napi->poll_owner = -1;
#endif
set_bit(NAPI_STATE_SCHED, &napi->state);
}
在调用该函数的时候传递了4个参数。分别是网卡对应的dev,和struct napi_struct,以及轮训处理函数,和最大能够处理的skb的个数。
说明:从上面的代码我们可知,每一个网卡都有一个napi_struct结构。都需要注册自己的处理函数(轮训函数),以及指定最大skb的个数(64)。#define RTL865X_NAPI_WEIGHT 64
napi_schedule函数
我们先来看看napi_schedule函数的源代码
static inline void napi_schedule(struct napi_struct *n)
{
if (napi_schedule_prep(n))
__napi_schedule(n);
}
该函数调用napi_schedule_prep来判断是否能够运行napi。如果napi没有被禁止且没有被调度则调用__napi_schedule
void __napi_schedule(struct napi_struct *n)
{
unsigned long flags;
local_irq_save(flags);
____napi_schedule(&__get_cpu_var(softnet_data), n);
local_irq_restore(flags);
}
1)保存中断
2)调用____napi_schedule函数,注意该函数的参数
3)恢复中断
这里的____napi_sule函数是一个重点函数,可以看到napi方式接收程序底层最主要调用的就是该函数。
我们首先来分析一下__get_cpu_var(softnet_data)。该函数是从cup中获取softnet_data结构体指针。获取当前CPU上的待轮询设备队列。
softnet_data结构
每一个cpu都有队列,用来接收进来的数据帧。因为每个cpu都有其数据结构来处理入口和出口的流量,因此,不同的cpu之间没有必要使用上锁机制。此队列就是用sodtnet_data来表示
struct softnet_data {
struct Qdisc *output_queue;
struct Qdisc **output_queue_tailp;
struct list_head poll_list;
struct sk_buff *completion_queue;
struct sk_buff_head process_queue;
/* stats */
unsigned int processed;
unsigned int time_squeeze;
unsigned int cpu_collision;
unsigned int received_rps;
#ifdef CONFIG_RPS
struct softnet_data *rps_ipi_list;
/* Elements below can be accessed between CPUs for RPS */
struct call_single_data csd ____cacheline_aligned_in_smp;
struct softnet_data *rps_ipi_next;
unsigned int cpu;
unsigned int input_queue_head;
unsigned int input_queue_tail;
#endif
unsigned int dropped;
//下面两个接口旧的接口,netif_rx 会把skb挂到input_packet_queue上
struct sk_buff_head input_pkt_queue;
struct napi_struct backlog;
};
- output_queue和output_queue_tailp:代表的是队列的策略,流控会使用到该数据结构(流控一般是在出口做流量控制)
- poll_list:是一个双向列表,其中的设备都带有输入帧等着被处理
- input_pkt_queue:该队列(在net_dev_init中初始化) 用来保存进来的数据帧(被驱动程序处理前)。非NAPI驱动程序也会使用此字段,那些还没有更新为使用NAPI的则会使用其自己的私有队列
那么这里衍生出另一个问题,该结构体在什么时候初始化的呢
static int __init net_dev_init(void)
{
int i, rc = -ENOMEM;
BUG_ON(!dev_boot_phase);
if (dev_proc_init())
goto out;
if (netdev_kobject_init())
goto out;
INIT_LIST_HEAD(&ptype_all);
for (i = 0; i < PTYPE_HASH_SIZE; i++)
INIT_LIST_HEAD(&ptype_base[i]);
INIT_LIST_HEAD(&offload_base);
if (register_pernet_subsys(&netdev_net_ops))
goto out;
/*
* Initialise the packet receive queues.
*/
for_each_possible_cpu(i) {
struct softnet_data *sd = &per_cpu(softnet_data, i);
memset(sd, 0, sizeof(*sd));
skb_queue_head_init(&sd->input_pkt_queue);
skb_queue_head_init(&sd->process_queue);
sd->completion_queue = NULL;
INIT_LIST_HEAD(&sd->poll_list);
sd->output_queue = NULL;
sd->output_queue_tailp = &sd->output_queue;
#ifdef CONFIG_RPS
sd->csd.func = rps_trigger_softirq;
sd->csd.info = sd;
sd->csd.flags = 0;
sd->cpu = i;
#endif
sd->backlog.poll = process_backlog;
sd->backlog.weight = weight_p;
sd->backlog.gro_list = NULL;
sd->backlog.gro_count = 0;
}
dev_boot_phase = 0;
/* The loopback device is special if any other network devices
* is present in a network namespace the loopback device must
* be present. Since we now dynamically allocate and free the
* loopback device ensure this invariant is maintained by
* keeping the loopback device as the first device on the
* list of network devices. Ensuring the loopback devices
* is the first device that appears and the last network device
* that disappears.
*/
if (register_pernet_device(&loopback_net_ops))
goto out;
if (register_pernet_device(&default_device_ops))
goto out;
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
hotcpu_notifier(dev_cpu_callback, 0);
dst_init();
rc = 0;
out:
return rc;
}
在这里我们着重强调一下该函数。该函数是在内核加载的时候被调用的。所以不需要我们手动调用,驱动也不会去调用。在非NAPI的方式中,我们已经分析了一部分代码。包括软中断的注册。这里又进行了softnet_data结构的初始化。
接下来我们继续分析____napi_schedule函数
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);
}
这里会触发NET_RX_SOFTIRQ软中断。即调用到了net_rx_action函数。这个函数是不是很熟悉,没错该函数和我们上一篇博客讲的非NAPI方式一样了。
那么在上文注册的rtl865x_poll函数什么时候调用呢.rtl865x_poll函数已经加入到了napi->poll = poll中.
我们再贴一次net_rx_action的代码
static void net_rx_action(struct softirq_action *h)
{
struct softnet_data *sd = &__get_cpu_var(softnet_data);
unsigned long time_limit = jiffies + 2;
int budget = netdev_budget;
void *have;
local_irq_disable();
while (!list_empty(&sd->poll_list)) {
struct napi_struct *n;
int work, weight;
可以看到从cpu从取出了softnet_data结构,然后调用了其中的poll_list函数。
我们按照上文继续贴出流程图
总结
非NAPI的方式和NAPI的方式在接收中断函数中产生出分支,在net_rx_action又回归到一条主线上。
欢迎大家加入qq群:610849576。个人知识有限,只有多讨论,多思考,才可以进步,欢迎大家一起学习
- 下一讲我们来讲解连接跟踪,然后基于连接跟踪写出自己的内核模块,增加协议栈转发速率(实测内网可以从200M提高的680M)。敬请期待