在以上文章中,没有分析过Linux内核网络关键的数据结构-套接字数据缓存struct sk_buff,本文将第一次分享到sk_buff,但鉴于其在内核网络中一些复杂情况,本次只简单介绍sk_buff内存空间布局情况与相关操作。
套接字数据缓存(socket buffer)在Linux内核中表示为:struct sk_buff,是Linux内核中数据包管理的基本单元,主要包含两个部分,其一:管理数据,即数据包的管理信息;其二:报文数据,保存了实际网络中传输的数据,在内核协议栈起承上启下的作用,也有很多值得关注的sk_buff操作。
1、sk_buff四大指针与相关操作
分配初始化:
struct sk_buff中四个指针都指向数据区,分别是head、data、tail、end,刚刚分配出来的sk_buff会立马进行四大指针的初始操作。分配sk_buff如下所示:
sk_stream_alloc_skb最终调用__alloc_skb函数进行内存分配,分配skb后,进行四大指针的初始化操作:
其中skb_reset_tail_pointer(skb):
最终四大指针初始化为以下图所示:
此时head、data、tail三个指针指向一起,end指向数据缓冲区的尾部。预留协议头空间:在sk_stream_alloc_skb调用__alloc_skb函数进行内存分配后,下一步就会预留协议头空间,使得head、tail、data指针分离:
skb_reserve如下,
操作后skb_buff的指针如下所示:
skb_reserve作用就是预留空间,而且是尽最大的空间预留,但它并没有把数据放到该空间,只是简单更新指针,预留空间!
因为很多头都会有可选项,那么不知道头部可选项是多大,所以只能按照最大的分配,同时也要明白一点,预留的空间headroom不一定使用完,可能还有剩余。当我们要增加协议头信息的时候,data指针向上移动,当增加数据的时候tail指针向下移动,完成数据包的封装。此时还没有数据,data和tail指向相同。
操作tailroom中用户数据块区域:skb_put用于修改指向数据区末尾的指针tail:
可以看到tail指针的移动是扩大数据区域,即数据区向下扩大len字节,并更新数据区长度len。
增加headroom区域的协议头:skb_push函数用于移动data指针,增加头部协议,与skb_reserve()类似,也并没有真正向数据缓存区中添加数据,而只是移动数据缓存区的头指针data。数据由其他函数复制到数据缓存区中。函数如下:
如下两张图分别是由传输层、网络层,数据包向下传递时data指针移动,进行头部协议的封装。
TCP层添加TCP首部。
SKB传递到IP层,IP层为数据包添加IP首部。
SKB传递到链路层,链路层为数据包添加链路层首部。
可以看到在数据包封装的过程中,每一层移动data指针进行数据报头的封装。
数据报文解封装,解除协议头:skb_pull通过将data指针向下移动,进行数据报文的解封装,函数如下所示:
如下图所示,在收包流程上,向上层协议,如下网络层向传输层传送的时候,调用skb_pull进行数据包的解封装。
以上就是struct sk_buff的四大指针的相关操作,通过分析可得:
head指向缓冲区的首地址,作为上边界
end指向缓冲区的尾地址,作为下边界
data指针在数据包头部封装和解封装的过程中移动,指向各层的协议头,skb_push函数将data的指向,向低地址移动(向上),完成协议头空间的占据,skb_pull函数将data的指向,向高地址移动(向下),完成协议头的解封装。
tail指针在增加应用层用户缓冲数据时移动,skb_put函数将该指针向高地址移动(向上),完成用户数据空间的占据。
2、非线性区域
在1、中,可以看到每张sk_buff的图:在end指针紧挨着一个非线性区域;
在struct sk_buff中没有指向skb_shared_info结构的指针,利用end指针,可以用skb_shinfo宏来访问:
其中skb_end_pointer函数如下,返回end指针
其中skb_frag_t如下:
nr_frags,frags,frag_list与IP分片存储有关。
frag_list的用法:
用于在接收分组后链接多个分片,组成一个完整的IP数据报
在UDP数据报输出中,将待分片的SKB链接到第一个SKB中,然后在输出过程中能够快速的分片
用于存放FRAGLIST类型的聚合分散I/O数据包
判断是否存在非线性缓冲区:
先说明struct sk_buff中关于长度的两个字段
len字段:无分片的报文,数据报文的大小
data_len字段:存在分散报文,data_len表示分片的部分大小
如下所示,没有开启分片的报文len = x,data_len = 0:
如下所示在Linux内核中,使用skb_is_nonlinear函数判断是否存在分片,即通过判断data_len的大小是否为0:
在没有开启分片的报文中,数据包长度在struct sk_buff中为len字段的大小,即data到tail的长度,nf_frags为0,frag_list为NULL。
普通聚合分散I/O的报文:
采用聚合分散I/O的报文,
frag_list为 NULL,nf_frags不等于0,说明这不是一个普通的分片,而是聚合分散I/O的报文。如下所示:nr_frags为2,而frag_list为NULL,说明这不是普通的分片,而是聚合分散I/O分片,数量为2,这两个分片指向同一物理分页,各自在分页中的偏移和长度分别是0/S1和S1/S2。
FRAGLIST类型的分散聚合I/O的报文:
采用FRAGLIST类型的分散聚合I/O报文,frag_list不为NULL,nf_frags等于0,数据长度len为x+S1,data_len为S1。
以上从struct sk_buff的四大指针以及操作、非线性区域对套接字缓存(socket buffer)进行分析,更多sk_buff的分析、实操等将在以后的文章中梳理。
评论
查看更多