前面写了这么多,终于可以开始分析数据报的传输过程了,那我们就愉快的开始吧!
我们知道,一个中断处理函数主要分两个部分,上半部和下半部,这篇文章主要介绍上半部分。
当一个数据包到达的时候,网卡驱动会完成接收并且触发中断,我们就从这个中断处理函数开始:
当一个中断产生并发送给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.
到这里可以得出的结论是:无论是NAPI接口还是非NAPI最后都是使用 net_rx_action 作为软中断处理函数。也就是中断的上半部分虽然有所不一样,但是他们下半部分的入口的是由net_rx_action,我们下篇文章将从这个函数开始分析。