linux内核网络 sk_buff 之队列管理函数

1 初始化函数 skb_queue_head_init()

首先获取到sk_buff_head结构体中自旋锁,因为队列管理函数都是原子操作(要么不操作,要不一定要操作完,操作时不能被打扰),所以获取到锁才可以操作,防止异步中断。然后创建个空的链表。函数实现如下:

static inline void skb_queue_head_init(struct sk_buff_head *list)
{
	spin_lock_init(&list->lock);//获得头结点中的自旋锁
	__skb_queue_head_init(list);
}
 
static inline void __skb_queue_head_init(struct sk_buff_head *list)
{
	list->prev = list->next = (struct sk_buff *)list;
	list->qlen = 0;
}
 
void sock_init_data(struct socket *sock, struct sock *sk)
{
	skb_queue_head_init(&sk->sk_receive_queue);
	skb_queue_head_init(&sk->sk_write_queue);
	skb_queue_head_init(&sk->sk_error_queue);
#ifdef CONFIG_NET_DMA
	skb_queue_head_init(&sk->sk_async_wait_queue);
#endif
 
	...
}

2 插入函数 skb_insert()

该函数实现的是在 prev 和 next之 间插入 newsk 结构体,所以如果是队列头部插入:则 prev 这个形参就要用头结点来传入,next 就要用头结点->next来传入;如果是队列尾部插入:则prev这个形参则用头结点->prev来传入,而next就用头结点来传入;最后再让链表节点个数qlen变量加1;代码如下:

void skb_insert(struct sk_buff *old, struct sk_buff *newsk, struct sk_buff_head *list)
{
	unsigned long flags;
 
	spin_lock_irqsave(&list->lock, flags);
	__skb_insert(newsk, old->prev, old, list);
	spin_unlock_irqrestore(&list->lock, flags);
}
 
static inline void __skb_insert(struct sk_buff *newsk,
				struct sk_buff *prev, struct sk_buff *next,
				struct sk_buff_head *list)
{
	newsk->next = next;
	newsk->prev = prev;
	next->prev  = prev->next = newsk;
	list->qlen++;
}

原理如下图:

3 出队列函数

有入队列函数(即插入函数)就一定会有对应的出队列函数。出队列函数也分为:从队列头部开始把第一个元素出队列,和从队列尾部开始把第一个元素出队列。

3.1 队头出队列函数 skb_dequeue()

struct sk_buff *skb_dequeue(struct sk_buff_head *list)
{
	unsigned long flags;
	struct sk_buff *result;
 
	spin_lock_irqsave(&list->lock, flags);
	result = __skb_dequeue(list);
	spin_unlock_irqrestore(&list->lock, flags);
	return result;
}
 
static inline struct sk_buff *__skb_dequeue(struct sk_buff_head *list)
{
	struct sk_buff *skb = skb_peek(list);
	if (skb)
		__skb_unlink(skb, list);
	return skb;
}
 
static inline struct sk_buff *skb_peek(const struct sk_buff_head *list_)
{
	struct sk_buff *skb = list_->next;//获取到头部节点开始的第一个元素
 
	if (skb == (struct sk_buff *)list_)//判断是否为头部节点自身,即:是否只有一个头节点的队列
		skb = NULL;
	return skb;
}
 

和所有队列操作函数一样,出队列函数实现步骤也是:上锁----》出队列操作-----》解锁。真正操作还是在_skb_unlink()函数,其他函数也只是定位要出哪个元素而已。

3.2 队尾出队列函数 skb_dequeue_tail()

看了尾部出队列操作函数,会发现头部出队列函数命名其实有点不同,按常理来说应该是:skb_dequeue_head()才能和尾部出队列函数:skb_dequeue_tail()相符合。但是不知为什么内核定义的确是skb_dequeue(),这着实不能理解,只能当做是作者的一个美丽失误吧。其实尾部出队列操作函数和头部出队列函数非常相似,其实大部分对应的操作函数的是非常相似,这是内核设计的模块化和代码耦合度考虑。下面来分析下真正的出队列操作函数_skb_unlink()函数。

 struct sk_buff *skb_dequeue_tail(struct sk_buff_head *list)
{
	unsigned long flags;
	struct sk_buff *result;
 
	spin_lock_irqsave(&list->lock, flags);
	result = __skb_dequeue_tail(list);
	spin_unlock_irqrestore(&list->lock, flags);
	return result;
}
 
static inline struct sk_buff *__skb_dequeue_tail(struct sk_buff_head *list)
{
	struct sk_buff *skb = skb_peek_tail(list);
	if (skb)
		__skb_unlink(skb, list);
	return skb;
}
 
static inline struct sk_buff *skb_peek_tail(const struct sk_buff_head *list_)
{
	struct sk_buff *skb = list_->prev;//获取到头部节点的前一个,即是尾部开始节点
 
	if (skb == (struct sk_buff *)list_)//判断是否是只有一个头节点的队列
		skb = NULL;
	return skb;
 
}

3.3 元素弹出操作 __skb_unlink()

static inline void __skb_unlink(struct sk_buff *skb, struct sk_buff_head *list)
{
	struct sk_buff *next, *prev;
 
	list->qlen--;
	next	   = skb->next;
	prev	   = skb->prev;
	skb->next  = skb->prev = NULL;
	next->prev = prev;
	prev->next = next;
}

_skb_unlink()函数实现原理图:

【文章福利】小编推荐自己的Linux内核技术交流群: 【977878001】整理一些个人觉得比较好得学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!

内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程

4 清空队列函数 skb_queue_purge()

内核中特意实现了一个清空所有队列元素的操作函数:skb_queue_purge();函数实现如下:

void skb_queue_purge(struct sk_buff_head *list) //传入一个链表头结点
{
	struct sk_buff *skb;
	while ((skb = skb_dequeue(list)) != NULL) //从头部开始循环出队列,直到最后一个头部节点
		kfree_skb(skb); //队列中每出一个元素就释放掉一个元素
}

5 遍历队列操作

内核定义了一系列宏来进行遍历队列操作,是从两方面考虑:第一、从哪个元素开始遍历;第一、是否要安全遍历,防止遍历时,元素被删除。

#define skb_queue_walk(queue, skb) \
		for (skb = (queue)->next;					\
		     prefetch(skb->next), (skb != (struct sk_buff *)(queue));	\
		     skb = skb->next)
// 上面的遍历是从queue头结点开始遍历,直到遍历循环回到queue结束。
// 也就是遍历整个队列操作,但该宏不能做删除skb操作,一旦删除了skb后,skb->next就是非法的(因为此时skb不存在)。
 
#define skb_queue_walk_safe(queue, skb, tmp)					\
		for (skb = (queue)->next, tmp = skb->next;			\
		     skb != (struct sk_buff *)(queue);				\
		     skb = tmp, tmp = skb->next)
// 这个宏也是从queue头结点开始遍历整个队列操作,唯一不同的是这个宏用了一个临时变量,就是防止遍历时要删除掉skb变量,
// 因为删除掉了skb后,也可以从skb=tmp中再次获得,然后依次tmp = skb->next;(此时skb是存在的)所以遍历时,可以做删除操作。
 
#define skb_queue_walk_from(queue, skb)						\
		for (; prefetch(skb->next), (skb != (struct sk_buff *)(queue));	\
		     skb = skb->next)
// 这个宏是从skb元素处开始遍历直到遇到头结点queue结束,该宏只能做查看操作,不能做删除skb操作,分析如第一个宏
 
#define skb_queue_walk_from_safe(queue, skb, tmp)				\
		for (tmp = skb->next;						\
		     skb != (struct sk_buff *)(queue);				\
		     skb = tmp, tmp = skb->next)
// 这个宏也是从skb元素开始遍历直到遇到queue元素结束,但该宏可以做删除skb元素操作,具体分析如第一个宏
 
#define skb_queue_reverse_walk(queue, skb) \
		for (skb = (queue)->prev;					\
		     prefetch(skb->prev), (skb != (struct sk_buff *)(queue));	\
		     skb = skb->prev)
// 这是个逆反遍历宏,就是从queue头结点的尾部开始(或者说从前驱元素开始)直到遇到queue元素节点。
// 也即是从头结点尾部开始遍历了整个队列,此宏和第一、第三个宏一样,不能做删除操作。

6 插入数据函数 skb_add_data()

插入数据函数和插入函数是不一样的,插入函数是把sk_buff结构体插入链表中,而插入数据函数是把数据插入sk_buff结构的数据区。函数实现如下:

// skb为被添加的sk_buff类型的结构体,from为将要添加的数据源,copy为数据源的长度
static inline int skb_add_data(struct sk_buff *skb,
			       char __user *from, int copy)
{
	const int off = skb->len;
 
	if (skb->ip_summed == CHECKSUM_NONE) {// 表示检验ip包的校验
		int err = 0;
// 数据拷贝操作,这里调用了skb_put()函数让tail往下移空出控件来存放将要拷贝的数据,并且返回tail指针
		__wsum csum = csum_and_copy_from_user(from, skb_put(skb, copy), 
							    copy, 0, &err); 
		if (!err) {
			skb->csum = csum_block_add(skb->csum, csum, off); // 这个应该是IP校验计算吧
			return 0;
		}
	} else if (!copy_from_user(skb_put(skb, copy), from, copy)) // 这是最本质的数据拷贝操作宏,同样调用了skb_put()函数返回tail指针
		return 0;
 
	__skb_trim(skb, off); // 这个是删除数据操作,将在下一个数据删除(skb_trim()函数)分析
	return -EFAULT;
}
 
static inline
__wsum csum_and_copy_from_user (const void __user *src, void *dst,
				      int len, __wsum sum, int *err_ptr)
{
	if (access_ok(VERIFY_READ, src, len)) // 判断数据长度关系
		return csum_partial_copy_from_user(src, dst, len, sum, err_ptr); // 调用拷贝函数
 
	if (len)
		*err_ptr = -EFAULT;
	return sum;
}
 
static __inline__
__wsum csum_partial_copy_from_user(const void __user *src,
                                         void *dst, int len, __wsum sum,
                                         int *err_ptr)
{
        if (copy_from_user(dst, src, len)) { // 拷贝操作
                *err_ptr = -EFAULT;
                return (__force __wsum)-1;
        }
        return csum_partial(dst, len, sum); // 设置校验和
}
 
// 这是调用memcpy()函数来对数据进行拷贝,to是tail指针,from是将要插入的数据源指针,n是数据源长度
#define copy_from_user(to, from, n)	(memcpy((to), (from), (n)), 0) 

7 删除数据函数

删除数据函数和插入数据函数相对应,和删除函数不一样,删除函数只是删除队列链表中的某个元素节点,而删除数据函数是删除sk_buff结构中的数据区数据以及删除sk_buff结构中分片结构的数据区中数据。这些删除操作都是从尾部tail指针开始删除

7.1 删除sk_buff结构中的数据区数据函数skb_trim()

void skb_trim(struct sk_buff *skb, unsigned int len)
{	
// 这里值得注意的是len不是要删除的数据长度,而是删除后的数据长度,即是新的数据长度。
// 所以新的数据长度不能比开始的skb的长度还大,否则就是插入增加数据函数而不是删除数据函数了
	if (skb->len > len) 
		__skb_trim(skb, len);// 调用函数进行删除数据操作
}
 
static inline void __skb_trim(struct sk_buff *skb, unsigned int len)
{
	if (unlikely(skb->data_len)) {
		WARN_ON(1);
		return;
	}
	skb->len = len; // 为新的skb赋上删除后的len值
	skb_set_tail_pointer(skb, len); // 调用函数删除操作
}
 
static inline void skb_set_tail_pointer(struct sk_buff *skb, const int offset)
{
	skb->tail = skb->data + offset; // 实质上没有对数据进行删除,只是让tail指针偏移,改变有效数据值
}

7.2 删除sk_buff结构中分片结构的数据区数据函数 pskb_trim()

static inline int pskb_trim(struct sk_buff *skb, unsigned int len)
{
	return (len < skb->len) ? __pskb_trim(skb, len) : 0; // 这个功能和上面类似,如果新len值小于skb原有的值,则做删除操作
}
static inline int __pskb_trim(struct sk_buff *skb, unsigned int len)
{
	if (skb->data_len)// 如果分片结构数据区有数据
		return ___pskb_trim(skb, len);// 则调用该函数来删除分片结构中的数据区数据
	__skb_trim(skb, len);// 这个和上面删除sk_buff结构中的数据区数据一样
	return 0;
}

pskb_trim()函数其实包含了skb_trim()函数,如果当分片结构数据区没有数据则skb_trim()函数和pskb_trim()函数是一样的。如果分片结构数据区有数据时,则pskb_trim()函数不仅要删除sk_buff结构数据区数据(skb_trim()函数功能),还要删除分片结构数据区数据。

8 拆分数据函数 skb_split()

拆分数据函数是把数据区数据拆分成两个存放到另外一个 skb 中,其实拆分数据函数并不复杂,只是一些指针的赋值,和控制。下面看函数实现:

// skb为原来的skb结构体(将要被拆分的),skb1为拆分后得到的子skb,len为拆分后的skb的新长度
void skb_split(struct sk_buff *skb, struct sk_buff *skb1, const u32 len)
{
	int pos = skb_headlen(skb);// pos = skb->len - skb->data_len,pos是skb结构中数据区的有效数据长度
 
	if (len < pos)	// 如果拆分长度小于skb数据区中的有效长度,则调用下面函数
		skb_split_inside_header(skb, skb1, len, pos);// 该函数只拆分skb数据区中的数据
	else  // 反之,如果拆分长度不小于skb数据区中的有效长度,则调用下面函数
		skb_split_no_header(skb, skb1, len, pos);// 拆分skb结构中的分片结构中数据区数据
}
 
// 这是只拆分sk_buff结构数据区的数据,其他参数不变,参数:pos则是sk_buff结构数据区中有效数据长度
static inline void skb_split_inside_header(struct sk_buff *skb,
					   struct sk_buff* skb1,
					   const u32 len, const int pos)
{
	int i;
	// 这是个把sk_buff结构中有效数据拷贝到新的skb1中,pos为有效数据长度,len为剩下数据长度,得:pos-len为要拷贝的数据长度
        // skb_put(skb1,pos-len)是移动tail指针让skb1结构数据区空出空间来存放将要拷贝的数据,该函数返回tail指针
	skb_copy_from_linear_data_offset(skb, len, skb_put(skb1, pos - len),
					 pos - len);
            // 为了方便理解,把该函数实现代码注释进来
            // skb为要被拆分的sk_buff结构,offset为剩下新的skb数据长度,to为skb1结构中tail指针,len为要拷贝的数据长度
            // static inline void skb_copy_from_linear_data_offset(const struct sk_buff *skb,
	    //				    const int offset, void *to,
	    //				    const unsigned int len)
	    // {
	    // 从skb要剩下的数据位置开始(即是skb->data+offset,skb->data和skb->data+offset之间的数据是要保留的)
            // to则是tail指针移动前返回的一个位置指针(详细请看skb_put()函数实现),拷贝len长度内容
	    //	<span style="white-space:pre">	</span>memcpy(to, skb->data + offset, len);
	    //<span style="white-space:pre">	</span>}
        // 如果对sk_buff结构及相关结构体中成员变量了解,则这些代码就非常好理解了。
        // nr_frags为多少个分片数据区,循环把所有分片数据拷贝到skb1中
	for (i = 0; i < skb_shinfo(skb)->nr_frags; i++)
		skb_shinfo(skb1)->frags[i] = skb_shinfo(skb)->frags[i];
	
	//下面做的都是些成员字段拷贝赋值操作,并且设置skb的字段
	skb_shinfo(skb1)->nr_frags = skb_shinfo(skb)->nr_frags;
	skb_shinfo(skb)->nr_frags  = 0;
	skb1->data_len		   = skb->data_len;
	skb1->len		   += skb1->data_len;
	skb->data_len		   = 0;
	skb->len		   = len;
	skb_set_tail_pointer(skb, len);// 下面把实现函数代码注释进来,方便理解	
 		//	static inline void skb_set_tail_pointer(struct sk_buff *skb, const int offset)
		//	{
		//		// 这是把tail指针移到数据区的最后面
		//		skb->tail = skb->data + offset;	
		//	}
}
 
 
// 这是拆分分片结构数据区数据,同理,其他参数不变,参数:pos则是sk_buff结构数据区中有效数据长度
static inline void skb_split_no_header(struct sk_buff *skb,
				       struct sk_buff* skb1,
				       const u32 len, int pos)
{
	int i, k = 0;
	// 开始设置sk_buff结构数据区内容
	const int nfrags = skb_shinfo(skb)->nr_frags;
	skb_shinfo(skb)->nr_frags = 0;
	skb1->len		  = skb1->data_len = skb->len - len;
	skb->len		  = len;
	skb->data_len		  = len - pos;
	
	// 这是循环拆分分片结构数据区数据
	for (i = 0; i < nfrags; i++) {
		int size = skb_shinfo(skb)->frags[i].size;
	// 其实拆分,数据区存储不会动,动的只是指向这些数据存储的位置指针
       //  下面都是把skb的一些指向分片结构数据区的指针赋值给skb1中的数据区相关变量
		if (pos + size > len) {
			skb_shinfo(skb1)->frags[k] = skb_shinfo(skb)->frags[i];
			if (pos < len) {
				get_page(skb_shinfo(skb)->frags[i].page);
				skb_shinfo(skb1)->frags[0].page_offset += len - pos;
				skb_shinfo(skb1)->frags[0].size -= len - pos;
				skb_shinfo(skb)->frags[i].size	= len - pos;
				skb_shinfo(skb)->nr_frags++;
			}
			k++;
		} else
			skb_shinfo(skb)->nr_frags++;
		pos += size;
	}
	skb_shinfo(skb1)->nr_frags = k;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
struct sk_buffLinux 内核网络子系统中的一个非常重要的数据结构,它代表了内核网络协议栈中的一个网络数据包。在 Linux 内核中,网络数据包都是封装在 sk_buff 中进行传输和处理的,因此可以说 sk_buffLinux 网络子系统中最核心的数据结构之一。 下面是 struct sk_buff 结构体的详细说明: ```c struct sk_buff { struct sk_buff *next; /* 下一个 sk_buff */ struct sk_buff *prev; /* 上一个 sk_buff */ ktime_t tstamp; /* 时间戳 */ struct sock *sk; /* socket */ struct net_device *dev; /* 网络设备 */ unsigned long _skb_dst; /* 目标地址 */ unsigned long _skb_src; /* 源地址 */ struct skb_shared_info *shinfo; /* 共享数据 */ atomic_t users; /* 引用计数 */ unsigned int len, data_len; /* 总长度和数据长度 */ __u16 protocol; /* 协议类型 */ __u16 vlan_proto; /* VLAN 协议 */ __u16 vlan_tci; /* VLAN 标记 */ union { __be16 ip4_frag_id; /* IPv4 报文分片标识 */ __u8 hdr_len; /* 首部长度 */ __u16 mac_len; /* MAC 头长度 */ }; __u16 queue_mapping; /* 网络队列映射 */ __u16 tc_index; /* 网络流量控制 */ __u16 pkt_type; /* 数据包类型 */ __u32 priority; /* 优先级 */ __u32 skb_mstamp; /* 时间戳 */ u32 secmark; /* 安全标记 */ unsigned int mark; /* skb 标记 */ unsigned int nf_trace; /* 网络跟踪 */ __u32 hash; /* 哈希值 */ __u16 nfctinfo; /* nf_conntrack 信息 */ __u8 queue_bypass; /* 是否绕队列 */ __u8 protocol_was_802_3; /* 协议是否是 802.3 */ __u8 encapsulation; /* 封装类型 */ __u8 transport_header_was; /* 传输层首部是否有效 */ union { __wsum csum; /* 校验和 */ struct { __u16 csum_start; /* 校验和起始位置 */ __u16 csum_offset; /* 校验和偏移量 */ }; }; union { void *dst; /* 目标地址 */ struct { __be32 saddr; /* 源 IP 地址 */ __be32 daddr; /* 目标 IP 地址 */ } ip4; struct { const void *hdr; /* MAC 头指针 */ const void *payload; /* 数据负载指针 */ } mac; struct { unsigned char *tail; /* 尾部指针 */ unsigned char *end; /* 结束指针 */ }; }; }; ``` 下面是各个字段的详细说明: - next 和 prev 字段:这两个字段分别指向下一个和上一个 sk_buff,用于将 sk_buff 组织成链表。这样可以方便地进行遍历和管理多个 sk_buff。 - tstamp 字段:这个字段表示 sk_buff 的时间戳,记录了 sk_buff 的创建时间。 - sk 字段:这个字段指向一个 socket,表示这个 sk_buff 相关联的 socket。 - dev 字段:这个字段指向一个网络设备,表示这个 sk_buff 是从哪个网络设备接收到的,或者将要发送到哪个网络设备。 - \_skb_dst 和 \_skb_src 字段:这两个字段是目标地址和源地址的指针,分别指向目标地址和源地址的内存空间。 - shinfo 字段:这个字段指向一个 skb_shared_info 结构体,用于共享数据。 - users 字段:这个字段是一个引用计数器,用于记录当前有多少个指针指向这个 sk_buff。 - len 和 data_len 字段:这两个字段分别表示 sk_buff 的总长度和数据长度。 - protocol 字段:这个字段表示 sk_buff 中数据的协议类型,例如 ETH_P_IP 表示 IPv4 协议,ETH_P_ARP 表示 ARP 协议等。 - vlan_proto 和 vlan_tci 字段:这两个字段用于处理 VLAN 标记。 - ip4_frag_id 字段:这个字段用于处理 IPv4 报文分片标识。 - queue_mapping 字段:这个字段表示网络队列映射。 - tc_index 字段:这个字段表示网络流量控制。 - pkt_type 字段:这个字段表示数据包的类型,例如数据包是从网络设备接收而来的、或者是要发送到网络设备的等。 - priority 字段:这个字段表示 sk_buff 的优先级。 - skb_mstamp 字段:这个字段表示 sk_buff 的时间戳,记录了 sk_buff 的最后修改时间。 - secmark 字段:这个字段用于安全标记。 - mark 字段:这个字段用于 skb 标记。 - nf_trace 字段:这个字段用于网络跟踪。 - hash 字段:这个字段用于哈希值。 - nfctinfo 字段:这个字段用于 nf_conntrack 信息。 - queue_bypass 字段:这个字段表示是否绕过队列。 - protocol_was_802_3 字段:这个字段表示协议是否是 802.3。 - encapsulation 字段:这个字段表示封装类型。 - transport_header_was 字段:这个字段表示传输层首部是否有效。 - csum 字段:这个字段用于校验和。 - csum_start 和 csum_offset 字段:这两个字段分别表示校验和的起始位置和偏移量。 - dst 字段:这个字段指向目标地址。 - ip4.saddr 和 ip4.daddr 字段:这两个字段分别表示 IPv4 报文的源 IP 地址和目标 IP 地址。 - mac.hdr 和 mac.payload 字段:这两个字段分别指向 MAC 头和数据负载的指针。 - tail 和 end 字段:这两个字段分别指向 sk_buff 数据的尾部和结束位置。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值