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

2024-10-14 08:40:00
admin
原创
240
摘要:问题描述:我正在尝试通过 Linux 套接字发送一些文件描述符,但不起作用。我做错了什么?应该如何调试这样的事情?我尝试将 perror() 放在所有可能的地方,但他们声称一切正常。以下是我写的:#include <stdio.h> #include <stdlib.h>...

问题描述:

我正在尝试通过 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);
    }

}

解决方案 1:

Stevens (et al) UNIX® 网络编程,第 1 卷:套接字网络 API在第 15 章Unix 域协议(特别是 §15.7传递描述符)中描述了在进程之间传输文件描述符的过程。完整描述起来很麻烦,但必须在 Unix 域套接字(AF_UNIX或)上完成AF_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, '', 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
");
}

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
");

    struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg);

    unsigned char * data = CMSG_DATA(cmsg);

    err_remark("About to extract fd
");
    int fd = *((int*) data);
    err_remark("Extracted fd %d
", 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
");

    int pid = fork();
    if (pid > 0)  // in parent
    {
        err_remark("Parent at work
");
        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
", filename);

        wyslij(sock, fd);

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

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

        int fd = odbierz(sock);
        printf("Read %d!
", fd);
        char buffer[256];
        ssize_t nbytes;
        while ((nbytes = read(fd, buffer, sizeof(buffer))) > 0)
            write(1, buffer, nbytes);
        printf("Done!
");
        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在评论中建议:

我可以建议您修改代码以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);

分配给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

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

鉴于 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, '', 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
");
}

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
");

    struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);

    err_remark("About to extract fd
");
    int fd;
    memmove(&fd, CMSG_DATA(cmsg), sizeof(fd));
    err_remark("Extracted fd %d
", 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
");

    int pid = fork();
    if (pid > 0)  // in parent
    {
        err_remark("Parent at work
");
        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
", 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]]
", nbytes, buffer);

        wyslij(sock, fd);

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

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

        int fd = odbierz(sock);
        printf("Read %d!
", fd);
        char buffer[256];
        ssize_t nbytes;
        while ((nbytes = read(fd, buffer, sizeof(buffer))) > 0)
            write(1, buffer, nbytes);
        printf("Done!
");
        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
$
相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1335  
  信创产业的蓬勃发展推动着各行业数字化转型加速,数据库迁移作为其中关键一环,面临诸多挑战。信创数据库迁移旨在将传统数据库平稳过渡到信创环境,以满足自主可控、安全可靠的需求。这一过程涉及技术、业务等多方面因素,稍有不慎就可能出现各种问题,影响业务的正常运行。深入探讨信创数据库迁移过程中的常见问题及解决方案,对于保障迁移工作...
2027年信创国产化   0  
  随着信息技术的飞速发展,信创国产化成为了国家战略的重要组成部分。国产化信创产品名录涵盖了众多领域,其在各个关键应用场景中发挥着重要作用。而信创国产化操作系统作为其中的核心环节,具备五大核心优势,为我国信息技术产业的自主可控发展提供了坚实支撑。关键应用场景之办公领域在办公领域,国产化信创产品有着广泛且深入的应用。如今,越...
国产信创系统   0  
  随着信息技术的飞速发展,信创国产化操作系统在政府部门的推广应用具有重要的战略意义。它不仅关乎国家信息安全,更是推动国内信息技术产业自主创新、实现科技自立自强的关键举措。在当前复杂的国际形势下,政府部门积极推广信创国产化操作系统,对于保障国家政务信息的安全稳定运行,提升信息技术的自主可控能力,具有不可替代的重要作用。推广...
信创产品有哪些   0  
  在企业数字化转型的进程中,信创数据库解决方案的选择至关重要。它不仅关乎企业数据的安全存储与管理,更影响着企业业务的稳定运行与未来发展。合适的信创数据库能够助力企业在复杂多变的市场环境中提升竞争力,保障数据主权与安全。然而,面对市场上众多的信创数据库产品和解决方案,企业往往感到困惑,不知如何做出正确的选择。接下来,我们将...
信创电脑   0  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用