Linux操作系统学习笔记(十九)网络通信之套接字

一. 前言

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

  经过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是套接口在网络层的最小表示,即最基本的网络层套接字信息,具体内容分析见注释。

struct sock_common {
    /* skc_daddr and skc_rcv_saddr must be grouped on a 8 bytes aligned
     * address on 64bit arches : cf INET_MATCH()
     */
    union {
        __addrpair	skc_addrpair;
        struct {
            __be32	skc_daddr;		// 外部/目的IPV4地址
            __be32	skc_rcv_saddr;	// 本地绑定IPV4地址
        };
    };
    union  {
        unsigned int	skc_hash;	// 根据协议查找表获取的哈希值
        __u16		skc_u16hashes[2]; // 2个16位哈希值,UDP专用
    };
    /* skc_dport && skc_num must be grouped as well */
    union {
        __portpair	skc_portpair;	// 
        struct {
            __be16	skc_dport;	    // inet_dport占位符
            __u16	skc_num;	    // inet_num占位符
        };
    };
    unsigned short		skc_family;	      // 网络地址family
    volatile unsigned char	skc_state;    // 连接状态
    unsigned char		skc_reuse:4;      // SO_REUSEADDR 标记位
    unsigned char		skc_reuseport:1;  // SO_REUSEPORT 标记位
    unsigned char		skc_ipv6only:1;   // IPV6标记位
    unsigned char		skc_net_refcnt:1; // 该套接字网络名字空间内引用数
    int			skc_bound_dev_if;		 // 绑定设备索引
    union {
        struct hlist_node	skc_bind_node;     // 不同协议查找表组成的绑定哈希表
        struct hlist_node	skc_portaddr_node; // UDP/UDP-Lite protocol二级哈希表
    };
    struct proto		*skc_prot;			  // 协议回调函数,根据协议不同而不同
......
    union {									
        struct hlist_node	skc_node;		    // 不同协议查找表组成的主哈希表
        struct hlist_nulls_node skc_nulls_node;  // UDP/UDP-Lite protocol主哈希表
    };
    unsigned short		skc_tx_queue_mapping;    // 该连接的传输队列
    unsigned short		skc_rx_queue_mapping;    // 该连接的接受队列
......
    union {
        int		skc_incoming_cpu; // 多核下处理该套接字数据包的CPU编号
        u32		skc_rcv_wnd;	  // 接收窗口大小
        u32		skc_tw_rcv_nxt; /* struct tcp_timewait_sock  */
    };
    refcount_t		skc_refcnt;   // 套接字引用计数
......
};

三. 套接字缓冲区结构体

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

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

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

img

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

img

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

img

  • 功能专用:当编译防火墙(Netfilter) 以及QOS等时才会用到的特殊字段,在此暂时不做详细介绍
  • 管理函数:由内核提供的简单的管理工具函数,用于对sk_buff元素和元素列表进行操作,如数据预留及对齐函数skb_put(), skb_push(),skb_pull(),skb_reserve()

img

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

img

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

img

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

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

img

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

struct sk_buff {
    union {
        struct {
            /* These two members must be first. 构成sk_buff链表*/
            struct sk_buff		*next;
            struct sk_buff		*prev;
            union {
                struct net_device	*dev;	//网络设备对应的结构体,很重要但是不是本文重点,所以不做展开
                /* Some protocols might use this space to store information,
                 * while device pointer would be NULL.
                 * UDP receive path is one user.
                 */
                unsigned long		dev_scratch;   // 对于某些不适用net_device的协议需要采用该字段存储信息,如UDP的接收路径
            };
        };
        struct rb_node		rbnode; /* used in netem, ip4 defrag, and tcp stack 将sk_buff以红黑树组织,在TCP中有用到*/
        struct list_head	list;   // sk_buff链表头指针
    };
    union {
        struct sock		*sk;       // 指向网络层套接字结构体
        int			ip_defrag_offset;
    };
    union {
        ktime_t		tstamp;    // 时间戳
        u64		skb_mstamp_ns; /* earliest departure time */
    };
    /* 存储私有信息
     * 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);
    union {
        struct {
            unsigned long	_skb_refdst;				   // 目标entry
            void		(*destructor)(struct sk_buff *skb);	// 析构函数
        };
        struct list_head	tcp_tsorted_anchor;			    // TCP发送队列(tp->tsorted_sent_queue)
    };
....
    unsigned int		len,	// 实际长度
                data_len;	    // 数据长度
    __u16			mac_len,    // mac层长度
                hdr_len;        // 可写头部长度
    /* Following fields are _not_ copied in __copy_skb_header()
     * Note that queue_mapping is here mostly to fill a hole.
     */
    __u16			queue_mapping;   // 多队列设备的队列映射
......
    /* fields enclosed in headers_start/headers_end are copied
     * using a single memcpy() in __copy_skb_header()
     */
    /* private: */
    __u32			headers_start[0];	
    /* public: */
......
    __u8			__pkt_type_offset[0];
    __u8			pkt_type:3;
    __u8			ignore_df:1;
    __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			__pkt_vlan_present_offset[0];
    __u8			vlan_present:1;
    __u8			csum_complete_sw:1;
    __u8			csum_level:2;
    __u8			csum_not_inet:1;
    __u8			dst_pending_confirm:1;
......
    __u8			ipvs_property:1;
    __u8			inner_protocol_type:1;
    __u8			remcsum_offload:1;
......
    union {
        __wsum		csum;
        struct {
            __u16	csum_start;
            __u16	csum_offset;
        };
    };
    __u32			priority;
    int			skb_iif;		// 接收到该数据包的网络接口的编号
    __u32			hash;
    __be16			vlan_proto;
    __u16			vlan_tci;
......
    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;			// mac层头部
    /* 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;
    refcount_t		users;
......
};

四. 创建套接字

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

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
    return __sys_socket(family, type, protocol);
}

int __sys_socket(int family, int type, int protocol)
{
    int retval;
    struct socket *sock;
    int flags;
......
    retval = sock_create(family, type, protocol, &sock);
......
    return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}

  应用层调用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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xevrWd9M-1600489872191)(E:\doc\homepage\themes\next\source\images\AF.png)]

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

int sock_create(int family, int type, int protocol, struct socket **res)
{
    return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
}

int __sock_create(struct net *net, int family, int type, int protocol,
             struct socket **res, int kern)
{
    int err;
    struct socket *sock;
    const struct net_proto_family *pf;
......
    /*
     *	Allocate the socket and allow the family to set things up. if
     *	the protocol is 0, the family is instructed to select an appropriate
     *	default.
     */
    sock = sock_alloc();
......
    sock->type = type;
......
    rcu_read_lock();
    pf = rcu_dereference(net_families[family]);
......
    err = pf->create(net, sock, protocol, kern);
......
    *res = sock;
    return 0;
......
}

//net/ipv4/af_inet.cstatic 
const struct net_proto_family inet_family_ops = { 
    .family = PF_INET, 
    .create = inet_create,//这个用于socket系统调用创建
    ......
}

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

struct socket *sock_alloc(void)
{
    struct inode *inode;
    struct socket *sock;
    inode = new_inode_pseudo(sock_mnt->mnt_sb);
    if (!inode)
        return NULL;
    sock = SOCKET_I(inode);
    inode->i_ino = get_next_ino();
    inode->i_mode = S_IFSOCK | S_IRWXUGO;
    inode->i_uid = current_fsuid();
    inode->i_gid = current_fsgid();
    inode->i_op = &sockfs_inode_ops;
    return sock;
}

struct inode *new_inode_pseudo(struct super_block *sb)
{
    struct inode *inode = alloc_inode(sb);
    if (inode) {
        spin_lock(&inode->i_lock);
        inode->i_state = 0;
        spin_unlock(&inode->i_lock);
        INIT_LIST_HEAD(&inode->i_sb_list);
    }
    return inode;
}

static inline struct socket *SOCKET_I(struct inode *inode)
{
    return &container_of(inode, struct socket_alloc, vfs_inode)->socket;
}

struct socket_alloc {
    struct socket socket;
    struct inode vfs_inode;
};

  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的基础上增加了很多新的特性。
static int inet_create(struct net *net, struct socket *sock, int protocol,
               int kern)
{
    struct sock *sk;
    struct inet_protosw *answer;
    struct inet_sock *inet;
    struct proto *answer_prot;
    unsigned char answer_flags;
    int try_loading_module = 0;
    int err;
......
    list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
        err = 0;
        /* Check the non-wild match. */
        if (protocol == answer->protocol) {
            if (protocol != IPPROTO_IP)
                break;
        } else {
            /* Check for the two wild cases. */
            if (IPPROTO_IP == protocol) {
                protocol = answer->protocol;
                break;
            }
            if (IPPROTO_IP == answer->protocol)
                break;
        }
        err = -EPROTONOSUPPORT;
    }
......
    sock->ops = answer->ops;
    answer_prot = answer->prot;
    answer_flags = answer->flags;
......
    sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
......
    inet = inet_sk(sk);
    inet->is_icsk = (INET_PROTOSW_ICSK & answer_flags) != 0;
    inet->nodefrag = 0;
    if (SOCK_RAW == sock->type) {
        inet->inet_num = protocol;
        if (IPPROTO_RAW == protocol)
            inet->hdrincl = 1;
    }
    if (net->ipv4.sysctl_ip_no_pmtu_disc)
        inet->pmtudisc = IP_PMTUDISC_DONT;
    else
        inet->pmtudisc = IP_PMTUDISC_WANT;
    inet->inet_id = 0;
    sock_init_data(sock, sk);
    sk->sk_destruct	   = inet_sock_destruct;
    sk->sk_protocol	   = protocol;
    sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv;
    inet->uc_ttl	= -1;
    inet->mc_loop	= 1;
    inet->mc_ttl	= 1;
    inet->mc_all	= 1;
    inet->mc_index	= 0;
    inet->mc_list	= NULL;
    inet->rcv_tos	= 0;
    sk_refcnt_debug_inc(sk);
    if (inet->inet_num) {
        /* It assumes that any protocol which allows
         * the user to assign a number at socket
         * creation time automatically
         * shares.
         */
        inet->inet_sport = htons(inet->inet_num);
        /* Add to protocol hash chains. */
        err = sk->sk_prot->hash(sk);
......
    }
    if (sk->sk_prot->init) {
        err = sk->sk_prot->init(sk);
......
}

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

static struct list_head inetsw[SOCK_MAX];

static int __init inet_init(void)
{
......
    /* Register the socket-side information for inet_create. */
    for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)
        INIT_LIST_HEAD(r);
    for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
        inet_register_protosw(q);
......
}

static struct inet_protosw inetsw_array[] =
{
    {
        .type =       SOCK_STREAM,
        .protocol =   IPPROTO_TCP,
        .prot =       &tcp_prot,
        .ops =        &inet_stream_ops,
        .flags =      INET_PROTOSW_PERMANENT | INET_PROTOSW_ICSK,
    },
    {
        .type =       SOCK_DGRAM,
        .protocol =   IPPROTO_UDP,
        .prot =       &udp_prot,
        .ops =        &inet_dgram_ops,
        .flags =      INET_PROTOSW_PERMANENT,
     },
     {
        .type =       SOCK_DGRAM,
        .protocol =   IPPROTO_ICMP,
        .prot =       &ping_prot,
        .ops =        &inet_sockraw_ops,
        .flags =      INET_PROTOSW_REUSE,
     },
     {
        .type =       SOCK_RAW,
        .protocol =   IPPROTO_IP,  /* wild card */
        .prot =       &raw_prot,
        .ops =        &inet_sockraw_ops,
        .flags =      INET_PROTOSW_REUSE,
     }
}

  至此,套接字的创建就算完成了。对应于虚拟文件系统,描述符fd保存于进程file变量的fdtable中,fd和该套接字的file对应,套接字socket sockfile通过指针可以互相访问,sock_mnt为虚拟文件系统sockfs的挂载点可以通过file访问。

总结

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

源码资料

[1] socket

[2] sk_buff

[3] socket()

[4] inet_create()

参考资料

[1] wiki

[2] elixir.bootlin.com/linux

[3] woboq

[4] Linux-insides

[5] 深入理解Linux内核

[6] Linux内核设计的艺术

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

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

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ch_ty

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值