学习Linux-4.12内核网路协议栈(2.2)——接口层数据包的接收(上半部)

时间:2021-10-15 11:03:23

前面写了这么多,终于可以开始分析数据报的传输过程了,那我们就愉快的开始吧!
我们知道,一个中断处理函数主要分两个部分,上半部和下半部,这篇文章主要介绍上半部分。

当一个数据包到达的时候,网卡驱动会完成接收并且触发中断,我们就从这个中断处理函数开始:

学习Linux-4.12内核网路协议栈(2.2)——接口层数据包的接收(上半部)

当一个中断产生并发送给CPU的时候,对于NAPI和不支持NAPI的设备来说处理结果是不一样的,NAPI调用的函数是napi_schedule,非NAPI调用的函数是netif_rx,这两个函数都是在网卡驱动的中断处理函数上半部分被调用的。

产生中断的每个设备都有一个相应的中断处理程序,是设备驱动程序的一部分。

每个网卡都有一个中断处理程序,用于通知网卡该中断已经被接收了,以及把网卡缓冲区的数据包拷贝到内存中。当网卡接收来自网络的数据包时,需要通知内核数据包到了。网卡立即发出中断:嗨,内核,我这里有最新的数据包了。内核通过执行网卡已注册的中断处理函数来做出应答。

中断处理程序开始执行,通知硬件,拷贝最新的网络数据包到内存,然后读取网卡更多的数据包。

这些都是重要、紧迫而又与硬件相关的工作。内核通常需要快速的拷贝网络数据包到系统内存,因为网卡上接收网络数据包的缓存大小固定,而且相比系统内存也要小得多。所以上述拷贝动作一旦被延迟,必然造成网卡缓存溢出 - 进入的数据包占满了网卡的缓存,后续的包只能被丢弃。当网络数据包被拷贝到系统内存后,中断的任务算是完成了,这时它把控制权交还给被系统中断前运行的程序,处理和操作数据包的其他工作在随后的下半部中进行。

我们现在知道了不管是否支持NAPI,对于驱动来说无非是调用napi_schedule或者netif_rx来通知内核,将数据包交给内核。所以如果不知道驱动使用的中断处理程序是哪个,那么只要搜索一下这两个函数就能定位出来了。下面我们来分析一下这两个函数,因为NAPI是基于前者发展出来的,所以我们先了解netif_rx。


一、非NAPI (netif_rx)

/**
 *  netif_rx    -   post buffer to the network code
 *  @skb: buffer to post
 *
 *  This function receives a packet from a device driver and queues it for
 *  the upper (protocol) levels to process.  It always succeeds. The buffer
 *  may be dropped during processing for congestion control or by the
 *  protocol layers.
 *
 *  return values:
 *  NET_RX_SUCCESS  (no congestion)
 *  NET_RX_DROP     (packet was dropped)
 *
 */
int netif_rx(struct sk_buff *skb)
{
trace_netif_rx_entry(skb);

return netif_rx_internal(skb);
}

static int netif_rx_internal(struct sk_buff *skb){    int ret;    net_timestamp_check(netdev_tstamp_prequeue, skb); //记录接收时间到skb->tstamp    trace_netif_rx(skb);#ifdef CONFIG_RPS    if (static_key_false(&rps_needed)) {        struct rps_dev_flow voidflow, *rflow = &voidflow;        int cpu;        preempt_disable();        rcu_read_lock();        cpu = get_rps_cpu(skb->dev, skb, &rflow); //如果有支持rps,则获取这个包交给了哪个cpu处理        if (cpu < 0)            cpu = smp_processor_id(); //如果上面获取失败,则用另外一种方式获取当前cpu的id        ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail); //调用该函数将包添加到queue->input_pkt_queue里面        rcu_read_unlock();        preempt_enable();    } else#endif    {        unsigned int qtail;        ret = enqueue_to_backlog(skb, get_cpu(), &qtail);        put_cpu();    }    return ret;}

这个函数最后调用enqueue_to_backlog将包添加到queue->input_pkt_queue的尾部,这个input_pkt_queue是每个cpu都有的一个队列,如果不够清楚它的作用,可以看看前面一篇文章的截图,这个队列的初始化在net_dev_init()中完成:

8568     for_each_possible_cpu(i) {
8569 struct work_struct *flush = per_cpu_ptr(&flush_works, i);
8570 struct softnet_data *sd = &per_cpu(softnet_data, i);
8571
8572 INIT_WORK(flush, flush_backlog);
8573
8574 skb_queue_head_init(&sd->input_pkt_queue);
8575 skb_queue_head_init(&sd->process_queue);
8576 INIT_LIST_HEAD(&sd->poll_list);
8577 sd->output_queue_tailp = &sd->output_queue;
8578 #ifdef CONFIG_RPS
8579 sd->csd.func = rps_trigger_softirq;
8580 sd->csd.info = sd;
8581 sd->cpu = i;
8582 #endif
8583
8584 sd->backlog.poll = process_backlog;
8585 sd->backlog.weight = weight_p;
8586 }

...
    open_softirq(NET_TX_SOFTIRQ, net_tx_action);
    open_softirq(NET_RX_SOFTIRQ, net_rx_action);

 

现在我们来看看enqueue_to_backlog函数怎么将包添加到queue->input_pkt_queue尾部的:

/*
* enqueue_to_backlog is called to queue an skb to a per CPU backlog
* queue (may be a remote CPU 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;

sd = &per_cpu(softnet_data, cpu); //获取当前cpu的softnet_data对象

local_irq_save(flags); //保存中断状态

rps_lock(sd);
if (!netif_running(skb->dev)) //确认net_device的dev->state是不是__LINK_STATE_START状态,如果该网络设备没有运行,直接退出,不进行包的处理
goto drop;
qlen = skb_queue_len(&sd->input_pkt_queue); //获取input_pkt_queue的当前长度
if (qlen <= netdev_max_backlog && !skb_flow_limit(skb, qlen)) { //如果当前长度小于最大长度,而且满足流量限制的要求
if (qlen) {
enqueue:
__skb_queue_tail(&sd->input_pkt_queue, skb); //关键在这里,将SKB添加到input_pkt_queue队列的后面
input_queue_tail_incr_save(sd, qtail); //队列尾部指针加1
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
         */
        if (!__test_and_set_bit(NAPI_STATE_SCHED, &sd->backlog.state)) {
            if (!rps_ipi_queued(sd))
                ____napi_schedule(sd, &sd->backlog); //把虚拟设备backlog添加到sd->poll_list中以便进行轮询,最后设置NET_RX_SOFTIRQ标志触发软中断
        }
        goto enqueue;
    }

drop:
    sd->dropped++; /* 如果接收队列满了就直接丢弃 */
    rps_unlock(sd);

    local_irq_restore(flags); /* 恢复本地中断 */

    atomic_long_inc(&skb->dev->rx_dropped);
    kfree_skb(skb);
    return NET_RX_DROP;
}

 

在非NAPI中,我们只要将skb添加到input_pkt_queue就可以了吗?我们要看到最后,它将backlog添加到了sd->poll_list里面,并且调用__napi_schedule()触发软中断。我们还记得,在协议栈初始化的时候,net_dev_init()有初始化软中断:

    open_softirq(NET_TX_SOFTIRQ, net_tx_action);
    open_softirq(NET_RX_SOFTIRQ, net_rx_action);

所以接下来,软中断会执行net_rx_action 函数。这个部分我们放到下篇文章数据包接收的下半部里面分析。

网络数据包在上半部的处理通常有两种模式:传统的netif_rx模式和NAPI(napi_schedule)模式,在这里我们主要讨论网络上半部的内容,无论上半部采用何种收包模式,都会调用__netif_rx_schedule()函数,netif_receive_skb函数会根据不同的协议调用不同的协议处理函数。


二、 NAPI(napi_schedule)

在分析NAPI前, 我们先来看看网卡驱动是怎么调用NAPI的函数的:

2235     if (likely(napi_schedule_prep(&nic->napi))) { //设置state为NAPI_STATE_SCHED
2236 e100_disable_irq(nic);
2237 __napi_schedule(&nic->napi); //将设备添加到 poll list,并开启软中断。
2238 }


/**
* napi_schedule - schedule NAPI poll
* @n: NAPI context
*
* Schedule NAPI poll routine to be called if it is not already
* running.
*/
static inline void napi_schedule(struct napi_struct *n)
{
if (napi_schedule_prep(n)) //确定设备处于运行,而且设备还没有被添加到网络层的POLL 处理队列中
__napi_schedule(n);
}

/**
 * __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);
}
EXPORT_SYMBOL(__napi_schedule);

/* 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); //将设备添加到poll队列
    __raise_softirq_irqoff(NET_RX_SOFTIRQ); //触发软中断
}
 

到这里可以看出,它间设备添加到poll队列以后触发了软中断,我们还记得在net_dev_init()里面我们注册了软中断的处理函数 net_rx_action,所以后面文章我们分析软中断处理函数net_rx_action.

学习Linux-4.12内核网路协议栈(2.2)——接口层数据包的接收(上半部)


到这里可以得出的结论是:无论是NAPI接口还是非NAPI最后都是使用 net_rx_action 作为软中断处理函数。也就是中断的上半部分虽然有所不一样,但是他们下半部分的入口的是由net_rx_action,我们下篇文章将从这个函数开始分析。

内核接收分组理解