深度探索套接字缓冲区

套接字缓冲区用结构体struct sk_buff表示,它用于在网络子系统中的各层之间传递数据,处于一个核心地位,非常之重要。它包含了一组成员数据用于承载网络数据,同时,也定义了在这些数据上操作的一组函数。下面是其完整的定义:
struct sk_buff {
struct sk_buff *next;
struct sk_buff *prev;

struct sock *sk;
struct skb_timeval tstamp;
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;
struct sec_path *sp;

char cb[48];

unsigned int len,
data_len,
mac_len,
csum;
__u32 priority;
__u8 local_df:1,
cloned:1,
ip_summed:2,
nohdr:1,
nfctinfo:3;
__u8 pkt_type:3,
fclone:2,
ipvs_property:1;
__be16 protocol;

void (*destructor)(struct sk_buff *skb);
#ifdef CONFIG_NETFILTER
__u32 nfmark;
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;
#ifdef CONFIG_NET_CLS_ACT
__u16 tc_verd;
#endif
#endif


unsigned int truesize;
atomic_t users;
unsigned char *head,
*data,
*tail,
*end;
};
这是一个比较宠大的结构体,为了便于理解,我们分成多块进行分析。
为了使用套接字缓冲区,内核创建了两个后备高速缓存(looaside cache),它们分别是skbuff_head_cache和skbuff_fclone_cache,协议栈中所使用到的所有的sk_buff结构都 是从这两个后备高速缓存中分配出来的。两者的区别在于skbuff_head_cache在创建时指定的单位内存区域的大小是sizeof(struct sk_buff),可以容纳任意数目的struct sk_buff,而skbuff_fclone_cache在创建时指定的单位内存区域大小是2*sizeof(struct sk_buff)+sizeof(atomic_t),它的最小区域单位是一对strcut sk_buff和一个引用计数,这一对sk_buff是克隆的,即它们指向同一个数据缓冲区,引用计数值是0,1或2,表示这一对中有几个sk_buff 已被使用。
创建一个套接字缓冲区,最常用的操作是alloc_skb,它在skbuff_head_cache中创建一个struct sk_buff,如果要在skbuff_fclone_cache中创建,可以调用__alloc_skb,通过特定参数进行。
struct sk_buff的成员head指向一个已分配的空间的头部,该空间用于承载网络数据,end指向该空间的尾部,这两个成员指针从空间创建之后,就不能被修 改。data指向分配空间中数据的头部,tail指向数据的尾部,这两个值随着网络数据在各层之间的传递、修改,会被不断改动。所以,这四个指针指向共同 的一块内存区域的不同位置,该内存区域由__alloc_skb在创建缓冲区时创建,四个指针间存在如下关系:
head <= data <= tail < end
那指向的这块内存区域有多大呢?一般由外部根据需要传入。外部设定这个大小时,会根据实际数据量加上各层协议的首部,再加15(为了处理对齐)传入,在 __alloc_skb中根据各平台不同进行长度向上对齐。但是,我们另外还要加上一个存放结构体struct skb_shared_info的空间,也就是说end并不真正指向内存区域的尾部,在end后面还有一个结构体struct skb_shared_info,下面是其定义:
struct skb_shared_info{
atomic_t dataref; //引用计数。
unsigned short nr_frags; //数据片段的数量。
unsigned short tso_size;
unsigned short tso_segs;
unsigned short ufo_size;
unsigned int ip6_frag_id;
struct sk_buff *frag_list; //数据片段的链表。
skb_frag_t frags[MAX_SKB_FRAGS]; //每一个数据片段的长度。
};
这个结构体存放分隔存储的数据片段,将数据分解为多个数据片段是为了使用分散/聚集I/O。
如果是在skbuff_fclone_cache中创建,则创建一个struct sk_buff后,还要把紧邻它的一个struct sk_buff的fclone成员置标志SKB_FCLONE_UNAVAILABLE,表示该缓冲区还没有被创建出来,同时置自己的fclone为 SKB_FCLONE_ORIG,表示自己可以被克隆。最后置引用计数为1。
最后,truesize表示缓存区的整体长度,置为sizeof(struct sk_buff)+传入的长度,不包括结构struct skb_shared_info的长度。
前面一篇文章分析了套接字缓冲区sk_buff的创建过程,但一般来讲,一个套接字缓冲区总是属于一个套接字,所以,除了调用sk_buff本身的 alloc_skb函数创建一个套接字缓冲区,套接字本身还要对sk_buff进行一些操作,以及设置自身的一些成员值。下面我们来分析这个过程。
如果检查到待发送数据报没有传输层协议头(不是传输层的tcp或udp数据报),套接字创建缓冲区的函数是sock_alloc_send_skb,它的函数原型是:
struct sk_buff *sock_alloc_send_skb(struct sock *sk, unsigned long size,
int noblock, int *errcode)
它直接调用函数:
static struct sk_buff *sock_alloc_send_pskb(struct sock *sk,
unsigned long header_len,
unsigned long data_len,
int noblock, int *errcode)
参数sk是要创建缓冲区的那个套接字,header_len是sk_buff中,成员data指向的那块数据区的长度,而data_len则是指除那块数 据区以外的被分片的数据的总长。noblock指示是否阻塞模式。对于非传输层协议包,不使用分散/聚集IO,所以,置data_len为0。
网络层代表一个套接字的结构体struct sock有两个成员sk_wmem_alloc和sk_sndbuf,sk_wmem_alloc表示在这个套接字上已经分配的写缓冲区(发送缓冲区)的 总长,每次分配完一个属于它的写sk_buff,这个值总是加上sk_buff->truesize。而sk_sndbuf则是这个socket所 允许的最大发送缓冲区。它的值在系统初始化的时候设为变量sysctl_wmem_max的值,可以通过系统调用进行修改。其缺省值 sysctl_wmem_max为107520字节,因为它的计算长度还包括了struct sk_buff,所以,一般认为其缺省值是64K数据。
而对于传输层协议包,我们使用sock_wmalloc创建套接字缓冲区,这是一个更为简单的创建函数,没有超时、出错判断机制,直接通过调用 alloc_skb创建一个sk_buff并返回。但对于传输层协议有一个不同点就是sk_wmem_alloc最大可以达到两倍sk_sndbuf,即 缺省的发送缓冲区可以达到128K。
到这里,我们就不难理解struct sk_buff中另外两个成员的含义了:
len是指数据包全部数据的长度,包括data指向的数据和end后面的分片的数据的总长,而data_len只包括分片的数据的长度。而truesize的最终值是len+sizeof(struct sk_buff)。
结构体struct sk_buff中共有三个联合体,分别是h, nh和mac,它们都是一些指针,指向协议栈各层协议的首部。从含有的首部类型来看,nh是h的子集,而mac是nh的子集。《Linux设备驱动程序》 第三版第522页这样介绍这三个联合体:h中包含有传输层的报文头,nh中包含有网络层的报文头,而mac中包含的是链路层的报文头。
光靠这样的一个解释可能过于抽象,让我们来看一个UDP数据报是怎么样穿过数千公里长的网线来到我们的网卡,通过网卡的驱动程序层层向上来到协议栈的上层的。
当网卡驱动程序收到一个UDP数据报后,它创建一个结构体struct sk_buff,确保data成员指向的空间足够存放收到的数据(对于数据报分片的情况,因为比较复杂,我们暂时忽略,我们假设一次收到的是一个完整的 UDP数据报)。把收到的数据全部拷贝到data指向的空间,然后,把skb->mac.raw指向data,此时,数据报的开始位置是一个以太网 头,所以skb->mac.raw指向链路层的以太网头。然后通过调用skb_pull剥掉以太网头,所谓剥掉以太网头,只是把data加上 sizeof(struct ethhdr),同时len减去这个值,这样,在逻辑上,skb已经不包含以太网头了,但通过skb->mac.raw还能找到它。这就是我们通常 所说的,IP数据报被收到后,在链路层被剥去以太网头。
在继续往上层的过程中,一直到我们的my_inet域的函数myip_local_deliver_finish中,我们通过 __skb_pull剥去IP首部,同样,我们可以通过skb->nh.raw找到它。最后,skb->h.raw指向data,即udp首 部,udp首部其实到最后都没有被剥去,应用程序在调用recv接收数据时,直接从skb->data+sizeof(struc udphdr)的位置开始拷贝。
我们可以看到,从网卡驱动开始,通过协议栈层层往上传送数据报时,通过增加skb->data的值,来逐步剥离协议首部,但通过h,nh,mac这三个联合指针,我们可以访问到这些协议首部,从而利用其提供的有效信息。
但必须指出的是,《Linux设备驱动程序》中的解释并不完全准确,mac中包含链路层报文头,这是毫无疑问的,nh中包含义网络层的报文头,也没有问 题,因为ARP协议也属于网络层协议,nh中包含IP首部或者ARP首部。当我们接收到一个icmp数据报时,在 myip_local_deliver_finish中剥去IP首部后,skb->h.raw指向的是icmp首部,但icmp显然不是传输层协 议,它是网络层的一个附属协议。igmp也是相同的情况,我想这也是为什么sk_buff的三个联合体不命名为th, nh, mac的原因,因为th(transprot header)不能准确反映它的内容。
正确的理解应该是三个联合体是按TCP/IP数据报的协议首部的排列顺序来制定的。排在最前面的是以太网头,包含在mac中,第二是网络层协议首部,包括IP和ARP,包含在nh中,第三包括传输层协议头(TCP, UDP)、ICMP, IGMP。
另外,再选择两个重要的数据成员作个简短介绍。
pkt_type,数据报的类型。这个值在网卡驱动程序中由函数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。
protocol, 它的值是以太网首部的第三个成员,即帧类型,对于IP数据来讲,就是ETH_P_IP(0x8000),对ARP数据报来讲,就是ETH_P_ARP(0x8086)。
sk_buff还有一组操作函数,在理解sk_buff本身的基础上,理解这些函数并不困难,这里不再作分析。关于套接字缓冲区的分析就到这里结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值