Linux网络系统底层机制分析(3)---- 报文接收

本文深入探讨Linux网络系统底层的报文接收机制,重点分析sk_buff结构及其重要域,包括数据大小、缓冲区边界、设备信息等。此外,文章还讨论了中断处理过程,如3c59x.c驱动中的中断例程,以及如何通过netif_rx调度软中断进行报文处理。同时,文章提到了NAPI接口与老接口的区别,阐述了两种接口在处理报文时的不同策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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接口的驱动则把自己挂在设备链上,稍后自己处理全部的工作。

这样,中断里面的工作算是完成了,在适当的时候(软中断的执行机会前面总结了),将运行软中断处理函数。

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值