协议栈skb _buff

sk_buff 在不同的网络层被使用(MAC 或其他在 L2 的协议,在 L3 的 IP 协议,在 L4 的 TCP 或 UDP 等),当它从一层传递到另一层时,各个字段也会发生变化。sk_buff中保存了L2,L3,L4层的头指针,这样在层传递时只需要对数据缓冲区改变头部信息,并调整sk_buff中的指针,而不需要拷贝数据,这样大大减少了内存拷贝的需要。由于要在 buff 的开头增加空间(与平时常见的在尾部追加空间相比)是一项复杂的操作,内核便提供了 skb_reserve 函数执行这个操作。因此,随着 buffer 从上层到下层的传递,每层协议做的第一件事就是调用 skb_reserve 去为它们的协议头在 buffer 的头部分配空间。在后面,我们将通过一个例子去了解内核如何在当 buffer 在各个层间传递时,确保为每一层保留了足够的空间让它们添加它们自己的协议头

 

各字段含义如下:

head:指向分配给的线性数据内存首地址。

data:指向保存数据内容的首地址。

tail:指向数据的结尾。 

end:指向分配的内存块的结尾。

len:数据的长度。

head room: 位于head至data之间的空间,用于存储:protocol header,例如:TCP header, IP header, Ethernet header等。

user data: 位于data至tail之间的空间,用于存储:应用层数据,一般系统调用时会使用到。 

tail room: 位于tail至end之间的空间,用于填充用户数据未使用完的空间。

skb_shared_info: 位于end之后,用于存储特殊数据结构skb_shared_info,该结构用于描述分片信息。

sk_buf的常用操作函数如下:

alloc_skb:分配sk_buf。

skb_reserve:为sk_buff设置header空间。

skb_put:添加用户层数据。

 skb_push:向header空间添加协议头。

skb_pull:复位data至数据区。

unsigned char *head      用于指向数据包的开始
unsigned char *data      用于指向数据包载荷的开始
unsigned char *tail      用于指向数据包载荷的结束
unsigned char *end       用于指向数据包的结束
unsigned int len         数据包包含的数据量

它在实际应用中是这样发挥作用的。设skb指向一个sk_buff,当数据包穿过协议栈各层时,skb->head,skb->data,skb->tail以及skb->end在数据包相关缓冲区上移动。如下图所示,指向正在处理数据包的协议栈头部。当一个数据包到达mac层时,skb->data指向以太网帧头部,当数据包继续到达IP层时,skb->data就移到IP头部的起始处。与此同时,skb->len也会更新。

这样的设计就是为了便于SKB数据结构在不同层之间灵活地增删头部,另外它还是一个双向链表,便于把不同的SKB串联起来,这也是Linux网络处理的一个优化,叫做“聚合分散IO”,也就是Linux网络处理中的零拷贝。由于有些报文发送时,会有多个分片,如果依次拷贝这些分片组装成一个单块,就会存在从用户空间多次内存拷贝到内核空间巨大的开销。因此聚合分散IO的想法就是在SKB上标注分片数目,将其他分片链接到第一个,在此过程中只拷贝记录分片数据位置和长度的数据缓存区到SKB中,而避免多次的数据包拷贝,而后由DMA模块直接将数据从内核缓冲区传递给协议模块。

skb_buff代码如下:

struct sk_buff {
	/* These two members must be first. */
	struct sk_buff		*next;  //  因为sk_buff结构体是双链表,所以有前驱后继。这是个指向后面的sk_buff结构体指针
	struct sk_buff		*prev;  //  这是指向前一个sk_buff结构体指针
	//老版本(2.6以前)应该还有个字段: sk_buff_head *list  //即每个sk_buff结构都有个指针指向头节点
	struct sock		    *sk;  // 指向拥有此缓冲的套接字sock结构体,即:宿主传输控制模块
	ktime_t			tstamp;  // 时间戳,表示这个skb的接收到的时间,一般是在包从驱动中往二层发送的接口函数中设置
	struct net_device	*dev;  // 表示一个网络设备,当skb为输出/输入时,dev表示要输出/输入到的设备
	unsigned long	_skb_dst;  // 主要用于路由子系统,保存路由有关的东西
	char			cb[48];  // 保存每层的控制信息,每一层的私有信息
	unsigned int		len,  // 表示数据区的长度(tail - data)与分片结构体数据区的长度之和。其实这个len中数据区长度是个有效长度,
                                      // 因为不删除协议头,所以只计算有效协议头和包内容。如:当在L3时,不会计算L2的协议头长度。
				data_len;  // 只表示分片结构体数据区的长度,所以len = (tail - data) + data_len;
	__u16			mac_len,  // mac报头的长度
				hdr_len;  // 用于clone时,表示clone的skb的头长度
	// 接下来是校验相关域,这里就不详细讲了。
	__u32			priority;  // 优先级,主要用于QOS
	kmemcheck_bitfield_begin(flags1);
	__u8			local_df:1,  // 是否可以本地切片的标志
				cloned:1,  // 为1表示该结构被克隆,或者自己是个克隆的结构体;同理被克隆时,自身skb和克隆skb的cloned都要置1
				ip_summed:2, 
				nohdr:1,  // nohdr标识payload是否被单独引用,不存在协议首部。                                                                                                      // 如果被引用,则决不能再修改协议首部,也不能通过skb->data来访问协议首部。</span></span>
				nfctinfo:3;
	__u8			pkt_type:3,  // 标记帧的类型
				fclone:2,   // 这个成员字段是克隆时使用,表示克隆状态
				ipvs_property:1,
				peeked:1,
				nf_trace:1;
	__be16			protocol:16;  // 这是包的协议类型,标识是IP包还是ARP包或者其他数据包。
	kmemcheck_bitfield_end(flags1);
	void	(*destructor)(struct sk_buff *skb);  // 这是析构函数,后期在skb内存销毁时会用到
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
	struct nf_conntrack	*nfct;
	struct sk_buff		*nfct_reasm;
#endif
#ifdef CONFIG_BRIDGE_NETFILTER
	struct nf_bridge_info	*nf_bridge;
#endif
	int			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
	kmemcheck_bitfield_begin(flags2);
	__u16			queue_mapping:16;
#ifdef CONFIG_IPV6_NDISC_NODETYPE
	__u8			ndisc_nodetype:2;
#endif
	kmemcheck_bitfield_end(flags2);
	/* 0/14 bit hole */
#ifdef CONFIG_NET_DMA
	dma_cookie_t		dma_cookie;
#endif
#ifdef CONFIG_NETWORK_SECMARK
	__u32			secmark;
#endif
	__u32			mark;
	__u16			vlan_tci;
	sk_buff_data_t		transport_header;      // 指向四层帧头结构体指针
	sk_buff_data_t		network_header;	       // 指向三层IP头结构体指针
	sk_buff_data_t		mac_header;	       // 指向二层mac头的头
	/* 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;		  // 表示总长度,包括sk_buff自身长度和数据区以及分片结构体的数据区长度
	atomic_t		users;                    // skb被克隆引用的次数,在内存申请和克隆时会用到
};   //end sk_buff

char cb[48];这个字段是skb信息控制块,也就是存储每层的一些协议信息,当数据包在哪一层时,存储的就是哪一层协议信息。这个字段由数据包所在层使用和维护,如果要访问本层协议信息,可以通过用一些宏来操作这个成员字段。如:#define TCP_SKB_CB(__skb) ((struct tcp_skb_cb *)&((__skb)->cb[0]))

_u8 fclone:2;这是个克隆状态标志,到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克隆得到的;

atomic_t users;这是个引用计数,表明了有多少实体引用了这个skb。其作用就是在销毁skb结构体时,先查看下users是否为零,若不为零,则调用函数递减下引用计数users即可;当某一次销毁时,users为零才真正释放内存空间。有两个操作函数:atomic_inc()引用计数增加1;atomic_dec()引用计数减去1;

sk_buff->data_len:只计算分片中数据的长度,即是分片结构体中page指向的数据区长度。这个在分片结构体中会再详细讲解下。

sk_buff->len:表示当前缓冲区中数据块的大小的总长度。它包括主缓冲中(即是sk_buff结构中指针data指向)的数据区的实际长度(data-tail)和分片中的数据长度。这个长度在数据包在各层间传输时会改变,因为分片数据长度不变,从L2到L4时,则len要减去帧头大小和网络头大小;从L4到L2则相反,要加上帧头和网络头大小。所以:len = (data - tail) + data_len;

sk_buff->truesize:这是缓冲区的总长度,包括sk_buff结构和数据部分。如果申请一个len字节的缓冲区,alloc_skb函数会把它初始化成len+sizeof(sk_buff)。当skb->len变化时,这个变量也会变化。所以:truesize = len + sizeof(sk_buff) = (data - tail) + data_len + sizeof(sk_buff);

另外一个博主的文章

 实际上skb_buf结构只是一块已经申请好的套接字缓冲区的指针和属性数据的描述集合,netdev_alloc_skb函数申请到一块套接字缓冲区后,返回记录这块缓冲区信息的skb_buf结构,在各个网络层传输的只是skb_buf结构,换句话说,仅仅是该套接字缓冲区的指针而已,各个网络层根据传来的指针,对指针进行操作,往已经申请好的固定套接字缓冲区里读写数据。

       sk_buff结构的成员skb->head指向一个已分配的空间的头部,即申请到的整个缓冲区的头,skb->end指向该空间的尾部,这两个成员指针从空间创建之后,就不能被修改。skb->data指向分配空间中数据的头部,skb->tail指向数据的尾部,这两个值随着网络数据在各层之间的传递、修改,会被不断改动。刚开始接触skb_buf的时候会产生一种错误的认识,就是以为协议头都会是放在skb->head和skb->data这两个指针之间,但实际上skb_buf的操作函数都无法直接对这一段内存进行操作,所有的操作函数所做的就仅仅是修改skb->data和skb->tail这两个指针而已,向套接字缓冲区拷贝数据也是由其它函数来完成的,所以不管是从网卡接受的数据还是上层发下来的数据,协议头都是被放在了skb->data到skb->tail之间,通过skb_push前移skb->data加入协议头,通过skb_pull后移skb->data剥离协议头。四个指针间存在如下关系:
            sk_buff结构被不同的网络层(MAC或者其他二层链路协议,三层的IP,四层的TCP或UDP等)使用,并且其中的成员变量(一般是数据指针)在结构从一层向另一层传递时改变。L4向L3传递前会添加一个L4的头部,同样,L3向L2传递前,会添加一个L3的头部。添加头部比在不同层之间拷贝数据的效率更高。由于在缓冲区的头部添加数据意味着要修改指向缓冲区的指针,这是个复杂的操作,所以内核提供了一个函数skb_reserve将数据部分往后移。协议栈中的每一层在往下一层传递缓冲区前,第一件事就是调用skb_reserve在缓冲区的头部给协议头预留一定的空间,再用skb_push函数将协议头查到skb->data指针之前。

     skb_reserve同样被设备驱动使用来对齐接收到包的包头。如果缓冲区向上层协议传递,旧的协议层的头部信息就没什么用了。例如,L2的头部只有在网络驱动处理L2的协议时有用,L3是不会关心它的信息的。但是,内核并没有把L2的头部从缓冲区中删除,而是用sk_buff结构中对应的头指针指向这个头,再把有效荷载的指针指向L3的头部,这样做,可以节省CPU时间。当接收一个包时,处理n层协议头的函数从n-1层收到一个缓冲区,因为在n-1层处理的最后,skb->data是指向n-1层的数据开始指针,所以到了n层后skb->data就是指向n层协议的头。处理n层协议的函数把本层的指针(例如,L3对应的是skb- >nh指针)初始化为skb->data,取出本层的协议头指针,在处理n层协议的函数结束时,在把包传递给n+1层的处理函数前,它会把skb->data指针指向n层协议头的末尾,这正好是n+1层协议的协议头(参见下图)。

     

           skb_buf在各个网络层传输过程。

        在网络数据发送过程中,由netdev_alloc_skb申请得到套接字缓冲区后,skb_reserve将整个数据块往后移动MAX_TCP_HEADER个字节,预留出skb中协议头的最大长度,确保在套接字从上层不断往下层传递的过程中,有足够的协议头空间。在数据每传到一个网络层的时候,通过skb_push函数前移skb->data将协议头插入数据区,一直到链路层将所有层的协议头加入到套接字中,skb_push函数就是用来前移skb->data,在数据头部插入协议头的。最后由驱动程序把数据(skb->data到skb->tail之间部分)发给网卡,释放掉套接字缓存,网卡负责把数据发到网络上。发送中加协议头的方式如下图:

        在网络数据接收过程中,网卡收到数据触发中断,驱动程序响应中断接收网卡缓存中数据,因为网卡中以太网帧头的长度为14个字节,而紧接着的IP协议头需要在16字节对齐的边界上,所以在申请到套接字缓存后,用skb_reserve函数先预留出2字节的空间,确保IP协议头的16字节对齐。刚申请到的套接字缓存,其sbk_buf的skb->data和skb->tail指向同一地址,是没有数据的,用skb_put函数将skb->tail往后拉数据包长度个字节空间,通过返回的原skb->tail地址,把网络数据从网卡中拷贝到套接字缓存里,skb_put函数就是在skb->tail和skb->end之间加数据。接着将数据往上层发送,在每一层中用skb_pull函数或者移动skb->data指针的方式,将各个层对应的协议头从数据中剥离开来,一直到最上面的应用层,剩下真正的从其他机器发过来的有用数据。

sk_buff API使用

alloc_skb	    用于分配sk_buff数据结构以及数据缓冲区
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_clone	    克隆sk_buff数据结构,双方的clone位置1,user置1,数据缓冲区内的dataref递增
skb_share_check	
                检查引用计数skb->users,并且当users字段说明该缓冲区是共享时可以克隆该缓冲区        

skb_copy	    拷贝sk_buff以及数据缓冲区
pskb_copy	    只拷贝sk_buff
—


版权声明:本文为CSDN博主「ToToSun」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq405180763/article/details/8797236
原文链接:https://blog.csdn.net/u012503639/article/details/104365087

  • 3
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值