Files
LDD-LinuxDeviceDrivers/study/kernel/04-net/03-socket_buffer
gatieme e2c7456c2a ...
2016-09-06 11:00:22 +08:00
..
...
2016-09-06 11:00:22 +08:00

进程虚拟地址空间

日期 内核版本 架构 作者 GitHub CSDN
2016-06-14 Linux-4.7 X86 & arm gatieme LinuxDeviceDrivers Linux内存管理

#1 管理套接字缓冲区数据

在内核分析(收到的)网络分组时, 底层协议的数据将传递到更高的层. 发送数据时顺序相反, 各种协议产生的数据(首部和净荷)依次向更低的层传递, 直至最终发送.

这些操作的速度对网络子系统的性能有决定性的影响, 因此内核使用了一种特殊的结构,称为套接字缓冲区(socket buffer).

#2 套接字缓冲区结构体sk_buff

套接字缓冲区(socket buffer)用struct sk_buff结构来表示, 定义在include/linux/skbuff.h?v=4.7, line 626

struct sk_buff {
    union {
        struct {
            /* These two members must be first. */
            struct sk_buff      *next;
            struct sk_buff      *prev;

            union {
                ktime_t     tstamp;
                struct skb_mstamp skb_mstamp;
            };
        };
        struct rb_node  rbnode; /* used in netem & tcp stack */
    };
    struct sock         *sk;
    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;
    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
    unsigned int        len,
                data_len;
    __u16           mac_len,
                hdr_len;

    /* 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;
    __u8            cloned:1,
                nohdr:1,
                fclone:2,
                peeked:1,
                head_frag:1,
                xmit_more: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;
    /* 3 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;
    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
    union {
#ifdef CONFIG_NETWORK_SECMARK
        __u32       secmark;
#endif
#ifdef CONFIG_NET_SWITCHDEV
        __u32       offload_fwd_mark;
#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      tail;
    sk_buff_data_t      end;
    unsigned char       *head,
                *data;
    unsigned int        truesize;
    atomic_t        users;
};

套接字缓冲区用于在网络实现的各个层次之间交换数据, 而无须来回复制分组数据, 对性能的提高很可观. 套接字结构是网络子系统的基石之一, 因为在产生和分析分组时,在各个协议层次上都需要处理该结构.

#3 使用套接字缓冲区管理数据

套接字缓冲区通过其中包含的各种指针与一个内存区域相关联, 网络分组的数据就位于该区域中. 假定我们使用的是32位系统(在64位机器上, 套接字缓冲区的组织稍有不同, 读者稍后就会看到).

##3.1 sk_buff的主要数据字段

struct sk_buff
{
	/*  ......  */
    __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.
     *  这些成员必须在末尾详见alloc_skb()  */
    sk_buff_data_t      tail;
    sk_buff_data_t      end;
    unsigned char       *head,
                *data;
    unsigned int        truesize;
    atomic_t        users;
};

套接字缓冲区和网络分组数据之间的关联

字段 描述
mac_header 指向MAC协议首部的起始.
network_header 指向网络层协议首部的起始.
transport_header 指向传输层协议首部的起始

##3.2 缓冲区的布局信息

套接字缓冲区的基本思想是, 通过操作指针来增删协议首部.

字段 描述
headend 指向数据在内存中的起始和结束位置
datatail 指向协议数据区域的起始和结束位置

这个区域可能大于实际需要的长度,因为在产生分组时,尚不清楚分组的长度.

套接字缓冲区在各个协议层之间传递时,对缓冲区的操作

内核实现了一些宏用来获取缓冲区数据段的结束位置. 这些函数定义在include/linux/skbuff.h?v=4.7, line 1165

#ifdef NET_SKBUFF_DATA_USES_OFFSET
static inline unsigned char *skb_end_pointer(const struct sk_buff *skb)
{
    return skb->head + skb->end;
}

static inline unsigned int skb_end_offset(const struct sk_buff *skb)
{
    return skb->end;
}
#else
static inline unsigned char *skb_end_pointer(const struct sk_buff *skb)
{
    return skb->end;
}

static inline unsigned int skb_end_offset(const struct sk_buff *skb)
{
    return skb->end - skb->head;
}
#endif

内核实现了一些宏用来获取协议数据区域的结束的结束位置tail的函数. 这些函数定义在include/linux/skbuff.h?v=4.7, line 1849

#ifdef NET_SKBUFF_DATA_USES_OFFSET	/*  64 bit  */
static inline unsigned char *skb_tail_pointer(const struct sk_buff *skb)
{
    return skb->head + skb->tail;
}

static inline void skb_reset_tail_pointer(struct sk_buff *skb)
{
    skb->tail = skb->data - skb->head;
}

static inline void skb_set_tail_pointer(struct sk_buff *skb, const int offset)
{
    skb_reset_tail_pointer(skb);
    skb->tail += offset;
}

#else /* NET_SKBUFF_DATA_USES_OFFSET */
static inline unsigned char *skb_tail_pointer(const struct sk_buff *skb)
{
    return skb->tail;
}

static inline void skb_reset_tail_pointer(struct sk_buff *skb)
{
    skb->tail = skb->data;
}

static inline void skb_set_tail_pointer(struct sk_buff *skb, const int offset)
{
    skb->tail = skb->data + offset;
}

#endif /* NET_SKBUFF_DATA_USES_OFFSET */

在字长32位的系统上数据类型sk_buff_data_t用来表示各种类型为简单指针的数据(char *), 定义在include/linux/skbuff.h?v=4.7, line 487, 在64位 CPU上, 可使用一点小技巧来节省一些空间. sk_buff_data_t的定义改为整型变量

#if BITS_PER_LONG > 32  /*  64位的linux系统中long是8个字节 > 4  */
#define NET_SKBUFF_DATA_USES_OFFSET 1
#endif

#ifdef NET_SKBUFF_DATA_USES_OFFSET		/*  64位系统  */
typedef unsigned int sk_buff_data_t;				/*  4个字节  */
#else											   /*  32位系统  */
typedef unsigned char *sk_buff_data_t;			  /*  4个字节  */
#endif

套接字缓冲区需要很多指针来表示缓冲区中内容的不同部分. 由于网络子系统必须保证较低的内存占用和较高的处理速度, 因而对struct sk_buff来说,我们需要保持该结构的长度尽可能小.

##3.3 通过偏移计算地址

datahead是常规的指针, 而所有sk_buff_data_t类型的成员现在都解释为相对于前两者的偏移量.

这使得内核可以将套接字缓冲区用于所有协议类型。正确地解释数据需要做简单的类型转换, 为此提供了几个辅助函数. 例如套接字缓冲区可以包含TCP或UDP分组. 来自传输层协议首部的对应信息分别可以用tcp_hdr和udp_hdr提取. 这两个函数都将原始指针(raw pointer)转换为某种适当的数据类型。其他传输层协议也提供了形如XXX_hdr的辅助函数, 这类函数需要一个指向struct sk_buff的指针作为参数, 并返回重新解释的传输首部数据.

例如, 观察如何从套接字缓冲区获取TCP首部, 该功能由tcp_hdr函数来完成. 定义在include/linux/tcp.h?v=4.7, line 27

在64位处理器的体系结构上, 整型变量占用的内存只有指针变量的一半(前者是4字节, 后者是8字节), 该结构的长度缩减了20字节, 但套接字缓冲区中包含的信息仍然是同样的

static inline struct tcphdr *tcp_hdr(const struct sk_buff *skb)
{
    return (struct tcphdr *)skb_transport_header(skb);
}

struct tcphdr是一个结构, 包含了TCP首部中的所有字段. 定义在include/uapi/linux/tcp.h?v=4.7, line 24

datatail使得在不同协议层之间传递数据时, 无须显式的复制操作

指向传输层首部的指针现在计算如下, 定义在include/linux/skbuff.h?v=4.7, line 2109

static inline unsigned char *skb_transport_header(const struct sk_buff *skb)
{
    return skb->head + skb->transport_header;
}

这种做法是有效的, 因为4字节偏移量足以描述长达4GB的内存区, 套接字缓冲区不可能超过这个长度.

还有其他类似的转换函数供网络子系统使用. 对我们来说, ip_hdr是最重要的它用于解释一个IP分组的内容. 定义在include/linux/ip.h?v=4.7, line 23

由于假定套接字缓冲区的内部表示对通用网络代码是不可见的, 所以提供了如下几个辅助函数来访问struct sk_buff的成员. 这些函数都定义在include/linux/skbuff.h?v=4.7中,编译时会自动选择其中适当的变体使用.

函数 描述
skb_transport_header(const struct sk_buff *skb) 从给定的套接字缓冲区获取传输层首部的地址.
skb_reset_transport_header(struct sk_buff *skb) 将传输层首部重置为数据部分的起始位置.
skb_set_transport_header(struct sk_buff *skb, const int offset) 根据数据部分中给定的偏移量来设置传输层首部的起始地址

对MAC层和网络层首部来说也有同样一组函数可用只需将transport分别替换为mac或network即可. 这些函数均定义在include/linux/skbuff.h?v=4.7

##3.3. 分组的产生于接收

在一个新分组产生时, TCP层首先在用户空间中分配内存来容纳该分组数据(首部和净荷). 分配的空间大于数据实际需要的长度,因此较低的协议层可以进一步增加首部.

分配一个套接字缓冲区, 使得head和end分别指向上述内存区的起始和结束地址而TCP数据位于datatail之间

在套接字缓冲区传递到互联网络层时, 必须增加一个新层. 只需要向已经分配但尚未占用的那部分内存空间写入数据即可, 除了data之外所有的指针都不变,data现在指向IP首部的起始处.

下面的各层会重复同样的操作,直至分组完成,即将通过网络发送.

接收的分组进行分析的过程是类似的. 分组数据复制到内核分配的一个内存区中, 并在整个分析期间一直处于该内存区中. 与该分组相关联的套接字缓冲区在各层之间顺序传递, 各层依次将其中的各个指针设置为正确值.

内核提供了一些用于操作套接字缓冲区的标准函数

函数 语 义
alloc_skb 分配一个新的sk_buff实例
skb_copy 创建套接字缓冲区和相关数据的一个副本
skb_clone 创建套接字缓冲区的一个副本,但原本和副本将使用同一分组数据
skb_tailroom 返回数据末端空闲空间的长度
skb_headroom 返回数据起始处空闲空间的长度
skb_realloc_headroom 在数据起始处创建更多的空闲空间. 现存数据不变

#4 管理套接字缓冲区数据

套接字缓冲区结构不仅包含上述指针,还包括用于处理相关的数据和管理套接字缓冲区自身的其他成员.

其中不常见的成员在本章中遇到时才会讨论。下面列出的是一些最重要的成员

##4.1 套接字缓冲区的部分字段

struct sk_buff {
    union {
        struct {
            /* These two members must be first. */
            struct sk_buff      *next;
            struct sk_buff      *prev;

            union {
                ktime_t     tstamp;
                struct skb_mstamp skb_mstamp;
            };
        };
    struct rb_node  rbnode; /* used in netem & tcp stack */
	struct sock         *sk;
    struct net_device       *dev;
	/*  ......  */
        int             skb_iif;

    };
字段 描述
tstamp 保存了分组到达的时间
dev 指定了处理分组的网络设备. dev在处理分组的过程中可能会改变, 例如, 在未来某个时候, 分组可能通过计算机的另一个设备发出
skb_iif 输入设备的接口索引号总是保存在iif中
sk 是一个指针指向用于处理该分组的套接字对应的socket实例
dst 表示接下来该分组通过内核网络实现的路由。这里使用了一个特殊的格式
next和prev 用于将套接字缓冲区保存到一个双链表中. 这里没有使用内核的标准链表实现, 而是使用了一个手工实现的版本
rbnode 将套接字缓冲区保存在红黑树中, 用于tcp的栈中, 这里使用了内核标准的红黑树结构

##4.2 套接字缓冲区的头节点sk_buff_head

我们知道套接字缓冲区sk_buff通过其next和prev指针域, 将所有的缓冲区保存在了一个双向链表中, 内核并没有使用标准的内核list数据结构, 而是对其进行了进一步的封装, 设计了一个带头节点的双向链表, 使用了一个表头sk_buff_head来实现套接字缓冲区的等待队列. 其结构定义如下

struct sk_buff_head {
    /* These two members must be first.
     *  这两个成员必须在最前面  */
    struct sk_buff  *next;
    struct sk_buff  *prev;

    __u32       qlen;
    spinlock_t      lock;
};

为什么需要设计一个单独的表头(头节点)呢?

  • 带头节点的双向链表在插入不需要特殊的处理.

  • 我们需要保存缓冲区的数目length, 这样设计一个带length和标识的头节点更灵活.

  • 带头节点的缓冲区更符合面向对象的思想.

字段 描述
qlen 指定了等待队列的长度,即队列中成员的数目
next和prev 用于创建一个循环双链表套接字缓冲区的list成员指回到表头
lock 双向链表的锁

分组通常放置在等待队列中,例如分组等待处理时,或需要重新组合已经分析过的分组时.

通过双链表管理套接字缓冲区

##4.3.3 skb_shared_info结构体

内核skb_shared_info结构体该类型用来管理数据包分片信息, 该结构定义在include/linux/skbuff.h?v=4.7, line 408

/* This data is invariant across clones and lives at
 * the end of the header data, ie. at skb->end.
 */
struct skb_shared_info {
    unsigned char   nr_frags;
    __u8        tx_flags;
    unsigned short  gso_size;		/*  尺寸  */
    /* Warning: this field is not always filled in (UFO)! */
    unsigned short  gso_segs;		/*  顺序  */
    unsigned short  gso_type;
    struct sk_buff  *frag_list;
    struct skb_shared_hwtstamps hwtstamps;		/*  硬件时间戳  */
    u32         tskey;
    __be32      ip6_frag_id;

    /*
     * Warning : all fields before dataref are cleared in __alloc_skb()
     */
    atomic_t    dataref;			/*  使用计数  */

    /* Intermediate layers must ensure that destructor_arg
     * remains valid until skb destructor */
    void *      destructor_arg;

    /* must be last field, see pskb_expand_head() */
    skb_frag_t      frags[MAX_SKB_FRAGS];
};

通过宏可以表示与skb的关系

/* Internal */
#define skb_shinfo(SKB) ((struct skb_shared_info *)(skb_end_pointer(SKB)))