1. 引入问题
内核收包主要有两种手段:轮询和中断。
通过轮询,内核可以不断持续的检查设备时候有包收上来,例如设置一个定时器,定期检查设备上的某个定时器。这种方法会轻易浪费掉很多系统资源。
如果采用中断收包,当设备收到包时,可以产生一个硬件中断通知内核,内核将中断其他活动,然后调用一个中断处理程序以满足设备的需求,内核只是将数据包放到某个队列中并通知内核中的收包模块。这种方式是非常常见的,在低流量负载下是很好的选择,但是在高流量负载下就无法良好的运行,每接收一个帧就产生一个中断,很快就会让CPU为处理中断而浪费所有的时间。
以太网驱动收包就是通过以太网设备产生收包中断通知内核来收包的,但是如上所述,不能每收一个帧都要产生一个中断,下面的内容将介绍驱动中如何结合论需和中断来收包。
2. 几个关键函数
2.1 netif_receive_skb()
该函数是内核收包的入口,驱动收到的数据包通过这个函数进入内核协议栈进行处理,我在这里不会分析它的实现,只要记住,接下来的几种驱动收包方式最终都是为了将数据包送到这个函数。
2.2 net_rx_action()
收包软中断处理函数,即中断下半部。中断处理函数要求尽可能快的执行完成,内核为了快速响应中断,在处理硬件中断时,只是将数据包放到CPU的某个队列中去,并调度软中断。而实际的数据包处理过程则交给中断下半部处理。
中断下半部的处理可以通过软中断或tasklet来完成:
1. 软中断:内核中定义好了一个收包软中断处理函数net_rx_action(),后面会分析该函数。
open_softirq(NET_RX_SOFTIRQ,net_rx_action);
2. tasklet:使用tasklet_init(t, func, data)注册你自己的下半部收包函数。
工作队列也可以实现延期执行一个函数,但网络代码中主要使用的是软中断和tasklet,所以我们不考虑工作队列。
2.3 dma_alloc_coherent()
函数原型为:
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t*dma_handle, gfp_t gfp);
该函数用于分配一个DMA一致性缓冲区。大多数以太网设备都支持DMA机制,设备收到数据包后,DMA将其放入内存中,并产生一个收包中断通知CPU从内存中拿走数据包。
DMA只能识别物理地址,而OS是操作虚拟地址的,这就需要缓存数据包的区域可以让DMA和OS都能操作。dma_alloc_coherent()函数就是为了达到这个目标,它分配size大小的一致性内存,其物理起始地址存放在dma_handle中,函数返回值为这段内存的虚拟起始地址,这样,设备向这块地址放数据包,OS响应中断后可以从这块地址拿包。
而由于我们要分配一致性内存(任何时候,cache中的内容和内存中的内容是相同的),所以返回的虚拟地址尽量是非缓存的,例如在mips中这个虚拟地址就是KSEG1中的地址。
3. 旧的收包接口netif_rx
在设备驱动在DMA中拿到一个数据包,做一些和设备相关的处理后,初始化一个skb实例,就可以将数据包交给netif_rx()来处理了。
内核中定义了全局的per-cpu收包队列softnet_data,其定义如下:
DEFINE_PER_CPU_ALIGNED(struct softnet_data, softnet_data);
EXPORT_PER_CPU_SYMBOL(softnet_data);
结构体struct softnet_data的定义:
/*
* Incoming packets are placed on per-cpuqueues
*/
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;
/* 统计数据 */
unsigned int processed;
unsigned int time_squeeze;
unsigned int cpu_collision;
unsigned int received_rps;
unsigned dropped; //被丢弃的包的数量
struct sk_buff_head input_pkt_queue; //收包队列
struct napi_struct backlog; //处理积压队列的napi结构
};
对于收包来讲,需要用到的成员已经给出注释。struct softnet_data结构体中,poll_list是NAPI设备列表。input_pkt_queue为per-cpu的收包队列。backlog是默认的处理收包的napi设备。
我们看到,NAPI的框架已经被整合到内核中,即使不使用NAPI机制,内核中也有其他地方使用napi_struct等相关结构。因此这里需要先说明一下napi结构:
struct napi_struct {
/* 链表指针,用于挂在softnet_data上 */
struct list_head poll_list;
/* 此NAPI设备当前的状态 */
unsigned long state;
/* 一个权重值,每次调度NAPI可处理数据包个数的限制 */
int weight;
/* poll函数,用于实际来处理数据包 */
int (*poll)(structnapi_struct *, int);
……
};
netif_rx()函数中主要是调用enqueue_to_backlog()将skb放入per-cpu的收包队列中去。
static intenqueue_to_backlog(struct sk_buff *skb, int cpu,