通过 Linux 套接字发送文件描述符

Posted

技术标签:

【中文标题】通过 Linux 套接字发送文件描述符【英文标题】:Sending file descriptor by Linux socket 【发布时间】:2015-03-16 05:40:05 【问题描述】:

我正在尝试通过 linux 套接字发送一些文件描述符,但它不起作用。我究竟做错了什么?应该如何调试这样的东西?我尝试将 perror() 放在任何可能的地方,但他们声称一切都很好。这是我写的:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <fcntl.h>

void wyslij(int socket, int fd)  // send fd by socket

    struct msghdr msg = 0;

    char buf[CMSG_SPACE(sizeof fd)];

    msg.msg_control = buf;
    msg.msg_controllen = sizeof buf;

    struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS;
    cmsg->cmsg_len = CMSG_LEN(sizeof fd);

    *((int *) CMSG_DATA(cmsg)) = fd;

    msg.msg_controllen = cmsg->cmsg_len;  // why does example from man need it? isn't it redundant?

    sendmsg(socket, &msg, 0);



int odbierz(int socket)  // receive fd from socket

    struct msghdr msg = 0;
    recvmsg(socket, &msg, 0);

    struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg);

    unsigned char * data = CMSG_DATA(cmsg);

    int fd = *((int*) data);  // here program stops, probably with segfault

    return fd;



int main()

    int sv[2];
    socketpair(AF_UNIX, SOCK_DGRAM, 0, sv);

    int pid = fork();
    if (pid > 0)  // in parent
    
        close(sv[1]);
        int sock = sv[0];

        int fd = open("./z7.c", O_RDONLY);

        wyslij(sock, fd);

        close(fd);
    
    else  // in child
    
        close(sv[0]);
        int sock = sv[1];

        sleep(0.5);
        int fd = odbierz(sock);
    


【问题讨论】:

我的意思是 CMSG_DATA 我不知道它们是什么但我想我找到了问题。 为什么要通过套接字发送文件描述符的?接收者应该用它做什么?如果接收者与发送者不在同一个进程中,则文件描述符对接收者将毫无意义,因为文件描述符是特定于进程的。在任何情况下,在接收端,当使用recvmsg() 时,您需要遍历接收到的消息头,寻找SOL_SOCKET/SCM_RIGHTS 头。现在,您假设每条消息都以SCM_RIGHTS 开头,但情况并非总是如此。 @RemyLebeau:只是为了检查一下——您知道可以使用SCM_RIGHTSAF_UNIX 套接字在进程之间发送(可用)文件描述符吗? 我撤回关于跨进程边界发送文件描述符的值无效的评论。但是我关于需要正确扫描收到的recvmsg() 标头以获取SCM_RIGHTS 的评论仍然适用。 @RemyLebeau:FWIW 它不是特定于 Linux 的(虽然我相信不是全部,但许多 Unix 版本都支持它)。但我同意扫描recvmsg() 标头是最佳做法:-) 【参考方案1】:

Stevens (et al) UNIX® Network Programming, Vol 1: The Sockets Networking API 在第 15 章Unix 域协议,特别是第 15.7 节传递描述符中描述了在进程之间传输文件描述符的过程。完整描述很麻烦,但必须在 Unix 域套接字上完成(AF_UNIXAF_LOCAL),并且发送方进程使用sendmsg(),而接收方使用recvmsg()

我从问题中得到了这个经过轻微修改(和检测)的代码版本,可以在带有 GCC 4.9.1 的 Mac OS X 10.10.1 Yosemite 上为我工作:

#include "stderr.h"
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

static
void wyslij(int socket, int fd)  // send fd by socket

    struct msghdr msg =  0 ;
    char buf[CMSG_SPACE(sizeof(fd))];
    memset(buf, '\0', sizeof(buf));
    struct iovec io =  .iov_base = "ABC", .iov_len = 3 ;

    msg.msg_iov = &io;
    msg.msg_iovlen = 1;
    msg.msg_control = buf;
    msg.msg_controllen = sizeof(buf);

    struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS;
    cmsg->cmsg_len = CMSG_LEN(sizeof(fd));

    *((int *) CMSG_DATA(cmsg)) = fd;

    msg.msg_controllen = CMSG_SPACE(sizeof(fd));

    if (sendmsg(socket, &msg, 0) < 0)
        err_syserr("Failed to send message\n");


static
int odbierz(int socket)  // receive fd from socket

    struct msghdr msg = 0;

    char m_buffer[256];
    struct iovec io =  .iov_base = m_buffer, .iov_len = sizeof(m_buffer) ;
    msg.msg_iov = &io;
    msg.msg_iovlen = 1;

    char c_buffer[256];
    msg.msg_control = c_buffer;
    msg.msg_controllen = sizeof(c_buffer);

    if (recvmsg(socket, &msg, 0) < 0)
        err_syserr("Failed to receive message\n");

    struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg);

    unsigned char * data = CMSG_DATA(cmsg);

    err_remark("About to extract fd\n");
    int fd = *((int*) data);
    err_remark("Extracted fd %d\n", fd);

    return fd;


int main(int argc, char **argv)

    const char *filename = "./z7.c";

    err_setarg0(argv[0]);
    err_setlogopts(ERR_PID);
    if (argc > 1)
        filename = argv[1];
    int sv[2];
    if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) != 0)
        err_syserr("Failed to create Unix-domain socket pair\n");

    int pid = fork();
    if (pid > 0)  // in parent
    
        err_remark("Parent at work\n");
        close(sv[1]);
        int sock = sv[0];

        int fd = open(filename, O_RDONLY);
        if (fd < 0)
            err_syserr("Failed to open file %s for reading\n", filename);

        wyslij(sock, fd);

        close(fd);
        nanosleep(&(struct timespec) .tv_sec = 1, .tv_nsec = 500000000, 0);
        err_remark("Parent exits\n");
    
    else  // in child
    
        err_remark("Child at play\n");
        close(sv[0]);
        int sock = sv[1];

        nanosleep(&(struct timespec) .tv_sec = 0, .tv_nsec = 500000000, 0);

        int fd = odbierz(sock);
        printf("Read %d!\n", fd);
        char buffer[256];
        ssize_t nbytes;
        while ((nbytes = read(fd, buffer, sizeof(buffer))) > 0)
            write(1, buffer, nbytes);
        printf("Done!\n");
        close(fd);
    
    return 0;

经过检测但未修复的原始代码版本的输出是:

$ ./fd-passing
fd-passing: pid=1391: Parent at work
fd-passing: pid=1391: Failed to send message
error (40) Message too long
fd-passing: pid=1392: Child at play
$ fd-passing: pid=1392: Failed to receive message
error (40) Message too long

注意,父级先于子级完成,所以提示出现在输出中间。

“固定”代码的输出是:

$ ./fd-passing
fd-passing: pid=1046: Parent at work
fd-passing: pid=1048: Child at play
fd-passing: pid=1048: About to extract fd
fd-passing: pid=1048: Extracted fd 3
Read 3!
This is the file z7.c.
It isn't very interesting.
It isn't even C code.
But it is used by the fd-passing program to demonstrate that file
descriptors can indeed be passed between sockets on occasion.
Done!
fd-passing: pid=1046: Parent exits
$

主要的重大变化是将struct iovec 添加到两个函数中struct msghdr 的数据中,并在接收函数(odbierz()) 中为控制消息提供空间。我报告了调试的一个中间步骤,我向父级提供了struct iovec,并且删除了父级的“消息太长”错误。为了证明它正在工作(传递了一个文件描述符),我添加了代码来从传递的文件描述符中读取和打印文件。原始代码有sleep(0.5),但由于sleep() 采用无符号整数,这相当于不睡觉。我使用 C99 复合文字让孩子睡 0.5 秒。父进程休眠 1.5 秒,以便子进程的输出在父进程退出之前完成。我也可以使用wait()waitpid(),但我懒得这样做。

我还没有回去检查是否所有的添加都是必要的。

"stderr.h" 标头声明了err_*() 函数。这是我编写的代码(1987 年之前的第一个版本),用于简洁地报告错误。 err_setlogopts(ERR_PID) 调用为所有带有 PID 的消息添加前缀。对于时间戳err_setlogopts(ERR_PID|ERR_STAMP) 也可以完成这项工作。

对齐问题

Nominal Animal 在comment 中建议:

我可以建议您修改代码以使用memcpy() 复制描述符int 而不是直接访问数据吗?它不一定正确对齐——这就是手册页示例也使用 memcpy() 的原因——并且在许多 Linux 架构中,未对齐的 int 访问会导致问题(直至 SIGBUS 信号终止进程)。

不仅是 Linux 架构:SPARC 和 Power 都需要对齐的数据,并且通常分别运行 Solaris 和 AIX。曾几何时,DEC Alpha 也要求这样做,但现在他们很少在现场看到。

手册页cmsg(3)中与此相关的代码是:

struct msghdr msg = 0;
struct cmsghdr *cmsg;
int myfds[NUM_FD]; /* Contains the file descriptors to pass. */
char buf[CMSG_SPACE(sizeof myfds)];  /* ancillary data buffer */
int *fdptr;

msg.msg_control = buf;
msg.msg_controllen = sizeof buf;
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int) * NUM_FD);
/* Initialize the payload: */
fdptr = (int *) CMSG_DATA(cmsg);
memcpy(fdptr, myfds, NUM_FD * sizeof(int));
/* Sum of the length of all control messages in the buffer: */
msg.msg_controllen = CMSG_SPACE(sizeof(int) * NUM_FD);

fdptr 的赋值似乎假设CMSG_DATA(cmsg) 已充分对齐以转换为int *,并且memcpy() 的使用假设NUM_FD 不只是1。话虽如此,它应该指向数组buf,并且可能没有像Nominal Animal建议的那样充分对齐,所以在我看来fdptr只是一个闯入者,如果使用示例会更好:

memcpy(CMSG_DATA(cmsg), myfds, NUM_FD * sizeof(int));

然后接收端的相反过程将是适当的。这个程序只传递一个文件描述符,所以代码可以修改为:

memmove(CMSG_DATA(cmsg), &fd, sizeof(fd));  // Send
memmove(&fd, CMSG_DATA(cmsg), sizeof(fd));  // Receive

我似乎还记得各种操作系统 w.r.t 的历史问题。没有正常有效负载数据的辅助数据,也通过发送至少一个虚拟字节来避免,但我找不到任何要验证的参考,所以我可能记错了。

鉴于 Mac OS X(基于 Darwin/BSD)至少需要一个 struct iovec,即使它描述的是零长度消息,我也愿意相信上面显示的代码,其中包括3 字节消息,是朝着正确大方向迈出的一大步。消息可能应该是一个空字节而不是 3 个字母。

我已将代码修改为如下所示。它使用memmove() 将文件描述符复制到cmsg 缓冲区和从缓冲区复制文件描述符。它传输单个消息字节,这是一个空字节。

在将文件描述符传递给子进程之前,它还让父进程读取(最多)32 个字节的文件。孩子从父母离开的地方继续阅读。这表明传输的文件描述符包括文件偏移量。

接收方应在将cmsg 视为文件描述符传递消息之前对其进行更多验证。

#include "stderr.h"
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

static
void wyslij(int socket, int fd)  // send fd by socket

    struct msghdr msg =  0 ;
    char buf[CMSG_SPACE(sizeof(fd))];
    memset(buf, '\0', sizeof(buf));

    /* On Mac OS X, the struct iovec is needed, even if it points to minimal data */
    struct iovec io =  .iov_base = "", .iov_len = 1 ;

    msg.msg_iov = &io;
    msg.msg_iovlen = 1;
    msg.msg_control = buf;
    msg.msg_controllen = sizeof(buf);

    struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS;
    cmsg->cmsg_len = CMSG_LEN(sizeof(fd));

    memmove(CMSG_DATA(cmsg), &fd, sizeof(fd));

    msg.msg_controllen = CMSG_SPACE(sizeof(fd));

    if (sendmsg(socket, &msg, 0) < 0)
        err_syserr("Failed to send message\n");


static
int odbierz(int socket)  // receive fd from socket

    struct msghdr msg = 0;

    /* On Mac OS X, the struct iovec is needed, even if it points to minimal data */
    char m_buffer[1];
    struct iovec io =  .iov_base = m_buffer, .iov_len = sizeof(m_buffer) ;
    msg.msg_iov = &io;
    msg.msg_iovlen = 1;

    char c_buffer[256];
    msg.msg_control = c_buffer;
    msg.msg_controllen = sizeof(c_buffer);

    if (recvmsg(socket, &msg, 0) < 0)
        err_syserr("Failed to receive message\n");

    struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);

    err_remark("About to extract fd\n");
    int fd;
    memmove(&fd, CMSG_DATA(cmsg), sizeof(fd));
    err_remark("Extracted fd %d\n", fd);

    return fd;


int main(int argc, char **argv)

    const char *filename = "./z7.c";

    err_setarg0(argv[0]);
    err_setlogopts(ERR_PID);
    if (argc > 1)
        filename = argv[1];
    int sv[2];
    if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) != 0)
        err_syserr("Failed to create Unix-domain socket pair\n");

    int pid = fork();
    if (pid > 0)  // in parent
    
        err_remark("Parent at work\n");
        close(sv[1]);
        int sock = sv[0];

        int fd = open(filename, O_RDONLY);
        if (fd < 0)
            err_syserr("Failed to open file %s for reading\n", filename);

        /* Read some data to demonstrate that file offset is passed */
        char buffer[32];
        int nbytes = read(fd, buffer, sizeof(buffer));
        if (nbytes > 0)
            err_remark("Parent read: [[%.*s]]\n", nbytes, buffer);

        wyslij(sock, fd);

        close(fd);
        nanosleep(&(struct timespec) .tv_sec = 1, .tv_nsec = 500000000, 0);
        err_remark("Parent exits\n");
    
    else  // in child
    
        err_remark("Child at play\n");
        close(sv[0]);
        int sock = sv[1];

        nanosleep(&(struct timespec) .tv_sec = 0, .tv_nsec = 500000000, 0);

        int fd = odbierz(sock);
        printf("Read %d!\n", fd);
        char buffer[256];
        ssize_t nbytes;
        while ((nbytes = read(fd, buffer, sizeof(buffer))) > 0)
            write(1, buffer, nbytes);
        printf("Done!\n");
        close(fd);
    
    return 0;

还有一个示例运行:

$ ./fd-passing
fd-passing: pid=8000: Parent at work
fd-passing: pid=8000: Parent read: [[This is the file z7.c.
It isn't ]]
fd-passing: pid=8001: Child at play
fd-passing: pid=8001: About to extract fd
fd-passing: pid=8001: Extracted fd 3
Read 3!
very interesting.
It isn't even C code.
But it is used by the fd-passing program to demonstrate that file
descriptors can indeed be passed between sockets on occasion.
And, with the fully working code, it does indeed seem to work.
Extended testing would have the parent code read part of the file, and
then demonstrate that the child codecontinues where the parent left off.
That has not been coded, though.
Done!
fd-passing: pid=8000: Parent exits
$

【讨论】:

谢谢,它正在工作!问题是 odbierz() 中的 msg.msg_control 缺少缓冲区。 很高兴它为您工作。在 Mac OS X 上,似乎需要 struct iovec 部分,但发送版本可以是 struct iovec io = .iov_base = "", .iov_len = 0 ;,这没关系。接收方使用char mbuffer[1];,但并非没有struct iovec @JonathanLeffler:我可以建议您修改代码以使用memcpy() 复制描述符 int 而不是直接访问数据吗?它不一定正确对齐——这就是手册页示例也使用memcpy() 的原因——并且有许多 Linux 体系结构,其中未对齐的 int 访问会导致问题(直到 SIGBUS 信号终止进程)。我似乎还记得各种操作系统的历史问题。没有正常有效负载数据的辅助数据,也通过发送至少一个虚拟字节来避免,但我找不到任何要验证的参考,所以我可能记错了。 @JonathanLeffler iovec 数据似乎是必需的,因为手册页 (unix(7)) 中的以下注释指出:“要通过 SOCK_STREAM 传递文件描述符或凭据,您需要发送或接收在同一个 sendmsg(2) 或 recvmsg(2) 调用中至少有一个字节的非辅助数据。" @CMCDragonkai 是的,可以在单个 sendmsg() 调用中传递多个控制消息。在 Linux 上,每种类型只能发送一个辅助消息(例如,一次 SCM_RIGHTS 加上一个 SCM_CREDENTIALS)。据报道,在 FreeBSD 上,可以在单个 sendmsg() 中发送多个相同类型的消息;我没有测试过这个。有关 Linux 示例,请参阅 man7.org/tlpi/code/online/dist/sockets/scm_multi_recv.c.html 和 man7.org/tlpi/code/online/dist/sockets/scm_multi_send.c.html

以上是关于通过 Linux 套接字发送文件描述符的主要内容,如果未能解决你的问题,请参考以下文章

linux一切皆文件之tcp socket描述符

通过 ioctl 调用套接字文件描述符获取数据包时间戳

linux文件描述符

Java(android)中socket.io的文件描述符?

为啥 Linux 会重用 pipe() 分配的文件描述符

linux lsof命令详解