《UNIX网络编程 卷1》 笔记: 描述符传递技术

来源:互联网 发布:易娱网络待遇好吗 编辑:程序博客网 时间:2024/06/03 18:13

Linux提供了一种从一个进程向另一个进程传递任意打开的描述符的技术,这两个进程可以无亲缘关系。这种技术要求首先在这两个进程之间创建一个Unix域套接字,然后使用sendmsg跨套接字发送一个特殊的消息,这个消息由内核来处理,会把打开的描述符传递到接收进程。

先来看看要使用的数据结构和函数。

struct msghdr {void *msg_name; /* optional address */socklen_t msg_namelen; /* size of address */struct iovec *msg_iov; /* scatter/gather array */size_t msg_iovlen; /* # elements in msg_iov */void *msg_control; /* ancillary data, see below */size_t msg_controllen; /* ancillary data buffer len */int msg_flags; /* flags on received message */};struct cmsghdr {size_t cmsg_len; /* Data byte count, including header (type is socklen_t in POSIX) */int cmsg_level; /* Originating protocol */int cmsg_type; /* Protocol-specific type *//* followed byunsigned char cmsg_data[]; */};ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
msghdr结构表示数据消息首部,msg_control指向辅助数据,msg_controllen指明了辅助数据的长度(包括辅助数据首部)。
cmsghdr结构体表示辅助数据首部。为了传递描述符,我们将cmsg_level取值为SOL_SOCKET,cmsg_type取值为SCM_RIGHTS,实际的辅助数据长度为4字节(描述符的大小)。为此我们定义了一个表示辅助数据的联合体:

union {struct cmsghdr cm; /*辅助数据首部*/char control[CMSG_SPACE(sizeof(int))]; /*包含4字节数据和辅助数据首部*/} control_un;
传递描述符的具体步骤如下:
    1. 如果是父子进程之间传递描述符,则父进程调用socketpair函数创建一个流管道。如果是无亲缘关系的进程,则进程之间使用Unix域套接字通信,就像上节我们给出的客户与服务器之间通信的程序一样。
    2. 发送进程打开一个描述符,创建一个msghdr结构,其中的辅助数据含有待传递的描述符,然后调用sendmsg函数发送描述符。即使之后进程调用close函数关闭描述符,但是对于接收进程它仍然保持打开的状态。因为发送一个描述符会使该描述符的引用计数加1。
    3. 接收进程调用recvmsg函数接收描述符。这个描述符的值并不一定和发送进程发送的描述符的值相同,但是它们都指向内核中相同的文件表项。

发送描述符的函数write_fd实现如下,参数fd是Unix域套接字描述符,参数sendfd是要发送的描述符。

ssize_t write_fd(int fd, void *ptr, size_t nbytes, int sendfd){struct msghdr msg;struct iovec iov[1];/*辅助数据*/union {struct cmsghdr cm;char control[CMSG_SPACE(sizeof(int))];} control_un;struct cmsghdr *cmptr;msg.msg_name = NULL;msg.msg_namelen = 0;iov[0].iov_base = ptr;iov[0].iov_len = nbytes;msg.msg_iov = iov;msg.msg_iovlen = 1;msg.msg_control = control_un.control;msg.msg_controllen = sizeof(control_un.control);cmptr = CMSG_FIRSTHDR(&msg);cmptr->cmsg_len = CMSG_LEN(sizeof(int));cmptr->cmsg_level = SOL_SOCKET;cmptr->cmsg_type = SCM_RIGHTS;*((int *)CMSG_DATA(cmptr)) = sendfd; /*要传递的描述符*/return sendmsg(fd, &msg, 0); /*发送数据*/}ssize_t Write_fd(int fd, void *ptr, size_t nbytes, int sendfd){ssize_t n;if ((n = write_fd(fd, ptr, nbytes, sendfd)) < 0)err_sys("write_fd error");return n;}
接收描述的函数read_fd的实现如下:

ssize_t read_fd(int fd, void *ptr, size_t nbytes, int *recvfd){struct msghdr msg;struct iovec iov[1];ssize_t n;/*辅助数据*/union {struct cmsghdr cm;char control[CMSG_SPACE(sizeof(int))];} control_un;struct cmsghdr *cmptr;msg.msg_name = NULL;msg.msg_namelen = 0;iov[0].iov_base = ptr;iov[0].iov_len = nbytes;msg.msg_iov = iov;msg.msg_iovlen = 1;msg.msg_control = control_un.control;msg.msg_controllen = sizeof(control_un.control);/*读取数据*/if ((n = recvmsg(fd, &msg, 0)) <= 0)return n;/*解析出辅助数据*/if ((cmptr = CMSG_FIRSTHDR(&msg)) != NULL && cmptr->cmsg_len == CMSG_LEN(sizeof(int))) {if (cmptr->cmsg_level != SOL_SOCKET)err_quit("control level != SOL_SOCKET");if (cmptr->cmsg_type != SCM_RIGHTS)err_quit("control type != SCM_RIGHTS");*recvfd = *((int *)CMSG_DATA(cmptr)); /*获取描述符*/} else *recvfd = -1;return n;}ssize_t Read_fd(int fd, void *ptr, size_t nbytes, int *recvfd){ssize_t n;if ( (n = read_fd(fd, ptr, nbytes, recvfd)) < 0)err_sys("read_fd error");return n;}

书中给出了一个描述符传递的例子,实现了两个程序mycat和openfile。mycat程序创建一个流管道,调用fork函数创建一个子进程,然后在子进程中调用execl函数执行openfile程序,将流管道一端的描述符(调用execl函数后已经打开的描述符不会关闭)和要打开的文件路径通过execl函数的传给openfile程序。openfile程序打开文件,然后将它的描述符通过流管道传递给父进程。父进程读取描述符,将文件输出到标准输出。

mycat程序的主体功能由my_open函数实现,代码如下:

int my_open(const char *pathname, int mode){int fd, sockfd[2], status;pid_t childpid;char c, argsockfd[10], argmode[10];/*创建一个流管道*/Socketpair(AF_LOCAL, SOCK_STREAM, 0, sockfd);if ((childpid = Fork()) == 0) {Close(sockfd[0]);/*流管道本进程端对应的描述符*/snprintf(argsockfd, sizeof(argsockfd), "%d", sockfd[1]);/*文件打开模式*/snprintf(argmode, sizeof(argmode), "%d", mode);/*int execl(const char *path, const char *arg0, ... , (char *)0 ); */execl("./openfile", "openfile", argsockfd, pathname, argmode, (char*)NULL); /*执行openfile程序*/err_sys("execl error");}Close(sockfd[1]);Waitpid(childpid, &status, 0); /*等待子进程终止*/if (WIFEXITED(status) == 0)err_quit("child did not terminate");if ((status = WEXITSTATUS(status)) == 0) /*子进程正常终止*/Read_fd(sockfd[0], &c, 1, &fd); /*读取子进程传递的文件描述符*/else { /*子进程执行出错*/errno = status; fd = -1;}Close(sockfd[0]);return fd;}
mycat程序的代码如下:

int main(int argc, char **argv){int fd, n;char buff[BUFFSIZE];if (argc != 2)err_quit("usage: mycat <pathname>");/*fork并execl openfile程序,打开一个文件传回其描述符到本进程*/if ((fd = my_open(argv[1], O_RDONLY)) < 0)err_sys("cannot open %s", argv[1]);while ((n = Read(fd, buff, BUFFSIZE)) > 0)Write(STDOUT_FILENO, buff, n);exit(0);}
openfile程序的代码如下:

int main(int argc, char **argv){int fd;if (argc != 4)err_quit("openfile <sockfd#> <filename> <mode>");if ((fd = open(argv[2], atoi(argv[3]))) < 0)exit((errno > 0) ? errno : 255);/*通过流管道发送描述符时,我们总是发送至少1字节数据*/if (write_fd(atoi(argv[1]), "", 1, fd) < 0)exit((errno > 0) ? errno : 255);exit(0);}
如注释所示:通过流管道发送描述符(辅助数据)时,我们总是发送至少1字节数据。

阅读全文
0 0
原创粉丝点击