linux 网络实现的数据结构-数据包结构

基本数据结构

数据包结构

 1:msghdr{}数据结构

struct msghdr {//bsd socket 层中的数据结构
	void	*	msg_name;	/* 保存的是这个套接字的名字, 一般都使用NULL初始化			*/
	int		msg_namelen;	/* Length of name		*/
	struct iovec *	msg_iov;	/* 保存的屙屎发送或者接收的数据缓冲区的地址数据,这个结构在下面会介绍		*/
	__kernel_size_t	msg_iovlen;	/* 保存的是	msg_iov中保存的数据包个数	*/
	void 	*	msg_control;	/* 一些辅助的控制数据及数据长度。由msg_control指向它,由msg_controllen指明它的长度*/
	__kernel_size_t	msg_controllen;	/* Length of cmsg list */
	unsigned	msg_flags;/*保存接收数据的时候使用的控制标志,可以由应用层操纵.例如在接收数据的时候使用非阻塞的接收方法,那么在msg_flags中会设置MSG_DONTWAIT位,表示在接收数据的时候如果没有数据,不阻塞等待,直接返回*/
};


struct iovec
{
	void *iov_base;		/* 用来保存数据缓冲区地址的成员,在bsd中使用caddr_t类型指针对他做类型转换。从msghdr 结构中的成员msg_iov指针引用的是一组iovec}{}类型的数据包,通过msg_iov指针的递增获取下一个数据包*/
	__kernel_size_t iov_len; /* 表示缓冲区中有效数据的长度 */
};

linux 网络实现的数据结构-数据包结构

 2:sk_buff{}数据结构定义

   sk_buff在inet socket和它以下的层次中用来存放网络接收或者需要发送的数据,因此它需要涉及的有足够的扩展性,从而可以支持不同层次、相同层次不同类型的网络协议。另一方面,因为sk_buff是工作在内核中的数据结构,所以除了前面所说的可扩展性外,还需要高效、紧凑。

sk_buff_head{}

通过sk_buff_head结构将一些sk_buff结构连接起来管理。

struct sk_buff_head {
	/* 这两个指针是保持 sk_buff结构组成一个双向链表的关键。sk_buff_head不一定处于链表的开端,但是通过这指针可以将一个链表中所有的数据引用到*/
	struct sk_buff	* next;
	struct sk_buff	* prev;

	__u32		qlen;//表示该链表的节点数。
	spinlock_t	lock;//做链表操作时,保证数据改动的同步
};

值得注意的是,sk_buff_head和sk_buff两个数据结构在开始的那段内存空间保持一样,都是两个sk_buff的指针数据。代码注释中特别强调了这一点,关键就在于,在对sk_buff_head进行操作的时候,实际上可以使用sk_buff结构类型做类型的强制转换来完成,反过来也是一样。

sk_buff{}

struct sk_buff {
	/* 和sk_buff_head结构相同,用于维系双向链表的指针 */
	struct sk_buff	* next;			/* Next buffer in list 				*/
	struct sk_buff	* prev;			/* Previous buffer in list 			*/

	struct sk_buff_head * list;		/* 指向sk_buff_head结构的指针,它是整个双向链表的链表头				*/
	struct sock	*sk;			/* 所属的sock 			*/
	struct timeval	stamp;			/* 值该数据包到达的sk_buff当前所在协议的时间,用于调度和统计数据用。使用jiffies的值				*/
	struct net_device	*dev;		/* 是指该数据包通过的网络接口设备,*/


    /*在网络传输的过程中,sk_buff主要工作在3个协议层上:inet socket层、ip层、硬件层。在每一层,针对不同的协议都有不同的协议头 ,那么需要提供不同的指针来获取这些协议头的位置并做出判断。提供对不同协议的支持由联合体实现		*/

	/* 传输层工作的协议头真真。联合h可能是tcp头指针,也可能是udp头指针等等 */
	union
	{
		struct tcphdr	*th;
		struct udphdr	*uh;
		struct icmphdr	*icmph;
		struct igmphdr	*igmph;
		struct iphdr	*ipiph;
		struct spxhdr	*spxh;
		unsigned char	*raw;
	} h;

	/* ip层工作的协议头指针nh。这一层工作的协议有ip协议,ipx协议等 */
	union
	{
		struct iphdr	*iph;
		struct ipv6hdr	*ipv6h;
		struct arphdr	*arph;
		struct ipxhdr	*ipxh;
		unsigned char	*raw;
	} nh;
  
	/* 硬件层上就是需要能够识别出网络设备的硬件头,在这里就是以太网数据包协议头 */
	union 
	{	
	  	struct ethhdr	*ethernet;
	  	unsigned char 	*raw;
	} mac;
    /*数据包可能用于传输的两种情况,一种是发送的过程,一种是接收的过程。如果用于发送,dst就是将要发送到的目标地址描述。一般情况,dst中存放的是路由路径中下一台主机的地址,因此,可以从dst_entry的指针向rtable的指针做类型转换。在进行数据发送的时候,通过dst_entry结构中的output函数指针,往硬件层传递发送数据。以以太网为例,就是dev_queue_xmit函数。这就是从ip层到硬件层的接口界面。系统中dst成员的初始化过程就是路由过程*/
	struct  dst_entry *dst;

	/* 
	 * 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.
	 */ 
    /*cb[]数组中存放的是每一协议层都可以*使用的一段空间,一般用来存放控制指令和控制数据。比如在tcp协议的实现过程中,在b中存放tcp_skb_cb类型的控制缓冲,里面存放的是tcp中的连接序号、flag等私有数据。因此这段空间预留的比较大。如果在你自己想实现的协议中又需要更大一点的空间,可以在cb中引出一个指针,在另一处存放你的控制数据*/
	char		cb[48];	 

	unsigned int 	len;			/* 整个数据包的长度,实际上就是后面要说的从data指针到tail指针之间的内容长度			*/
	unsigned int	csum;			/* 校验和,一般由硬件进行计算*/
	volatile char 	used;			/* 如果数据已经拷贝到用户态并且flags中没有MSG_PEEK标记,那么used设置为1,否则为0	*/
	unsigned char	cloned, 		/* 指这个数据包是否被克隆过。如果一个sk_buff是克隆过来的,那么它的cloned成员和源sk_buff结构的cloned都是1。通过skb_clone函数完成一次克隆操作,克隆过后的sk_buff结构不存在于链表中,也不和某个特征的inet socket相关联。通过检查成员users可以知道这个sk_buff结构是否被克隆过。通过sk_buff的克隆体可以进行一些关于数据包的操作。例如在数据的接收过程中,如果发现当前需要接收的sk_buff结构正在使用,那么调用skb_clone函数将当前需要使用的sk_buff结构拷贝出来,继续数据的接收过程,提高数据传输的效率。*/
  			pkt_type,		/* 数据包类型					*/
  			ip_summed;		/* 检查ip数据包的校验和的计算成员			*/
	__u32		priority;		/* 代表该sk_buff中的数据包的派对优先级		*/
	atomic_t	users;			/* User count - see datagram.c,tcp.c 		*/
	unsigned short	protocol;		/* 代表该sk_buff中的数据包的排队优先级		*/
	unsigned short	security;		/* 数据包的安全保密机制			*/
	unsigned int	truesize;		/* 用来存放数据的缓冲区的长度,并不是指有效数据的长度					*/
    /*head data tail end四个指针是用来标记数据缓冲区位置的。每一个sk_buff可以有一个数据包保存区,每一个数据保存区通过这四个成员指针,不仅可以标记数据的内容,还可以根据sk_buff所在的不同协议层次,改动指针的位置,获得必要的数据。大部分对sk_buff的基本操作都是对这四个指针的操作*/
	unsigned char	*head;			/* Head of buffer 				*/
	unsigned char	*data;			/* Data head pointer				*/
	unsigned char	*tail;			/* Tail pointer					*/
	unsigned char 	*end;			/* End pointer					*/
	void 		(*destructor)(struct sk_buff *);	/* 在释放sk_buff占用内存空间的时候,如果提供了destructor函数,就通过调用这个函数来完成释放的操作,比如为某些特殊操作提供一个途径		*/
#ifdef CONFIG_NETFILTER
	/* Can be used for communication between hooks. */
        unsigned long	nfmark;
	/* Cache info */
	__u32		nfcache;
	/* Associated connection, if any */
	struct nf_ct_info *nfct;
#ifdef CONFIG_NETFILTER_DEBUG
        unsigned int nf_debug;
#endif
#endif /*CONFIG_NETFILTER*/

#if defined(CONFIG_HIPPI)
	union{
		__u32	ifield;
	} private;
#endif

#ifdef CONFIG_NET_SCHED
       __u32           tc_index;               /* traffic control index */
#endif
};

sk_buff结构基本操作函数

 对sk_buff进程操作的函数和宏很多,比如申请和释放一个sk_buff,释放一个sk_buff结构的函数是kfree_skb。注意,这里的申请和释放并不一定是直接针对内核态的内存进行操作的,而可能指针修改引用,或者是直接从已经申请但是遭到废弃的一段内核态内存中直接获取的。

   先介绍申请和释放函数。

/*申请sk_buff结构的空间并且对一些必要的成员做初始化。size表示需要申请的数据区的大小,gfp_mask表示申请内存空间的参数,主要是在下一级调用kmalloc的时候指定 Get Free Page的优先级。在这里的内存申请都使用GTP_ATOMIC的优先级,*/
struct sk_buff *alloc_skb(unsigned int size,int gfp_mask)
{
	struct sk_buff *skb;
	u8 *data;

	if (in_interrupt() && (gfp_mask & __GFP_WAIT)) {
		static int count = 0;
		if (++count < 5) {
			printk(KERN_ERR "alloc_skb called nonatomically "
			       "from interrupt %p\n", NET_CALLER(size));
 			BUG();
		}
		gfp_mask &= ~__GFP_WAIT;
	}

	/* Get the HEAD */
	skb = skb_head_from_pool();//从全局变量skb_head_pool中获取一个sk_buff结构。对smp系统的每一个CPU来说,都维系了skb_head_pool[]数组中的一个元素,将该CPU用于处理的sk_buff结构连接起来。在释放一个sk_buff的时候,一般都是调用函数skb_head_to_pool将sk_buff结构的变量存放起来。因为sk_buff的申请是在内核中进行的,而且申请的都是优先级较高的缓存中的地址,如果申请次数过于频繁,或者申请量较多,系统的性能会受到很大的影响。通过skb_head_pool链表管理,规定每个CPU都只能有限量的sk_buff变量,是提高系统性能,保证稳定性的要求决定的。
	if (skb == NULL) {
		skb = kmem_cache_alloc(skbuff_head_cache, gfp_mask);
		if (skb == NULL)
			goto nohead;
	}

	/* Get the DATA. Size must match skb_add_mtu(). */
	size = ((size + 15) & ~15); 
	data = kmalloc(size + sizeof(atomic_t), gfp_mask);//申请数据包所在的内存空间,并且满足mtu的大小要求
	if (data == NULL)
		goto nodata;

	/* XXX: does not include slab overhead */ 
	skb->truesize = size + sizeof(struct sk_buff);

	/* Load the data pointers. */
	skb->head = data;
	skb->data = data;
	skb->tail = data;
	skb->end = data + size;

	/* Set up other state */
	skb->len = 0;
	skb->cloned = 0;

	atomic_set(&skb->users, 1); 
	atomic_set(skb_datarefp(skb), 1);//调用skb_datarefp将数据空间的引用计数置1.这里有个一很聪明节约的用法,就是将sk_buff指向的数据段的最后一个整型数据长度的空间用作这段数据的引用计数。
	return skb;

nodata:
	skb_head_to_pool(skb);
nohead:
	return NULL;
}

真正发送数据的时候,需要针对低层的协议申请出一个sk_buff空间来存放需要发送的数据包。dev_alloc_skb就是通过调用alloc_skb函数来建立以太网上发送数据包的sk_buff结构的。

static inline struct sk_buff *dev_alloc_skb(unsigned int length)
{
	struct sk_buff *skb;

	skb = alloc_skb(length+16, GFP_ATOMIC);//除了length长度之外,还需要多申请16字节的长度。这是为了存放以太网上硬件头而预留的空间。rfc中规定以太网头部长度14字节,但是为了让硬件头后面的ip头和long型地址对齐,这里申请了16字节。
	if (skb)
		skb_reserve(skb,16);
	return skb;
}

kfree_skb函数对应alloc_skb

static inline void kfree_skb(struct sk_buff *skb)
{
	if (atomic_read(&skb->users) == 1 || atomic_dec_and_test(&skb->users))//如果users为1 或者users减1成功了,才能调用函数__kfree_skb,可见释放skb的空间并不是真正释放空间,而是将skb添加到一个可供再次使用的pool中。因此__kfree_skb函数中要将skb的成员指针占用的一些空间释放,然后将skb添加到skb_head_pool数组中去。
		__kfree_skb(skb);
}

void __kfree_skb(struct sk_buff *skb)
{
	if (skb->list) {//如果skb还在一个链表中,不能在此处释放其空间,而应该先将其和链表的关系脱离,成为单独的一个结构的时候才能释放。
	 	printk(KERN_WARNING "Warning: kfree_skb passed an skb still "
		       "on a list (from %p).\n", NET_CALLER(skb));
		BUG();
	}

	dst_release(skb->dst);//递减引用
	if(skb->destructor) {
		if (in_irq()) {
			printk(KERN_WARNING "Warning: kfree_skb on hard IRQ %p\n",
				NET_CALLER(skb));
		}
		skb->destructor(skb);//指定了释放函数,执行
	}
#ifdef CONFIG_NETFILTER
	nf_conntrack_put(skb->nfct);
#endif
	skb_headerinit(skb, NULL, 0);  /* 将skb的内容清空,就像刚申请出来的一个新的sk_buff结构一样 */
	kfree_skbmem(skb);//释放数据空间,链入pool
}

dev_kfree_skb就是kfree_skb,只是一个宏替换而已。

函数名 描述
skb_put 将数据添加到先由数据尾部
skb_push 将数据增加到现有数据头部
skb_headroom 得到该sk_buff可以做skb_push的空间大小
skb_tailroom 得到该sk_buff可以做skb_put的空间大小
skb_reserve 空出一部分空间在数据区的头部
skb_pull 从数据区头部删除数据
skb_trim 从数据区的尾部删除数据
... ...

在sk_buff中的四个指针data、head、tail、end初始化的时候,data、head、tail都指向申请到的数据区的头部,end指向数据区的尾部。在以后的操作中,一般都是通过data和tail来获取在sk_buff中有用的数据的开始和结束位置,而head和end就表示sk_buff中存放的数据包最大可扩展的空间范围。

需要说明一点就是,因为sk_buff经常被克隆成另一个sk_buff结构,而且两个sk_buff结构可以共用一段数据空间,而不共用sk_buff的控制结构,因此在data中需要有一项数据来证明自己被引用的数目,从而在释放的时候判断是否应该将这段数据空间释放。linux程序员非常聪明,它们在编程的时候使用skb->end指向的那段空间(一个整型数据的长度)从不被引用,并且定义一个内部函数skb_datarefp从这段空间中得到改数据段被sk_buff结构引用的计数。

static inline atomic_t *skb_datarefp(struct sk_buff *skb)
{
	return (atomic_t *)(skb->end);
}

sk_buff链表操作

下面介绍关于sk_buff链表的操作。在介绍sk_buff_head结构的时候说明了sk_buff结构的数据通过个sk_buff_head的结构引用。sk_buff_head结构中的lock成员控制整个链表的修改操作。

skb_insert 在链表中插入一个sk_buff结构
__skb_insert 对应的无锁操作
skb_append 在链表中指定的一个sk_buff后插入一个sk_buf
__skb_append 对应的无锁操作
skb_queue_head 在链表头插入一个sk_buff节点
__skb_queue_head 对应的无锁操作
skb_queue_tail 在链表尾插入一个sk_buff节点
__skb_queue_tail 对应的无锁操作
skb_unlink 从链表中删除一个sk_buff节点
__skb_unlink 对应的无锁操作
skb_dequeue 从链表头取出一个sk_buff节点,并且从链表中删除
__skb_dequeue 对应的无锁操作
skb_dequeue_tail 从链表尾取出一个sk_buff节点,并且从链表中删除
__skb_dequeue_tail 对应的无锁操作
/*函数将skb克隆一份,只是不像skb一样处于链表中,而是单独的一个sk_buff节点,而且对于sk_buff结构管理的数据区不复制,这两个结构都指向相同的数据区*/
struct sk_buff *skb_clone(struct sk_buff *skb, int gfp_mask)
{
	struct sk_buff *n;

	n = skb_head_from_pool();//从缓冲区获取一个空闲的sk_buff结构
	if (!n) {
		n = kmem_cache_alloc(skbuff_head_cache, gfp_mask);//缓冲区没有,就申请一块
		if (!n)
			return NULL;
	}

	memcpy(n, skb, sizeof(*n));//将sk_buff结构中的内容完全复制过来
	atomic_inc(skb_datarefp(skb));//这段空间的引用计数增加1
	skb->cloned = 1;//表示已经被克隆过了
       
	dst_clone(n->dst);//计数也增加去
	n->cloned = 1;//表示该新sk_buff是被克隆出来的
	n->next = n->prev = NULL;//链表指针清空
	n->list = NULL;
	n->sk = NULL;
	atomic_set(&n->users, 1);//增加使用者
	n->destructor = NULL;
#ifdef CONFIG_NETFILTER
	nf_conntrack_get(skb->nfct);
#endif
	return n;
}

上一篇:5G的前世今生


下一篇:一个免费Android教程