套接字缓冲区
目录
套接字缓冲区
套接字缓冲区整体结构
使用套接字缓冲区管理数据
管理套接字缓冲区数据
套接字缓冲区整体结构
在内核分析(收到的)网络分组时,底层协议的数据将传递到更高的层。发送数据时顺序相反,各种协议产生的数据(首部和净荷)依次向更低的层传递,直至最终发送。这些操作的速度对网络子系统的性能有决定性的影响,因此内核使用了一种特殊的结构,称为套接字缓冲区 (socket buffer),定义如下:
//<skbuff.h>
struct sk_buff {
/* 这两个成员必须在最前面*/
struct sk_buff *next;
struct sk_buff *prev;
struct sock *sk;
ktime_t tstamp;
struct net_device *dev;
struct dst_entry *dst;
char cb[48];
unsigned int len,
data_len;
__u16 mac_len,
hdr_len;
union {
__wsum csum;
struct {
__u16 csum_start;
__u16 csum_offset;
};
};
__u32 priority;
__u8 local_df:1,
cloned:1,
ip_summed:2,
nohdr:1,
nfctinfo:3;
__u8 pkt_type:3,
fclone:2,
ipvs_property:1;
nf_trace:1;__be16 protocol;
...
void (*destructor)(struct sk_buff *skb);
...
int iif;
...
sk_buff_data_t transport_header;
sk_buff_data_t network_header;
sk_buff_data_t mac_header;
/* 这些成员必须在末尾,详见alloc_skb()*/
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char *head, *data;
unsigned int truesize;
atomic_t users;
};
套接字缓冲区用于在网络实现的各个层次之间交换数据,而无须来回复制分组数据,对性能的提高很可观。
使用套接字缓冲区管理数据
套接字缓冲区通过其中包含的各种指针与一个内存区域相关联,网络分组的数据就位于该区域中,如图所示。图中假定我们使用的是32位系统。
套接字缓冲区的基本思想是,通过操作指针来增删协议首部。
- head 和end 指向数据在内存中的起始和结束位置。
- data 和tail 指向协议数据区域的起始和结束位置。
- mac_header 指向MAC协议首部的起始,而network_header和transport_header 分别指向网络层和传输层协议首部的起始。在字长32位的系统上,数据类型sk_buff_data_t 用来表示各种类型为简单指针的数据。
在一个新分组产生时,TCP层首先在用户空间中分配内存来容纳该分组数据(首部和净荷)。分配的空间大于数据实际需要的长度,因此较低的协议层可以进一步增加首部。
在套接字缓冲区传递到互联网络层时,必须增加一个新层。只需要向已经分配但尚未占用的那部分内存空间写入数据即可,除了data 之外所有的指针都不变,data 现在指向IP首部的起始处。下面的各层会重复同样的操作,直至分组完成,即将通过网络发送。在不同的协议层,data指针会变化,指向不同层的网络。
对接收的分组进行分析的过程是类似的。分组数据复制到内核分配的一个内存区中,并在整个分析期间一直处于该内存区中。与该分组相关联的套接字缓冲区在各层之间顺序传递,各层依次将其中的各个指针设置为正确值。
套接字缓冲区需要很多指针来表示缓冲区中内容的不同部分。由于网络子系统必须保证较低的内存占用和较高的处理速度,因而对struct sk_buff 来说,我们需要保持该结构的长度尽可能小。在64位 CPU上,可使用一点小技巧来节省一些空间。sk_buff_data_t 的定义改为整型变量:
//<skbuff.h>
typedef unsigned int sk_buff_data_t;
由于在此类体系结构上,整型变量占用的内存只有指针变量的一半(前者是4字节,后者是8字节),该结构的长度缩减了20字节。但套接字缓冲区中包含的信息仍然是同样的。data 和head 仍然是常规的指针,而所有sk_buff_data_t 类型的成员现在都解释为相对于前两者的偏移量。指向传输层首部的指针现在计算如下:
//<skbuff.h>
static inline unsigned char *skb_transport_header(const struct
sk_buff *skb)
{
return skb->head + skb->transport_header;
}
由于假定套接字缓冲区的内部表示对通用网络代码是不可见的,所以提供了如下几个辅助函数来访问struct sk_buff 的成员。这些函数都定义在<skbuff.h> 中,编译时会自动选择其中适当的变体使用。
- skb_transport_header(const struct sk_buff *skb)从给定的套接字缓冲区获取传输层首部的地址。
- skb_reset_transport_header(struct sk_buff *skb)将传输层首部重置为数据部分的起始位置。
- skb_set_transport_header(struct sk_buff *skb,const int offset) 根据数据部分中给定的偏移量来设置传输层首部的起始地址。
对MAC层和网络层首部来说,也有同样一组函数可用,只需将transport 分别替换为mac 或network 即可。
管理套接字缓冲区数据
套接字缓冲区结构不仅包含上述指针,还包括用于处理相关的数据和管理套接字缓冲区自身的其他成员。
其中不常见的成员在本章中遇到时才会讨论。下面列出的是一些最重要的成员。
- tstamp 保存了分组到达的时间。
- dev 指定了处理分组的网络设备。dev 在处理分组的过程中可能会改变,例如,在未来某个时候,分组可能通过计算机的另一个设备发出。
- 输入设备的接口索引号总是保存在iif 中。
- sk 是一个指针,指向用于处理该分组的套接字对应的socket 实例(。
- dst表示接下来该分组通过内核网络实现的路由。这里使用了一个特殊的格式,将在12.8.5节讨论。
- next 和prev 用于将套接字缓冲区保存到一个双链表中。这里没有使用内核的标准链表实现,而是使用了一个手工实现的版本。
- 使用了一个表头来实现套接字缓冲区的等待队列。其结构定义如下:
struct sk_buff_head {
/* 这两个成员必须在最前面*/
struct sk_buff *next;
struct sk_buff *prev;
__u32 qlen;
spinlock_t lock;
};
qlen 指定了等待队列的长度,即队列中成员的数目。sk_buff_head 和sk_buff 的next 和prev 用于创建一个循环双链表,套接字缓冲区的list 成员指回到表头。分组通常放置在等待队列中,例如分组等待处理时,或需要重新组合已经分析过的分组时。