套接字

一. 前言

  在前面我们逐一分析了进程间通信的各种方法:信号,管道,共享内存和信号量,本文开始将分析更为复杂也是更为常用的另一套进程间通信:网络通信。网络通信和其他进程间通信最大的区别在于不局限于单机,因此成为了互联网时代的主流选择,无论是分布式、云计算、微服务、容器及自动化运营都离不开网络通信,其重要性可想而知。

  经过30多年的发展,网络协议栈已经变得极为复杂,远远不是一两篇文章能够说清楚的东西,所以这里着重剖析我们更为关注的东西:网络编程涉及到的相关协议栈。从本文开始,将分别介绍套接字及其创建、网络连接的建立、网络包的发送、网络包的接收、Netfilter剖析、select, poll 及 epoll剖析。除此之外,介于之前有新同学请教TCP的一些基础问题,打算写一篇扩展篇从设计理念的角度出发好好分析TCP协议的方法面面。

二. 套接字结构体

  网络协议封装为多层,因此套接字结构体定义也有着多层结构,但是这里有一点要注意的:在网络通信中,我们通过网卡获取到的数据包至少包括了物理层,链路层和网络层的内容,因此套接字结构体仅仅从网络层开始,即通常我们只定义了传输层的套接字socket和网络层的套接字socksocket 是用于负责对上给用户提供接口,并且和文件系统关联。而 sock负责向下对接内核网络协议栈。

  首先看传输层的socket结构体,这个结构体表征BSD套接字的通用特性。首先是状态state,用以表示连接情况。type是套接字类型,如SOCK_STREAMwq是等待队列,在后续文章中会说明。file是套接字对应的文件指针,毕竟一切皆文件,所以需要统一的文件系统。sock结构体的sk变量则为网络层的套接字,ops是协议相关的一系列套接字操作。

struct socket {
    socket_state        state;
    short           type;
    unsigned long       flags;
    struct socket_wq    *wq;
    struct file     *file;
    struct sock     *sk;
    const struct proto_ops  *ops;
};

  接着看看网络层,这一层即IP层,该结构体sock中包含了一个基本结构体sock_common,整体较为复杂,所以对于其重要变量进行了说明,以注释的形式在每个变量后进行分析。

struct sock {
    struct sock_common  __sk_common;       // 网络层套接字通用结构体
......
    socket_lock_t       sk_lock;           // 套接字同步锁
    atomic_t        sk_drops;              // IP/UDP包丢包统计
    int         sk_rcvlowat;               // SO_RCVLOWAT标记位
......
    struct sk_buff_head sk_receive_queue;   // 收到的数据包队列
......
    int         sk_rcvbuf;                // 接收缓存大小
......
    union {
        struct socket_wq __rcu  *sk_wq;     // 等待队列
        struct socket_wq    *sk_wq_raw;
    };
......
    int         sk_sndbuf;                 // 发送缓存大小
    /* ===== cache line for TX ===== */
    int         sk_wmem_queued;            // 传输队列大小
    refcount_t      sk_wmem_alloc;          // 已确认的传输字节数
    unsigned long       sk_tsq_flags;       // TCP Small Queue标记位
    union {
        struct sk_buff  *sk_send_head;      // 发送队列对首
        struct rb_root  tcp_rtx_queue;       
    };
    struct sk_buff_head sk_write_queue;      // 发送队列
......
    u32         sk_pacing_status; /* see enum sk_pacing 发包速率控制状态*/ 
    long            sk_sndtimeo;            // SO_SNDTIMEO 标记位
    struct timer_list   sk_timer;           // 套接字清空计时器
    __u32           sk_priority;            // SO_PRIORITY 标记位
......
    unsigned long       sk_pacing_rate; /* bytes per second 发包速率*/
    unsigned long       sk_max_pacing_rate;  // 最大发包速率
    struct page_frag    sk_frag;            // 缓存页帧
......
    struct proto        *sk_prot_creator;
    rwlock_t        sk_callback_lock;
    int         sk_err,                   // 上次错误
                sk_err_soft;              // “软”错误:不会导致失败的错误
    u32         sk_ack_backlog;            // ack队列长度
    u32         sk_max_ack_backlog;        // 最大ack队列长度
    kuid_t          sk_uid;               // user id
    struct pid      *sk_peer_pid;          // 套接字对应的peer的id
......
    long            sk_rcvtimeo;          // 接收超时
    ktime_t         sk_stamp;             // 时间戳
......
    struct socket       *sk_socket;        // Identd协议报告IO信号
    void            *sk_user_data;        // RPC层私有信息
......
    struct sock_cgroup_data sk_cgrp_data;   // cgroup数据
    struct mem_cgroup   *sk_memcg;         // 内存cgroup关联
    void            (*sk_state_change)(struct sock *sk);    // 状态变化回调函数
    void            (*sk_data_ready)(struct sock *sk);      // 数据处理回调函数
    void            (*sk_write_space)(struct sock *sk);     // 写空间可用回调函数
    void            (*sk_error_report)(struct sock *sk);    // 错误报告回调函数
    int         (*sk_backlog_rcv)(struct sock *sk, struct sk_buff *skb);    // 处理存储区回调函数
......
    void                    (*sk_destruct)(struct sock *sk);    // 析构回调函数
    struct sock_reuseport __rcu *sk_reuseport_cb;              // group容器重用回调函数
......
};

   sock_common是套接口在网络层的最小表示,即最基本的网络层套接字信息,具体内容分析见注释。

三. 套接字缓冲区结构体

  套接字结构体用于表征一个网络连接对应的本地接口的网络信息,而sk_buff则是该网络连接对应的数据包的存储。sk_buff的详细介绍宜参考《Linux网络技术内幕》,专门有一章来描述该结构体。对于我们学习源码来说,最重要的是了解其重点成员变量以及其整体结构。

  其源码大致可以分为四部分:

  • 布局:方便搜索以及组织结构,主要是一个双向链表用于管理全部的sk_buff。每个sk_buff对应一个数据包,多个sk_buff以双向链表的形式组合而成。

sk_buff链表结构图

除此之外还有指向sock的指针,缓冲区数据块大小,缓冲区及数据边界tail,end,head,data,truesize

sk_buff指针示意图
  • 通用字段:与特定内核无关的字段,主要包括时间戳tstamp,网络设备dev,源设备input_device,L2-L4层包头对应的mac_header, network_header, transport_header等。其头部组织结构如下所示

头部组织结构图
  • 功能专用:当编译防火墙(Netfilter) 以及QOS等时才会用到的特殊字段,在此暂时不做详细介绍

  • 管理函数:由内核提供的简单的管理工具函数,用于对sk_buff元素和元素列表进行操作,如数据预留及对齐函数skb_put(), skb_push(),skb_pull(),skb_reserve()

sk_buff操作函数示意图

  再比如分配回收函数alloc_skb()dev_alloc_skb()

sk_buff分配图

  释放内存函数kfree_skb()dev_kfree_skb()

释放逻辑图

  除此之外还有克隆,复制等函数,不做过多展开介绍。

  sk_buff的整体填充过程如下图所示:

填充流程图

  通过以上学习,对sk_buff应该有了较为全面系统的了解,其详细源码如下所示,对于重点部分已写明中文注释,其他参见英文注释。

四. 创建套接字

  众所周知我们通过socket()生成套接字,其系统调用如下,主要调用sock_create()创建结构体socket,并通过sock_map_fd()将其和文件描述符进行绑定。

  应用层调用socket()函数会传入三个参数:

  • family:表示使用什么 IP 层协议。AF_INET 表示 IPv4AF_INET6 表示 IPv6。这里需要注意的是,我们会常见到AF_INET, AF_PACKET,AF_UNIX等,AF_UNIX用于主机内进程间通信,AF_INETAF_PACKET的区别在于前者只能看到IP层以上,而后者可以看到链路层信息,即作用域不同。

  • type:表示 socket 类型。SOCK_STREAM 是面向数据流的,协议 IPPROTO_TCP 属于这种类型。SOCK_DGRAM 是面向数据报的,协议 IPPROTO_UDP 属于这种类型。如果在内核里面看的话,IPPROTO_ICMP 也属于这种类型。SOCK_RAW 是原始的 IP 包,IPPROTO_IP 属于这种类型。

  • protocol: 表示的协议,包括 IPPROTO_TCPIPPTOTO_UDP

  sock_create()实际调用__sock_create()。这里首先调用sock_alloc()分配套接字结构体sock并赋值类型为type,接着调用对应的create()函数按照protocolsock进行填充。

  sock_alloc()中我们看到了熟悉的东西:new_inode_pseudo(),即依照着虚拟文件系统的方式为套接字生成inode,接着通过SOCKET_I()获取其对应的socket,再进行填充。

  inet_create()主要逻辑如下

  • 通过循环list_for_each_entry_rcu查看 inetsw[sock->type],该数组会根据type找对应的协议号,如果找到了则得到了符合用户指定的 family->type->protocolstruct inet_protosw *answer 对象。

  • struct socket *sockops 成员变量被赋值为 answerops。对于 TCP 来讲,就是 inet_stream_ops。后面任何用户对于这个 socket 的操作都是通过 inet_stream_ops 进行的。

  • 调用sk_alloc()创建一个 网络层struct sock *sk 对象并赋值

  • 调用inet_sk()创建一个 struct inet_sock 结构并赋值。上文已说明INET作用域,而inet_sock即是对sockINET形式封装,在sock的基础上增加了很多新的特性。

  inetsw数组里面的内容是 struct inet_protosw,对于每个类型的协议均有一项,这一项里面是属于这个类型的协议。inetsw 数组是在系统初始化的时候初始化的,一个循环会将 inetsw 数组的每一项都初始化为一个链表。接下来一个循环将 inetsw_array 注册到 inetsw 数组里面去。

  至此,套接字的创建就算完成了。

总结

  本文重点分析了套接字这一网络编程中的重要结构体以及其创建函数背后的逻辑,为后文网络编程的源码解析打下基础。

源码资料

[1] socketarrow-up-right

[2] sk_buffarrow-up-right

[3] socket()arrow-up-right

[4] inet_create()arrow-up-right

参考资料

[1] wiki

[2] elixir.bootlin.com/linuxarrow-up-right

[3] woboqarrow-up-right

[4] Linux-insides

[5] 深入理解Linux内核

[6] Linux内核设计的艺术

[7] 极客时间 趣谈Linux操作系统

[8] 深入理解Linux网络技术内幕

最后更新于

这有帮助吗?