Linux网络协议栈相关视频解析:
Linux网络协议栈是内核中最大的组件之一,由于网络部分应用的范围很广,也相对较热,该部分现有的资料很多,学起来也比较容易。首先,我们看看贯穿网络协议栈各层的一个最关键数据结构——套接字缓冲区(sk_buff结构)。
一个封包就存储在这个数据结构中。所有网络分层都会使用这个结构来存储其报头、有关数据的信息,以及用来协调工作的其他内部信息。在内核的进化历程中,这个结构经历多次变动,本文及后面的文章都是基于2.6.20版本,在2.6.32中该结构又变化了很多。该结构字段可粗略划分为集中类型:布局、通用、专用、可选(可用宏开关)。
SKB在不同网络层之间传递,可用于不同的网络协议。协议栈中的每一层往下一层传递SKB之前,首先就是调用skb_reserve函数在数据缓存区头部预留出的一定空间以保证每一层都能把本层的协议首部添加到数据缓冲区中。如果是向上层协议传递skb,则下层协议层的首部信息就没有用了,内核实现上用指针改变指向来实现。
下面看看该结构体中的字段,大部分都给了注释,后面的方法与实现中我们将看到他的各个字段的应用与实际意义。
struct sk_buff { /* These two members must be first. */ /*链表,放在结构头,用于强制转化得到,链表头为skb_buff_head*/ struct sk_buff *next; struct sk_buff *prev; /*指向拥有此缓冲区的sock数据结构,当数据在本地 产生或者正由本地进程接收时,就需要这个指针 因为该数据以及套接字相关的信息会由L4 以及用户应用程序使用。当缓冲去只是被 转发时,该指针为NULL*/ struct sock *sk; /*通常只对一个以及接收的封包才有意义 这是一个时间戳记,用于表示封包何时被 接收,或者有时用于表示封包预定传输时间 */ struct skb_timeval tstamp; /*此字段描述一个网络设备,该字段的作用与该SKB是发送包还是 接受包有关*/ struct net_device *dev; /*接收报文的原始网络设备,主要用于流量控制*/ struct net_device *input_dev; 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;/*二层协议首部*/ /*目的路由缓存项*/ struct dst_entry *dst; /*IPSec协议用来跟踪传输的信息*/ struct sec_path *sp; /* * 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. */ /*SKB信息控制块,是每层协议的私有信息存储空间,由每层协议自己使用并维护,并只有在本层有效*/ char cb[48]; /*数据总长度,包括线性缓冲区数据长度(data指向),SG类型的聚合分散I/O的数据 以及FRAGLIST类型的聚合分散I/O的数据长度,也包括协议首部的长度*/ unsigned int len, /*SG类型和FRAGLIST类型聚合分散I/O存储区中的数据长度*/ data_len, /*二层首部长度。实际长度与网络介质有关,在以太网中为以太网桢首部的长度*/ mac_len; union { __wsum csum; __u32 csum_offset; }; /*发送或转发QOS类别。*/ __u32 priority; /*表示此skb在本地允许分片*/ __u8 local_df:1, /*标记所属SKB是否已经克隆*/ cloned:1, /*标记传输层校验和的状态*/ ip_summed:2, /*标识payload是否被单独引用,不存在协议首部*/ nohdr:1, /*防火墙使用*/ nfctinfo:3; /*桢类型,分类是由二层目的地址来决定的,对于以太网设备 来说,该字段由eth_type_trans()初始化*/ __u8 pkt_type:3, /*当前克隆状态*/ fclone:2, ipvs_property:1; /*从二层设备角度看到的上层协议,即链路层承载的三层协议类型*/ __be16 protocol; /*skb析构函数指针,在释放skb时被调用,完成某些必要的工作*/ void (*destructor)(struct sk_buff *skb); /*在数据结构中定义的宏不能编译成模块;原因在于内核编译之后,开启该选项所得的多数结果为不可逆的,一般而言,任何引起内核数据结构改变的选项,都不适合编译成一个模块,编译选项和特定的#ifdef符号相配,以了解一个代码区块什么时候会包含到内核中,这种关联在源码树的Kconfig文件中*/#ifdef CONFIG_NETFILTER /*防火墙使用*/ struct nf_conntrack *nfct;#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE) struct sk_buff *nfct_reasm;#endif#ifdef CONFIG_BRIDGE_NETFILTER /*防火墙使用*/ struct nf_bridge_info *nf_bridge;#endif#endif /* CONFIG_NETFILTER */#ifdef CONFIG_NET_SCHED/*用于流量控制*/ __u16 tc_index; /* traffic control index */#ifdef CONFIG_NET_CLS_ACT /*用于流量控制*/ __u16 tc_verd; /* traffic control verdict */#endif#endif#ifdef CONFIG_NET_DMA dma_cookie_t dma_cookie;#endif#ifdef CONFIG_NETWORK_SECMARK __u32 secmark;#endif __u32 mark; /* These elements must be at the end, see alloc_skb() for details. */ /*全部数据的长度+该结构的长度,如果申请了一个len字节的缓冲区 该字段为len+sizeof(sk_buff)*/ unsigned int truesize; /*引用计数,使用这个缓冲区的实例的数目*/ atomic_t users; /*head和end指向数据在内存中的起始和结束位置,即已经分配的缓冲区空间的开端和尾端 data和tail指向数据区域的起始和结束位置,即实际数据的开端和尾端*/ unsigned char *head, *data, *tail, *end;};
可选功能字段
在网络模块中同时也提供了很多有用的功能,虽然这些功能都不是必须的,但对现在的应用来讲是不可缺少的一部分,例如,防火墙、组播等。为了支持这些功能,一般都需要在内核数据结构sk_buff中添加相应的成员变量。因此,sk_buff结构中包含很多想#ifdef这样的预编译指令。如下面的两个宏定义。
struct sk_buff {……/*在数据结构中定义的宏不能编译成模块;原因在于内核编译之后,开启该选项所得的多数结果为不可逆的,一般而言,任何引起内核数据结构改变的选项,都不适合编译成一个模块,编译选项和特定的#ifdef符号相配,以了解一个代码区块什么时候会包含到内核中,这种关联在源码树的Kconfig文件中*/#ifdef CONFIG_NET_SCHED __u16 tc_index; /* traffic control index */#ifdef CONFIG_NET_CLS_ACT __u16 tc_verd; /* traffic control verdict */#endif ……}
我们打开内核文件夹net->sched下面的Kconfig文件,发现有下面文字:
menu "QoS and/or fair queueing" config NET_SCHED bool "QoS and/or fair queueing"……config NET_CLS_ACT bool "Actions" select NET_ESTIMATOR ---help---……endif # NET_SCHED endmenu
与上面数据结构中的宏对应就显然了,如果需要了解内核配置选项与对应的宏,查看对应的Kconfig文件就可以了。需要指出的是,内核编译之后,由某些选项所控制的数据结构是固定的而不是动态变化的。一般来说,如果某些选项修改了内核数据结构,则包含该选项的组件就不能被编译成内核模块。
【文章福利】需要C/C++ Linux服务器架构师学习资料加群812855908(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等)
数据定位与操作
head,end,data,tail四个字段用来指向线性数据缓存区及数据部分的边界。Head和end分别指向缓存区的头与尾;而data和tail则分别指向数据的头与尾。在发送时,每一层协议会在head与data之间填充协议首部,还可能在tail和end之间添加数据。
Skb初始化
网络模块中,有两个用来分配SKB描述符的高速缓存,在SKB模块初始化函数skb_init中被创建
void __init skb_init(void){ /*一般情况下,SKB都是从该高速缓存中分配的*/ skbuff_head_cache = kmem_cache_create("skbuff_head_cache", sizeof(struct sk_buff), 0, SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL, NULL); /*如果在分配SKB时就知道会被克隆,那么应该从这个高速缓存 中分配空间*/ skbuff_fclone_cache = kmem_cache_create("skbuff_fclone_cache", (2*sizeof(struct sk_buff)) + sizeof(atomic_t), 0, SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL, NULL);}
分配skb
Alloc_skb()用来分配SKB,数据缓存区描述符是两个不同的实体,这就意味着,在分配一个SKB时,需要分配两块内存,一块是数据缓存区,一块是SKB描述符。
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask, int fclone, int node){ struct kmem_cache *cache; struct skb_shared_info *shinfo; struct sk_buff *skb; u8 *data; /*根据参数选着从那个高速缓存中分配空间*/ cache = fclone ? skbuff_fclone_cache : skbuff_head_cache; /* Get the HEAD */ /*分配空间,从cache中*/ skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node); if (!skb) goto out; /* Get the DATA. Size must match skb_add_mtu(). */ /*对其到size*/ size = SKB_DATA_ALIGN(size); /*分配数据缓冲区,其长度为size大小加上skb_shared_info结构大小*/ data = kmalloc_node_track_caller(size + sizeof(struct skb_shared_info), gfp_mask, node); if (!data) goto nodata; memset(skb, 0, offsetof(struct sk_buff, truesize)); /*设置truesize大小为size+sizeof(struct sk_buff)*/ skb->truesize = size + sizeof(struct sk_buff); atomic_set(&skb->users, 1);/*设置引用计数为1*/ skb->head = data; skb->data = data; skb->tail = data; /*设置end为data+size大小*/ skb->end = data + size; /* make sure we initialize shinfo sequentially */ /*skb_shared_info处在skb->end开始处*/ shinfo = skb_shinfo(skb); /*初始化该结构*/ atomic_set(&shinfo->dataref, 1); shinfo->nr_frags = 0; shinfo->gso_size = 0; shinfo->gso_segs = 0; shinfo->gso_type = 0; shinfo->ip6_frag_id = 0; shinfo->frag_list = NULL; if (fclone) {/*如果设置了克隆标志*/ struct sk_buff *child = skb + 1;/*获得下一个skb结构*/ /*获得引用计数*/ atomic_t *fclone_ref = (atomic_t *) (child + 1); /*设置克隆标志*/ skb->fclone = SKB_FCLONE_ORIG; atomic_set(fclone_ref, 1);/*克隆引用标志为1*/ child->fclone = SKB_FCLONE_UNAVAILABLE; }out: return skb;nodata: kmem_cache_free(cache, skb); skb = NULL; goto out;}
调用该函数后生成的图如下所示:
对于skb数据结构的其他操作主要放在skbuff.h文件中,主要有skb_reserve()、skb_put()、skb_push()、skb_pull()、skb_trim()等等,都是对skb的head、data、tail、end、len等字段进行操作。代码不难,都能看懂,后面涉及到具体的协议再来看这些。
链表管理
在对skb链表的操作中,为了防止被其他异步操作打断,在操作前都必须现获取SKB头节点中(sk_buff_head结构)的自旋锁,然后才能访问队列中的元素。该链表头结构如下:
struct sk_buff_head { /* These two members must be first. */ struct sk_buff *next; struct sk_buff *prev; /*链表中的节点数,即队列长度*/ __u32 qlen; /*用来控制SKB链表并发操作的自旋锁*/ spinlock_t lock;};
对链表操作也增加了很多函数,包括初始化、入队列、出队列等等,也在skbuff.h中。
Skb_shared_info结构
在alloc_skb()看到,其中中分配数据部分分配了一个该结构,在数据缓存区的末尾,保存了数据块的附加信息。如下:
#define skb_shinfo(SKB) ((struct skb_shared_info *)((SKB)->end))
该结构定义如下:
struct skb_shared_info { /*引用计数,当一个数据缓存区被多个SKB的描述符引用时就会设置相应的计数*/ atomic_t dataref; /*ip分片的存储有关,片段数*/ unsigned short nr_frags; /*生成GSO段时的MSS,因为GSO段的长度是与发送该段的套接口中合适MSS的整数倍*/ unsigned short gso_size; /* Warning: this field is not always filled in (UFO)! *//*GSO段的长度是gso_size的整数倍,即用gso_size来分割大段时产生的段数*/ unsigned short gso_segs; /*该SKB中的数据支持的GSO类型*/ unsigned short gso_type; __be32 ip6_frag_id; /*ip分片的存储有关,使用情况如下:1,用于在接收分片组后连接多核分片,组成一个完整的IP数据报;2,在UDP数据报的传输中,将待分片的SKB连接到第一个SKB中,然后在传输过程中能够快速的分片;3,用于存放FRAGLIST类型的聚合分散I/O的数据包,如果输出网络设备支持FRAGLIST类型的聚合分散I/O,则可以直接输出*/ struct sk_buff *frag_list; /*ip分片的存储有关,片段以关联的方式存储在该数组中*/ skb_frag_t frags[MAX_SKB_FRAGS];};其中skb_frag_t类型如下:struct skb_frag_struct { /*指向文件系统缓存页的指针*/ struct page *page; /*数据起始地址在文件系统缓存页中的偏移*/ __u16 page_offset; /*数据在文件系统缓存页面中使用的长度*/ __u16 size;};