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

时间:2022-02-17 22:00:15

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 by
unsigned 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字节数据。