linux内核网络协议栈实现中的几个问题

1.关于sk_buff的链表组织

sk_buff 的next和prev指针用来将sk_buff连接成链表,但是具体哪些skb被连接在一起就要看当前的skb在哪一个层次了,并且skb中还有一个 sk_buff_head结构体,它表示了所有连接在一起的skb的链表头,而实际上这个链表头只有在skb具有全局意义的层次才是有意义的,其它的层次意义不大,当然还是能派上一些用场的,那么何时skb具有全局意义呢?答案就是在链路层以及之下,网卡通过中断接收到数据包之后就会将skb插入到一个链表当中,然后上层的程序会从这个链表中将skb退掉,然后处理并继续向上传递,具体来说就是:

struct sk_buff *skb_dequeue(struct sk_buff_head *list)

{

unsigned long flags;

struct sk_buff *result;

spin_lock_irqsave(&list->lock, flags);

result = __skb_dequeue(list);

spin_unlock_irqrestore(&list->lock, flags);

return result;

}

看得出传入的参数是一个sk_buff_head,这个head应该是一个全局意义的head,比如系统会为arp准备一个队列,另外,对于链表操作,不管是插入还是退出都是要么在表头要么在表尾进行,因此保存一个head是十分有意义的,head的next和prev指向表头或者表尾,__skb_dequeue的实现就是在表头指向的链表的头部退出一个skb。对于入队操作和出队操作,另一个更有意义的事情就是网络的Qos实现,对于队列管理来讲,这个head也是必要的,一种简单的实现就是为每一种队列都保存一个head结构,然后每次需要将skb插入到该队列或者该队列被调度时需要从中退出一个skb时,使用head可以瞬间找到头或者尾,十分方便,这也许就是为何对于skb来讲并没有使用list_head的原因了,list_head是一种“没有头”的链表,而skb的实现方式更能体现出一种公平,头部的意义很明确。但是对于更高的协议栈层次,这个head的意义就不是那么明显了,比如说对于IP分段这个机制,其实也可以将每一个属于一个IP报文的碎片准备一个该sk_buff_head结构,然后虽有的碎片连接在一起,但是由于IP分段有更好的机制和数据结构比如ipq和哈希链表可以实现,这个sk_buff_head也就是没有被用到了,在处理IP分段的时候,skb的next用来链接所有属于一个IP报文的碎片,起码某种更容易理解的方式可以这么说。

前面说了skb的链接方式不使用list_head的方式,说list_head是没有头的,而实际上它是用头的,看看list_add实现方式,其实就是加到了链表的头部,并且保留了原来的头还是最开始的头,但是看看list_del,它可以随便退出一个list实体,而对于skb的方式来讲,它只能从头部或者尾部来退出那里的一个实体,其实这并没有什么大不了的,这么说来,list_head和skb前面的那连个指针的意义一样,只是前者对链表进行了封装而已,我个人也觉得没有什么特殊的原因让skb不用list_head而非要自己实现链接,如果非要说出一个原因那就是没有人愿意无偿做出这种清扫工作,linux内核现在工作的很好,那就够了。我觉得sk_buff_head可以改为:

struct sk_buff_head {

list_head list;

__u32 qlen;

spinlock_t lock;

};

然后将sk_buff的前两个字段改为list_head结构体

2.关于sk_buff的skb_shared_info含义

struct skb_shared_info {

atomic_t dataref;

unsigned int nr_frags;

unsigned short tso_size;

unsigned short tso_segs;

struct sk_buff *frag_list;

skb_frag_t frags[MAX_SKB_FRAGS];

};

这个结构体实际上是对skb结构体的扩展,主要表现在两个方面,一个是对同一个skb的对内扩展,另一个是不同skb的对外扩展,对内扩展的意思是无论如何扩展数据是属于这个skb的,在网络协议栈的意义上无论是skb中本身的还是skb_shared_info中的数据都是属于一个数据包的,而对外扩展的意思就正好相反,对外扩展的所有的数据的载体是其它的skb,只不过这些skb最终可能组成一个大的skb或者根本从来不组合而被其它逻辑所应用,这里需要指出的是,一个要发送出去的数据不一定仅存在于一个skb,相反一个预想接收的数据也不一定能被一个skb所容纳,记住,skb仅仅是数据的容器!对内扩展的例子就是分散/聚集IO,这种机制减少了内存的拷贝,数据可以停留在原始的页面上,不用拷贝到连续的内存区域,在网卡IO的时候,这些页面可以被网卡通过skb_shared_info的frags字段寻找到。对外扩展的例子就是IP分片的重组,在重组的时候,所有的碎片连接成一个链表,这个链表就在skb_shared_info的frag_list,所有的这么些skb最终作为一个整体被上层读取和处理。注意以上两例的区分,分散聚集IO是网卡的功能,而IP分片重组则是协议的要求。

3.关于IP的checksum的计算和验证

tcp/udp以及ip都要有checksum,然而传输层的checksum要考虑伪头部,因此计算量稍微大一些,实际上传输层考虑ip层的信息是不符合分层原则的,但是为了安全什么也不顾了。考虑到计算量,很多网卡可以通过硬件来计算这个传输层的checksum,以此来卸载掉cpu的一些压力,至于硬件有没有这个能力,在设备初始化的时候会将信息写到net_device结构体当中。考虑到伪头只有在网络层以下才可以看到,因此linux内核中并不在传输层计算check,而是在device的 xmit函数dev_queue_xmit中在再说。现在看看IP层的checksum的计算,很简单,计算量比较小,就是按照字节加和然后回绕即可,因此软件就可以瞬间完成,那么当系统接收到数据包的时候,怎么进行校验呢,很简单,不需要重新算一遍,而是直接将所有的数据连同按照字节加和,如果结果为 0,那就对了,因此当初计算的时候校验码checksum是所有数据不连同checksum本身加和后取反的,所以此时所有数据加和就会是 0,ip_fast_csum在ip头的checksum字段为0时会返回checksum,说明这是在计算checksum,而不为0时说明这是在进行校验,返回0。

4.IP分段重组

这是一个复杂的过程,需要考虑数据重合问题,高效的方式就是linux的实现方式,请看ip_defrag

你可能感兴趣的:(linux内核网络协议栈实现中的几个问题)