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

我想通过Linux套接字发送一些文件描述符,但它不工作。 我究竟做错了什么? 如何debugging这样的东西? 我试图把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); } } 

Solutions Collecting From Web of "通过Linux套接字发送文件描述符"

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

我从Mac OS X 10.10.1版本中得到了这个问题的代码稍微修改(和仪表化)的版本为我工作与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, &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)也可以完成这项工作。

对齐问题

名义动物在评论中建议:

我可以建议你修改代码复制描述符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->cmsg_len; 

fdptr的赋值似乎假定CMSG_DATA(cmsg)已经被很好的对齐,可以被转换为int *并且在假设NUM_FD不仅仅是1的情况下使用了memcpy() 。据说这应该是指向在阵列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(基于达尔文/ 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->cmsg_len; 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 $