Linux网络系统底层机制分析(3)
----报文接收
前面介绍了两种接口:老接口,NAPI接口。两者在报文接收的处理机制是不一样的,下面结合内核中实际的驱动仔细分析。
1.sk_buff结构
sk_buff结构可能是贯穿整个子系统的结构,它表明了接收到的或者要发送的数据的一些重要信息。其包含的域非常多,定义在include/linux/skbuff..h中。内核利用其next,prev指针把所有的sk_buff结构组织成一个双向链表,但其结构比一般的传统链表要复杂得多。为保证每一个sk_buff都能快速地找到链表头,另外加了一个类似的结构:sk_buff_head来表明整个链表的表头,另外每一个sk_buff有一个list域指向此头结构。在sk_buff_head中有一个锁防止并发(但是锁整个链表是不是太大了?)。
其他的重要的域有以下几个(参考《Understanding LINUX network internal》P22):
Struct sock *sk 指向拥有此缓冲区(sk_buff)的sock结构。但要注意此结构并不一定有效。他的存在主要是为了为四层及以上和用户空间程序提供一些信息,所以只有那些本地产生或者由本地进程接收到的数据才有。对于那些被转发(我的理解是在三层或者二层就被处理,但是书中的解释似乎不是,没大看懂),此域为NULL。
Unsigned int len 指示了缓冲区中的数据大小,只是表明了当前的有效数据长度,他会随着在协议栈中的处理位置不同而变化。
Unsigned char *head, *end,*data,*tail 此四个指针表明了缓冲区的边界(head,end),在其中的数据边界(data,tail)。在每一层的处理中,可能会分配一些额外的缓冲区,这时的指针可能就需要改动。
Struct net_device *dev 当包是接收到的,那么dev表明了收到该包的网络接口net_device,由接口卡的驱动复杂更新;当包是要被发送出去的,那么它将要从此dev代表的设备发送出去,对于支持虚拟设备驱动的话,那么他的意义会有一些小小的变化。
Struct net_device *input_dev input_dev指明了接收到该包的设备。如果是本地产生的报文,input_dev为NULL。对于以太设备,在eth_type_trans函数中设置该值。在流量控制中会比较频繁地使用该值。
Struct net_device *real_dev 此域仅对虚拟设备有效。在Bonding,VLAN接口中会使用。
Union {...} h, Union {...} nh, Union {...} mac 这些指向在TCP/IP协议栈中的协议头。其中h用于L4,nh用于L3,mac用于L2。
Struct dst_entry dst 在路由子系统中使用。
Char cb[40] 这是一个控制缓冲,供各层内部使用,自己维护,用以保存本层一些私有数据。比如tcp协议处理中可能会用它来保存一个称之为tcp_skb_sb的结构。当前该私有缓冲的大小固定为40字节。
Unsigned char pkt_type 基于二层目的地址给出该报文的类型:PACKET_HOST,PACKET_MULTICAST,PACKET_BROADCAST,PACKET_OTHERHOST,PACKET_OUTGOING,PACKET_LOOPBACK等。
Unsigend short protocol 该字段的值是相对L2来说的上层协议(next-higher protocol),比如IP,IPV6,ARP等(完整的列表可以参考include/linux/if_ether.h)。该字段被驱动用来通知上层该用哪一个协议来处理该帧。在驱动中调用netif_rx函数来触发上层处理函数,所以在此之前此字段必须被初始化。
上面提到了struct sock结构,在内核代码中的解释是:sockets在网络层的表示,是不是指和socket是相关的?以后接触到了再仔细看看。
2.中断处理
《Understanding LINUX network internals》中描述的中断处理函数完成的工作有:
1)将帧拷贝到一个sk_buff数据结构中;
2)初始化sk_buff的参数以便上层系统使用(最明显的是protocol参数);
3)更新某些设备私有的数据;
4)告诉内核在适当的时候调度软中断以便帧得到处理;
来看一个具体的例子----drivers/net/3c59x.c,“This driver is for the 3Com "Vortex" and "Boomerang" series ethercards”。函数vortex_interrupt是中断处理函数,如下语句:
if (status & RxComplete)
vortex_rx(dev);
判断当前的状态,如果包含了RxComplete,则调用vortex_rx函数进行实际的收报处理。vortex_rx的核心处理如下:
/* The packet length: up to 4.5K!. */
int pkt_len = rx_status & 0x1fff;
struct sk_buff *skb;
skb = dev_alloc_skb(pkt_len + 5);
if (skb != NULL) {
skb_reserve(skb, 2); /* Align IP on 16 byte boundaries */
...
/*利用DMA或者适当直接读硬件内存的方法填充skb的*/
...
skb->protocol = eth_type_trans(skb, dev);/*初始化protocol字段*/
netif_rx(skb);/*通知内核有报文到达,调度软中断*/
/维护设备的私有信息*/
dev->last_rx = jiffies;
vp->stats.rx_packets++;
...
continue;
} else if (vortex_debug > 0)
printk(KERN_NOTICE "%s: No memory to allocate a sk_buff of "
"size %d./n", dev->name, pkt_len);
vp->stats.rx_dropped++;
可以看到基本就是进行了上面说的4件工作,但是,该驱动有个值得注意的地方:它在一个中断中可以处理多个到达的报文,过后休息一会等待下一次的定时中断(不大确定是否一定是定时中断?)。下面看一下eth_type_trans函数。
eth_type_trans解析二层以太头,设置sk_buff的protocol字段。它首先找到二层头,取出目的mac地址,判断是否是广播(PACKET_BROADCAST),多播(PACKET_MULTICAST),还是到其他主机的单播(PACKET_OTHERHOST)----设置sk_buff的pkt_type,然后根据协议号,如果大于1536,则返回报文头的协议号,即sk_buff的protocol赋值为报文里面的protocol字段的值;否则判断是否是IPX,802.2 LLC协议,并返回适当的协议值。
在上面的例子中,驱动调用了netif_rx(skb)以通知内核,调度软中断。来看一些netif_rx的处理流程,在此之前,看一下softnet_data结构,
每一个cpu都有自己的入报文队列,各自独立管理接口的流量,也就没有必要在cpu之间加锁。内核中用struct softnet_data来表示单个cpu的入/出报文队列或者设备:
struct softnet_data
{
struct net_device *output_queue;
struct sk_buff_head input_pkt_queue;/*上面说过作用,每个cpu一个,管理待处理的入报文*/
struct list_head poll_list;
struct sk_buff *completion_queue;
struct net_device backlog_dev;
#ifdef CONFIG_NET_DMA
struct dma_chan *net_dma;
#endif
};
树上说的字段有的在源代码里面找不到:throttle,avg_blog,cng_level,这三个都是和拥塞管理,难道拥塞管理不再基于单个cpu?(个人觉得全局来管理更合理一些)。input_pkt_queue在net_dev_init中被初始化,上面总结sk_buff中提到了他的作用,只适用于不采用NAPI接口的驱动(因为采用NAPI接口的驱动自己管理队列)。Backlog_dev,这一这不是一个指针,他表示了该CPU上一个已经调度了net_rx_action的设备,适用于非NAPI接口的驱动。Poll_list维护了一个有入报文待处理的设备的双向链表,用于NAPI接口的驱动。output_queue, completion_queue在发送报文有用到,将来再说。
这里提到了NAPI接口和老接口,在前一篇中有简单的介绍和比较。其实对于两种接口来说,驱动中中断处理函数的行为会有不同。可以看看e100.c中的中断处理函数,它在判断出是入报文到达后,禁止掉本设备的中断,然后调用__netif_rx_schedule(dev)函数,该函数的行为下面将谈及。可以看到处理非常简单,而把分配sk_buff等等其他工作交给了poll函数,这也是NAPI接口的特点。
差不多了,下面来看netif_rx和__netif_rx_schedule函数。Netif_rx的骨干代码如下:
local_irq_save(flags);
queue = &__get_cpu_var(softnet_data);/*queue类型是struct softnet_data *,指向本CPU的softnet_data 变量。*/
__get_cpu_var(netdev_rx_stat).total++;
/*netdev_max_backlog表明了单个cpu上的入frame队列的深度,不影响采用了NAPI接口的设备,因为NAPI接口不调用此函数*/
if (queue->input_pkt_queue.qlen <= netdev_max_backlog) {
if (queue->input_pkt_queue.qlen) {/*sk_buff_head是本CPU上待处理的sk_buff的链表头,如果qlen为零,也就是当前没有待处理的入报文,那么需要调度一次backlog"设备",否则直接把传入的sk_buff挂到链表的尾部*/
enqueue:
dev_hold(skb->dev);
__skb_queue_tail(&queue->input_pkt_queue, skb);
local_irq_restore(flags);
return NET_RX_SUCCESS;
}
/*第一个报文需要调度一下处理入报文的软中断*/
netif_rx_schedule(&queue->backlog_dev);
goto enqueue;
}
/*发生了拥塞,丢包并统计*/
__get_cpu_var(netdev_rx_stat).dropped++;
local_irq_restore(flags);
netif_rx_schedule封装了__netif_rx_schedule,乍一看,以为和NAPI接口一样,都是调用__netif_rx_schedule,但是传入的参数是不一样的:NAPI传入的是收到报文的dev;非NAPI接口传入的是本cpu上softnet_data->backlog_dev。原来非NAPI接口把报文放到backlog_dev设备的“入报文队列”(在softnet_data结构中)中,就像是该设备收到的一样,后面的行为都一样;而NAPI接口的驱动则把自己挂在设备链上,稍后自己处理全部的工作。
这样,中断里面的工作算是完成了,在适当的时候(软中断的执行机会前面总结了),将运行软中断处理函数。