博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
深度探索套接字缓冲区
阅读量:2221 次
发布时间:2019-05-08

本文共 5776 字,大约阅读时间需要 19 分钟。

    套接字缓冲区用结构体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本身的基础上,理解这些函数并不困难,这里不再作分析。关于套接字缓冲区的分析就到这里结束。

转载地址:http://ppjfb.baihongyu.com/

你可能感兴趣的文章
【托业】【新东方托业全真模拟】TEST05~06-----P5~6
查看>>
【托业】【新东方托业全真模拟】TEST09~10-----P5~6
查看>>
【托业】【新东方托业全真模拟】TEST07~08-----P5~6
查看>>
solver及其配置
查看>>
JAVA多线程之volatile 与 synchronized 的比较
查看>>
Java集合框架知识梳理
查看>>
笔试题(一)—— java基础
查看>>
Redis学习笔记(三)—— 使用redis客户端连接windows和linux下的redis并解决无法连接redis的问题
查看>>
Intellij IDEA使用(一)—— 安装Intellij IDEA(ideaIU-2017.2.3)并完成Intellij IDEA的简单配置
查看>>
Intellij IDEA使用(二)—— 在Intellij IDEA中配置JDK(SDK)
查看>>
Intellij IDEA使用(三)——在Intellij IDEA中配置Tomcat服务器
查看>>
Intellij IDEA使用(四)—— 使用Intellij IDEA创建静态的web(HTML)项目
查看>>
Intellij IDEA使用(五)—— Intellij IDEA在使用中的一些其他常用功能或常用配置收集
查看>>
Intellij IDEA使用(六)—— 使用Intellij IDEA创建Java项目并配置jar包
查看>>
Eclipse使用(十)—— 使用Eclipse创建简单的Maven Java项目
查看>>
Eclipse使用(十一)—— 使用Eclipse创建简单的Maven JavaWeb项目
查看>>
Intellij IDEA使用(十三)—— 在Intellij IDEA中配置Maven
查看>>
面试题 —— 关于main方法的十个面试题
查看>>
集成测试(一)—— 使用PHP页面请求Spring项目的Java接口数据
查看>>
使用Maven构建的简单的单模块SSM项目
查看>>