转自:http://www.2cto.com/os/201502/376226.html
可以说sk_buff结构体是Linux网络协议栈的核心中的核心,几乎所有的操作都是围绕sk_buff这个结构体进行的,它的重要性和BSD的mbuf类似(看过《TCP/IP详解 卷2》的都知道),那么sk_buff是什么呢?
网络分层模型这是一切的本质。网络被设计成分层的,所以网络的操作就可以称作一个“栈”,这就是网络协议栈的名称的由来。在具体的操作上,数据包最终形成的过程就是一层一层封装的过程,在栈上形成一段连续的数据,我们可以称作是一层一层的push操作。同样的,数据包的解封装的过程,则可以认为是一层一层的pop操作。
sk_buff的操作要想形成一个最终的数据包,即以太帧(不考虑其它的链路层)。要进行以下的操作:
1.分配一个skb结构体
可以看出基本的模式,即“定位/设置”两步骤操作,有点区别的是应用层操作,这是因为应用层的操作一般都是在socket接口之上完成的。但是既然本文讲述的是skb的通用操作,就不再区分这个了。
skb的核心操作在上面一小节,我们展示了skb的封装逻辑,但是具体到接口层面,就涉及到了skb的核心操作。
1.分配skb这个是由alloc_skb完成的,完成同一任务的接口形成一个接口族,但是alloc_skb是最基本的接口。
该alloc_skb接口完成两件事,即分配skb结构体以及skb数据包缓冲区,设置初始值。size参数表示skb的数据包缓冲区的大小,这个大小包括所有层的总和。如果该函数成功返回,那么就相当于你已经有了一个大小为size的空数据包缓冲区以及操作该数据包缓冲区的skb元数据。如下图所示:
skb的逐层封装的关键在于写指针的定位,即这一层从哪个位置开始写。从协议封装的压栈形象来看,这个定位应该是顺序有规律的。初始定位十分重要,后面的定位就是例行公事了。初始定位当然是定位到应用层的末端,从这里开始,逐层将协议头push到skb的数据包缓冲区。初始定位图示如下:
当skb分配好了之后,需要将协议“栈”的位置定位在数据包的“最低处”,这是初始定位,这样才可以把每一层的数据或者协议头push到栈上,这个操作由skb_reserve来完成。应用层数据已经在socket之上封装好了,那么就把skb的数据包缓冲区写指针定位到应用数据的开始处,此时的写指针在应用层缓冲区的末尾,因此需要使用skb_push操作将写指针定位到应用层开始处,这等于说压入了应用层栈帧。
将应用层栈帧压入协议栈之后,就可以在写指针位置开始,往后连续写n字节的应用层数据了,一般而言,这些数据来自socket。
4.设置传输层头部和应用层的操作类似,这次需要把传输层栈帧压入协议栈中,如下图所示:
接下来就可以愉快地在skb_push返回的位置设置传输层头部了,UDP,TCP,就看你对传输层的理解了。设置传输层头部其实就是在skb_push返回的位置开始写数据,写入的长度由skb_push的参数指定,即n。
5.设置IP层头部和应用层以及传输层操作类似,这次需要把IP层的栈帧压入协议栈中,如下图所示:
接下来就可以愉快地在skb_push返回的位置设置IP层头部了,如何设置,就看你对IP层的理解了。由于只是演示skb如何封装,因此没有涉及IP层相当重要的IP路由过程。
6.设置以太帧头部这个就不说了,和上述的类似...如下图所示:
到此为止,我封装了一个完整的以太帧,可以直接通过dev_queue_xmit发送的那种。一路下来,你会发现,skb数据包缓冲区以“压栈(push)”的方式逐渐被填充,每一层,都是通过skb_push接口压入一个栈帧,返回写指针,然后按照该层的协议逻辑从写指针开始写入栈帧长度的数据。
7.在应用数据后面追加PADDING目前为止,从最后的图示上可以看到,在skb数据包缓冲区中,还有两块区域没有使用,一个headroom,一个是tailroom,这些是干什么用的呢?作为一个练习的例子,由于存在某种对齐原则,在封装完成后,我需要在数据包的最后追加一些填充,或者说我需要在最前面加一个前导码,或者最常见的,我要在数据包的最后加一个纠错码,此时应该怎么办呢?
这个时候就需要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 iphdr) + 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);
按照接口编码而不是按照实现编码这好像是Effective C++里面的一条,同样也适合于skb的操作场景。典型的就是“如何让skb记住IP层协议头,传输层协议头,mac头的位置”,接口是:
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
#if BITS_PER_LONG > 32 #define NET_SKBUFF_DATA_USES_OFFSET 1 #endif #ifdef NET_SKBUFF_DATA_USES_OFFSET typedef unsigned int sk_buff_data_t; #else typedef unsigned char *sk_buff_data_t; #endif 节约空间之外,对于和大小相关的操作,接口实现也更加统一。这就是细节,而这些细节并不是玩网络协议栈的人所要关注的,不是吗?这完全是系统实现的层面,和业务逻辑是无关的。
为何未竟全功本文讲述到此为止。事实上,sk_buff还有更多的,相当多的细节,但是不能再一一描述了,因为那样就违背了本文一开始的初衷,即用最简单的方式揭露本质,如果一一描述了,那么本文将成为一个文档而非一篇感悟,时隔多年以后,相信自己也不会看下去的。
alloc_skb:分配一个skb;
skb_reserver:写指针向后移动到一个位置p,确定为数据包尾部,自始,写指针开始从该位置前移封装数据包;
skb_push:写指针前移n,更新数据包长度,从它返回的位置可以写n个字节数据-即封装n字节的协议;
skb_put:写指针移动到数据包尾部,返回尾部指针,可以从此位置写n字节数据,同时更新尾指针和数据包长度;