网络协议栈是一个层次架构的软件结构,层与层之间通过预订的接口传递报文。网络报文中包含了在协议各层使用到的各种信息。由于网络报文之间的大小不是固定的,因此采用合适的数据结构来存储这些网络报文就显得非常重要。内核层和用户层在网络方面的差别很大,在内核的网络层中 sk_buff结构占有重要的地位,几乎所有的处理均与此结构有关系。
struct sk_buff {
union {
struct {
/* These two members must be first. */
struct sk_buff *next; // sk_buff链表中的下一个sock缓冲区
struct sk_buff *prev; // sk_buff链表中的前一个sock缓冲区
//上述两个人变量将sk_buff链接到一个双向链表中
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; //内核链表结构,用于快速定位链表头 sk_buff_head
};
union {
struct sock *sk; // 网络报文所属的sock结构,此值仅在本机发出的报文中有效,从网络收到的报文此值为空。
int ip_defrag_offset; // 用于分片管理中
};
union {
ktime_t tstamp; // 收到此报文的时间戳
u64 skb_mstamp_ns; /* earliest departure time */ //最早出发时间
};
/*
* 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); //用于控制此缓冲区,每层都可以使用此指针,将私有数据放置于此。
union {
struct {
unsigned long _skb_refdst;
void (*destructor)(struct sk_buff *skb);
};
struct list_head tcp_tsorted_anchor;
};
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
unsigned long _nfct;
#endif
unsigned int len,
data_len; // len表示有效数据长度、data_len表示数据长度
__u16 mac_len, //连接层头部长度、对于以太网 指mac地址长度,为6
hdr_len; //skb的可写数据长度
/* Following fields are _not_ copied in __copy_skb_header()
* Note that queue_mapping is here mostly to fill a hole.
*/
__u16 queue_mapping;
/* if you move cloned around you also must adapt those constants */
#ifdef __BIG_ENDIAN_BITFIELD
#define CLONED_MASK (1 << 7)
#else
#define CLONED_MASK 1
#endif
#define CLONED_OFFSET() offsetof(struct sk_buff, __cloned_offset)
/* private: */
__u8 __cloned_offset[0];
/* public: */
__u8 cloned:1,
nohdr:1,
fclone:2,
peeked:1,
head_frag:1,
pfmemalloc:1;
#ifdef CONFIG_SKB_EXTENSIONS
__u8 active_extensions;
#endif
/* fields enclosed in headers_start/headers_end are copied
* using a single memcpy() in __copy_skb_header()
*/
/* private: */
__u32 headers_start[0];
/* public: */
/* if you move pkt_type around you also must adapt those constants */
#ifdef __BIG_ENDIAN_BITFIELD
#define PKT_TYPE_MAX (7 << 5)
#else
#define PKT_TYPE_MAX 7
#endif
#define PKT_TYPE_OFFSET() offsetof(struct sk_buff, __pkt_type_offset)
/* private: */
__u8 __pkt_type_offset[0];
/* public: */
__u8 pkt_type:3; //包类别
__u8 ignore_df:1;
__u8 nf_trace:1;
__u8 ip_summed:2;
__u8 ooo_okay:1;
__u8 l4_hash:1;
__u8 sw_hash:1;
__u8 wifi_acked_valid:1;
__u8 wifi_acked:1;
__u8 no_fcs:1;
/* Indicates the inner headers are valid in the skbuff. */
__u8 encapsulation:1;
__u8 encap_hdr_csum:1;
__u8 csum_valid:1;
#ifdef __BIG_ENDIAN_BITFIELD
#define PKT_VLAN_PRESENT_BIT 7
#else
#define PKT_VLAN_PRESENT_BIT 0
#endif
#define PKT_VLAN_PRESENT_OFFSET() offsetof(struct sk_buff, __pkt_vlan_present_offset)
/* private: */
__u8 __pkt_vlan_present_offset[0];
/* public: */
__u8 vlan_present:1;
__u8 csum_complete_sw:1;
__u8 csum_level:2;
__u8 csum_not_inet:1;
__u8 dst_pending_confirm:1;
#ifdef CONFIG_IPV6_NDISC_NODETYPE
__u8 ndisc_nodetype:2;
#endif
__u8 ipvs_property:1;
__u8 inner_protocol_type:1;
__u8 remcsum_offload:1;
#ifdef CONFIG_NET_SWITCHDEV
__u8 offload_fwd_mark:1;
__u8 offload_l3_fwd_mark:1;
#endif
#ifdef CONFIG_NET_CLS_ACT
__u8 tc_skip_classify:1;
__u8 tc_at_ingress:1;
#endif
#ifdef CONFIG_NET_REDIRECT
__u8 redirected:1;
__u8 from_ingress:1;
#endif
#ifdef CONFIG_TLS_DEVICE
__u8 decrypted:1;
#endif
#ifdef CONFIG_NET_SCHED
__u16 tc_index; /* traffic control index */
#endif
union {
__wsum csum; //检验和
struct {
__u16 csum_start; //当开始计算检验和时从skb->head的偏移
__u16 csum_offset; // 从csum_start开始的偏移
};
};
__u32 priority; // 包队列的优先级
int skb_iif;
__u32 hash;
__be16 vlan_proto;
__u16 vlan_tci;
#if defined(CONFIG_NET_RX_BUSY_POLL) || defined(CONFIG_XPS)
union {
unsigned int napi_id;
unsigned int sender_cpu;
};
#endif
#ifdef CONFIG_NETWORK_SECMARK
__u32 secmark;
#endif
union {
__u32 mark;
__u32 reserved_tailroom;
};
union {
__be16 inner_protocol;
__u8 inner_ipproto;
};
__u16 inner_transport_header;
__u16 inner_network_header;
__u16 inner_mac_header;
__be16 protocol; //协议
__u16 transport_header; //传输层头部
__u16 network_header; // 网络层头部
__u16 mac_header; // 数据链路层头部
/* private: */
__u32 headers_end[0];
/* public: */
/* 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; //数据头指针
unsigned int truesize; // 报文缓冲区的大小
refcount_t users;
#ifdef CONFIG_SKB_EXTENSIONS
/* only useable after checking ->active_extensions != 0 */
struct skb_ext *extensions;
#endif
};
data_len记录的是在frags和frag_list里面存储的报文的长度
len记录网络报文的总长度
truesize是head所指的存储区的大小
数据缓冲区尾端有个名为skb_shared_info的数据结构,用以保持此数据区块的附加信息。此数据结构紧接在标记数据尾端的end指针之后。
struct skb_shared_info {
__u8 __unused;
__u8 meta_len;
__u8 nr_frags; // 用于处理IP片段
__u8 tx_flags; // 用于处理IP片段
unsigned short gso_size;
unsigned short gso_segs;
struct sk_buff *frag_list; // 用于处理IP片段
struct skb_shared_hwtstamps hwtstamps;
unsigned int gso_type;
u32 tskey;
atomic_t dataref; // 数据块的“用户”数目
void * destructor_arg;
skb_frag_t frags[MAX_SKB_FRAGS];
};
sk_buff结构中没有指向skb_shared_info数据结构的字段。为了访问该结构体,函数必须使用返回end指针的skb_shinfo宏:
#define skb_shinfo(SKB) ((struct skb_shared_info *)(skb_end_pointer(SKB)))
通过这个指针去访问里面的成员
u32 nr_frags = skb_shinfo(skb)->nr_frags + 1
sk_buff包括如下几个部分:
(1) struct sk_buff–用于维护socket buffer状态和描述信息
(2) 网络报文缓冲区-独立于sk_buff结构体的数据缓冲区,用来存放报文分组,使各层协议的header存储在连续的空间中,以方便协议栈对其操作
(3) skb_shared_info 作为网络报文缓冲区的补充,用于存储ip分片,其中sk_buff *frag_list是一系列子skbuff链表,而frag[]是由一组单独的page组成的数据缓冲区
skb buff结构图如下:
网络报文存储空间是在应用层发送网络数据或者网络设备收到网络数据时动态分配的,分配成功之后,将接收或者发送的网络数据填充到这个存储空间中去。将网络数据填充到存储空间时,在存储空间的头部预留了一定数量的空隙,然后从此偏移量开始将网络报文复制到存储空间中。skb_shared_info数据结构可以包含一个sk_buff结构列表(链接到一个frag_list字段)。
结构sk_buff 以 sk_buff_head构成一个环状的链,如下图所示, next变量指向下一个sk_buff结构, prev变量指向前一个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;
};
API | 说明 |
---|---|
dev_alloc_skb | 在中断模式下的缓冲区分配,即原子操作的alloc_skb |
kfree_skb | 释放缓冲区 |
dev_kfree_skb | 释放缓冲区,调用kfree_skb,当skb->users减为0才释放内存 |
skb_reserve | 在缓冲区的头部预留一些空间,允许插入一个报头。此函数只是移动了data和tail指针 |
skb_put | 将 tail 指针向数据区的末尾移动,增加了 len 字段的长度 |
skb_push | 将 data 指针向数据区的前端移动,增加 了len 字段的长度。 |
skb_pull | 将 data 指针向数据区的末尾移动,减少了len 字段的长度。 |
skb_trim | 将网络报文的长度缩减到len,丢弃了网络报文尾部的填充值 |
skb_clone | 克隆sk_buff数据结构,双方的clone位置1,user置1,数据缓冲区内的dataref递增 |
skb_share_check | 检查引用计数skb->users,并且当users字段说明该缓冲区是共享时可以克隆该缓冲区 |
skb_copy | 拷贝sk_buff以及数据缓冲区 |
pskb_copy | 只拷贝sk_buff |
alloc_skb | 用于分配sk_buff数据结构以及数据缓冲区 |
skb_queue_head_init | 用一个元素为空的队列初始化sk_buff_head |
skb_queue_head | 把一个缓冲区分别添加到队列的头 |
skb_queue_tail | 把一个缓冲区分别添加到队列的尾 |
skb_dequeue_head | 把一个元素分别从队列的头去掉 |
skb_dequeue_tail | 把一个元素分别从队列的尾去掉 |
skb_queue_purge | 把队列变为空队列 |
skb_queue_walk | 依次循环运行队列里的每个元素 |
当同一个缓冲区需要不同的程序修改sk_buff描述符的内容,为了提高效率,内核不需要完全拷贝sk_buff结构和相关联的数据缓冲区,内核仅克隆原始值,也就是只拷贝sk_buff结构体,然后使用引用计数,以免过早释放共享的数据块。缓冲区的克隆由skb_clone函数实现,如下图:
struct sk_buff *skb_clone(struct sk_buff *skb, int gfp_mask)
从控制结构skb中clone出一个新的控制结构,它们都指向同一个网络报文。clone成功之后,将新的控制结构和原来的控制结构的is_clone,cloned两个标记都置为1。同时还增加网络报文的引用计数(这个引用计数存放在存储空间的结束地址的内存中,由函数atomic_t *skb_datarefp(struct sk_buff *skb)访问,引用计数记录了这个存储空间有多少个控制结构)。由于存在多个控制结构指向同一个存储空间的情况,所以在修改存储空间里面的内容时,先要确定这个存储空间的引用计数为1,或者用下面的拷贝函数复制一个新的存储空间,然后才可以修改它里面的内容。
当一个缓冲区被克隆时,数据区块的内容不能修改。当函数不仅需要修改sk_buff结构的内容,而且也需要修改数据时,就必须连数据区块一起克隆。这样有两种选择:
1.修改skb—>head和skb->end之间的数据内容,可以使用pskb_copy函数
2.除上述1之外,片段数据区块(skb_shared_info)的内容也要修改时,必须使用skb_copy函数
pskb_copy实现如下图
skb_copy实现如下图:
struct sk_buff *skb_copy(struct sk_buff *skb, int gfp_mask)
复制控制结构skb和它所指的存储空间的内容。复制成功之后,新的控制结构和存储空间与原来的控制结构和存储空间相对独立。所以新的控制结构里的is_clone,cloned两个标记都是0,而且新的存储空间的引用计数是1。
alloc_skb函数原型
struct sk_buff *alloc_skb(unsigned int size,int gfp_mask)
分配大小为size的存储空间存放网络报文,同时分配它的控制结构。size的值是16字节对齐的,gfp_mask是内存分配的优先级。常见的内存分配优先级有GFP_ATOMIC,代表分配过程不能被中断,一般用于中断上下文中分配内存;GFP_KERNEL,代表分配过程可以被中断,相应的分配请求被放到等待队列中。分配成功之后,因为还没有存放具体的网络报文,所以sk_buff的data,tail指针都指向存储空间的起始地址,len的大小为0,而且is_clone和cloned两个标记的值都是0。
定义在net/core/skbuff.c中的alloc_skb是分配缓冲区的主要函数。数据缓冲区和报头(sk_buff数据结构)是两种不同的实例
建立一个缓冲区有两次的内存分配(一个是分配缓冲区,另一个是分配sk_buff结构)。
dev_alloc_skb是由设备驱动程序使用的缓冲区分配函数,这个函数是在中断中执行。这个函数是alloc_skb函数的包装,为了做优化,在申请的大小之上再加需要的字节。由于中断中处理函数调用,会要求原子操作
__alloc_skb
-> skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node); // 从一个缓存中取得一个sk_buff数据结构
-> size = SKB_DATA_ALIGN(size); // 强制对齐
-> size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
-> data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc);
两个函数会释放一个缓冲区,使其返回缓冲池(缓存)。kfree_skb是直接由dev_kfree_skb调用并启动的。
只有当skb->users计数器为1时(该缓冲区已无任何用户时),这个基本函数才会释放一个缓冲区。否则,只是递减该计数器。
在sk_buff底端的skb_shared_info数据结构可以持有一些指向其他内存片段的指针。kfree_skb也会释放这些片段所持有的内存。
void kfree_skb(struct sk_buff *skb)
释放控制结构skb和它所指的存储空间。由于一个存储空间可以有多个控制结构,所以只有在存储空间的引用计数为1的情况下才释放存储空间,一般情况下,只释放控制结构skb。
void skb_reserve(struct sk_buff *skb, unsigned int len)
将data指针和tail指针同时下移。这个操作在存储空间的头部预留len长度的空隙。
unsigned char *skb_put(struct sk_buff *skb, unsigned int len)
将tail指针下移,并增加skb的len值。data和tail之间的空间就是可以存放网络报文的空间。这个操作增加了可以存储网络报文的空间,但是增加不能使tail的值大于end的值,skb的len值大于truesize的值。
unsigned char *skb_push(struct sk_buff *skb, unsigned int len)
将data指针上移,并增加skb的len值。这个操作在存储空间的头部增加了一段可以存储网络报文的空间
unsigned char * skb_pull(struct sk_buff *skb, unsigned int len)
将data指针下移,并减小skb的len值。这个操作使data指针指向下一层网络报文的头部。
void skb_trim(struct sk_buff *skb, unsigned int len)
将网络报文的长度缩减到len,丢弃了网络报文尾部的填充值。
参考文献:Linux网络编程
《Understanding Linux Network Internals》