Linux socket - 源码分析(一)

这篇文章主要分析socket原理和创建流程
参考kernel msm-4.4源码

进程和进程间通信

  进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。不同进程间,内存资源相互独立,无法直接获取和修改,因此不同进程间需要通过特殊的方式进行传递信息。
  进程间通信(IPC,Interprocess communication)是一组编程接口,协调不同的进程,使之能在一个操作系统里同时运行,这些接口给多个进程之间交换信息提供了可能。目前,比较常见的几种跨进程通信方式有:管道(PIPE)、有名管道(FIFO)、消息队列、信号量、共享内存、套接字(socket)等。

  • PIPE:一般指无名管道,只能用于有亲缘关系的父子进程或者兄弟进程间的通信,半双工,数据只能由一端流量另一端
  • FIFO:有名管道与无名管道不同,可以在无关的进程间通信,
  • 消息队列:消息队列由kernel维护的消息链表,一个消息队列由一个标识符确定
  • 信号量:信号量是一个计数器,用于控制多个进程对资源的访问,主要用于实现进程间互斥与同步,不能传递复杂消息
  • 共享内存:由一个进程创建,多个进程可以共享的内存段,需要结合信号量来同步对共享内存的访问
  • socket:网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。这是一种全双工的通信方式,socket的两端既可以读,又可以写。

虚拟文件系统(VFS)

  “Linux下,一切皆是文件“,socket当然也不例外,在系统启动之初,Linux内核也会为socket注册和挂载自己类型的文件系统,为后期socket的创建和使用打下基础。因此,在介绍socket前,有必要简单了解下Linux虚拟文件系统。
  
  虚拟文件系统(Virtual File System,VFS):为了支持挂载不同类型的文件系统,并且为用户进程提供统一的文件操作接口,Linux内核在用户进程和具体的文件系统之间引入了一个抽象层,这个抽象层就称为虚拟文件系统。VSF中四个代表核心概念的结构体:file,dentry,inode,super_block,接下来一一解析:

  • super_block: 对于已挂载的文件系统,VSF在内核中都会生成一个超级块(super_block结构),用于存储文件系统的控制信息,如文件系统类型,大小,所有inode对象等
  • inode:与文件一一对应,在文件系统中是文件的唯一标识,inode包含了文件信息的元数据(大小,权限,类型,时间)和指向文件数据的指针,但不包含文件名。
  • dentry:directory entry(目录项)的缩写,用于建立文件名和相关的inode之间的联系。在上一次扫描时,内核建立和缓存了目录的树状结构,称为dentry cache。通过dentry cache找到file对应的inode,如果没有找到,则需要从磁盘中读取。这样极大加快了查找inode的效率。
  • file:用户进程相关,代表一个打开的文件。每个file结构体都有一个指向dentry的指针,通过dentry可以查找对应的inode。file和inode是多对一的关系,对于同一个文件,系统会为每一次打开都创建一个file结构。

除了上述VSF的4个基本概念,我们也来了解下用户程序操作文件时经常用到的”文件描述符”相关概念:

  • 文件描述符:在Linux中,进程通过文件描述符(file descriptor, fd )来访问文件,fd其实是一个非负整数,是文件描述符表中的编号,通过fd可以在文件描述符表中查找到对应的file结构体。
  • 文件描述符表:每个进程都有自己的文件描述符表,用于记录已经打开过的文件。表中的每一项都有一个指向file结构体的指针,用于查找打开文件的file结构体。

根据上面的描述,基本可以描述用户程序操作一个文件的过程:用户进程通过文件描述符在文件描述表中查找指向对应的file结构体指针,file结构体中包含了dentry指针,内核通过dentry查找到file对应的inode,inode中包含了指向super_block或者保存在disk上的实际文件数据的指针,进而找到实际的文件数据。

注明:对于VFS,这里只是简单描述,具体的原理可以参考如下博文:
https://blog.csdn.net/jnu_simba/article/details/8806654

socket文件系统注册

  socket有自己的文件系统,在内核初始化并调用 do_initcalls 时将socket文件系统注册到内核中。注册过程(net/socket.c):core_initcall(sock_init)

/** core_initcall 宏定义(linux/init.h)*/
#define core_initcall(fn)       __define_initcall(fn, 1)

#define __define_initcall(fn, id) \
    static initcall_t __initcall_##fn##id __used \
    __attribute__((__section__(".initcall" #id ".init"))) = fn; \

  __define_initcall(fn, id) 指示编译器在编译时声明函数指针__initcall_fn_id并初始化为 fn,同时,将该函数指针变量放置到名为 “.initcall#id.init” 的section数据段中。因此,对于core_initcall(sock_init) ,编译器在编译时会声明__intcallsock_int1并初始化其指向函数指针sock_init,同时存放到.initcall1.init中。这样,内核初始化时通过遍历section拿到sock_init的函数指针地址,可以完成对socket文件系统的注册。
  sock_init:创建,分配socket和inode所需的slab缓存,用于后期使用socket和inode;向内核注册socket文件系统。

/** sock_init (net/socket.c)*/
static int __init sock_init(void)
{
    int err;
    //初始化network sysctl支持
    err = net_sysctl_init();
    if (err)
        goto out;
    //分配skbuff_head_cache和skbuff_fclone_cache slab高速缓存
    skb_init();
    //分配sock_inode_cache slab高速缓存
    init_inodecache();
    //注册socket文件系统
    err = register_filesystem(&sock_fs_type);
    if (err)
        goto out_fs;    
    //挂载socket文件系统
    sock_mnt = kern_mount(&sock_fs_type);
    if (IS_ERR(sock_mnt)) {
        err = PTR_ERR(sock_mnt);
        goto out_mount;
    }
#ifdef CONFIG_NETFILTER
    //初始化netfilter
    err = netfilter_init();
    if (err)
        goto out;
    // ...
#endif

  skbuff_head_cache / skbuff_fclone_cache:与sk_buff相关的两个后备高速缓存(looaside cache),协议栈中使用的所有sk_buff结构都是从这两个高速缓存中分配出来的。两者的不同在于前者指定的单位内存区域大小为 sizeof(struct sk_buff);后者为 sizeof(struct sk_buff_fclones),即一对sk_buff和一个引用计数,这一对sk_buff是克隆的,引用计数值为0,1,2,表示这一对sk_buff中有几个已被使用。
  sock_inode_cache:用于分配和释放 socket_alloc 的高速缓存,socket创建和释放inode都会在该缓存区操作。socket_alloc 结构体包含 socket 和 inode,将两者紧密联系到一起。

/** sock_fs_type (net/socket.c)*/
static struct file_system_type sock_fs_type = {
    .name =     "sockfs",          // 文件系统名称
    .mount =    sockfs_mount,      // 挂载sockfs函数,其中会创建super block
    .kill_sb =  kill_anon_super,   // 销毁super block函数
};

  sock_fs_type:包含了文件系统系统的名称sockfs,创建和销毁super block的函数指针。

/** sockfs_mount (net/socket.c)*/
static struct dentry *sockfs_mount(struct file_system_type *fs_type,
             int flags, const char *dev_name, void *data)
{
    return mount_pseudo(fs_type, "socket:", &sockfs_ops,
        &sockfs_dentry_operations, SOCKFS_MAGIC);
}

  sockfs_mount:在文件系统挂载(kern_mount)时执行,创建一个super block(包含一个对应的dentry和inode),其根目录为”socket:”,inode操作函数为 sockfs_ops

/** sockfs_ops (net/socket.c)*/
static const struct super_operations sockfs_ops = {
    .alloc_inode    = sock_alloc_inode,      // 分配inode
    .destroy_inode  = sock_destroy_inode,    // 释放inode
    .statfs     = simple_statfs,             // 用于获取sockfs文件系统的状态信息
};

  sockfs_ops:定义inode操作函数,sock_alloc_inode 用于分配inode,在socket创建时使用;sock_destroy_inode 用于释放inode,在socket销毁时被调用。

socket创建过程
  
  在用户进程中,socket(int domain, int type, int protocol) 函数用于创建socket并返回一个与socket关联的fd,该函数实际执行的是系统调用 sys_socketcallsys_socketcall几乎是用户进程socket所有操作函数的入口:

/** sys_socketcall (linux/syscalls.h)*/
asmlinkage long sys_socketcall(int call, unsigned long __user *args);

  sys_socketcall 实际调用的是 SYSCALL_DEFINE2

/** SYSCALL_DEFINE2 (net/socket.c)*/
SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
    unsigned long a[AUDITSC_ARGS];
    unsigned long a0, a1;
    int err;
    unsigned int len;
    // 省略...
    a0 = a[0];
    a1 = a[1];

    switch (call) {
    case SYS_SOCKET:
        // 与 socket(int domain, int type, int protocol) 对应,创建socket
        err = sys_socket(a0, a1, a[2]);  
        break;
    case SYS_BIND:
        err = sys_bind(a0, (struct sockaddr __user *)a1, a[2]); 
        break;
    case SYS_CONNECT:
        err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
        break;
// 省略...
}

  在 SYSCALL_DEFINE2 函数中,通过判断call指令,来统一处理 socket 相关函数的事务,对于socket(…)函数,实际处理是在 sys_socket 中,也是一个系统调用,对应的是 SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)

/** SYSCALL_DEFINE3 net/socket.c*/
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
    int retval;
    struct socket *sock;
    int flags;
    // SOCK_TYPE_MASK: 0xF; SOCK_STREAM等socket类型位于type字段的低4位
    // 将flag设置为除socket基本类型之外的值
    flags = type & ~SOCK_TYPE_MASK;

    // 如果flags中有除SOCK_CLOEXEC或者SOCK_NONBLOCK之外的其他参数,则返回EINVAL
    if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
        return -EINVAL;

    // 取type中的后4位,即sock_type,socket基本类型定义
    type &= SOCK_TYPE_MASK;

    // 如果设置了SOCK_NONBLOCK,则不论SOCK_NONBLOCK定义是否与O_NONBLOCK相同,
    // 均将flags中的SOCK_NONBLOCK复位,将O_NONBLOCK置位
    if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
        flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

    // 创建socket结构,(重点分析)
    retval = sock_create(family, type, protocol, &sock);
    if (retval < 0)
        goto out;

    if (retval == 0)
        sockev_notify(SOCKEV_SOCKET, sock);

    // 将socket结构映射为文件描述符retval并返回,(重点分析)
    retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
    if (retval < 0)
        goto out_release;
out:
    return retval;
out_release:
    sock_release(sock);
    return retval;
}

  SYSCALL_DEFINE3 中主要判断了设置的socket类型type,如果设置了除基本sock_type,SOCK_CLOEXEC和SOCK_NONBLOCK之外的其他参数,则直接返回;同时调用 sock_create 创建 socket 结构,使用 sock_map_fd 将socket 结构映射为文件描述符并返回。在分析 sock_create 之前,先看看socket结构体:

/** socket结构体 (linux/net.h)*/
struct socket {
    socket_state        state;       // 连接状态:SS_CONNECTING, SS_CONNECTED 等
    short           type;            // 类型:SOCK_STREAM, SOCK_DGRAM 等
    unsigned long       flags;       // 标志位:SOCK_ASYNC_NOSPACE(发送队列是否已满)等
    struct socket_wq __rcu  *wq;     // 等待队列
    struct file     *file;           // 该socket结构体对应VFS中的file指针
    struct sock     *sk;             // socket网络层表示,真正处理网络协议的地方
    const struct proto_ops  *ops;    // socket操作函数集:bind, connect, accept 等
};

  socket结构体中定义了socket的基本状态,类型,标志,等待队列,文件指针,操作函数集等,利用 sock 结构,将 socket 操作与真正处理网络协议相关的事务分离。
  
  回到 sock_create 继续看socket创建过程,sock_create 实际调用的是 __sock_create

/** __sock_create (net/socket.c)*/
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;

    // 检查是否是支持的地址族,即检查协议
    if (family < 0 || family >= NPROTO)
        return -EAFNOSUPPORT;
    // 检查是否是支持的socket类型
    if (type < 0 || type >= SOCK_MAX)
        return -EINVAL;

    // 省略...

    // 检查权限,并考虑协议集、类型、协议,以及 socket 是在内核中创建还是在用户空间中创建
    // 可以参考:https://www.ibm.com/developerworks/cn/linux/l-selinux/
    err = security_socket_create(family, type, protocol, kern);
    if (err)
        return err;

    // 分配socket结构,这其中创建了socket和关联的inode (重点分析)
    sock = sock_alloc();
    if (!sock) {
        net_warn_ratelimited("socket: no more sockets\n");
        return -ENFILE; /* Not exactly a match, but its the
                   closest posix thing */
    }
    sock->type = type;
    // 省略...
}

  __socket_create 检查了地址族协议和socket类型,同时,调用 security_socket_create 检查创建socket的权限(如:创建不同类型不同地址族socket的SELinux权限也会不同)。接着,来看看 sock_alloc

/** sock_alloc (net/socket.c)*/
static struct socket *sock_alloc(void)
{
    struct inode *inode;
    struct socket *sock;

    // 在已挂载的sockfs文件系统的super_block上分配一个inode
    inode = new_inode_pseudo(sock_mnt->mnt_sb);
    if (!inode)
        return NULL;

    // 获取inode对应socket_alloc中的socket结构指针
    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的操作函数指针指向 sockfs_inode_ops 函数地址
    inode->i_op = &sockfs_inode_ops;

    this_cpu_add(sockets_in_use, 1);
    return sock;
}

  new_inode_pseudo 函数实际调用的是 alloc_inode(struct super_block *sb) 函数:

/** alloc_inode (fs/inode.c)*/
static struct inode *alloc_inode(struct super_block *sb)
{
    struct inode *inode;

    // 如果文件系统的超级块已经指定了alloc_inode的函数,则调用已经定义的函数去分配inode
    // 对于sockfs,已经将alloc_inode指向sock_alloc_inode函数指针
    if (sb->s_op->alloc_inode)
        inode = sb->s_op->alloc_inode(sb);
    else
        // 否则在公用的 inode_cache slab缓存上分配inode
        inode = kmem_cache_alloc(inode_cachep, GFP_KERNEL);

    if (!inode)
        return NULL;

    // 编译优化,提高执行效率,inode_init_always正常返回0
    if (unlikely(inode_init_always(sb, inode))) {
        if (inode->i_sb->s_op->destroy_inode)
            inode->i_sb->s_op->destroy_inode(inode);
        else
            kmem_cache_free(inode_cachep, inode);
        return NULL;
    }

    return inode;
}

  从前文 “socket文件系统注册” 提到的:”.alloc_inode = sock_alloc_inode” 可知,alloc_inode 实际将使用 sock_alloc_inode 函数去分配 inode:

/** sock_alloc_inode (net/socket.c)*/
static struct inode *sock_alloc_inode(struct super_block *sb)
{
    // socket_alloc 结构体包含一个socket和一个inode,将两者联系到一起
    struct socket_alloc *ei;
    struct socket_wq *wq;

    // 在sock_inode_cachep缓存上分配一个socket_alloc
    // sock_inode_cachep: 前文"socket文件系统注册"中已经提到,专用于分配socket_alloc结构
    ei = kmem_cache_alloc(sock_inode_cachep, GFP_KERNEL);
    if (!ei)
        return NULL;
    // 分配socket等待队列结构
    wq = kmalloc(sizeof(*wq), GFP_KERNEL);
    if (!wq) {
        kmem_cache_free(sock_inode_cachep, ei);
        return NULL;
    }
    // 初始化等待队列
    init_waitqueue_head(&wq->wait);
    wq->fasync_list = NULL;
    wq->flags = 0;
    // 将socket_alloc中socket的等待队列指向wq
    RCU_INIT_POINTER(ei->socket.wq, wq);

    // 初始化socket的状态,标志,操作集等
    ei->socket.state = SS_UNCONNECTED;
    ei->socket.flags = 0;
    ei->socket.ops = NULL;
    ei->socket.sk = NULL;
    ei->socket.file = NULL;

    // 返回socket_alloc中的inode
    return &ei->vfs_inode;
}

  sock_alloc_inode:在sockfs文件系统的 sock_inode_cachep SLAB缓存区分配一个 socket_alloc 结构,socket_alloc 中包含socket和inode,将两者联系一起;分配和初始化socket_wq 等待队列,初始化socket状态/标志/操作集;返回socket_alloc中的inode。

  socket_alloc:包含一个 socket 和一个 inode,该结构将这两者联系在一起。前文介绍VFS时,我们知道通过文件描述可以找到内核中与文件对应的一个inode,而对于socket而言,通过文件描述符找到inode之后,也就能通过socket_alloc结构找到对应的socket了。

/** socket_alloc 结构体 (net/sock.h)*/
struct socket_alloc {
    struct socket socket;
    struct inode vfs_inode;
};

  回到 sock_alloc 函数中,通过调用 new_inode_pseudo 最终是在 super_block 上创建了一个 socket_alloc 结构,同时返回了该结构中的 inode。继续分析 sock_alloc 函数:

/** sock_alloc函数 (net/socket.c)*/
static struct socket *sock_alloc(void)
{
    // 省略...
    // 获取inode对应socket_alloc中的socket结构指针
    sock = SOCKET_I(inode);
    // 省略..
    // 将inode的操作函数指针指向 sockfs_inode_ops 函数地址
    inode->i_op = &sockfs_inode_ops;
    return sock;
}

  SOCKET_I:内联函数,返回的是inode所在的socket_alloc中的socket结构体指针,慢慢分析:

/** SOCKET_I (net/sock.h)*/
static inline struct socket *SOCKET_I(struct inode *inode)
{
    // 使用container_of拿到inode对应的socket_alloc指针的首地址
    // 通过socket_alloc指针拿到inode对应的socket结构体指针
    return &container_of(inode, struct socket_alloc, vfs_inode)->socket;
}

  container_of:用于从包含在某个结构体中的指针获取结构体本身的指针。在这里就是指从inode指针获取包含inode的socket_alloc结构体的指针:

/** container_of (linux/kernel.h)*/
#define container_of(ptr, type, member) ({          \
    // 定义一个与ptr相同的临时指针变量__mptr
    const typeof(((type *)0)->member) * __mptr = (ptr); \ 
    // 将__mptr的指针地址减去member在type中的偏移
    // 得到的就是type的首地址
    (type *)((char *)__mptr - offsetof(type, member)); })

  总结一下sock_alloc函数:
  sock_alloc:在 sockfs 文件系统的 sock_inode_cachep 超级块上分配一个 socket_alloc 结构,初始化该结构的 inode 和 socket,同时,返回该 socket_alloc 结构中的socket。
  
  至此,SYSCALL_DEFINE3 中对 sock_create 函数的调用分析完毕,通过该函数,分配和初始化了 socket 和与VFS相关的inode,并通过 socket_alloc 结构体将两者关联。接下来,继续分析 SYSCALL_DEFINE3

/** SYSCALL_DEFINE3 net/socket.c*/

// 将socket结构映射为文件描述符retval并返回,(重点分析)
retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
/** sock_map_fd (net/socket.c)*/
static int sock_map_fd(struct socket *sock, int flags)
{
    struct file *newfile;
    // 从本进程的文件描述符表中获取一个可用的文件描述符
    int fd = get_unused_fd_flags(flags);
    if (unlikely(fd < 0))
        return fd;

    // 创建一个新的file,并将file和inode以及socket关联
    // file的private_data指针指向该socket
    newfile = sock_alloc_file(sock, flags, NULL);
    if (likely(!IS_ERR(newfile))) {
        // 将file指针存放到该进程已打开的文件列表中,其索引为fd
        fd_install(fd, newfile);
        return fd;
    }
    // 省略...
}

  sock_map_fd:从该进程的文件描述符表中分配一个空闲的文件描述符;创建一个新的文件,并将该文件与socket 互相绑定在一起;将创建的新文件指针存放到该进程的已打开文件列表中,其索引为刚分配的fd。

/** sock_alloc_file (net/socket.c)*/
struct file *sock_alloc_file(struct socket *sock, int flags, const char *dname)
{
    struct qstr name = { .name = "" };
    struct path path;
    struct file *file;

    // 省略..

    // 初始化文件路径path,其目录项的父目录项为超级块对应的根目录
    path.dentry = d_alloc_pseudo(sock_mnt->mnt_sb, &name);
    if (unlikely(!path.dentry))
        return ERR_PTR(-ENOMEM);
    // 设置path的装载点为sock_mnt
    path.mnt = mntget(sock_mnt);

    // 将socket对应的inode设置到path目录项dentry的d_inode中
    // SOCK_INODE 与 SOCKET_I 原理一致,这里用于获取sock在socket_alloc结构中对应的inode
    d_instantiate(path.dentry, SOCK_INODE(sock));

    // 分配file结构并初始化,file的f_path为path,file的f_inode为path->dentry的d_inode
    // 设置file的操作集为socket_file_ops
    file = alloc_file(&path, FMODE_READ | FMODE_WRITE,
          &socket_file_ops);
    // 省略..

    // 关联socket和新创建的file
    sock->file = file;
    file->f_flags = O_RDWR | (flags & O_NONBLOCK);
    file->private_data = sock;
    return file;
}

  sock_alloc_file:新创建一个文件;初始化该文件对应的 inode 为 socket 在 socket_alloc 结构中的inode;初始化该文件的操作集为 socket_file_ops;将 socket 的 file 指针指向新创建的文件指针;将 socket 保存到新创建 file 的 private_data中。该函数完成了 socket 和文件的绑定。

/** __fd_install (fs/file.c)*/
void __fd_install(struct files_struct *files, unsigned int fd,
        struct file *file)
{
    struct fdtable *fdt;

    // 省略...

    // 从file_struct中通过RCU取出其中的fdt
    // fdt: 文件描述符表
    fdt = rcu_dereference_sched(files->fdt)
    BUG_ON(fdt->fd[fd] != NULL);
    // 将fd数组下标为fd的元素的指针指向file
    rcu_assign_pointer(fdt->fd[fd], file);
}

files_struct:该进程所有已打开文件表结构。进程描述符数组中存放了一个进程所访问的所有文件,把这个文件描述符数组和这个数组在系统中的一些动态信息组合到一起,就形成了一个新的数据结构——进程打开文件表。
__fd_install:将新创建的file指针存放在该进程已打开的文件列表中,索引为fd,这样进程就可以通过fd找到对应的file指针,inode和socket。

文章结语

  本文介绍了socket文件系统和socket的创建过程,同时结合VFS的概念,理解socket和文件系统的关联性,以及用户进程如何通过file descriptor找到对应的socket。由于到此篇幅已经较长,这里没有对socket其他操作函数作继续分析,有时间会作进一步的详解。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页