万事俱备,只欠东风。现在就差如何进行程序结构设计了。我们已经知道,可以通过辅助数据传递描述符了,那么接下来怎么做?
1. 目标
我们的目标是让进程 fork 一个子进程,子进程继承 unix 域套接字。子进程打开某个文件,然后将该文件描述符通过 sendmsg 发送给父进程。很好,这没什么难度。程序结构大概就如下:
int main() { /****************** 封装成 myOpen *******************/ // 调用 socketpair pid = fork(); if (pid == 0) { // 执行 openfile,它打开 filename 文件,将把描述符 sendmsg 给父进程 execl("./openfile", "./openfile", sockfd[1], filename, mode); exit(1); } // 从子进程读取数据 recvmsg(sockfd[0], &msg, 0); // 从 msg.msg_control 拿到描述符 fd ... /****************** 封装成 myOpen *******************/ // 从文件读取数据并打印到屏幕 read(fd, buf); write(STDOUT_FILENO, buf); }
上面的程序看起来非常清晰,但是实现起来还是挺麻烦。为了让程序结构看起来更清晰,我们将其模块化。
本文使用的程序在 gitos 上可以找到:
git clone https://git.oschina.net/ivan_allen/unp.git
如果你已经 clone 过这个代码了,请使用 git pull
更新一下。本节程序所使用的程序路径是 unp/program/unixdomainprotocols/passdescriptor
.
2. 父进程 mycat
为了将问题聚焦,我们将第 1 节中拿到文件描述符的代码封装到 myOpen 函数中,该函数与系统调用 open 的参数完全一样。
char buf[4096]; int main(int argc, char* argv[]) { int nr, fd; if (argc < 2) { fprintf(stderr, "usage: %s <filename>\n", argv[0]); return 1; } // 重点在于 myOpen 函数 fd = myOpen(argv[1], O_RDONLY); nr = read(fd, buf, 4096); write(STDOUT_FILENO, buf, nr); return 0; }
2.1 myOpen 函数
为了程序结构清晰,这里删除了所有错误处理。
- myOpen 函数
myOpen 函数主要做两件事:
1)fork 子进程,并 execlp,它把 unix 套接字描述符通过命令行参数传递给子进程 openfile(openfile 程序在第 3 节讲)。
2)执行 readfd 函数,拿到描述符
int myOpen(const char* pathname, int mode) { pid_t pid; int fd, ret, status, sockfd[2]; char c; socketpair(AF_LOCAL, SOCK_STREAM, 0, sockfd); pid = fork(); if (pid == 0) { // child close(sockfd[0]); execlp("./openfile", "./openfile", itoa(sockfd[1]), pathname, itoa(mode), NULL); } waitpid(pid, &status, 0); // readfd 主要是调用 recvmsg,拿到辅助数据中的描述符 readfd(sockfd[0], &c, 1, &fd); close(sockfd[0]); return fd; }
- readfd 函数
相信这里你应该不陌生,上一篇文章我们已经对辅助数据做了练习。readfd 函数主要做两件事:
1)recvmsg 读取子进程发来的数据
2)从辅助数据中取得描述符并返回
int readfd(int fd, char* buf, int size, int *recvfd) { int nr; struct msghdr msg; struct iovec iov[1]; struct cmsghdr *cmptr; union { struct cmsghdr cm; char control[CMSG_SPACE(sizeof(int))]; } control_un; msg.msg_control = control_un.control; msg.msg_controllen = sizeof(control_un.control); msg.msg_name = NULL; msg.msg_namelen = 0; iov[0].iov_base = buf; iov[0].iov_len = size; msg.msg_iov = iov; msg.msg_iovlen = 1; // 接收子进程发来的 msg nr = recvmsg(fd, &msg, 0); if ((cmptr = CMSG_FIRSTHDR(&msg)) != NULL && cmptr->cmsg_len == CMSG_LEN(sizeof(int))) { // 注意下面的条件判断 if (cmptr->cmsg_level != SOL_SOCKET) { ERR_QUIT("readfd: control level != SOL_SOCKET"); } if (cmptr->cmsg_type != SCM_RIGHTS) { ERR_QUIT("readfd: control type != SCM_RIGHTS"); } // 从辅助数据中取到描述符,保存到参数 recvfd 指向的内存 *recvfd = *((int*)CMSG_DATA(cmptr)); } else { *recvfd = -1; } return nr; }
上面值得一提的是,如果要使用辅助数据传递进程描述符,那么 cmsg_level 必须是 SOL_SOCKET,cmsg_type 必须是 SCM_RIGHTS 才行。只有将这两个成员设置成这样,内核才知道你要传递描述符。
有关 cmsg_level 和 cmsg_type 设置成不同的值表示什么含义,请参考第 5 节附录。
3. 子进程 openfile
子进程的框架也非常简单,大体上做两件事:
1)打开文件
2)使用 writefd 函数发送描述符
#include "common.h" int main(int argc, char* argv[]) { int sockfd, mode, fd, ret; char c; char *pathname; if (argc < 4) { ERR_QUIT("usage: <sockfd> <pathname> <mode>\n"); } // 从命令行解析套接字描述符,要打开的文件路径以及打开方式 sockfd = atoi(argv[1]); pathname = argv[2]; mode = atoi(argv[3]); // 打开文件 fd = open(pathname, mode); // 测试数据,并没有什么用 c = 'x'; // 将描述符发送出去 writefd(sockfd, &c, 1, fd); return 0; }
3.1 writefd 函数
该函数主要做两件事情:
1)填充辅助数据结构,把描述符装进去
2)使用 sendmsg 发送出去
int writefd(int fd, char* buf, int size, int sendfd) { int nw; struct msghdr msg; struct iovec iov[1]; struct cmsghdr *cmptr; union { struct cmsghdr cm; char control[CMSG_SPACE(sizeof(int))]; } control_un; 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; msg.msg_name = NULL; msg.msg_namelen = 0; iov[0].iov_base = buf; iov[0].iov_len = size; msg.msg_iov = iov; msg.msg_iovlen = 1; // 发送出去 nw = sendmsg(fd, &msg, 0); return nw; }
4. 程序运行
4.1 环境准备
图1 要准备的文件
我们想要查看的文件是 test,可惜的是,使用 cat 命令会提示权限不够。因为 test 文件的所有者是 zoro,所有组是 zoro. 当前用户是 allen,再加上 test 文件不允许 other 查看,allen 用户当然是没有权限去查看文件内容的。
另一方面,注意到 openfile 程序是所有组是 zoro,同时它开启了组 suid 属性(它的意思是执行 openfile 程序时,有效用户组 id 就是 openfile 的组 id)。
因此,mycat 程序就可以借助 openfile 打开 test 文件,从而读取文件内容。
4.2 实验结果
图2 运行结果
mycat 程序输出的第一行,是子进程 openfile 打开 test 时拿到的文件描述符的值,是 3. 输出的第二行,是 mycat 父进程 recv 到的描述符的值,注意它已经变化了,现在是 5.
这没有什么奇怪的,因为不同进程都有自己的进程空间,虽然两个进程的描述符值不一样,但是它们指向的内核中的文件表都是同一个。
图3 文件描述符与文件表
5. 附录
辅助数据的 cmsg_level 和 cmsg_type 为不同值时,传递的数据类型。
协议 | cmsg_level | cmsg_type | 说明 |
---|---|---|---|
IPv4 | IPPROTO_IP | IP_RECVDSTADDR IP_RECVIF | 随 UDP 数据报接收目的地址 随 UDP 数据报接收接口索引 |
Unix 域 | SOL_SOCKET | SCM_RIGHTS SCM_CREDENTIALS | 发送/接收描述符 发送/接收用户凭证 |
6. 总结
- 掌握使用 Unix 域协议在进程间传递描述符的方法
思考:子进程 openfile 在 writefd 后就立即退出了,没有关系吗?会不会出现子进程退出,而父进程还没来得及读取文件,就被关闭了?