/*
socket buffers,简称skb,中文名字叫套接字缓存。
它作为网络数据包的存放地点,使得协议栈中每个层都可以对数据进行操作,从而实现了数据包自底向上的传递。
该结构维护一个收到的或者要发送的网络包。但其本身并不包含存放网络包的数据的存储区。
存储区是另外单独分配的内存空间,但该结构说明了如何访问存储区空间,如何维护多个存储区空间以及存储网络包解析的成果。
所有的sk_buff是通过一个双向链表进行维护的。需要说明的是该双向链表中的一个元素是struct sk_buff_head类型。它相当于该双向链表的表头,其中有锁,链表元素个数等维护链表的相关信息。链表中其它的元素都是sk_buff类型。
这个结构被网络的不同层(MAC或者其他二层链路协议,三层的IP,四层的TCP或UDP等)使用,并且其中的成员变量在结构从一层向另一层传递时改变。 L4向L3传递前会添加一个L4的头部,同样,L3向L2传递前,会添加一个L3的头部。添加头部比在不同层之间拷贝数据的效率更高。由于在缓冲区的头部添加数据意味着要修改指向缓冲区的指针,这是个复杂的操作,所以内核提供了一个函数skb_reserve来完成这个功能。协议栈中的每一层在往下一层传递缓冲区前,第一件事就是调用skb_reserve在缓冲区的头部给协议头预留一定的空间。
skb_reserve同样被设备驱动使用来对齐接收到包的包头。如果缓冲区向上层协议传递,旧的协议层的头部信息就没什么用了。例如,L2的头部只有在网络驱动处理L2的协议时有用,L3是不会关心它的信息的。但是,内核并没有把L2的头部从缓冲区中删除,而是把有效荷载的指针指向L3的头部,这样做,可以节省CPU时间。
*/
struct sk_buff {
/* These two members must be first. */
// next和prev,这两个域是用来连接相关的skb的(例如如果有分片,将这些分片连接在一起可以)
struct sk_buff *next; //Next buffer in list
struct sk_buff *prev; // Previous buffer in list
//next和prev指针:协议栈中经常用到sk_buff的队列,队列通过sk_buff中的指针相连接。这两个指针用来连接相关的skb的(例如有分片)
ktime_t tstamp; //报文到达或者离开的时间戳; Time we arrived 表示这个skb的接收到的时间,一般是在包从驱动中往二层发送的接口函数中设置
struct sock *sk; //指向报文所属的套接字指针;Socket we are owned by 表示从属于那个socket,主要是被4层用到。
/*
这是一个指向拥有这个sk_buff的sock结构的指针。
这个指针在网络包由本机发出或者由本机进程接收时有效,因为插口相关的信息被L4(TCP或 UDP)或者用户空间程序使用。
如果sk_buff只在转发中使用(这意味着,源地址和目的地址都不是本机地址),这个指针是NULL
*/
struct net_device *dev; //表示一个接收或者发送报文的网络设备; Device we arrived on/are leaving by
//这个表示一个网络设备,当skb为输出时它表示skb将要输出的设备,当接收时,它表示输入设备。要注意,这个设备有可能会是虚拟设备(在3层以上看来)
char cb[48] __aligned(8); //保存每一层的控制信息,以及私有信息;Control buffer. Free for use by every layer. Put private vars here
/*
cb 这个域很重要,我们下面会详细说明。这里只需要知道这个域是保存每层的控制信息的就够
中的48个字节是控制字段,配合各层协议工作,为每层存储必要的控制信息。
这个字段是skb信息控制块,也就是存储每层的一些协议信息,当数据包在哪一层时,存储的就是哪一层协议信息。
这个字段由数据包所在层使用和维护,如果要访问本层协议信息,可以通过用一些宏来操作这个成员字段。
如:#define TCP_SKB_CB(__skb) ((struct tcp_skb_cb *)&((__skb)->cb[0])) )
*/
unsigned long _skb_refdst; // destination entry (with norefcount bit)
///这里其实应该是dst_entry类型,不知道为什么内核要改为ul。这个域主要用于路由子系统。这个数据结构保存了一些路由相关信息
#ifdef CONFIG_XFRM
struct sec_path *sp; 安全路径,用于xfrm
#endif
unsigned int len, // Length of actual data //当前协议数据包的长度,包括主缓冲区中数据长度和分片中数据长度
//(个人理解为当前协议层包头和数据区域长度)。skb的组成是sk_buff控制+线性数据+非线性数据(skb_shared_info)组成!
// 表示当前协议数据包的长度。它包括主缓冲区中的数据长度(data指针指向它)和分片中的数据长度
//这个长度表示当前的skb中的数据的长度,这个长度即包括buf中的数据也包括切片的数据,也就是保存在skb_shared_info中的数据。
//这个值是会随着从一层到另一层而改变的。下面我们会对比这几个长度的。
data_len; //Data length ;//分片中数据长度 和len不同,data_len只计算分片中数据的长度 // 这个长度只表示切片数据的长度,也就是skb_shared_info中的长度。
__u16 mac_len, // Length of link layer header 这是mac头的长度 //这个长度表示mac头的长度(2层的头的长度)
hdr_len; //writable header length of cloned skb 用于clone时,表示clone的skb的头长度;//这个主要用于clone的时候,它表示clone的skb的头的长度。
///接下来是校验相关的域。
union {
__wsum csum; // Checksum (must include start/offset pair)
struct {
__u16 csum_start; //skb->head即校验和计算起始点 Offset from skb->head where checksumming should start
__u16 csum_offset; //Offset from csum_start where checksum should be stored 校验和存储位置偏移(以csum_start处起始偏移csum_offset大小)
};
};
///优先级,主要用于QOS。
__u32 priority; //Packet queueing priority 报文排队优先级,取决于ip中的tos域
kmemcheck_bitfield_begin(flags1);
///接下来是一些标志位。
__u8 local_df:1, //是否可以本地切片的标志。
cloned:1, ///为1说明头可能被clone。 保存当前的skb_buff是克隆的还是原始数据
ip_summed:2, ///这个表示校验相关的一个标记,表示硬件驱动是否为我们已经进行了校验(前面的blog有介绍)
nohdr:1, ///这个域如果为1,则说明这个skb的头域指针已经分配完毕,因此这个时候计算头的长度只需要head和data的差就可以了。
nfctinfo:3;
__u8 pkt_type:3, ///pkt_type主要是表示数据包的类型,比如多播,单播,回环等等。
/*
这个变量表示帧的类型,分类是由L2的目的地址来决定的。这个值在网卡驱动程序中由函数eth_type_trans通过判断目的以太网地址来确定。
如果目的地址是FF:FF:FF:FF:FF:FF,则为广播地址,pkt_type = PACKET_BROADCAST;
如果最高位为1,则为组播地址,pkt_type = PACKET_MULTICAST;如果目的mac地址跟本机mac地址不相等,则不是发给本机的数据报,
pkt_type = PACKET_OTHERHOST;否则就是缺省值PACKET_HOST。
*/
fclone:2, ///这个域是一个clone标记。主要是在fast clone中被设置,我们后面讲到fast clone时会详细介绍这个域。
/*
这是个克隆状态标志,到sk_buff结构内存申请时会使用到。这里提前讲下:若fclone = SKB_FCLONE_UNAVAILABLE,则表明SKB未被克隆;
若fclone = SKB_FCLONE_ORIG,则表明是从skbuff_fclone_cache缓存池(这个缓存池上分配内存时,每次都分配一对skb内存)中分配的父skb,可以被克隆;
若fclone = SKB_FCLONE_CLONE,则表明是在skbuff_fclone_cache分配的子SKB,从父SKB克隆得到的;
*/
ipvs_property:1, ///ipvs拥有的域。
peeked:1, ///这个域应该是udp使用的一个域。表示只是查看数据。
nf_trace:1; ///netfilter使用的域。是一个trace 标记
kmemcheck_bitfield_end(flags1);
__be16 protocol; ///这个表示L3层的协议。比如IP,IPV6等等。
void (*destructor)(struct sk_buff *skb); //skb的析构函数,一般都是设置为sock_rfree或者sock_wfree.
///netfilter相关的域。
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
struct nf_conntrack *nfct;
#endif
#ifdef NET_SKBUFF_NF_DEFRAG_NEEDED
struct sk_buff *nfct_reasm;
#endif
#ifdef CONFIG_BRIDGE_NETFILTER
struct nf_bridge_info *nf_bridge;
#endif
int skb_iif; //接收设备的index。
///流量控制的相关域。
#ifdef CONFIG_NET_SCHED
__u16 tc_index; /* traffic control index */
#ifdef CONFIG_NET_CLS_ACT
__u16 tc_verd; /* traffic control verdict */
#endif
#endif
__u32 rxhash;
kmemcheck_bitfield_begin(flags2);
///多队列设备的映射,也就是说映射到那个队列。
__u16 queue_mapping:16;
#ifdef CONFIG_IPV6_NDISC_NODETYPE
__u8 ndisc_nodetype:2;
#endif
__u8 ooo_okay:1;
kmemcheck_bitfield_end(flags2);
/* 0/13 bit hole */
#ifdef CONFIG_NET_DMA
dma_cookie_t dma_cookie;
#endif
#ifdef CONFIG_NETWORK_SECMARK
__u32 secmark;
#endif
union {
__u32 mark; // skb的标记。
__u32 dropcount;
};
__u16 vlan_tci; //vlan标签控制信息;
sk_buff_data_t transport_header; //传输层头
sk_buff_data_t network_header; // 网络层头指针
sk_buff_data_t mac_header; //链路层头
/* 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; //保存数据内容的首地址;(实际数据区域开始位置)
/*
sk_buff->head sk_buff->data sk_buff->tail sk_buff->end 它们表示缓冲区和数据部分的边界。
在每一层申请缓冲区时,它会分配比协议头或协议数据大的空间。head和end指向缓冲区的头部和尾部,而data和 tail指向实际数据的头部和尾部。
每一层会在head和data之间填充协议头,或者在tail和end之间添加新的协议数据。数据部分会在尾部包含一 个附加的头部。
*/
unsigned int truesize; //该缓冲区分配的所有总的内存,包括:skb_buff + 所有数据大小;//这个表示整个skb的大小,包括skb本身,以及数据。
/*这是缓冲区的总长度,包括sk_buff结构和数据部分。如果申请一个len字节的缓冲区,alloc_skb函数会把它初始化成len+sizeof(sk_buff)。当skb->len变化时,这个变量也会变化
*/
atomic_t users; //保存引用skb_buff的数量
/*
这是一个引用计数,用于计算有多少实体引用了这个sk_buff缓冲区。
它的主要用途是防止释放sk_buff后,还有其他实体引用这个sk_buff。
因此,每个引用这个缓冲区的实体都必须在适当的时候增加或减小这个变量。
这个计数器只保护sk_buff结构本身,而缓冲区的数据部分由类似的计数器 (dataref)来保护.有时可以用atomic_inc和atomic_dec函数来直接增加或减小users,
但是,通常还是使用函数 skb_get和kfree_skb来操作这个变量。
atomic_t users;这是个引用计数,表明了有多少实体引用了这个skb。其作用就是在销毁skb结构体时,先查看下users是否为零,
若不为零,则调用函数递减下引用计数users即可;当某一次销毁时,users为零才真正释放内存空间。
有两个操作函数:atomic_inc()引用计数增加1;atomic_dec()引用计数减去1;
*/
};
/*
2. SKB 的分配时机
SKB 的分配时机主要有两种,最常见的一种是在网卡的中断中,有数据包到达的时,系统分配 SKB 包进行包处理;
5. 中断环境下 SKB 的分配流程
当数据到达网卡后,会触发网卡的中断,从而进入 ISR 中,系统会在 ISR 中计算出此次接收到的数据的字节数 : pkt_len, 然后调用 SKB 分配函数来分配 SKB :
skb = dev_alloc_skb(pkt_len+5);
我们可以看到, 实际上传入的数据区的长度还要比实际接收到的字节数多,这实际上是一种保护机制.
实际上,在 dev_alloc_skb 函数调用 __dev_alloc_skb 函数,而 __dev_alloc_skb 函数又调用 alloc_skb 函数 时,
其数据区的大小又增加了 128 字节, 这 128 字节就事前面我们所说的 reserve 机制预留的 header 空间.
第二种情况是主动分配 SKB 包用于各种调试或者其他处理环境.
3. SKB 的 reserve 操作
SKB 在分配的过程中使用了一个小技巧 : 即在数据区中预留了 128 个字节大小的空间作为协议头使用, 通过移动 SKB 的 data 与 tail 指针的位置来实现这个功能.
*/
/* lock用于防止对链表的并发访问
Struct sk_buffer 是 Linux TCP/IP stack 中,用于管理Data Buffer的结构。Sk_buffer 在数据包的发送和接收中起着重要的作用。
为了提高网络处理的性能,应尽量避免数据包的拷贝。Linux 内核开发者们在设计 sk_buffer 结构的时候,充分考虑到这一点。目前 Linux 协议栈在接收数据的时候,需要拷贝两次:数据包进入网卡驱动后拷贝一次,从内核空间递交给用户空间的应用时再拷贝一次。
Sk_buffer结构随着内核版本的升级,也一直在改进。
*/
/*
sk_buff结构体数据区:
sk_buff结构体只是网络数据包中的一些配置,真正包含传输内容和传输协议的都是在sk_buff结构体中几个指针所指向的数据区中。
这里先简称数据区,数据区的大小是:(skb->end - skb->head);对于每个数据包来说这个大小都是固定的,
而且在传输过程中skb->end和skb->head所指向的地址都是不变的。这块数据区是用来存放应用层发下来的数据和各层的协议信息。
但在计算数据长度或者操作协议信息时,一般都要和实际的数据存放指针为准。
实际数据指针为data和tail,data指向实际数据开始的地方,tail指向实际数据结束的地方。
*/
/*
Fast SKB cloning函数,这个函数存在的主要原因是,以前我们每次skb_clone一个skb的时候,
都是要调用kmem_cache_alloc从cache中alloc一块新的内存。而现在当我们拥有了fast clone之后,
通过调用alloc_skb_fclone函数来分配一块大于sizeof(struct sk_buff)的内存,
也就是在这次请求的skb的下方多申请了一些内存,然后返回的时候设置返回的skb的fclone标记为SKB_FCLONE_ORIG,
而多申请的那块内存的sk_buff的fclone为SKB_FCLONE_UNAVAILABLE,
这样当我们调用skb_clone克隆这个skb的时候看到fclone的标记就可以直接将skb的指针+1,而不需要从cache中取了。
这样的话节省了一次内存存取,提高了clone的效率,不过调用flcone 一般都是我们确定接下来这个skb会被clone很多次。
sk_buff 详解(一)
最新推荐文章于 2024-06-14 10:45:29 发布