本文共 5680 字,大约阅读时间需要 18 分钟。
可以说sk_buff结构体是Linux网络协议栈的核心中的核心,几乎所有的操作都是围绕sk_buff这个结构体进行的,它的重要性和BSD的mbuf类似(看过《》的都知道),那么sk_buff是什么呢?
sk_buff就是网络数据包本身以及针对它的操作元数据。 想要理解sk_buff,最简单的方式就是凭着自己对网络协议栈的理解封装一个直到以太层的数据帧并且成功发送出去,个人认为这比看代码/看文档或者在网上搜资料强多了。当然,网上已经有了大量的这方面的文章,但是我认为很多都太复杂了,它们都细化到了sk_buff结构体的每一个指针字段,并且还都画出了图,但一般都逃不过《》这本书的圈子。试想,如果以后内核版本升级了,字段新增了或者名字变了,怎么办?这些文章包括那本经典的《》还能有帮助吗? 因此,本文绝不深入到sk_buff的细节,但是相信这种简单的方式可以让自己在多年以后早已忘了什么是Linux协议栈的情况下,瞬间理解Linux是如何通过sk_buff封装数据包的。我们从网络的分层模型开始。该alloc_skb接口完成两件事,即分配skb结构体以及skb数据包缓冲区,设置初始值。size参数表示skb的数据包缓冲区的大小,这个大小包括所有层的总和。如果该函数成功返回,那么就相当于你已经有了一个大小为size的空数据包缓冲区以及操作该数据包缓冲区的skb元数据。如下图所示:
这个时候就需要headroom或者tailroom了,以在数据包最后追加数据为例,请看下图:
实际上,skb_put的操作就是,在数据包的末尾追加数据。至于说headroom如何使用,我就不多说了,其实还是skb_push,headroom有什么用呢?前导码,X over Y封装,不一而足。
skb = alloc_skb(1500, GFP_ATOMIC); skb->dev = dev; // 例行填充skb元数据 /* 保留skb区域 */skb_reserve (skb, 2 + sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr) + sizeof(app_data));/* 构造数据区 */p = skb_push(skb, sizeof(app_data));memcpy(p, &app_data[0], sizeof(app_data));p = skb_push(skb, sizeof(struct udphdr));udphdr = (struct udphdr *)p; // 填充udphdr字段,略skb_reset_transport_header(skb);/* 构造IP头 */p = skb_push(skb, sizeof(struct iphdr));iphdr = (struct iphdr*)p;// 填充iphdr字段,略skb_reset_network_header(skb);/* 构造以太头 */p = skb_push(skb, sizeof(struct ethhdr));ethhdr = (struct ethhdr*)p;// 填充ethhdr字段,略skb_reset_mac_header(skb);/* 发射 */dev_queue_xmit(skb);
解封装的过程和封装的过程相反,解封装的过程是协议栈栈帧逐层pop的过程,但是Linux协议栈并没有用栈的术语来定义接口名字,而是使用了push的反义词,即pull来定义的,skb_pull就是核心接口,和skb_push严格相对。我就不再一一画图了。 skb_reset_mac_header skb_reset_network_header skb_reset_transport_header调用时机为skb_push返回的当时。曾几何时,我按照下面的方式设置了协议头的位置:
/* 构造IP头 / p = skb_push(skb, sizeof(struct iphdr)); iphdr = (struct iphdr)p; // 填充iphdr字段,略 //skb_reset_network_header(skb); skb->network_header = p;有错吗?咋一看是没错的,但是却报错了: protocol 0008 is buggy, dev eth2 这是怎么回事?原因就在于skb纪录的协议头位置是错误的!难道以上的设置skb的network_header字段的方式有何不妥吗?当然不妥!这就是没有按照接口编码的恶果。 原因在于,系统设置skb的network_header字段的方式有两种,通过一个宏来识别:NET_SKBUFF_DATA_USES_OFFSET。也就是说,可以通过相对于skb的head指针的偏移来定位协议头的位置,也可以通过绝对地址来定位,具体使用哪一种取决于系统有没有定义NET_SKBUFF_DATA_USES_OFFSET宏,以上的skb->network_header = p明显是通过绝对地址来定位的,一旦系统定义了NET_SKBUFF_DATA_USES_OFFSET宏,肯定就不对了。既然宏定义在编译期确定,那么通过定义接口就可以在编译期唯一确定一种实现,程序员不必在乎是否定义了NET_SKBUFF_DATA_USES_OFFSET宏,这就是通过接口编程的益处。如果基于skb的实现来编程,你不得不针对所有的情况编写好几套实现,而以上错误的实现只是其中一种,而且还用错了场景!这是多么痛的领悟! NET_SKBUFF_DATA_USES_OFFSET宏是一个细节问题,如果使用接口编程便不必关注这个细节,否则你就必须搞清楚系统为何这么设计,即便这并不是你所关注的!为何呢? 由于指针的长度大小在32位系统和64位系统中是不一样的,所以按理说skb中的指针型的元数据大小也会不同,且64位系统的将会是32位系统的两倍,为了平滑掉这个差别,使元数据大小一致,就必须让64位系统的对应指针类型变为4个字节,而这是不可能的。因此在64位系统中,使用偏移来定位元数据,而偏移的类型为固定不变的unsigned int,即4个字节。为了支持上述说法,skb中加入了一个新的层次,即定义了一种新的数据类型sk_buff_data_t,该类型在编译期确定:
#if BITS_PER_LONG > 32 #define NET_SKBUFF_DATA_USES_OFFSET 1 #endif
typedef unsigned int sk_buff_data_t; typedef unsigned char *sk_buff_data_t; 节约空间之外,对于和大小相关的操作,接口实现也更加统一。这就是细节,而这些细节并不是玩网络协议栈的人所要关注的,不是吗?这完全是系统实现的层面,和业务逻辑是无关的。