在Linux上,我能否将文件描述符共享给另一个进程,还是它们只属于本进程?

69

假设我有2个进程,进程A和进程B。如果我在进程A中执行int fd=open(somefile),那么我能否将文件描述符fd的值通过IPC传递给进程B,并使其操作相同的文件?


2
请查看这个问题 - bmargulies
1
请勿重复:https://dev59.com/4nI-5IYBdhLWcg3wJEwK - Zitrax
6个回答

67

你可以通过Unix域套接字将文件描述符传递给另一个进程。这是从Unix网络编程中获取的传递文件描述符的代码:

ssize_t
write_fd(int fd, void *ptr, size_t nbytes, int sendfd)
{
    struct msghdr   msg;
    struct iovec    iov[1];

#ifdef  HAVE_MSGHDR_MSG_CONTROL
    union {
      struct cmsghdr    cm;
      char              control[CMSG_SPACE(sizeof(int))];
    } control_un;
    struct cmsghdr  *cmptr;

    msg.msg_control = control_un.control;
    msg.msg_controllen = sizeof(control_un.control);

    cmptr = CMSG_FIRSTHDR(&msg);
    cmptr->cmsg_len = CMSG_LEN(sizeof(int));
    cmptr->cmsg_level = SOL_SOCKET;
    cmptr->cmsg_type = SCM_RIGHTS;
    *((int *) CMSG_DATA(cmptr)) = sendfd;
#else
    msg.msg_accrights = (caddr_t) &sendfd;
    msg.msg_accrightslen = sizeof(int);
#endif

    msg.msg_name = NULL;
    msg.msg_namelen = 0;

    iov[0].iov_base = ptr;
    iov[0].iov_len = nbytes;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

    return(sendmsg(fd, &msg, 0));
}
/* end write_fd */

以下是接收文件描述符的代码

ssize_t
read_fd(int fd, void *ptr, size_t nbytes, int *recvfd)
{
    struct msghdr   msg;
    struct iovec    iov[1];
    ssize_t         n;
    int             newfd;

#ifdef  HAVE_MSGHDR_MSG_CONTROL
    union {
      struct cmsghdr    cm;
      char              control[CMSG_SPACE(sizeof(int))];
    } control_un;
    struct cmsghdr  *cmptr;

    msg.msg_control = control_un.control;
    msg.msg_controllen = sizeof(control_un.control);
#else
    msg.msg_accrights = (caddr_t) &newfd;
    msg.msg_accrightslen = sizeof(int);
#endif

    msg.msg_name = NULL;
    msg.msg_namelen = 0;

    iov[0].iov_base = ptr;
    iov[0].iov_len = nbytes;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

    if ( (n = recvmsg(fd, &msg, 0)) <= 0)
        return(n);

#ifdef  HAVE_MSGHDR_MSG_CONTROL
    if ( (cmptr = CMSG_FIRSTHDR(&msg)) != NULL &&
        cmptr->cmsg_len == CMSG_LEN(sizeof(int))) {
        if (cmptr->cmsg_level != SOL_SOCKET)
            err_quit("control level != SOL_SOCKET");
        if (cmptr->cmsg_type != SCM_RIGHTS)
            err_quit("control type != SCM_RIGHTS");
        *recvfd = *((int *) CMSG_DATA(cmptr));
    } else
        *recvfd = -1;       /* descriptor was not passed */
#else
/* *INDENT-OFF* */
    if (msg.msg_accrightslen == sizeof(int))
        *recvfd = newfd;
    else
        *recvfd = -1;       /* descriptor was not passed */
/* *INDENT-ON* */
#endif

    return(n);
}
/* end read_fd */

23
注意,两个进程中文件描述符的实际数值通常会不同。 - caf
3
你可以用这种方式传递数字。这并不意味着它会神奇地在两端都作为文件描述符正常工作。 - user207421
12
SCM_RIGHTS的想法是它将会像dup()一样在不相关的进程之间工作。虽然我现在想不出任何问题,但我确定有一些需要注意的地方。 - nos

9

如果两个进程属于同一用户,则可以简单地利用procfs。

char fd_path[64];  // actual maximal length: 37 for 64bit systems
snprintf(fd_path, sizeof(fd_path), "/proc/%d/fd/%d", SOURCE_PID, SOURCE_FD);
int new_fd = open(fd_path, O_RDWR);

当然,您需要一些IPC机制来共享SOURCE_FD的值。例如,请参见“Linux C:在接收到信号时,是否可以知道发送者的PID?”。


你的意思是在snprintf中使用"/proc/%d/fd/%d"吗? - kaiwan
4
针对未来读者:这适用于真实文件,但绝对不适用于未命名的域套接字(socketpair()),可能也不适用于命名域套接字,我不确定管道是否可用。对于域套接字,如果在文件系统路径上调用 open(),你将得到 ENXIOconnect() 会失败,因为它只有在未连接的域套接字上下文中才有意义;bind() 则会显示地址已在使用中。 - Brian Vandenberg
尝试按照您的建议打开一个TCP连接套接字时,使用open("/proc/9256/fd/5", O_WRONLY|O_CREAT|O_TRUNC, 0666) = -1 ENXIO (No such device or address)的方式。 然而,open(2)函数并不允许从/proc//fd目录下打开套接字文件。 - Marcelo Pacheco
这个答案是错误的。这并不像在执行fork()时那样共享文件描述符。而是打开一个指向同一文件的新文件描述符。1. 对于许多种类的文件描述符,这种方法根本不起作用。2. 即使这种方法起作用,"文件描述"的状态,比如读写位置,也不会被共享。 - undefined

8

在2020年,Linux 5.6及以上版本中新增了一个系统调用,使用 pidfd_getfd() 系统调用可以让一个进程获取由另一个进程使用 pidfd 引用的文件描述符的副本。


3

简短回答:

尝试使用pidfd_getfd系统调用。

详细回答:

pidfd_getfd()系统调用会在调用进程(进程B)中分配一个新的文件描述符。这个新的文件描述符是由进程A中的现有文件描述符targetfd复制而来,而进程A则是通过PID文件描述符pidfd引用的。当然,您需要一种机制来从进程A中获取目标文件描述符targetfd

newfd = syscall(SYS_pidfd_getfd, int pidfd, int targetfd, 0);

我们使用pidfd_open()函数获取PID文件描述符pidfd。

 pidfd = syscall(SYS_pidfd_open, pid_t pid, 0);
< p > pidfd_getfd()的效果类似于使用SCM_RIGHTS消息,但为了使用SCM_RIGHTS消息传递文件描述符,两个进程必须首先建立一个UNIX域套接字连接,因此需要被复制文件描述符的进程的合作。相比之下,使用pidfd_getfd()时不需要这样的合作。

一个虚拟示例

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <string.h>
#define MMAP_NAME "/tmp/mmap"
struct shared_mem{
    int targetfd;
};
int main(void){
    int fd;
    struct shared_mem *shmp;
    unlink(MMAP_NAME);
    fd = open(MMAP_NAME, O_CREAT | O_RDWR, 00600);
    ftruncate(fd, sizeof(struct shared_mem));
    shmp = (struct shared_mem *)mmap(NULL,
                                     sizeof(struct shared_mem),
                                     PROT_READ | PROT_WRITE,
                                     MAP_SHARED,
                                     fd,
                                     0);
    if (fork() == 0){
        sleep(5);
        write(syscall(SYS_pidfd_getfd,
                      syscall(SYS_pidfd_open, getppid(), 0),
                      shmp->targetfd,
                      0),
              "Messege from Child\n",
              strlen("Messege from Child\n"));
        close(shmp->targetfd);
        exit(EXIT_SUCCESS);
    }else{
        shmp->targetfd = open("foo.txt", O_RDWR | O_CREAT);
        write(shmp->targetfd, "Messege from Parent\n", strlen("Messege from Parent\n"));
        wait(NULL);
    }
    munmap(shmp, sizeof(struct shared_mem));
    return EXIT_SUCCESS;
}

..

# cat foo.txt
Messege from Parent
Messege from Child

假设在fork()之前我们无法创建文件描述符。 - frankscitech
请问您能否告诉我,我如何获取一个进程的所有打开文件描述符? - user786

2
请注意上面的例子中,接收时变量的设置,例如:
msg.msg_name = NULL;
msg.msg_namelen = 0;

iov[0].iov_base = ptr;
iov[0].iov_len = nbytes;
msg.msg_iov = iov;
msg.msg_iovlen = 1;

这并非必需。消息结构中包含标头的整个想法是,接收网站不必知道它所读取的内容,并且通过检查(第一个)标头,可以确定消息类型和预期内容。


虽然你技术上是正确的,在这种情况下指定缓冲区有一个很好的理由:为了发送OOB消息(在此情况下为套接字控制消息),您需要指定一个非空消息(请参见unix_stream_sendmsg,例如http://lxr.free-electrons.com/source/net/unix/af_unix.c#L1836)。在没有iovect的情况下接收时,Linux将一遍又一遍地传递该消息。因此,要读取多个OOB消息,必须在某个时候读取消息数据。 - Michael

2
您可以使用本线程中nos所描述的方法,或者更传统的方法,通过在相关进程(通常是父子进程或兄弟进程)之间共享它来创建它,fork出的进程会自动接收到一份副本。
事实上,fork出的进程会获取所有您的文件描述符,并且除非关闭它们(这通常是一个好主意),否则可以使用它们。
因此,如果父进程fork了两个子进程,并且他们都有一个未关闭的文件描述符,则现在该文件描述符被共享了(即使父进程随后关闭了它)。例如,这可能是从一个子进程到另一个子进程的管道。这就是shell重定向的原理。
ls -l | more

工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接