套接字socket 的地址族和类型、工作原理、创建过程

注:本分类下文章大多整理自《深入分析linux内核源代码》一书,另有参考其他一些资料如《linux内核完全剖析》、《linux c 编程一站式学习》等,只是为了更好地理清系统编程和网络编程中的一些概念性问题,并没有深入地阅读分析源码,我也是草草翻过这本书,请有兴趣的朋友自己参考相关资料。此书出版较早,分析的版本为2.4.16,故出现的一些概念可能跟最新版本内核不同。

一、套接字socket

(一)、套接字在网络中的地位和作用

socket 在网络系统中的作用如下。

(1)socket 位于网络协议之上,屏蔽了不同网络协议之间的差异。

(2)socket 是网络编程的入口,它提供了大量的系统调用,构成了网络程序的主体。

(3)在Linux 系统中,socket 属于文件系统的一部分,网络通信可以被看作是对文件的读取,使得我们对网络的控制和对文件的控制一样方便。

(二)、套接字接口的种类

Linux 支持多种套接字种类,不同的套接字种类称为“地址族”,这是因为每种套接字种类拥有自己的通信寻址方法。Linux 所支持的套接字地址族见表12.3。Linux 将上述套接字地址族抽象为统一的 BSD 套接字接口,应用程序关心的只是 BSD 套接字接口,而 BSD 套接字由各地址族专有的软件支持。一般而言,BSD 套接字可支持多种套接字类型,不同的套接字类型提供的服务不同,Linux 所支持的部分 BSD 套接字类型见表12.4,但表12.3 中的套接字地址族并不一定全部支持表12.4 中的这些套接字类型。

(三)、套接字的工作原理

INET 套接字就是支持 Internet 地址族的套接字,它位于TCP 之上,BSD 套接字之下,如图12.8 所示,这里也体现了Linux 网络模块分层的设计思想。

INET 和 BSD 套接字之间的接口通过 Internet 地址族套接字操作集实现,这些操作集实际是一组协议的操作例程,在include/linux/net.h 中定义为struct proto_ops:

struct proto_ops
{
    int family;
    int (*release) (struct socket *sock);
    int (*bind) (struct socket *sock, struct sockaddr *umyaddr,
                 int sockaddr_len);
    int (*connect) (struct socket *sock, struct sockaddr *uservaddr,
                    int sockaddr_len, int flags);
    int (*socketpair) (struct socket *sock1, struct socket *sock2);
    int (*accept) (struct socket *sock, struct socket *newsock,
                   int flags);
    int (*getname) (struct socket *sock, struct sockaddr *uaddr,
                    int *usockaddr_len, int peer);
    unsigned int (*poll) (struct file *file, struct socket *sock, struct poll_table_struct
                          *wait);
    int (*ioctl) (struct socket *sock, unsigned int cmd,
                  unsigned long arg);
    int (*listen) (struct socket *sock, int len);
    int (*shutdown) (struct socket *sock, int flags);
    int (*setsockopt) (struct socket *sock, int level, int optname,
                       char *optval, int optlen);
    int (*getsockopt) (struct socket *sock, int level, int optname,
                       char *optval, int *optlen);
    int (*sendmsg) (struct socket *sock, struct msghdr *m, int total_len, struct
                    scm_cookie *scm);
    int (*recvmsg) (struct socket *sock, struct msghdr *m, int total_len, int flags,
                    struct scm_cookie *scm);
    int (*mmap) (struct file *file, struct socket *sock, struct vm_area_struct *vma);
    ssize_t (*sendpage) (struct socket *sock, struct page *page, int offset, size_t size, int flags);
};

这个操作集类似于文件系统中的file_operations 结构。BSD 套接字层通过调用proto_ops 结构中的相应函数执行任务。BSD 套接字层向 INET 套接字层传递 socket 数据结构来代表一个 BSD 套接字,socket 结构在include/linux/net.h 中定义如下:

struct socket
{
    socket_state state;
    unsigned long flags;
    struct proto_ops *ops;
    struct inode *inode;
    struct fasync_struct *fasync_list; /* Asynchronous wake up list */
    struct file *file; /* File back pointer for gc */
    struct sock *sk;
    wait_queue_head_t wait;
    short type;
    unsigned char passcred;
};

但在 INET 套接字层中,它利用自己的 sock 数据结构来代表该套接字,因此,这两个结构之间存在着链接关系,sock 结构定义于include/net/sock.h。在 BSD 的 socket 数据结构中存在一个指向sock 的指针sk,而在sock 中又有一个指向socket 的指针,这两个指针将 BSD socket 数据结构和sock 数据结构链接了起来。通过这种链接关系,套接字调用就可以方便地检索到 sock 数据结构。实际上,sock 数据结构可适用于不同的协议,它也定义有自己的协议操作集proto_ops。在建立套接字时,sock数据结构的协议操作集指针指向所请求的协议操作集。如果请求 TCP,则 sock 数据结构的协议操作集指针将指向 TCP 的协议操作集。

BSD 套接字上的详细操作与具体的底层地址族有关,底层地址族的不同实际意味着寻址方式、采用的协议等的不同。Linux 利用 BSD 套接字层抽象了不同的套接字接口。在内核的初始化阶段,内建于内核的不同地址族分别以 BSD 套接字接口在内核中注册。然后,随着应用程序创建并使用 BSD 套接字。内核负责在 BSD 套接字和底层的地址族之间建立联系。这种联系通过交叉链接数据结构以及地址族专有的支持例程表建立。在内核中, 地址族和协议信息保存在inet_protos 向量中, 其定义于include/net/protocol.h:

struct inet_protocol *inet_protos[MAX_INET_PROTOS];
/* This is used to register protocols. */
struct inet_protocol
{
    int (*handler)(struct sk_buff *skb);//The Linux kernel uses an sk_buff data structure to describe each packet.
    void (*err_handler)(struct sk_buff *skb, u32 info);
    struct inet_protocol *next;
    unsigned char protocol;
    unsigned char copy: 1;
    void *data;
    const char *name;
};

每个地址族由其名称以及相应的初始化例程地址代表。在引导阶段初始化套接字接口时,内核调用每个地址族的初始化例程,这时,每个地址族注册自己的协议操作集。协议操作集实际是一个例程集合,其中每个例程执行一个特定的操作。

(四)、套接字的创建过程

Linux 在利用socket()系统调用建立新的套接字时,需要传递套接字的地址族标识符、套接字类型以及协议,其函数定义于net/socket.c 中:

asmlinkage long sys_socket(int family, int type, int protocol)
{
    int retval;
    struct socket *sock;
    retval = sock_create(family, type, protocol, &sock);
    if (retval < 0)
        goto out;
    retval = sock_map_fd(sock);
    if (retval < 0)
        goto out_release;
out:
    /* It may be already another descriptor 8) Not kernel problem. */
    return retval;
out_release:
    sock_release(sock);
    return retval;
}

实际上,套接字对于用户程序而言就是特殊的已打开的文件。内核中为套接字定义了一种特殊的文件类型,形成一种特殊的文件系统sockfs,其定义于net/socket.c:

static struct vfsmount *sock_mnt;
static DECLARE_FSTYPE(sock_fs_type, "sockfs", sockfs_read_super, FS_NOMOUNT);

在系统初始化时,要通过kern_mount()安装这个文件系统。安装时有个作为连接件的vfsmount 数据结构,这个结构的地址就保存在一个全局的指针sock_mnt 中。所谓创建一个套接字,就是在sockfs 文件系统中创建一个特殊文件,或者说一个节点,并建立起为实现套接字功能所需的一整套数据结构。所以,函数sock_create()首先是建立一个socket 数据结构,然后将其“映射”到一个已打开的文件中,进行socket 结构和sock 结构的分配和初始化。

新创建的 BSD socket 数据结构包含有指向地址族专有的套接字例程的指针,这一指针实际就是 proto_ops 数据结构的地址。

BSD 套接字的套接字类型设置为所请求的 SOCK_STREAM 或 SOCK_DGRAM 等。然后,内核利用 proto_ops 数据结构中的信息调用地址族专有的创建例程。之后,内核从当前进程的 fd 向量中分配空闲的文件描述符,该描述符指向的 file 数据结构被初始化。初始化过程包括将文件操作集指针指向由 BSD 套接字接口支持的 BSD 文件操作集。所有随后的套接字(文件)操作都将定向到该套接字接口,而套接字接口则会进一步调用地址族的操作例程,从而将操作传递到底层地址族,如图12.10 所示。

实际上,socket 结构与sock 结构是同一事物的两个方面。如果说socket 结构是面向进程和系统调用界面的,那么sock 结构就是面向底层驱动程序的。可是,为什么不把这两个数据结构合并成一个呢?我们说套接字是一种特殊的文件系统,因此,inode 结构内部的union 的一个成分就用作socket 结构,其定义如下:

struct inode {
        .......
        union
    {
        ........
        struct socket socket_i;
    }
}

由于套接字操作的特殊性,这个结构中需要大量的结构成分。可是,如果把这些结构成分全都放在socket 结构中,则inode 结构中的这个union 就会变得很大,从而inode 结构也会变得很大,而对于其他文件系统,这个union 成分并不需要那么庞大。因此,就把套接字所需的这些结构成分拆成两部分,把与文件系统关系比较密切的那一部分放在socket 结构中,把与通信关系比较密切的那一部分则单独组成一个数据结构,即sock 结构。由于这两部分数据在逻辑上本来就是一体的,所以要通过指针互相指向对方,形成一对一的关系。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Lambda

常用Lambda表达式实例

集合操作 从集合中过滤出某一个字段存入到新集合 // 从商品集合中过滤出商品类目id为一个新 List<Integer>集合 List<Integer> c...

1928
来自专栏瓜大三哥

UVM模型(四)

UVM模型(四) ? 1.常用到的uvm_component uvm_driver:所有的driver都要派生自uvm_driver。driver的功能...

2329
来自专栏Golang语言社区

Goroutine背后的系统知识

Go语言从诞生到普及已经三年了,先行者大都是Web开发的背景,也有了一些普及型的书籍,可系统开发背景的人在学习这些书籍的时候,总有语焉不详的感觉,网上也有若干流...

3256
来自专栏Golang语言社区

goroutine背后的系统知识

Go语言从诞生到普及已经三年了,先行者大都是Web开发的背景,也有了一些普及型的书籍,可系统开发背景的人在学习这些书籍的时候,总有语焉不详的感觉,网上也有若干流...

2445
来自专栏Python

Python Redis pipeline操作和Redis乐观锁保持数据一致性

Redis是建立在TCP协议基础上的CS架构,客户端client对redis server采取请求响应的方式交互。

750
来自专栏皮振伟的专栏

[algorithm][heap]基于小顶堆和hash map的虚拟机管理方法

前言: 集群中,存在大量的虚拟机。如何对大量的虚拟机进行高效的管理,也逐渐成为问题。 分析: 1,上报和拉取 逻辑层上,如果想获得虚拟机的信息,无非有两种方式...

2699
来自专栏Golang语言社区

goroutine背后的系统知识

Go语言从诞生到普及已经三年了,先行者大都是Web开发的背景,也有了一些普及型的书籍,可系统开发背景的人在学习这些书籍的时候,总有语焉不详的感觉,网上也有若干...

3258
来自专栏腾讯AlloyTeam的专栏

脚本错误量极致优化-监控上报与 Script error

在前端开发工作中,除了项目开发保质保量上线以外,项目的数据监控也应该配套起来,确保线上的正常运转。如上报 pv 监控项目是否正常运转;测速上报反应项目质量。

1470
来自专栏后端技术探索

PHP并发IO编程之路

并发IO问题一直是后端编程中的技术挑战,从最早的同步阻塞Fork进程,到多进程/多线程,到现在的异步IO、协程。PHP程序员因为有强大的LAMP框架,对底层方面...

611
来自专栏java思维导图

如何设计restful风格接口

URL定位资源,用HTTP动词(GET,POST,DELETE,DETC)描述操作。

892

扫码关注云+社区