sk_buff的意思是socket buffer,这是Linux网络子系统中的核心数据结构。
定义在
sk_buff
在不同的网络层被使用(MAC 或其他在 L2 的协议,在 L3 的 IP 协议,在 L4 的 TCP 或 UDP 等),当它从一层传递到另一层时,各个字段也会发生变化。在被传递到 L3 之前,L4 会追加头信息,然后在被传递到 L2 之前,L3 会追加头信息。从一层传递到另一层时,通过追加头信息的方式比将数据在层之间拷贝会更有效率。由于要在 buff 的开头增加空间(与平时常见的在尾部追加空间相比)是一项复杂的操作,内核便提供了 skb_reserve
函数执行这个操作。因此,随着 buffer 从上层到下层的传递,每层协议做的第一件事就是调用 skb_reserve
去为它们的协议头在 buffer 的头部分配空间。在后面,我们将通过一个例子去了解内核如何在当 buffer 在各个层间传递时,确保为每一层保留了足够的空间让它们添加它们自己的协议头。
在接收数据时,buffer 会被从下层到上层传递,在从下到上的过程中,前一层的协议头对于当前层来说已经没有用了。比如:L2 的协议头只会被处理 L2 协议的设备驱动程序使用,L3 并不关心 L2 的头。那么内核怎么做的呢? 内核的实现是:** sk_buff 中有一个指针会指向当前位于的层次的协议的协议头的内存开始地址,于是从 L2 到 L3 时,只需将指向 L2 头部的指针移动到 L3 的头部即可**(又是一步追求效率的操作)。
Linux 内核把系统中所有的 sk_buff 实例维护在一个双向链表中,sk_buff 链表的每个节点也通过 next
和 prev
分别指向后继和前驱节点。但是 sk_buff 链表还要求:每个节点必须能够很快的找到整个链表的头节点。为了实现这个要求,一个额外的数据结构(sk_buff_head
)被添加到链表的头部,作为一个空节点:
struct sk_buff_head {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;
__u32 qlen;
spinlock_t lock;
};
sk_buff 和 sk_buff_head 开始的两个节点(next
prev
)是相同的。即使 sk_buff_head 比 sk_buff 更轻量化,也允许这两种结构在链表中共存。另外,可以使用相同函数来操作 sk_buff 和 sk_buff_head。
为了实现通过每个节点都能快速找到链表头,每个节点都会包含一个指向链表中唯一的 sk_buff_head 的指针(list
)。
unsigned char *head, *data, *tail, *end;
这一节介绍先行数据区在sk_buff创建过程中的变化,图中暂时省略了非线性数据区:
sk_buff结构数据区刚被申请好,此时 head 指针、data 指针、tail 指针都是指向同一个地方。记住前面讲过的:head 指针和 end 指针指向的位置一直都不变,而对于数据的变化和协议信息的添加都是通过 data 指针和 tail 指针的改变来表现的。
开始准备存储应用层下发过来的数据,通过调用函数 skb_reserve(m) 来使 data 指针和 tail 指针同时向下移动,空出一部分空间来为后期添加协议信息。m一般为最大协议头长度,内核中定义。
开始存储数据了,通过调用函数 skb_put() 来使 tail 指针向下移动空出空间来添加数据,此时 skb->data 和 skb->tail 之间存放的都是数据信息,无协议信息。
这时就开始调用函数 skb_push() 来使 data 指针向上移动,空出空间来添加各层协议信息,添加协议信息也是用skb_put()。直到最后到达二层,添加完帧头然后就开始发包了。
/* include/linux/skbuff.h */
#if (65536/PAGE_SIZE + 1) < 16
#define MAX_SKB_FRAGS 16UL
#else
#define MAX_SKB_FRAGS (65536/PAGE_SIZE + 1)
#endif
struct skb_shared_info {
unsigned char nr_frags; /*表示有多少分片结构*/
__u8 tx_flags;
unsigned short gso_size;
/* Warning: this field is not always filled in (UFO)! */
unsigned short gso_segs;
unsigned short gso_type;
struct sk_buff *frag_list; /*一种类型的分配数据*/
struct skb_shared_hwtstamps hwtstamps;
u32 tskey;
__be32 ip6_frag_id;
/*
* Warning : all fields before dataref are cleared in __alloc_skb()
*/
atomic_t dataref; /*用于引用计数,克隆一个skb结构体时会增加一个引用计数*/
/* Intermediate layers must ensure that destructor_arg
* remains valid until skb destructor */
void * destructor_arg;
/* must be last field, see pskb_expand_head() */
skb_frag_t frags[MAX_SKB_FRAGS]; /*保存分页数据,skb->data_len = 所有数组数据长度之和*/
};
一个skb用于存储一个报文,如果一个报文特别大的话,一个skb存储不下,就会启用非线性数据区作为数据区域的扩展。
从上图中可以看出,skb->end的下一个字节就作为非线性数据区的开始。end指针的下个字节可以作为分片结构的开始,获取end指针的位置要强行转成分片结构,内核中有定义好的宏:
#define skb_shinfo(SKB) ((struct skb_shared_info *)(skb_end_pointer(SKB)))
非线性区域有两种组织方式:
线性存储区放不下就需要多个skb来存储,这就是下面frag_list的作用,保存连续的skb,但是如果内核支持分散聚集技术的话,并且报文长度刚好又不大于mtu,就不必重新分配一个skb来存储,可以使用一些内存碎片来存储,就是下面的frags数组表示的内存页面片段。
(1)用数组存储的分片数据区。
采用是是结构体中的frags[MAX_SKB_FRAGS]。skb_frag_struct结构体如下:
/* include/linux/skbuff.h */
typedef struct skb_frag_struct skb_frag_t;
struct skb_frag_struct {
struct {
struct page *p; /*指向分片数据区的指针,类似于sk_buff中的data指针*/
} page;
#if (BITS_PER_LONG > 32) || (PAGE_SIZE >= 65536)
__u32 page_offset;
__u32 size;
#else
__u16 page_offset; /*偏移量,表示相对开始位置的页偏移量*/
__u16 size; /*page中的数据长度*/
#endif
};
下图显示了frags是怎么分配分片数据的:
(2)frag_list指针来指向的分片数据。
unsigned int len, data_len, mac_len;
unsigned int truesize;
这里要声明两个概念的区别,后续直接用这两个概念,注意区分:
(1)线性区总长度:skb_data_len = skb->end - skb->head;(不包括 skb_shared_info)
(2)线性区有效数据长度:linear_buffer_len = skb->tail - skb->data;不包含线性数据中的头空间和尾空间。
skb->data_len: skb中的分片数据(非线性数据)的长度。
skb->len: skb中的数据块的总长度,数据块包括实际线性数据和非线性数据,非线性数据为data_len,所以skb->len= (tail - data) + data_len。
skb->truesize: skb的总长度,包括sk_buff结构和数据部分,skb=sk_buff控制信息 + 线性数据(包括头空间和尾空间) + skb_shared_info控制信息 + 非线性数据,所以skb->truesize = sizeof(struct sk_buff) + (end - head) + sizeof(struct skb_shared_info) + data_len。
依次是传输层、网络层、mac层的头部指针,用union表示是互斥的,只能表示其中的一种
union {
struct tcphdr *th;
struct udphdr *uh;
struct icmphdr *icmph;
struct igmphdr *igmph;
struct iphdr *ipiph;
struct ipv6hdr *ipv6h;
unsigned char *raw;
} h;
union {
struct iphdr *iph;
struct ipv6hdr *ipv6h;
struct arphdr *arph;
unsigned char *raw;
} nh;
union {
unsigned char *raw;
} mac
当接收到数据包时,从底层向顶层传递数据的时候,只需要把对应的skb->data字段指向相应的头部即可。
struct dst_entry dst
struct dst_entry *dst;
struct sec_path *sp
dst_entry
指向需要转发包的函数,这个指针在sk_buff在IP层传输前必须是合法的sec_path
是一个可选的有关网络安全的成员 char cb[48]
每层协议私有的信息存储空间,由每一层自己维护和使用,并只在本层有效
TCP, for example, uses that space to store a tcp_skb_cb data structure, which is defined in include/net/tcp.h:
struct tcp_skb_cb { ... ... ... _ _u32 seq; /* Starting sequence number */ _ _u32 end_seq; /* SEQ + FIN + SYN + datalen*/ _ _u32 when; /* used to compute rtt's */ _ _u8 flags; /* TCP header flags. */ ... ... ... };
这是 TCP 代码访问结构的宏。宏仅由一个指针转换组成:
#define TCP_SKB_CB(_ _skb) ((struct tcp_skb_cb *)&((_ _skb)->cb[0]))
int tcp_v4_rcv(struct sk_buff *skb) { ... ... ... th = skb->h.th; TCP_SKB_CB(skb)->seq = ntohl(th->seq); TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin + skb->len - th->doff * 4); TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq); TCP_SKB_CB(skb)->when = 0; TCP_SKB_CB(skb)->flags = skb->nh.iph->tos; TCP_SKB_CB(skb)->sacked = 0; ... ... ... }
报文到达或者离开的时间戳,一般表示何时收到报文,一般是在包从驱动中往二层发送的接口函数中设置。它在接收函数 netif_rx
(kernel/linux/net/core/dev.c)中设置。
/include/linux/skbuff.h
static inline void __net_timestamp(struct sk_buff *skb)
{
skb->tstamp = ktime_get_real();
}
指向网络设备,作用与该sk_buff是发送包还是接收包有关。在初始化网络设备驱动,分配接收缓存队列时,将该指针指向收到数据包的网络设备
This is the device the packet has been received from. It is a NULL pointer when the packet has been generated locally. 指向接收报文的原始网络设备,如果包是本地生成的,则该值为NLL,主要用于流量控制
虚拟设备才有效
atomic_t users;
/**
* struct sk_buff - socket buffer
* @next: Next buffer in list
* @prev: Previous buffer in list
* @tstamp: Time we arrived/left
* @rbnode: RB tree node, alternative to next/prev for netem/tcp
* @sk: Socket we are owned by
* @dev: Device we arrived on/are leaving by
* @cb: Control buffer. Free for use by every layer. Put private vars here
* @_skb_refdst: destination entry (with norefcount bit)
* @sp: the security path, used for xfrm
* @len: Length of actual data
* @data_len: Data length
* @mac_len: Length of link layer header
* @hdr_len: writable header length of cloned skb
* @csum: Checksum (must include start/offset pair)
* @csum_start: Offset from skb->head where checksumming should start
* @csum_offset: Offset from csum_start where checksum should be stored
* @priority: Packet queueing priority
* @ignore_df: allow local fragmentation
* @cloned: Head may be cloned (check refcnt to be sure)
* @ip_summed: Driver fed us an IP checksum
* @nohdr: Payload reference only, must not modify header
* @nfctinfo: Relationship of this skb to the connection
* @pkt_type: Packet class
* @fclone: skbuff clone status
* @ipvs_property: skbuff is owned by ipvs
* @peeked: this packet has been seen already, so stats have been
* done for it, don't do them again
* @nf_trace: netfilter packet trace flag
* @protocol: Packet protocol from driver
* @destructor: Destruct function
* @nfct: Associated connection, if any
* @nf_bridge: Saved data about a bridged frame - see br_netfilter.c
* @skb_iif: ifindex of device we arrived on
* @tc_index: Traffic control index
* @tc_verd: traffic control verdict
* @hash: the packet hash
* @queue_mapping: Queue mapping for multiqueue devices
* @xmit_more: More SKBs are pending for this queue
* @pfmemalloc: skbuff was allocated from PFMEMALLOC reserves
* @ndisc_nodetype: router type (from link layer)
* @ooo_okay: allow the mapping of a socket to a queue to be changed
* @l4_hash: indicate hash is a canonical 4-tuple hash over transport
* ports.
* @sw_hash: indicates hash was computed in software stack
* @wifi_acked_valid: wifi_acked was set
* @wifi_acked: whether frame was acked on wifi or not
* @no_fcs: Request NIC to treat last 4 bytes as Ethernet FCS
* @napi_id: id of the NAPI struct this skb came from
* @secmark: security marking
* @mark: Generic packet mark
* @vlan_proto: vlan encapsulation protocol
* @vlan_tci: vlan tag control information
* @inner_protocol: Protocol (encapsulation)
* @inner_transport_header: Inner transport layer header (encapsulation)
* @inner_network_header: Network layer header (encapsulation)
* @inner_mac_header: Link layer header (encapsulation)
* @transport_header: Transport layer header
* @network_header: Network layer header
* @mac_header: Link layer header
* @tail: Tail pointer
* @end: End pointer
* @head: Head of buffer
* @data: Data head pointer
* @truesize: Buffer size
* @users: User count - see {datagram,tcp}.c
*/
struct sk_buff {
union {
struct {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;
union {
ktime_t tstamp;
struct skb_mstamp skb_mstamp;
};
};
struct rb_node rbnode; /* used in netem & tcp stack */
};
struct sock *sk;
struct net_device *dev;
/*
* 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);
unsigned long _skb_refdst;
void (*destructor)(struct sk_buff *skb);
#ifdef CONFIG_XFRM
struct sec_path *sp;
#endif
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
struct nf_conntrack *nfct;
#endif
#if IS_ENABLED(CONFIG_BRIDGE_NETFILTER)
struct nf_bridge_info *nf_bridge;
#endif
unsigned int len,
data_len;
__u16 mac_len,
hdr_len;
/* Following fields are _not_ copied in __copy_skb_header()
* Note that queue_mapping is here mostly to fill a hole.
*/
kmemcheck_bitfield_begin(flags1);
__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)
__u8 __cloned_offset[0];
__u8 cloned:1,
nohdr:1,
fclone:2,
peeked:1,
head_frag:1,
xmit_more:1,
pfmemalloc:1;
kmemcheck_bitfield_end(flags1);
/* 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)
__u8 __pkt_type_offset[0];
__u8 pkt_type:3;
__u8 ignore_df:1;
__u8 nfctinfo:3;
__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;
__u8 csum_complete_sw:1;
__u8 csum_level:2;
__u8 csum_bad: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;
#endif
/* 2, 4 or 5 bit hole */
#ifdef CONFIG_NET_SCHED
__u16 tc_index; /* traffic control index */
#ifdef CONFIG_NET_CLS_ACT
__u16 tc_verd; /* traffic control verdict */
#endif
#endif
union {
__wsum csum;
struct {
__u16 csum_start;
__u16 csum_offset;
};
};
__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;
atomic_t users;
};
ref:
Linux 内核网络协议栈 ------sk_buff 结构体 以及 完全解释 (2.6.16) - 明明是悟空 - 博客园
Linux SKB结构体中各个长度字段的含义(len, data_len, headlen, pagelen)_mrsonko的博客-CSDN博客_skb_headlen
https://abcdxyzk.github.io/download/kernel/sk_buff%E8%AF%A6%E8%A7%A3.pdf
Linux内核中sk_buff结构详解 - 简书
理解Linux内部网络实现之关键数据结构 sk_buff – Yang Blog
Section 2.1. The Socket Buffer: sk_buff Structure--深入理解linux网络技术内幕--嵌入式linux中文站
Linux内核网络源码解析1——sk_buff结构
http://wiki.dreamrunner.org/public_html/Linux/Networks/sk_buff-structure-analysis.html