通过Linux socket发送文件描述符

我试图通过 linux socket发送一些文件描述符,但它不起作用.我究竟做错了什么?应该如何调试这样的东西?我试图把perror()到处都是可能的,但他们声称一切都OK.这是我写的:
#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,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,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);
    }

}

解决方法

Stevens(et al) UNIX® Network Programming,Vol 1: The Sockets Networking API描述了在第15章Unix域协议,特别是§15.7传递描述符之间的进程之间传输文件描述符的过程.完全描述,但必须在Unix域套接字(AF_UNIX或AF_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->cmsg_len;

    if (sendmsg(socket,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,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,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,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,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
$

主要的重大变化是将结构iovec添加到两个函数中的struct msghdr中的数据中,并为接收函数(odbierz())提供控制消息的空间.我在调试过程中报告了一个中间步骤,我将结构iovec提供给父级,父级的“消息太长”错误删除.为了证明它正在工作(传递了一个文件描述符),我添加了从传递的文件描述符中读取和打印文件代码.原始代码有睡眠(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

May I suggest you modify the code to copy the descriptor int using memcpy() instead of accessing the data directly? It is not necessarily correctly aligned — which is why the man page example also uses memcpy() — and there are many Linux architectures where unaligned int access causes problems (up to SIGBUS signal killing the process).

不仅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->cmsg_len;

对fdptr的赋值似乎假定CMSG_DATA(cmsg)足够好地对齐以转换为int *,并且假设NUM_FD不只是1,所以使用memcpy().据说,它应该是指向在数组buf,并且可能不能像Nominal Animal所示的那样很好地对齐,所以在我看来,fdptr只是一个interloper,如果使用的例子会更好:

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

然后在接收端的相反过程将是适当的.该程序只传递一个文件描述符,因此代码修改为:

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

I also seem to recall historical issues on varIoUs OSes w.r.t. ancillary data with no normal payload data,avoided by sending at least one dummy byte too,but I cannot find any references to verify,so I might remember wrong.

鉴于Mac OS X(其具有达尔文/ BSD基础)至少需要一个结构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,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),sizeof(fd));

    msg.msg_controllen = cmsg->cmsg_len;

    if (sendmsg(socket,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,even if it points to minimal data */
    char m_buffer[1];
    struct iovec io = { .iov_base = m_buffer,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,sizeof(fd));
    err_remark("Extracted fd %d\n",filename);

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

        wyslij(sock,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
$

相关文章

文件查找(find) 1 find 简单的说,就是实时查找指定的内容或条件。特点:最新、最快、最准确。 用法:...
非交互式添加分区 方法一 添加/deb/sdb 下的分区,其实位置为1到1000M,第二个分区位置为1001至3000M,...
编译安装httpd 1 去官网下载源码包 为避免非法软件,一定要去官网下载http://www.apache.org httpd-2.4...
gdisk用法 gdisk - InteractiveGUIDpartitiontable (GPT) manipulator GPTfdisk (akagdisk) isatext-mo...
1 一定用快捷键 这里简单的说下几个常用的快捷按键。 1.1 移动光标快捷键 Crtl + a 光标回到命令行...
bash shell中测试命令 test命令提供了if-than语句中测试不同条件的途径。如果test命令中列出的条件成立...