从内核看文件描述符传递的实现(基于5.9.9)

2021-07-08 16:05:15 浏览数 (1)

前言:文件描述符是内核提供的一个非常有用的技术,典型的在服务器中,主进程负责接收请求,然后把请求传递给子进程处理。本文分析在内核中,文件描述符传递是如何实现的。

我们先看一下文件描述符传递到底是什么概念。假设主进程打开了一个文件,我们看看进程和文件系统的关系。

如果这时候主进程fork出一个子进程,那么架构如下。

我们看到主进程和子进程都指向同一个文件。那么如果这时候住进程又打开了一个文件。架构如下。

我们看到新打开的文件,子进程是不会再指向了的。假设文件底层的资源是TCP连接,而主进程想把这个关系同步到子进程中,即交给子进程处理,那怎么办呢?这时候就需要用到文件描述符传递。下面是我们期待的架构。

通常主进程会close掉对应的文件描述符,即解除引用关系。不过这里我们可以不关注这个。文件描述符这种能力不是天然,需要内核支持,如果我们单纯把fd(文件描述符)当作数据传给子进程,子进程无法指向对应的文件的。下面我们如何使用这个技术并通过内核来看看如何实现这个技术。下面使用代码摘自Libuv。

代码语言:javascript复制
    int fd_to_send;
    // 核心数据结构
    struct msghdr msg;
    struct cmsghdr *cmsg;
    union {
      char data[64];
      struct cmsghdr alias;
    } scratch;
    // 获取需要发送的文件描述符
    fd_to_send = uv__handle_fd((uv_handle_t*) req->send_handle);

    memset(&scratch, 0, sizeof(scratch));

    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    msg.msg_iov = iov;
    msg.msg_iovlen = iovcnt;
    msg.msg_flags = 0;

    msg.msg_control = &scratch.alias;
    msg.msg_controllen = CMSG_SPACE(sizeof(fd_to_send));

    cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS;
    cmsg->cmsg_len = CMSG_LEN(sizeof(fd_to_send));

    /* silence aliasing warning */
    {
      void* pv = CMSG_DATA(cmsg);
      int* pi = pv;
      *pi = fd_to_send;
    }
    // fd是Unix域对应的文件描述符
    int fd = uv__stream_fd(stream);
    // 发送文件描述符
    sendmsg(fd, &msg, 0);

我们看到发送文件描述符是比较复杂的,使用的主要数据结构是msghdr。把需要发送的文件描述符保存到msghdr中,并设置一些标记。然后通过Unix域发送(Unix是唯一一种支持文件描述符传递的进程间通信方式)。我们下来主要来分析内核对sendmsg的实现。

代码语言:javascript复制
case SYS_SENDMSG:
        err = __sys_sendmsg(a0, (struct user_msghdr __user *)a1,
                    a[2], true);

该系统调用对应的是__sys_sendmsg。

代码语言:javascript复制
long __sys_sendmsg(int fd, struct user_msghdr __user *msg, unsigned int flags,
           bool forbid_cmsg_compat){
    int fput_needed, err;
    struct msghdr msg_sys;
    struct socket *sock;
    // 根据fd找到socket
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    ___sys_sendmsg(sock, msg, &msg_sys, flags, NULL, 0);}

后面的链路很长syssendmsg->__sys_sendmsg->sock_sendmsg->sock_sendmsg_nosec。

代码语言:javascript复制
static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg){
    int ret = INDIRECT_CALL_INET(sock->ops->sendmsg, inet6_sendmsg,
                     inet_sendmsg, sock, msg,
                     msg_data_left(msg));
    BUG_ON(ret == -EIOCBQUEUED);
    return ret;}

我们看到最后调用sock->ops->sendmsg,我们看看Unix域对sendmsg的实现。Unix域有SOCK_STREAM和SOCK_DGRAM两种模式,我们选第一个分析就可以。

代码语言:javascript复制
static int unix_stream_sendmsg(struct socket *sock, struct msghdr *msg,
                   size_t len){
    struct sock *sk = sock->sk;
    struct sock *other = NULL;
    int err, size;
    struct sk_buff *skb;
    int sent = 0;
    struct scm_cookie scm;
    bool fds_sent = false;
    int data_len;
    // 把文件描述符信息复制到scm
    scm_send(sock, msg, &scm, false);
    // 通信的对端
    other = unix_peer(sk);
    // 不断构建数据包skbuff发送,直到发送完毕
    while (sent < len) {
        // 分配一个sdk承载消息
        skb = sock_alloc_send_pskb(sk, size - data_len, data_len,
                       msg->msg_flags & MSG_DONTWAIT, &err,
                       get_order(UNIX_SKB_FRAGS_SZ));
        // 把scm复制到skb              
        err = unix_scm_to_skb(&scm, skb, !fds_sent);
        // 把数据写到skb
        err = skb_copy_datagram_from_iter(skb, 0, &msg->msg_iter, size);
        // 插入对端的消息队列
        skb_queue_tail(&other->sk_receive_queue, skb);
        // 通知对端有数据可读
        other->sk_data_ready(other);
        sent  = size;
    }
    // ...}

我们看到,整体的逻辑不负责,主要是根据数据构造skb结构体,然后插入对端消息队列,最后通知对端有消息可读,我们这里只关注文件描述符的处理。首先我们看看scm_send。

代码语言:javascript复制
static __inline__ int scm_send(struct socket *sock, struct msghdr *msg,
                   struct scm_cookie *scm, bool forcecreds){
    memset(scm, 0, sizeof(*scm));
    scm->creds.uid = INVALID_UID;
    scm->creds.gid = INVALID_GID;
    unix_get_peersec_dgram(sock, scm);
    if (msg->msg_controllen <= 0)
        return 0;
    return __scm_send(sock, msg, scm);}int __scm_send(struct socket *sock, struct msghdr *msg, struct scm_cookie *p){
    struct cmsghdr *cmsg;
    int err;

    for_each_cmsghdr(cmsg, msg) {
        switch (cmsg->cmsg_type)
        {
        case SCM_RIGHTS:
            err=scm_fp_copy(cmsg, &p->fp);
            if (err<0)
                goto error;
            break;
        }
    }}

我们看到__scm_send遍历待发送的数据,然后判断cmsg->cmsg_type的值,我们这里是SCM_RIGHTS(见最开始的使用代码),接着调用scm_fp_copy。

代码语言:javascript复制
static int scm_fp_copy(struct cmsghdr *cmsg, struct scm_fp_list **fplp){
    int *fdp = (int*)CMSG_DATA(cmsg);
    struct scm_fp_list *fpl = *fplp;
    struct file **fpp;
    int i, num;

    num = (cmsg->cmsg_len - sizeof(struct cmsghdr))/sizeof(int);
    if (!fpl)
    {
        fpl = kmalloc(sizeof(struct scm_fp_list), GFP_KERNEL);
        *fplp = fpl;
        fpl->count = 0;
        fpl->max = SCM_MAX_FD;
        fpl->user = NULL;
    }
    fpp = &fpl->fp[fpl->count];
    // 遍历然后把fd对应的file保存到fpp中。
    for (i=0; i< num; i  )
    {
        int fd = fdp[i];
        struct file *file;

        if (fd < 0 || !(file = fget_raw(fd)))
            return -EBADF;
        *fpp   = file;
        fpl->count  ;
    }

    if (!fpl->user)
        fpl->user = get_uid(current_user());

    return num;}

我们看到scm_fp_copy遍历然后把fd对应的file保存到fpp中。而fpp属于fpl属于fplp属于最开始的struct scm_cookie scm(unix_stream_sendmsg函数),即最后把fd对应的file保存到了scm中。接着我们回到unix_stream_sendmsg看unix_scm_to_skb。

代码语言:javascript复制
static int unix_scm_to_skb(struct scm_cookie *scm, struct sk_buff *skb, bool send_fds){
    int err = 0;

    UNIXCB(skb).pid  = get_pid(scm->pid);
    UNIXCB(skb).uid = scm->creds.uid;
    UNIXCB(skb).gid = scm->creds.gid;
    UNIXCB(skb).fp = NULL;
    unix_get_secdata(scm, skb);
    if (scm->fp && send_fds)
        // 写到skb
        err = unix_attach_fds(scm, skb);

    skb->destructor = unix_destruct_scm;
    return err;}

接着看unix_attach_fds。

代码语言:javascript复制
int unix_attach_fds(struct scm_cookie *scm, struct sk_buff *skb){
    int i;
    // 复制到skb
    UNIXCB(skb).fp = scm_fp_dup(scm->fp);
    return 0;}struct scm_fp_list *scm_fp_dup(struct scm_fp_list *fpl){
    struct scm_fp_list *new_fpl;
    int i;

    new_fpl = kmemdup(fpl, offsetof(struct scm_fp_list, fp[fpl->count]),
              GFP_KERNEL);
    if (new_fpl) {
        for (i = 0; i < fpl->count; i  )
            get_file(fpl->fp[i]);
        new_fpl->max = new_fpl->count;
        new_fpl->user = get_uid(fpl->user);
    }
    return new_fpl;}

至此我们看到数据和文件描述符都写到了skb中,并插入了对端的消息队列。我们接着分析对端时如何处理的。我们从recvmsg函数开始,对应Uinix域的实现时unix_stream_recvmsg。

代码语言:javascript复制
static int unix_stream_recvmsg(struct socket *sock, struct msghdr *msg,
                   size_t size, int flags){
    struct unix_stream_read_state state = {
        .recv_actor = unix_stream_read_actor,
        .socket = sock,
        .msg = msg,
        .size = size,
        .flags = flags
    };

    return unix_stream_read_generic(&state, true);}

接着看

代码语言:javascript复制
static int unix_stream_read_generic(struct unix_stream_read_state *state,
                    bool freezable){
    struct scm_cookie scm;
    struct socket *sock = state->socket;
    struct sock *sk = sock->sk;
    struct unix_sock *u = unix_sk(sk);

    // 拿到一个skb
    skb = skb_peek(&sk->sk_receive_queue);
    // 出队
    skb_unlink(skb, &sk->sk_receive_queue);
    // 复制skb数据到state->msg
    state->recv_actor(skb, skip, chunk, state);
    // 处理文件描述符
    if (UNIXCB(skb).fp) {
        scm_stat_del(sk, skb);
        // 复制skb的文件描述符信息到scm
        unix_detach_fds(&scm, skb);
    }
    if (state->msg)
        scm_recv(sock, state->msg, &scm, flags);}

内核首先通过钩子函数recv_actor把skb数据数据复制到state->msg,recv_actor对应函数是unix_stream_read_actor。

代码语言:javascript复制
static int unix_stream_read_actor(struct sk_buff *skb,
                  int skip, int chunk,
                  struct unix_stream_read_state *state){
    int ret;

    ret = skb_copy_datagram_msg(skb, UNIXCB(skb).consumed   skip,
                    state->msg, chunk);
    return ret ?: chunk;}

接着看unix_detach_fds。

代码语言:javascript复制
void unix_detach_fds(struct scm_cookie *scm, struct sk_buff *skb){
    int i;
    // 写到scm中
    scm->fp = UNIXCB(skb).fp;
    UNIXCB(skb).fp = NULL;}

unix_detach_fds把文件描述符信息写到scm。最后调用scm_recv处理文件描述符。

代码语言:javascript复制
static __inline__ void scm_recv(struct socket *sock, struct msghdr *msg,
                struct scm_cookie *scm, int flags){
    scm_detach_fds(msg, scm);}void scm_detach_fds(struct msghdr *msg, struct scm_cookie *scm){
    int fdmax = min_t(int, scm_max_fds(msg), scm->fp->count);
    int i;

    for (i = 0; i < fdmax; i  ) {
        err = receive_fd_user(scm->fp->fp[i], cmsg_data   i, o_flags);
        if (err < 0)
            break;
    }}

scm_detach_fds中调用了receive_fd_user处理一个文件描述符。其中第一个入参scm->fp->fp[i]是file结构体,即需传递到文件描述符对应的file。我们看看怎么处理的。

代码语言:javascript复制
static inline int receive_fd_user(struct file *file, int __user *ufd,
                  unsigned int o_flags){
    return __receive_fd(-1, file, ufd, o_flags);}int __receive_fd(int fd, struct file *file, int __user *ufd, unsigned int o_flags){
    int new_fd;
    // fd是-1,申请一个新的
    if (fd < 0) {
        new_fd = get_unused_fd_flags(o_flags);
    } else {
        new_fd = fd;
    }

    if (fd < 0) {
        fd_install(new_fd, get_file(file));
    } else {
        // ...
    }
    return new_fd;}

我们看到首先申请了当前进程的一个新的文件描述符。然后调用fd_install关联到file。

代码语言:javascript复制
void fd_install(unsigned int fd, struct file *file){   
    // current->files是当前进程打开的文件描述符列表
    __fd_install(current->files, fd, file);}void __fd_install(struct files_struct *files, unsigned int fd,
        struct file *file){
    struct fdtable *fdt;
    // 拿到管理文件描述符的数据结构
    fdt = rcu_dereference_sched(files->fdt);
    // 给某个元素赋值指向file
    rcu_assign_pointer(fdt->fd[fd], file);}

最后形成下图所示的架构。

后记,我们看到文件描述符传递的核心就是在发送的数据中记录要传递的文件描述符对应的file结构体,然后发送做好标记,接着接收的过程中,重新建立新的fd到传递的file的关联关系。

0 人点赞