• linux源码解读(十九):网络通信简介——sk_buff结构体介绍


      1、时至今日,已经找不到单机设备了,所有的IT硬件设备都会联网和其他的IT设备通信。设备之间传递数据总要遵守特定的协议规范吧,避免出现“鸡同鸭讲”的尴尬局面,这个就是至今世界范围内最流行的tcp/ip协议! 为了简化,又被分成了5层,各种体系的对应关系如下图:

          

            看网络原理解析的各种技术文章时,经常会提起报文、数据包、包头这些名词,然后配上协议不同层级的包头字段图示,初学者可能会懵逼:这些概念到底指的是啥了?概念背后的本质又是啥了?先说说我个人的理解:所谓的报文也好、数据包也好、包头也好,本质就是个字符串!不同层级的封装,本质就是不停地在字符串前面添加新的字符!理解这个本质后,网络数据包的构造过程就很容易理解了,图示如下:

             

            假如李雷想给韩梅梅发一条内容为“hello”的消息,操作系统怎么才能把这消息准确无误地发送给韩梅梅了?很简单:操作系统通过网卡发送的数据包遵从TCP/IP协议即可!李雷和韩梅梅之间可能有很多路由器、交换机这些帮忙转发数据包的设备,为了能正确识别并转发,需要操作系统发送的数据有特定的格式,这种特定格式的数据包制作过程如上如图所以:应用层的app构造“hello”字符串,然后调用send函数发送数据。操作系统提供的send函数会继续在“hello”这个字符串前面添加各种标识的字段(这就是所谓的包头,本质还是字符串)。比如:

    •   应用层的下一层是传输层,这一层是tcp或udp协议,需要加上端口(识别进程)和其他tcp或udp的属性字段;
    •        再往下是网络层,需要加上源和目的ip地址,以及其他ip协议的属性字段
    •        继续往下是链路层,加上网卡的硬件id,也就是MAC号

            以上一切都做完后,由网卡发送出去!本质就是网卡发送了一串字符串,用户负责构造字符串的应用层,然后调用操作系统提供的send函数!操作系统负责继续构造字符串的传输层、网络层和链路层!整个网络通信数据源构造的原理就是这样的,其实并不复杂,搞清楚协议每一层需要添加的字段就行了,没啥难的!原理搞懂了,linux操作系统在代码层面又是怎么做的了?

          2、操作系统既然发出去的是字符串,围绕着这段字符串有以下几点需要明确:

    • 肯定需要在内存找个地方存储这串字符串
    • 应用有很多,不同的应用可能会发送不同的应用数据;就算是同一个应用,也可能在不同的时间段发送不同的数据;换句话说这类的字符串有很多很多,绝对不止1个!

              那么问题来了:大量的字符串该怎么管理了?linux操作系统使用了sk_buff结构体!这个结构体非常大,个人觉得重要的字段额外加了注释:

    /** 
     *    struct sk_buff - socket buffer
     *    @next: Next buffer in list
     *    @prev: Previous buffer in list
     *    @tstamp: Time we arrived/left
     *    @rbnode: RB tree node, alternative to next/prev for netem/tcp
     *    @sk: Socket we are owned by
     *    @dev: Device we arrived on/are leaving by
     *    @cb: Control buffer. Free for use by every layer. Put private vars here
     *    @_skb_refdst: destination entry (with norefcount bit)
     *    @sp: the security path, used for xfrm
     *    @len: Length of actual data
     *    @data_len: Data length
     *    @mac_len: Length of link layer header
     *    @hdr_len: writable header length of cloned skb
     *    @csum: Checksum (must include start/offset pair)
     *    @csum_start: Offset from skb->head where checksumming should start
     *    @csum_offset: Offset from csum_start where checksum should be stored
     *    @priority: Packet queueing priority
     *    @ignore_df: allow local fragmentation
     *    @cloned: Head may be cloned (check refcnt to be sure)
     *    @ip_summed: Driver fed us an IP checksum
     *    @nohdr: Payload reference only, must not modify header
     *    @nfctinfo: Relationship of this skb to the connection
     *    @pkt_type: Packet class
     *    @fclone: skbuff clone status
     *    @ipvs_property: skbuff is owned by ipvs
     *    @peeked: this packet has been seen already, so stats have been
     *        done for it, don't do them again
     *    @nf_trace: netfilter packet trace flag
     *    @protocol: Packet protocol from driver
     *    @destructor: Destruct function
     *    @nfct: Associated connection, if any
     *    @nf_bridge: Saved data about a bridged frame - see br_netfilter.c
     *    @skb_iif: ifindex of device we arrived on
     *    @tc_index: Traffic control index
     *    @tc_verd: traffic control verdict
     *    @hash: the packet hash
     *    @queue_mapping: Queue mapping for multiqueue devices
     *    @xmit_more: More SKBs are pending for this queue
     *    @ndisc_nodetype: router type (from link layer)
     *    @ooo_okay: allow the mapping of a socket to a queue to be changed
     *    @l4_hash: indicate hash is a canonical 4-tuple hash over transport
     *        ports.
     *    @sw_hash: indicates hash was computed in software stack
     *    @wifi_acked_valid: wifi_acked was set
     *    @wifi_acked: whether frame was acked on wifi or not
     *    @no_fcs:  Request NIC to treat last 4 bytes as Ethernet FCS
      *    @napi_id: id of the NAPI struct this skb came from
     *    @secmark: security marking
     *    @mark: Generic packet mark
     *    @vlan_proto: vlan encapsulation protocol
     *    @vlan_tci: vlan tag control information
     *    @inner_protocol: Protocol (encapsulation)
     *    @inner_transport_header: Inner transport layer header (encapsulation)
     *    @inner_network_header: Network layer header (encapsulation)
     *    @inner_mac_header: Link layer header (encapsulation)
     *    @transport_header: Transport layer header
     *    @network_header: Network layer header
     *    @mac_header: Link layer header
     *    @tail: Tail pointer
     *    @end: End pointer
     *    @head: Head of buffer
     *    @data: Data head pointer
     *    @truesize: Buffer size
     *    @users: User count - see {datagram,tcp}.c
     */
    
    struct sk_buff {
        union {
            struct {
                /* These two members must be first. */
                /*双向链表结构,用来存储网络数据包*/
                struct sk_buff        *next;
                struct sk_buff        *prev;
    
                union {
                    /*报文到达或者离开的时间戳; Time we arrived 表示这个skb的接收到的时间,
                    一般是在包从驱动中往二层发送的接口函数中设置 */
                    ktime_t        tstamp;
                    struct skb_mstamp skb_mstamp;
                };
            };
            /**/
            struct rb_node    rbnode; /* used in netem & tcp stack */
        };
        struct sock        *sk;//该数据包属于哪个socket
        struct net_device    *dev;//收到这个报文的设备
    
        /*
         * 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.
         */
        char            cb[48] __aligned(8);
    
        unsigned long        _skb_refdst;
        //析构函数,一般都是设置为sock_rfree或者sock_wfree
        void            (*destructor)(struct sk_buff *skb);
    #ifdef CONFIG_XFRM
        struct    sec_path    *sp;
    #endif
    #if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
        struct nf_conntrack    *nfct;
    #endif
    #if IS_ENABLED(CONFIG_BRIDGE_NETFILTER)
        struct nf_bridge_info    *nf_bridge;
    #endif
        /*表示当前的skb中的数据的长度,这个长度即包括buf中的数据也包括切片的数据,
        也就是保存在skb_shared_info中的数据*/
        unsigned int        len,
                    data_len;//只表示切片数据的长度,也就是skb_shared_info中的长度
        __u16            mac_len,//mac头的长度
                    hdr_len;//用于clone的时候,它表示clone的skb的头的长度
    
        /* Following fields are _not_ copied in __copy_skb_header()
         * Note that queue_mapping is here mostly to fill a hole.
         */
        kmemcheck_bitfield_begin(flags1);
        __u16            queue_mapping;//多队列设备的映射,也就是说映射到那个队列。 
    
    /* if you move cloned around you also must adapt those constants */
    #ifdef __BIG_ENDIAN_BITFIELD
    #define CLONED_MASK    (1 << 7)
    #else
    #define CLONED_MASK    1
    #endif
    #define CLONED_OFFSET()        offsetof(struct sk_buff, __cloned_offset)
    
        __u8            __cloned_offset[0];
        __u8            cloned:1,
                    nohdr:1,
                    fclone:2,
                    peeked:1,
                    head_frag:1,
                    xmit_more:1,
                    __unused:1; /* one bit hole */
        kmemcheck_bitfield_end(flags1);
    
        /* fields enclosed in headers_start/headers_end are copied
         * using a single memcpy() in __copy_skb_header()
         */
        /* private: */
        __u32            headers_start[0];
        /* public: */
    
    /* if you move pkt_type around you also must adapt those constants */
    #ifdef __BIG_ENDIAN_BITFIELD
    #define PKT_TYPE_MAX    (7 << 5)
    #else
    #define PKT_TYPE_MAX    7
    #endif
    #define PKT_TYPE_OFFSET()    offsetof(struct sk_buff, __pkt_type_offset)
    
        __u8            __pkt_type_offset[0];
        __u8            pkt_type:3;
        __u8            pfmemalloc:1;
        __u8            ignore_df:1;
        __u8            nfctinfo:3;
    
        __u8            nf_trace:1;
        __u8            ip_summed:2;
        __u8            ooo_okay:1;
        __u8            l4_hash:1;
        __u8            sw_hash:1;
        __u8            wifi_acked_valid:1;
        __u8            wifi_acked:1;
    
        __u8            no_fcs:1;
        /* Indicates the inner headers are valid in the skbuff. */
        __u8            encapsulation:1;
        __u8            encap_hdr_csum:1;
        __u8            csum_valid:1;
        __u8            csum_complete_sw:1;
        __u8            csum_level:2;
        __u8            csum_bad:1;
    
    #ifdef CONFIG_IPV6_NDISC_NODETYPE
        __u8            ndisc_nodetype:2;
    #endif
        __u8            ipvs_property:1;
        __u8            inner_protocol_type:1;
        __u8            remcsum_offload:1;
    #ifdef CONFIG_NET_SWITCHDEV
        __u8            offload_fwd_mark:1;
    #endif
        /* 2, 4 or 5 bit hole */
    
    #ifdef CONFIG_NET_SCHED
        __u16            tc_index;    /* traffic control index */
    #ifdef CONFIG_NET_CLS_ACT
        __u16            tc_verd;    /* traffic control verdict */
    #endif
    #endif
    
        union {
            __wsum        csum;
            struct {
                __u16    csum_start;
                __u16    csum_offset;
            };
        };
        __u32            priority;/*优先级,主要用于QOS*/
        int            skb_iif;
        __u32            hash;
        __be16            vlan_proto;
        __u16            vlan_tci;
    #if defined(CONFIG_NET_RX_BUSY_POLL) || defined(CONFIG_XPS)
        union {
            unsigned int    napi_id;
            unsigned int    sender_cpu;
        };
    #endif
    #ifdef CONFIG_NETWORK_SECMARK
        __u32        secmark;
    #endif
    
        union {
            __u32        mark;
            __u32        reserved_tailroom;
        };
    
        union {
            __be16        inner_protocol;
            __u8        inner_ipproto;
        };
    
        __u16            inner_transport_header;
        __u16            inner_network_header;
        __u16            inner_mac_header;
    
        __be16            protocol;//协议类型
        __u16            transport_header;//传输层头部
        __u16            network_header;//网络层头部
        __u16            mac_header;//链路层头部
    
        /* private: */
        __u32            headers_end[0];
        /* public: */
    
        /* These elements must be at the end, see alloc_skb() for details. 
        sk_buff_data_t就是unsigned char *
        */
        sk_buff_data_t        tail;//指向报文尾巴
        sk_buff_data_t        end;//指向报文最后一个字节
        unsigned char        *head,//分配的内存块的起始位置;指向数据区中开始的位置(非实际数据区域开始位置)
                    *data;//保存数据内容的首地址;(实际数据区域开始位置)
        /*缓冲区的总长度,包括sk_buff结构和数据部分。
        如果申请一个len字节的缓冲区,alloc_skb函数会把它初始化成len+sizeof(sk_buff)。
        当skb->len变化时,这个变量也会变化*/
        unsigned int        truesize;
        /*atomic_t  users;这是个引用计数,表明了有多少实体引用了这个skb。
        其作用就是在销毁skb结构体时,先查看下users是否为零,
        若不为零,则调用函数递减下引用计数users即可;当某一次销毁时,users为零才真正释放内存空间。
        有两个操作函数:atomic_inc()引用计数增加1;atomic_dec()引用计数减去1;*/
        atomic_t        users;
    };

       有几点需要注意:

    •   这个结构体并不直接存储网络数据包,而是存放了数据包的指针,就是上面的tail、end、head、data等!
    •        这几个指针的关系如图所示:这下看明白了吧!应用层数据前面加上协议其他层级的头部数据,用data指针保存!应用层尾部用tail指针保存!如果是从L4传输到L2,则是通过往sk_buff结构体中增加该层协议头来操作;如果是从L4到L2,则是通过移动sk_buff结构体中的data指针来实现,不会删除各层协议头,这样做可以提高CPU的工作效率!

           

       3、结构体有了,接着就是操作这些结构体的方法了!既然网络通信最核心的就是构造数据包,落实到结构体就是移动head、data、tail、end这4大指针了!linux内核采用了__skb_put、__skb_push、__pskb_pull、skb_reserve 4大函数,这4个函数参数是一样的,都有啥区别了?

        (1)先看看put函数:在数据区的尾部添加数据,也就是增加tail指针!

    /*在数据区的末端添加某协议的尾部*/
    static inline unsigned char *__skb_put(struct sk_buff *skb, unsigned int len)
    {
        unsigned char *tmp = skb_tail_pointer(skb);//获取当前skb->tail
        SKB_LINEAR_ASSERT(skb);
        skb->tail += len;
        skb->len  += len;
        return tmp;
    }

         如下图所示:tail指针增加了n

         

       (2)再看看push函数:这次是在数据区前面填充数据,所以是data指针减少!从push的名称就可以看出来,类似于栈,往栈里写数据时,栈指针减少,所以这里的data作用类似sp指针

    /*在数据区的前端添加某协议的头部*/
    static inline unsigned char *__skb_push(struct sk_buff *skb, unsigned int len)
    {
        skb->data -= len;
        skb->len  += len;
        return skb->data;
    }

        如下图所示:data指针减少n

        

        (3)再看看pull函数:把data指针增加n,相当于弹出数据!

    /*把data指针增加n,相当于弹出数据*/
    unsigned char *skb_pull(struct sk_buff *skb, unsigned int len);
    static inline unsigned char *__skb_pull(struct sk_buff *skb, unsigned int len)
    {
        skb->len -= len;
        BUG_ON(skb->len < skb->data_len);
        return skb->data += len;
    }

               如下如所示:

         (4)skb_reserve函数:当skb还是空的时候,需要给协议不同层级预留存储头部信息的空间

    /**
     *    skb_reserve - adjust headroom
     *    @skb: buffer to alter
     *    @len: bytes to move
     *
     *    Increase the headroom of an empty &sk_buff by reducing the tail
     *    room. This is only allowed for an empty buffer.
        给协议预留head的存储空间,只能对空的skb使用;
     */
    static inline void skb_reserve(struct sk_buff *skb, int len)
    {
        skb->data += len;
        skb->tail += len;
    }

      如下图所示:

         

        

    参考:

    1、https://www.jianshu.com/p/3738da62f5f6  sk_buff结构体详解

    2、https://blog.csdn.net/farmwang/article/details/54234176  sk_buff详解

    3、http://www.360doc.com/content/14/0310/16/2306903_359316839.shtml  sk_buff操作函数

  • 相关阅读:
    在IIS上部署 .Net Core 3.0 项目踩坑实录
    .net core3.0部署Linux服务器 使用Docker容器和Nginx反代理教程
    播放器 AxWindowsMediaPlayer控件的使用
    Github下载慢和下载过程中断等情况的解决方案
    GitHub第一次上传遇到的问题
    DataGridView && 增加复选框(checkbox)方法
    努力
    绘图:drawImage一个用法
    Tuple<T1,T2,.........T> 元组简单使用
    随机的标识符GUID
  • 原文地址:https://www.cnblogs.com/theseventhson/p/15858194.html
Copyright © 2020-2023  润新知