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

Dek*_*ruk 15 c sockets linux file-descriptor

我试图通过linux socket发送一些文件描述符,但它不起作用.我究竟做错了什么?一个人应该如何调试这样的东西?我尝试在可能的地方放置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);
    }

}
Run Code Online (Sandbox Code Playgroud)

Jon*_*ler 36

Stevens(等)UNIX®网络编程,第1卷:套接字网络API描述了在第15章Unix域协议中的进程之间传输文件描述符的过程,特别是§15.7 传递描述符.完整地描述它是非常繁琐的,但它必须在Unix域套接字(AF_UNIXAF_LOCAL)上完成,并且发送方进程sendmsg()在接收方使用时使用recvmsg().

我在Mac OS X 10.10.1 Yosemite和GCC 4.9.1上得到了这个问题的温和修改(和仪表化)版本的代码.

#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;
}
Run Code Online (Sandbox Code Playgroud)

原始代码的已检测但未修复版本的输出为:

$ ./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
Run Code Online (Sandbox Code Playgroud)

请注意,父项在子项之前完成,因此提示出现在输出的中间.

"固定"代码的输出是:

$ ./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
$
Run Code Online (Sandbox Code Playgroud)

主要的重大变化是在两个函数中添加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评论中建议:

我建议您修改代码以int使用memcpy()而不是直接访问数据来复制描述符吗?它不一定是正确对齐的 - 这就是手册页示例也使用的原因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);
Run Code Online (Sandbox Code Playgroud)

分配fdptr似乎假设CMSG_DATA(cmsg)已经足够好地对齐以转换为a int *并且在memcpy()假设NUM_FD不仅仅是1时使用.据说,它应该指向数组buf,并且可能不够好与Nominal Animal建议对齐,所以在我看来,这fdptr只是一个闯入者,如果使用的例子会更好:

memcpy(CMSG_DATA(cmsg), myfds, NUM_FD * sizeof(int));
Run Code Online (Sandbox Code Playgroud)

然后接收端的反向过程是合适的.此程序仅传递单个文件描述符,因此代码可修改为:

memmove(CMSG_DATA(cmsg), &fd, sizeof(fd));  // Send
memmove(&fd, CMSG_DATA(cmsg), sizeof(fd));  // Receive
Run Code Online (Sandbox Code Playgroud)

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

鉴于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;
}
Run Code Online (Sandbox Code Playgroud)

并运行示例:

$ ./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
$
Run Code Online (Sandbox Code Playgroud)

  • @JonathanLeffler:我可以建议您修改代码以使用`memcpy()`复制描述符int,而不是直接访问数据吗?它不一定正确对齐-这就是手册页示例中还使用`memcpy()`的原因-并且在许多Linux体系结构中,未对齐的int访问会导致问题(多达SIGBUS信号会杀死进程)。我似乎还想起了各种操作系统上的历史问题。没有正常有效载荷数据的辅助数据,也可以通过发送至少一个虚拟字节来避免,但是我找不到任何要验证的引用,因此我可能会记错。 (2认同)
  • @CMCDragonkai 是的,可以在单个 sendmsg() 调用中传递多个控制消息。在 Linux 上,每种类型只能发送一个辅助消息(例如,一次 SCM_RIGHTS 加一个 SCM_CREDENTIALS)。据报道,在 FreeBSD 上,可以在单个 sendmsg() 中发送多个相同类型的消息;我没有测试过这个。有关 Linux 示例,请参阅 http://man7.org/tlpi/code/online/dist/sockets/scm_multi_recv.c.html 和 http://man7.org/tlpi/code/online/dist/sockets/scm_multi_send。 .html (2认同)