Linux 中的 socketfs

2014-02-10 linux network

BSD socket 是用户程序与网络协议栈之间的接口层,用户通过调用 socket API 将报文传给协议栈,以及从协议栈读取报文。实际上,Linux 对于网络提供了一个与虚拟文件系统相似的接口,也就是可以通过 socket 接口打开一个类似的文件,而内核中实际是通过 sockfs 文件系统实现的。

接下来我们就在这篇文章中查看下与 socketfs 相关的内容。

Socket层,系统调用

系统调用 socket()、bind()、connect()、accept()、send()、release() 等都是在 net/socket.c 实现的,下面介绍通过 socket 层和用户的衔接。

对于这么多的协议,都是通过 socket() 向用户提供统一的接口,下面是一个典型的 TCP 协议通讯。

# 服务端
listenfd = socket(AF_INET, SOCK_STREAM, 0);                 # 新建socket
bind(sock_descriptor, servaddr, size);                      # 绑定端口
listen(listenfd, 5);                                        # 开始监听端口
accept(listenfd, cliaddr, clilen);                          # 接收新请求

# 客户端
sock_descriptor = socket(AF_INET, SOCK_STREAM, 0);          # 新建socket
connect(sock_descriptor, sockaddr, size);                   # 与服务端建立链接
send(sock_descriptor, "hello world");                       # 发送数据
recv(sock_descriptor, buffer, 1024, 0);                     # 接收数据

socket() 的声明和实现如下,三个参数分别为协议族、协议类型 (面向连接或无连接) 以及协议。

int socket(int domain, int type, int protocol);

socketfs 初始化

对于用户态而言, 返回的 socket 就是一个特殊的已经打开的文件,为了对 socket 抽像出文件的概念,内核中为 socket 定义了一个专门的文件系统类型 sockfs 。

static struct vfsmount *sock_mnt __read_mostly;
static struct file_system_type sock_fs_type = {
    .name     = "sockfs",
    .get_sb   = sockfs_get_sb,
    .kill_sb  = kill_anon_super,
};

socket 系统的初始化通过 sock_init() 函数完成,通过如下程序实现。

core_initcall(sock_init);

通过 core_initcall() 宏实现,其中定义的函数会在系统初始化时调用,通过 sock_init() 模块初始化的时候,会安装该文件系统。

sock_init()
  |-net_sysctl_init()
  |-skb_init()
  |-init_inodecache()
  |-register_filesystem()             # 将文件系统添加到一个列表中
  |-kern_mount()                      # 挂载文件系统
    |-kern_mount_data()
      |-vfs_kern_mount()
        |-alloc_vfsmnt()
        |-mount_fs()                  # 返回root dentry

在 vfs_kern_mount() 中,会申请文件系统 mnt 结构,调用之前注册的 sock_fs_type 的 get_sb(),获取相应的超级块,并将 mnt->mnt_sb 指向 sock_fs_type 中的超级块。

这里就是先获取/分配一个超级块,然后初始化超级块的各成员,包括 s_op,它封装了对应的功能函数表。s_op 自然就指向了 sockfs_ops,那前面提到的 new_inode() 函数分配 inode 时调用的,这个函数实际对应 sock_alloc_inode() 函数。

sock_mnt->mnt_sb->s_op->alloc_inode(sock_mnt->mnt_sb);

可以看到 sock_alloc_inode() 是如何分配一个 inode 节点的,函数先分配了一个用于封装 socket 和 inode 的 ei,然后在高速缓存中为之申请了一块空间,这样 inode 和 socket 就同时都被分配了。

至目前为止,分配 inode、socket 以及两者如何关联,都已一一分析了。最后一个关键问题,就是如何把 socket 与一个已打开的文件,建立映射关系。

sys_socketcall() 包含了所有 socket API 的入口。

socket()

有了文件系统后,对内核而言,创建一个 socket,就是在 sockfs 文件系统中创建一个文件节点(inode),并建立起为了实现 socket 功能所需的一整套数据结构,包括 struct inode 和 struct socket。而 struct socket 结构在内核中就代表了一个 socket,然后再将其与一个已打开的文件 “建立映射关系”,这样,用户态就可以用抽像的文件的概念来操作 socket 了。

而用户看到的是一个类似于文件描述符的 int 类型。在内核中 struct task_struct current 用来表示当前进程,用 struct file 描述一个已经打开的文件,当然一个进程可以打开多个文件,所以通过 struct file *fd_array[] 表示,文件描述符即对应该数组的下标。

通过文件描述符即可以找到对应内核中的 struct file 结构。

因此,对于网络编程需要做的是将 socket 与一个已经打开的文件建立映射,也就是为 socket 分配一个 struct file 以及相应的文件描述符 fd 。

如前所述,一个 socket 总是与一个 inode 密切相关的,为此内核引入了一个 socket_alloc 结构,当已知一个 inode 可以通过宏 SOCKET_I() 获取对应的 socket 。

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

这也同时意味着在正常分配一个 inode 后,必须再分配一个 socket_alloc 结构,并实现对应的封装。

接着申请分配一个相应的文件描述符 fd,因为 socket 并不支持 open() 方法,所以不能期望用户通过调用 open() API 分配一个 struct file,而是 sock_alloc_file() 获取,并通过让 current 的 files 指针的 fd 数组的 fd 索引项指向该 file 。

sys_socket(family, type, protocol)
  |-sock_create()                         # 分配inode和socket_alloc
  | |-__sock_create()
  |   |-security_socket_create()          # 调用安全接口,一般是selinux,可忽略
  |   |-socket *sock=sock_alloc()         # 主要的分配函数,分配一个struct socket
  |   | |-new_inode_pseudo()              # 通过sock_mnt->mnt_sb生成一个inode
  |   | | |-alloc_inode()                 # 新建inode,并添加到sb.s_inodes链表中
  |   | |-socket *sock=SOCKET_I(inode)    # 根据上述的inode,获取sock
  |   |
  |   |-sock->type=type
  |   |-pf=net_families[family]
  |   |-pf->create(..., protocol, ...)    # 其中TCP对应了inet_create()函数
  |   | |-inetsw[sock->type]              # 遍厉该数组的对象
  |   | |-sock->ops=answer->ops           ### struct proto_ops类型,后续收发操作的主要函数
  |   | |-sock *sk=sk_alloc()             # 分配sock结构体
  |   |
  |   |-security_socket_post_create()
  |
  |-sock_map_fd()
    |-get_unused_fd_flags()               # 获取未使用的fd
    |-sock_alloc_file()                   # 分配一个struct file
    |-fd_install()

在 alloc_inode() 函数中,会调用 sock_mnt->mnt_sb->s_op->alloc_inode(sock_mnt->mnt_sb) 返回一个 inode 结构体,而该 alloc_inode() 函数实际对应的是 sockfs_ops 中的成员变量。

在 Linux 中,通过 net_families[] 数组标示不同的 family 类型,对于 TCP/IP 采用的是 AF_INET 。

#define PF_UNIX     AF_UNIX
#define PF_INET     AF_INET

static const struct net_proto_family __rcu *net_families[NPROTO] __read_mostly;

static const struct net_proto_family inet_family_ops = {
    .family = PF_INET,
    .create = inet_create,
    .owner  = THIS_MODULE,
};

其中,仍以 TCP 为例,其中入参采用 SOCK_STREAM,其中的 inetsw_array[] 会在 inet_init() 函数中通过 inet_register_protosw() 函数注册到 static struct list_head inetsw[SOCK_MAX] 中。

const struct proto_ops inet_stream_ops = {
    .family        = PF_INET,
    .owner         = THIS_MODULE,
    .sendmsg       = inet_sendmsg,
    .recvmsg       = inet_recvmsg,
    ... ...
};

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,
    },
    ... ...
};

static int __init inet_init(void)
{
    ... ...
    for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
        inet_register_protosw(q);
    ... ...
}

那么,在 sys_socket() 函数中就会根据 family、type 的参数,通过 sock->ops=answer->ops 进行赋值,后面包括报文的收发操作等,都是根据该结构体进行操作。

小结

上面已经介绍了在协议栈中是如何选择相关操作函数的,在此仍然一步步查看下具体的排查过程。

首先,socket() 系统调用会传入三个参数,接口如下。

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
    ... ...
    retval = sock_create(family, type, protocol, &sock);
    ... ...
}

实际最终会调用如下的函数。

int __sock_create(..., int family, int type, int protocol, ...) {
    ... ...
    struct socket *sock = sock_alloc();
    sokc->type = type;

    const struct net_proto_family *pf;
    pf = rcu_dereference(net_families[family]);   //## 关系到如下函数的调用

    err = pf->create(net, sock, protocol, kern);
    ... ...
}

其中 net_families[] 数组通过 sock_register() 函数进行注册,通过如下命令看到所有注册的协议,在此以 AF_INET 或者 PF_INET 为例,注册的是 inet_family_ops 。

$ cd net && grep -rne '\<sock_register('
... ...
ipv4/af_inet.c:1704:    (void)sock_register(&inet_family_ops);
... ...

而对应的结构体定义为:

static const struct net_proto_family inet_family_ops = {
    .family = PF_INET,
    .create = inet_create,
    .owner  = THIS_MODULE,
};

也就是接下来实际调用的函数是:

static int inet_create(struct net *net, struct socket *sock, int protocol, int kern)
{
    ... ...
    err = -ESOCKTNOSUPPORT;
    rcu_read_lock();
    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;
        }
    }
    ... ...
    sock->ops = answer->ops;
    ... ...
}

而其中 inetsw[] 数组实际是通过 inetsw_array[] 注册的结果。

send()

在 Linux 中,应用层可以使用以下 socket 函数来发送数据:

ssize_t write(int fd, const void *buf, size_t count);
ssize_t send(int s, const void *buf, size_t len, int flags);
ssize_t sendto(int s, const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);
ssize_t sendmsg(int s, const struct msghdr *msg, int flags);
int sendmmsg(int s, struct mmsghdr *msgvec,  unsigned int vlen, unsigned int flags);

当 flags 为 0 时,send() 和 write() 功能相同;而 sendto(…, NULL, 0) 功能与 send() 的功能相同;write() 和 send() 在套接字处于连接状态时可以使用,而 sendto()、sendmsg() 和 sendmmsg() 在任何时候都可用。

对于 send() 而言,其入口函数就是 sys_send() 。

sys_send()
  |-sys_sendto()
    |-sockfd_lookup_light()         # 通过文件描述符fd,找到对应的socket实例
    |-... ...                       # 初始化消息头信息
    |-move_addr_to_kernel()         # 把套接字地址从用户空间拷贝到内核空间
    |-sock_sendmsg()                # 调用统一的发送入口函数
    | |-__sock_sendmsg()
    |   |-__sock_sendmsg_nosec()
    |     |-sock->ops->sendmsg()
    |
    |-fput_light()

在 sockfd_lookup_light() 函数中,以 fd 为索引从当前进程的文件描述符表 files_struct 实例中找到对应的 file 实例,然后从 file 实例的 private_data 成员中获取 socket 实例。

其中 sock->ops->sendmsg() 操作相关的结构体如下,其中 sock->ops 在 sys_socket() 中初始化。

struct socket {
    ... ...
    const struct proto_ops  *ops;
};

struct proto_ops {
    int     family;
    struct module   *owner;
    int     (*release)   (struct socket *sock);
    int     (*bind)      (struct socket *sock, struct sockaddr *myaddr, int sockaddr_len);
    int     (*connect)   (struct socket *sock, struct sockaddr *vaddr, int sockaddr_len, int flags);
    int     (*socketpair)(struct socket *sock1, struct socket *sock2);
    int     (*accept)    (struct socket *sock, struct socket *newsock, int flags);
    int     (*listen)    (struct socket *sock, int len);
    int     (*shutdown)  (struct socket *sock, int flags);
    int     (*sendmsg)   (struct kiocb *iocb, struct socket *sock,
                      struct msghdr *m, size_t total_len);
    int     (*recvmsg)   (struct kiocb *iocb, struct socket *sock,
                      struct msghdr *m, size_t total_len, int flags);
};

其中 socket(AF_INET, SOCK_STREAM, 0) 会将 sock->ops 设置为 inet_stream_ops 变量,那么后续的所有操作都会使用该结构体定义的函数进行操作。