2014/5

skbuff 内存模型

众所周知,struct sk_buff 是 Linux 内核网络子系统中最重要的一个结构体,它是内核对网络数据包的一个抽象表示。要了解 Linux 网络子系统除了了解网络协议之外,了解内核如何表示和操作数据包也是至关重要的。Linux 内核中的 struct sk_buff (以下简称 skb)提供了一套非常复杂的内存模型,也对它进行了大量的优化,这也是为什么 Linux 网络如此高效。

我们从最简单的开始,分配一个最普通的 skb:

内核分配 skb 的函数是 __alloc_skb(),它有很多种包装供不同的场景使用。如上图所示,在这里我们分配了两部分内存:1) skb struct 本身,也叫元数据( meta data);2) 实际存放网络数据包的内存。这里你可以把 skb 看作是一堆指针,这些指针指向一块内存的不同区域。下面我们会看到,很多时候我们都是对指针本身进行操作。

注意:其实严格的说上图中所示的指针并不一定都是指针,Linux 内核进行了优化。在 64 位系统上,一个指针也是64位,储存指针的开销太大。而数据包本身不可能太大(以太网 MTU 是 1500 字节,就算是 jumbo frame 也才 9000 字节),因此 Linux 内核仅仅把 head 和 data 保存为指针,其它的只要存储相对于它们的偏移即可。下面的所有图中把它们表示为指针仅仅是为了方便。

而一个完整的网络数据包在内存中的表示通常有四个部分:

1) head room,位于 skb->head 和 skb->data 之间,也就是存放网络协议头的地方,比如 TCP,IP 头,以太网头都是位于此;

2) 用户数据,通常由应用层通过系统调用填充,介于 skb->data 和 skb->tail 之间;

3) tail room,介于 skb->tail 和 skb->end 之间,这部分是内核在用户数据后面填充的一部分;

4) skb->end 之后存放的是一个特殊的结构体 struct skb_shared_info,下面会讲到它。

刚分配出来的 skb 除了 skb->end 指向末尾的 struct skb_shared_info,其它指针都指向了开始处。通常第一步就是初始化 head room,调用 skb_reserve() 这个函数,如下图所示:

比如 TCP 层要发送一个数据包,head room 至少要是 tcphdr + iphdr + ethhdr。然后在协议头的后面开始预留数据区,调用的函数是 skb_put(),如下图所示:

到此为止,前面提到的四个部分才全部初始化完毕,通常情况下,这些操作要么是由传输层完成(发送),要么是由网卡驱动完成(接收)。一点一点往里填充数据或者向外读取数据则是各层协议的事情。数据包入栈时(即接收),数据是一层一层往里读取,协议头是一层一层剥掉。数据包出栈时(发送),协议头则是一层一层往外填写。

内核提供的剥掉协议头的操作函数是 skb_pull(),如名字所示,它是往下拉 skb->data:

类似地,往外推 skb->data 的操作叫做 skb_push():

很形象吧?

好,终于到前面一直默默无闻的 skb_shared_info 出场了。想象这么一种常见的情况:我们用 tcpdump 捕捉一个网卡收到的数据包。此时数据包入栈后会有两个部分同时进行读:一是协议栈本身,另一个就是 tcpdump 了。这种情况下要不要完全复制一份 skb 呢?没有必要,因为两部分都是读,网络数据本身是不变的,变的只是 strcut sk_buff 里面的指针,也就是说,我们只要复制一份 skb 让它指向同样的内存区域就行了!这正是 skb_clone() 所做的:

如图所示,skb_shared_info 此时记录了一些变化,dataref 变成了2,代表有两个 skb 同时指向这块内存区域。由此可见,skb_shared_info 保存的是多个 skb 之间共享的,但又不属于网络数据的数据(也就是不会被传输的数据),所以才放在 skb->end 之后啊。

这还没完,Linux 内核开发者觉得还不够,每次 clone 都还要分配一个 skb 结构体,对于要 clone 很多次的情形还可以进行优化——使用 fast clone(缩写为fclone)。fclone 很有技巧性,它在分配 skb 的 cache 里做了手脚,每次分配 skb 时,它都是分配一对,一个紧紧地跟在另一个后面,如下图所示:

不用的时候你完全看不到它,用的时候通过 skb+1 就能找得到!当然了,它需要在 skb 结构体里保存一些状态信息表明这部分到底有没有用到。

在写的情况下我们还是要完全拷贝 skb 及其数据的,调用 skb_copy() :

到此为止都比较简单,我们假设了所有的数据都是放在 skb->head 和 skb->end 之间,也就是所谓的线性(linear)。世界要是这么简单就好了,可惜现实远比这复杂。内核至少还提供了另外三种非线性的 skb 模型。

一种是网卡驱动常用的模型,数据存放在物理页面的不同位置,skb_shared_info 里有一个数组,存放一组 (页面、偏移、大小) 的信息,用来记录这些数据:

另一种是组装 IP 数据包分片(fragment)时用到的 frag_list 模型:

分片的数据有各自的 skb 结构体,它们通过 skb->next 链接成一个单链表,表头是第一个 skb 的 shared_info 中的 frag_list。

最后一种是 GSO 进行分段(segmentation)用到的一种模型,当一个大的 TCP 数据包被切割成几个 MTU 大小的数据时,它们也是通过 skb->next 链接到一起的:

与分片不同的是,相关的信息是记录在最前面一个 skb 的 skb_shared_info 里面的 gso_segs 和 gso_size 里。

正因为存在以上各种复杂的模型,很多 skb API 的实现(比如 skb_segment())非常复杂,其大部分代码都是用来处理非线性的这几种情况,这使得 skbuff.c 里的很多代码都不容易读懂。希望本文能够对你理解这部分代码有所帮助。

参考资料:

1. http://vger.kernel.org/~davem/skb_data.html

长期提供 Twitter 内推

今年断断续续收到不少求内推的邮件,在这里干脆直接写清楚好了,免得每次都得回邮件说明。

任何人都可以找我内推,无论是不是程序员,你只要把你感兴趣的职位链接和你的英文简历发到我的邮箱(xiyou 点 wangcong 在 gmail 点 com)即可。Cover letter 可有可无,你自己掌握。Twitter 公司的空缺职位在这里:https://about.twitter.com/careers,国内需翻墙,因为众所周知的原因。

我不提供修改简历服务,你发给我什么我就上传什么,也不会告诉你简历好不好之类的,那是 HR 的工作,不是我的。另外,网上教你写英文简历的文章一大把,我没义务更没时间告诉你怎么写。如果非得说应聘 Twitter 有何特殊要求,你不觉得简历里有一个你的推特帐号会更好么?:-)

因为 H-1b 签证的原因,国内的应聘者最好在每年的1月份到3月份之间应聘,或者更提前一些。根据这几年的情况来看,H-1b 配额4月份一出来就会被用光,不光如此还得抽签。所以4月份之后根本没戏,除非当年美国经济很差,H-1b 用不完了(这意味着职位空缺也少)。当然了,已经在美国本土的不受此限制。

在 Twitter 工作比较轻松,公司提供免费的三餐,工作时间自由,带薪休假理论上无上限(只要你的上司批准),在这里工作的中国人也很多。欢迎投简历!