进程间文件描述符传递原理

进程间文件描述符传递原理

进程中文件的管理以及fork

每个进程的文件描述符是独立的,即一个进程打开的文件描述符是记录在进程对象上的(task_struct)。

task_struct {

    files_struct *files;
}

files_struct {
    struct fdtable __rcu *fdt;
}

fdtable {
    struct file __rcu **fd;
}


下图展示了一个进程打开3个文件时,内核 task_struct是如何描述自己打开文件的。
在这里插入图片描述

下图展示了一个进程fork后,子进程和父进程各自打开1个文件后,进程中文件如何管理的。
在这里插入图片描述

fork如何完成文件描述符的继承

调用链:
copy_process -> copy_files -> dup_fd

将老的 遍历 老的 current->files->fdt 中的元素,然后通过get_file将其引用计数 加 1 。

	for (i = open_files; i != 0; i--) {
		struct file *f = *old_fds++;
		if (f) {
			get_file(f);
		} else {
			/*
			 * The fd may be claimed in the fd bitmap but not yet
			 * instantiated in the files array if a sibling thread
			 * is partway through open().  So make sure that this
			 * fd is available to the new process.
			 */
			__clear_open_fd(open_files - i, new_fdt);
		}
		rcu_assign_pointer(*new_fds++, f);
	}

上面的逻辑解释了图二中新老进程共享已打开文件的原理。2个父子关系的进程,共享文件fork前的描述符,是"天赋",下面聊聊2个独立的进程如何共享文件描述符。

两个独立进程之间如何共享文件描述符

从上面fork的例子可以猜到,两个独立的进程(或者fork后各自独立的进程)希望共享已打开的文件,那么已打开文件进程的fd1 对应的 file告诉目标进程,目标进程创建一个fd2,然后将fd2对应的file指向fd1对应的file,然后完成文件的共享。

Linux提供和scm方式来完成上述的逻辑。
scm: Socket control messages

例子

先看一段例子:
逻辑很简单,一个是为了完成进程间的通信,建立的一个socketpari,你也可以使用其他进程间通信的方式,这里采用了socketpari。其次,通过sendmsg发送fd,然后通过recvmsg接收fd。
发送方的fd可能和接收到的fd值不一样。
该方式的核心在于2个点,首先,创建的socket类型是UNIX socket,其次发送时设置msg.msg_control,其值为struct cmsghdrcmptr->cmsg_type设置成SCM_RIGHTS,内核根据cmsg_type值,会对发送方的fd进行转换,转换成接收到的fd,并且将接收方的fd映射为发送方fd对应的文件。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
 
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
 
#include <sys/socket.h>
 
#define CONTROLLEN CMSG_LEN(sizeof(int))
 
int send_fd(int fd, int fd_to_send) {
    struct iovec    iov[1];
    struct msghdr   msg;
    char        buf[1];
    struct cmsghdr  *cmptr = NULL;
 
    iov[0].iov_base = buf;     
    iov[0].iov_len  = 1;       
    msg.msg_iov = iov;
    msg.msg_iovlen  = 1;
    msg.msg_name    = NULL;
    msg.msg_namelen = 0;
 
    cmptr = malloc(CONTROLLEN);
    cmptr->cmsg_level = SOL_SOCKET;
    cmptr->cmsg_type  = SCM_RIGHTS;
    cmptr->cmsg_len    = CONTROLLEN;
    msg.msg_control   = cmptr;
    msg.msg_controllen= CONTROLLEN;
    *(int*)CMSG_DATA(cmptr) = fd_to_send;
    buf[0] = 0;
  
    printf("[father]: fd_to_send %d\n", fd_to_send); 
    if (sendmsg(fd, &msg, 0) != 1) {
        return -1;
    }
    return 0;
}
 
int recv_fd(int fd, int *fd_to_recv) {
    int         nr;
    char        buf[1];
    struct iovec    iov[1];
    struct msghdr   msg;
    struct cmsghdr  *cmptr = NULL;
     
    iov[0].iov_base = buf;
    iov[0].iov_len  = 1;
    msg.msg_iov = iov;
    msg.msg_iovlen  = 1;
    msg.msg_name    = NULL;
    msg.msg_namelen = 0;
 
    cmptr = malloc(CONTROLLEN);
    msg.msg_control = cmptr;
    msg.msg_controllen = CONTROLLEN;
 
    if(recvmsg(fd, &msg, 0) < 0) {
        printf("recvmsg error\n");
        return -1;
    }
 
    if(msg.msg_controllen < CONTROLLEN) {
        printf("recv_fd get invalid fd\n");
        return -1;
    }
     
    *fd_to_recv = *(int*)CMSG_DATA(cmptr);
    printf("[child]: fd_to_recv %d\n", *fd_to_recv);
    return 0;
}
 
int main() {
    int fd;
    pid_t   pid;
    int sockpair[2];
    int status;
    char    fname[256];
 
    status = socketpair(AF_UNIX, SOCK_STREAM, 0, sockpair);
    if (status < 0) {
        printf("Call socketpair error, errno is %d\n", errno);
        return errno;
    }
 
    pid = fork();
    if (pid == 0) {
        close(sockpair[1]);
         
        status = recv_fd(sockpair[0], &fd);
        if (status != 0) {
            printf("[child]: recv error, errno is %d\n", status);
            return status;
        }
        printf("[child]:write fd %d\n", fd); 
        status = write(fd, "123", sizeof("123") - 1);
        if (status < 0) {
            printf("[child]: write error, errno is %d\n", status);
            return -1;
        } else {
            printf("[child]: append logo successfully\n");
        }
        close(fd);
 
        exit(0);
    }
     
    printf("[father]: enter the filename:\n");
    scanf("%s", fname);
 
    fd = open(fname, O_RDWR | O_APPEND);
    if (fd < 0) {
        perror("[father]");
        return -1;
    }
 
    status = send_fd(sockpair[1], fd);
    if (status != 0) {
        perror("[father]");
        return -1;
    }
    close(fd);
 
    wait(NULL);
    return 0;
}

原理

sendmsg 如果是正常调用,即没有设置cmsghdr,那么只是简单的send操作,但是如果设置了cmsghdr值且cmptr->cmsg_type设置成了SCM_RIGHTS,那么会有如下一个scm_fp_copy操作:
sendmsg->unix_stream_sendmsg->scm_send->__scm_sen->scm_fp_copy

scm_fp_copy 将获得 fd 对应 的 struct file *保存在scm_fp_list中。


static int scm_fp_copy(struct cmsghdr *cmsg, struct scm_fp_list **fplp)
{
    ......
	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++;
	}

    ....
}

接着:
unix_stream_sendmsg->unix_scm_to_skb->unix_attach_fds

接着在 unix_scm_to_skb时,借用 skb->cb 指向scm_fp_list

static int unix_attach_fds(struct scm_cookie *scm, struct sk_buff *skb)
{
    ......
	UNIXCB(skb).fp = scm_fp_dup(scm->fp);
    ......
}

skb是数据报文传递的载体,对于domainsocket(UNIX SOCKET),skb直接回扔给接收方,而不会进行二三层网络传输。

我们来怎么取的
unix_stream_recvmsg->unix_stream_read_generic

首先将skb中的fd拷贝到自己sock结构体中,方便后续处理

    if (UNIXCB(skb).fp)
        siocb->scm->fp = scm_fp_dup(UNIXCB(skb).fp);

接着调用 scm_recv->scm_detach_fds,这是接收方的核心函数,完成了接收方进程fd的创建以及文件的映射。


void scm_detach_fds(struct msghdr *msg, struct scm_cookie *scm)
{

    ......

	for (i=0, cmfptr=(__force int __user *)CMSG_DATA(cm); i<fdmax;
	     i++, cmfptr++)
	{
		struct socket *sock;
		int new_fd;
		err = security_file_receive(fp[i]);
		if (err)
			break;
            
        //获得当前进程未使用的fd
		err = get_unused_fd_flags(MSG_CMSG_CLOEXEC & msg->msg_flags
					  ? O_CLOEXEC : 0);
		if (err < 0)
			break;
		new_fd = err;
		err = put_user(new_fd, cmfptr);
		if (err) {
			put_unused_fd(new_fd);
			break;
		}
		/* Bump the usage count and install the file. */
		sock = sock_from_file(fp[i], &err);
		if (sock) {
			sock_update_netprioidx(sock->sk);
			sock_update_classid(sock->sk);
		}
        
        //当前进程fd关联 到 发送进程的file
		fd_install(new_fd, get_file(fp[i]));
	}
    ......

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值