本篇介绍网络栈中使用的核心数据结构sk_buff,从而帮助我们更快更透彻地理解网络栈的实现细节。
论对于网络栈的重要性,sk_buff说第二,就没有谁敢说第一。
从传输层到链路层,它是存放数据的通用结构,为了保持高效率,数据在传递过程中尽量不发生额外的拷贝。因此,从高层到低层的时候,会不断地在数据前加头,因此每一层的协议都会调用skb_reserve,为自己的报头预留空间。至于从低层到高层,去掉低层报头的方式就是移动一下指针,指向高层头,非常简单。
sk_buff的成员和函数大致可分为以下几类:
- 组织布局(Layout)
- 通用数据成员(General)
- 用于实现特定功能的数据成员(Feature-specific)
- 管理
sk_buff结构体的函数(Management functions)
我们来看以上的1、2、4点。
1. Layout成员
sk_buff是怎样组织起来的?便于追加、删除,双链表是再合适不过了。然而除此之外,sk_buff还有必要能在O(1)时间内获得双链表的头节点,所以sk_buff_head和list_head是有一点不同之处的。我们看看sk_buff的布局:

struct sk_buff_head {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;
__u32 qlen;
spinlock_t lock;
};
qlen是链表中元素的数量,lock用于在可能的并发访问时保护双链表结构体。
sk_buff中的next、prev指向相邻的sk_buff。不过在有些情况下sk_buff不是用双链表而是用红黑树组织的,那么有效的域是rbnode。
值得一提的是,5.10中,list域是一个list_head结构体,而非sk_buff_head,后面我们会回答这一问题。
union {
struct {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;
union {
struct net_device *dev;
/* Some protocols might use this space to store information,
* while device pointer would be NULL.
* UDP receive path is one user.
*/
unsigned long dev_scratch;
};
};
struct rb_node rbnode; /* used in netem, ip4 defrag, and tcp stack */
struct list_head list;
};
还有一些关键的数据结构:
union {
struct sock *sk;
int ip_defrag_offset;
};
sk指向拥有该sk_buff的套接字。ip_defrag_offset会用来处理IPv4报文分片,老古董了属于是。
再例如,
/* These elements must be at the end, see alloc_skb() for details. */
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char *head,
*data;
我们看看插图就知道怎么一回事了。

在headroom和tailroom可以插入一些数据,例如协议头等等。实际上tail和end在5.10中是unsigned类型,表示相对head的偏移量。
unsigned int truesize;
refcount_t users;
truesize表示skb使用的大小,包括skb结构体以及它所指向的数据;users是一个引用计数器。
unsigned int len,
data_len;
__u16 mac_len,
hdr_len;
len这个东西把所有数据的长度都算上了,包括分片的数据以及协议头。data_len只算了分片的数据。mac_len是链路层帧头长度。hdr_len是被拷贝的skb中可写的头部长度,skb是否是被拷贝出来的,取决于1比特的cloned字段。
2. General成员
这一部分包括了skb的通用成员,它们与协议类型或内核特性无关:
union {
ktime_t tstamp;
u64 skb_mstamp_ns; /* earliest departure time */
};
这是时间戳标志。一般来说tstamp只有在收包的时候比较有意义。skb_mstamp_ns可以用于标记最早的发包时间,即重传计时器的起点。
union {
struct net_device *dev;
/* Some protocols might use this space to store information,
* while device pointer would be NULL.
* UDP receive path is one user.
*/
unsigned long dev_scratch;
};
dev是一个很重要的数据结构,后面会讲,它用来表示从哪个设备收到报文,或将把报文发到哪个设备。有时有些协议用不着dev,其实应该把它置空,但实际上却利用这8字节存放了一些别的信息,所以就有了dev_scratch。
/*
* This is the control buffer. It is free to use for every
* layer. Please put your private variables there. If you
* want to keep them across layers you have to do a skb_clone()
* first. This is owned by whoever has the skb queued ATM.
*/
char cb[48] __aligned(8);
这是skb能被各层共用的精髓。它的大小是48,这其实是TCP的控制块的大小。
struct tcp_skb_cb {
__u32 seq; /* Starting sequence number */
__u32 end_seq; /* SEQ + FIN + SYN + datalen */
/* ... */
__u8 tcp_flags; /* TCP header flags. (tcp[13]) */
/* ... */
};
要获得这一字段,使用:
#define TCP_SKB_CB(__skb) ((struct tcp_skb_cb *)&((__skb)->cb[0]))
还有一个字段表示数据包类型:
__u8 pkt_type:3;
它的类型是由目标MAC地址决定的,比如:
#define PACKET_HOST 0 /* To us */
#define PACKET_BROADCAST 1 /* To all */
#define PACKET_MULTICAST 2 /* To group */
#define PACKET_OTHERHOST 3 /* To someone else */
#define PACKET_OUTGOING 4 /* Outgoing of any type */
#define PACKET_LOOPBACK 5 /* MC/BRD frame looped back */
#define PACKET_USER 6 /* To user space */
#define PACKET_KERNEL 7 /* To kernel space */
__be16 protocol;
该字段标识了L2上层的协议类型,种类可多了,这里只列一些家喻户晓的:
/*
* These are the defined Ethernet Protocol ID's.
*/
#define ETH_P_IP 0x0800 /* Internet Protocol packet */
#define ETH_P_X25 0x0805 /* CCITT X.25 */
#define ETH_P_ARP 0x0806 /* Address Resolution packet */
#define ETH_P_IPV6 0x86DD /* IPv6 over bluebook */
有了它,就能在收包的时候选择合适的处理函数。
__u16 transport_header;
__u16 network_header;
__u16 mac_header;
这些字段标识了各层头部相对于head的偏移量。
3. 管理函数
大多数管理函数都成对出现:包括do_sth和__do_sth。一般前者只是个wrapper,用于进行安全检查或函数特化等,后者执行真正的功能。
3.1. 分配和释放内存
static inline struct sk_buff *alloc_skb(unsigned int size, gfp_t priority)
用它就能获得一个sk_buff,附赠一片size大小(经过对齐)的数据缓冲区。核心代码如下。
skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node);
/* ... */
size = SKB_DATA_ALIGN(size);
size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc);
/* kmalloc(size) might give us more room than requested.
* Put skb_shared_info exactly at the end of allocated zone,
* to allow max possible filling before reallocation.
*/
size = SKB_WITH_OVERHEAD(ksize(data));
/* ... */
skb->head = data;
skb->data = data;
skb_reset_tail_pointer(skb);
skb->end = skb->tail + size;
其实skb->end指向的是一个skb_shared_info结构体。head、data、tail初始化的时候都是重合的,但end可能会偏后一点点,留出padding(其实也是一片tailroom),从而使读取为主的skb_shared_info与前面的数据不在一个缓存行。说到这里还是非常抽象,不要紧,要是会让读者看不懂那就不叫神书了:

dev_alloc_skb只接收一个参数length,本质上还是__alloc_skb的wrapper。它在设备驱动中工作(即中断上下文),所以只能以GFP_ATOMIC标志申请内存。此外,出于一些优化的目的,它会偷偷多申请16字节空间并用skb_reserve预留出来以供他用。
3.2. 数据缓冲区指针操作
四大天王:skb_reserve、skb_put、skb_push、skb_pull。一张图搞懂:

形象来说,put就是在后面追加,push是从数据内部往外推,pull是往里拉。reserve就是给非数据留出空间来,skb创建出来之后就调用,可以预留用作协议头。
skb_reserve还有一种应用场景,例如以太网帧头有14字节,那么skb创建出来的时候会先把data往后挪2字节,好让IP头进行16字节对齐,如此暖心真是令人泪目(bushi
skb_reserve(skb, 2); /* Align IP on 16 byte boundaries */

再以发送数据包为例,skb缓冲区是这样被填满的:

先预留足够的空间放头,TCP载荷可以put到剩下的空间里去。之后,不断地push,就可以将缓冲范围扩大,从而可以显式地用其它什么函数把不同的协议头写入数据缓冲。
3.3. 链表和数据操作
skb的操作有很多,我们会在后面遇到的时候再讲解。