Unix域套接字可以用于在同一个主机上的不同进程之间传递描述符,它可以视为IPC方法之一。它可以在进程间传递的描述符不限类型,这就是我们称之为“描述符传递”,而不是“文件描述符传递”。描述符是通过辅助数据发送的(结构体 msghdr 的 msg_control 成员),在发送和接收描述符时,总是发送至少 1 个字节的数据,即使这个数据没有任何实际意义。否则当接收返回 0 时,接收方将不能区分这意味着“没有数据”(但辅助数据可能有套接字)还是“文件结束符”
在前面的《linux在线调试摄像头驱动》中,我们在图像传输的进程中打开的camera的设备文件描述符进行图像实时传输。然后我们在新建一个调试用的进程,通过消息队列的方式把调试信息发送到图像传输的进程,然后再通过该进程把控制命令发送到驱动。在这里我们有另外的一种方式实现该功能,也就是将camera设备文件描述符直接发送到调试进程,在调试进程中直接操作camera。下面的代码是子进程的描述符传递到父进程的一个实例。
/*============================================================================= # FileName: unixdomain.c # Desc: child process send describe to father process # Author: licaibiao # LastChange: 2017-02-14 =============================================================================*/ #include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<unistd.h> #include<stdlib.h> #include<errno.h> #include<arpa/inet.h> #include<netinet/in.h> #include<string.h> #include<signal.h> #include<fcntl.h> #include<sys/un.h> #define MAXLINE 1024 #define LISTENLEN 10 #define HAVE_MSGHDR_MSG_CONTROL void sig_chld(int signo) { pid_t pid; int stat; while ((pid = waitpid(-1,stat,WNOHANG))>0) { //printf("child %d terminated \n",pid); } return ; } /* @fd :发送 TCP 套接字接口;这个可以是使用socketpair返回的发送套接字接口 @ptr :发送数据的缓冲区指针; @nbytes :发送的字节数; @sendfd :向接收进程发送的描述符; */ int write_fd(int fd, void *ptr, int nbytes, int sendfd) { struct msghdr msg; struct iovec iov[1]; // 有些系统使用的是旧的msg_accrights域来传递描述符,Linux下是新的msg_control字段 #ifdef HAVE_MSGHDR_MSG_CONTROL union{ // 前面说过,保证cmsghdr和msg_control的对齐 struct cmsghdr cm; char control[CMSG_SPACE(sizeof(int))]; }control_un; struct cmsghdr *cmptr; // 设置辅助缓冲区和长度 msg.msg_control = control_un.control; msg.msg_controllen = sizeof(control_un.control); // 只需要一组附属数据就够了,直接通过CMSG_FIRSTHDR取得 cmptr = CMSG_FIRSTHDR(&msg); // 设置必要的字段,数据和长度 cmptr->cmsg_len = CMSG_LEN(sizeof(int)); // fd类型是int,设置长度 cmptr->cmsg_level = SOL_SOCKET; cmptr->cmsg_type = SCM_RIGHTS; // 指明发送的是描述符 *((int*)CMSG_DATA(cmptr)) = sendfd; // 把fd写入辅助数据中 #else msg.msg_accrights = (caddr_t)&sendfd; // 这个旧的更方便啊 msg.msg_accrightslen = sizeof(int); #endif // UDP才需要,无视 msg.msg_name = NULL; msg.msg_namelen = 0; // 别忘了设置数据缓冲区,实际上1个字节就够了 iov[0].iov_base = ptr; iov[0].iov_len = nbytes; msg.msg_iov = iov; msg.msg_iovlen = 1; return sendmsg(fd, &msg, 0); } int read_fd(int fd, void *ptr, int nbytes, int *recvfd) { struct msghdr msg; struct iovec iov[1]; int n; int newfd; #ifdef HAVE_MSGHDR_MSG_CONTROL union{ // 对齐 struct cmsghdr cm; char control[CMSG_SPACE(sizeof(int))]; }control_un; struct cmsghdr *cmptr; // 设置辅助数据缓冲区和长度 msg.msg_control = control_un.control; msg.msg_controllen = sizeof(control_un.control); #else msg.msg_accrights = (caddr_t) &newfd; // 这个简单 msg.msg_accrightslen = sizeof(int); #endif // TCP无视 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; // 设置结束,准备接收 if((n = recvmsg(fd, &msg, 0)) <= 0) { return n; } #ifdef HAVE_MSGHDR_MSG_CONTROL // 检查是否收到了辅助数据,以及长度,回忆上一节的CMSG宏 cmptr = CMSG_FIRSTHDR(&msg); if((cmptr != NULL) && (cmptr->cmsg_len == CMSG_LEN(sizeof(int)))) { // 还是必要的检查 if(cmptr->cmsg_level != SOL_SOCKET) { printf("control level != SOL_SOCKET/n"); exit(-1); } if(cmptr->cmsg_type != SCM_RIGHTS) { printf("control type != SCM_RIGHTS/n"); exit(-1); } // 好了,描述符在这 *recvfd = *((int*)CMSG_DATA(cmptr)); } else { if(cmptr == NULL) printf("null cmptr, fd not passed./n"); else printf("message len[%d] if incorrect./n",(int)cmptr->cmsg_len); *recvfd = -1; // descriptor was not passed } #else if(msg.msg_accrightslen == sizeof(int)) *recvfd = newfd; else *recvfd = -1; #endif return n; } int main(int argc, char **argv) { int writefd, readfd, sockfd[2]; pid_t childpid; char *write_ptr, *read_ptr; write_ptr = (char*)malloc(1); read_ptr = (char*)malloc(1); signal(SIGCHLD, sig_chld); socketpair(AF_LOCAL, SOCK_STREAM, 0, sockfd); if ( (childpid = fork()) == 0) { close(sockfd[0]); writefd = open(argv[1], O_RDWR|O_CREAT|O_APPEND); write_fd(sockfd[1], write_ptr, 1, writefd); } close(sockfd[1]); read_fd(sockfd[0], read_ptr, 1, &readfd); write(readfd, argv[2], strlen(argv[2])); close(readfd); free(write_ptr); free(read_ptr); return 0; }
上面程序,在子进程中打开一个文件,然后再把该文件描述符传递到父进程,随后父进程往该文件中写入数据,运行结果如下:
root@ubuntu:/home/share/test# gcc unixdomain.c -o unixdomain root@ubuntu:/home/share/test# ./unixdomain /tmp/testfile licaibiao root@ubuntu:/home/share/test# cat /tmp/testfile licaibiao root@ubuntu:/home/share/test#创建文件/tmp/testfile,然后写入字符串licaibiao。
上面的程序是赋值进程间传递描述符,其实它还可以在没有亲属关系的进程间传递描述符。
下面看发送端程序:
/*============================================================================= # FileName: senddescribe.c # Desc: send describe to other process # Author: licaibiao # LastChange: 2017-02-14 =============================================================================*/ #include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<sys/un.h> #include<unistd.h> #include<stdlib.h> #include<errno.h> #include<arpa/inet.h> #include<netinet/in.h> #include<string.h> #include<signal.h> #include <unistd.h> #include <fcntl.h> #define MAXLINE 1024 #define LISTENLEN 10 #define SERV_PORT 6666 #define UNIXSTR_PATH "/tmp/path" #define HAVE_MSGHDR_MSG_CONTROL void sig_chld(int signo) { pid_t pid; int stat; while ((pid = waitpid(-1,stat,WNOHANG))>0) { //printf("child %d terminated \n",pid); } return ; } int write_fd(int fd, void *ptr, int nbytes, int sendfd) { struct msghdr msg; struct iovec iov[1]; // 有些系统使用的是旧的msg_accrights域来传递描述符,Linux下是新的msg_control字段 #ifdef HAVE_MSGHDR_MSG_CONTROL union{ // 前面说过,保证cmsghdr和msg_control的对齐 struct cmsghdr cm; char control[CMSG_SPACE(sizeof(int))]; }control_un; struct cmsghdr *cmptr; // 设置辅助缓冲区和长度 msg.msg_control = control_un.control; msg.msg_controllen = sizeof(control_un.control); // 只需要一组附属数据就够了,直接通过CMSG_FIRSTHDR取得 cmptr = CMSG_FIRSTHDR(&msg); // 设置必要的字段,数据和长度 cmptr->cmsg_len = CMSG_LEN(sizeof(int)); // fd类型是int,设置长度 cmptr->cmsg_level = SOL_SOCKET; cmptr->cmsg_type = SCM_RIGHTS; // 指明发送的是描述符 *((int*)CMSG_DATA(cmptr)) = sendfd; // 把fd写入辅助数据中 #else msg.msg_accrights = (caddr_t)&sendfd; // 这个旧的更方便啊 msg.msg_accrightslen = sizeof(int); #endif // UDP才需要,无视 msg.msg_name = NULL; msg.msg_namelen = 0; // 别忘了设置数据缓冲区,实际上1个字节就够了 iov[0].iov_base = ptr; iov[0].iov_len = nbytes; msg.msg_iov = iov; msg.msg_iovlen = 1; return sendmsg(fd, &msg, 0); } int openfile(void) { int fd; char *write_buff = "I am Server"; fd = open("/tmp/test1",O_RDWR|O_CREAT|O_APPEND); write(fd, write_buff, strlen(write_buff)); return fd; } int main(int argc, char **argv) { int listenfd, connfd ,sendfd; pid_t childpid; socklen_t clilen; struct sockaddr_un cliaddr, servaddr; char *ptr; sendfd = openfile(); ptr = (char*)malloc(1); listenfd = socket(AF_LOCAL, SOCK_STREAM, 0); unlink(UNIXSTR_PATH); bzero(&servaddr, sizeof(servaddr)); servaddr.sun_family = AF_LOCAL; strcpy(servaddr.sun_path, UNIXSTR_PATH); bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)); listen(listenfd, LISTENLEN); signal(SIGCHLD, sig_chld); connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &clilen); write_fd(connfd, ptr, 1,sendfd); sleep(1); close(sendfd); free(ptr); }再看接收端的程序:
/*============================================================================= # FileName: recvdescribe.c # Desc: read describe from other process # Author: licaibiao # LastChange: 2017-02-14 =============================================================================*/ #include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<sys/un.h> #include<unistd.h> #include<stdlib.h> #include<errno.h> #include<arpa/inet.h> #include<netinet/in.h> #include<string.h> #include<signal.h> #define MAXLINE 1024 #define LISTENLEN 10 #define SERV_PORT 6666 #define UNIXSTR_PATH "/tmp/path" #define HAVE_MSGHDR_MSG_CONTROL int read_fd(int fd, void *ptr, int nbytes, int *recvfd) { struct msghdr msg; struct iovec iov[1]; int n; int newfd; #ifdef HAVE_MSGHDR_MSG_CONTROL union{ // 对齐 struct cmsghdr cm; char control[CMSG_SPACE(sizeof(int))]; }control_un; struct cmsghdr *cmptr; // 设置辅助数据缓冲区和长度 msg.msg_control = control_un.control; msg.msg_controllen = sizeof(control_un.control); #else msg.msg_accrights = (caddr_t) &newfd; // 这个简单 msg.msg_accrightslen = sizeof(int); #endif // TCP无视 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; // 设置结束,准备接收 if((n = recvmsg(fd, &msg, 0)) <= 0) { return n; } #ifdef HAVE_MSGHDR_MSG_CONTROL // 检查是否收到了辅助数据,以及长度,回忆上一节的CMSG宏 cmptr = CMSG_FIRSTHDR(&msg); if((cmptr != NULL) && (cmptr->cmsg_len == CMSG_LEN(sizeof(int)))) { // 还是必要的检查 if(cmptr->cmsg_level != SOL_SOCKET) { printf("control level != SOL_SOCKET/n"); exit(-1); } if(cmptr->cmsg_type != SCM_RIGHTS) { printf("control type != SCM_RIGHTS/n"); exit(-1); } // 好了,描述符在这 *recvfd = *((int*)CMSG_DATA(cmptr)); } else { if(cmptr == NULL) printf("null cmptr, fd not passed./n"); else printf("message len[%d] if incorrect./n", (int)cmptr->cmsg_len); *recvfd = -1; // descriptor was not passed } #else if(msg.msg_accrightslen == sizeof(int)) *recvfd = newfd; else *recvfd = -1; #endif return n; } int main(int argc, char **argv) { int sockfd, recvfd; struct sockaddr_un servaddr; char *ptr; ptr = (char*)malloc(1); sockfd = socket(AF_LOCAL, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sun_family = AF_LOCAL; strcpy(servaddr.sun_path, UNIXSTR_PATH); connect(sockfd, (struct sockaddr*) &servaddr, sizeof(servaddr)); read_fd(sockfd, ptr, 1, &recvfd); write(recvfd, argv[1], strlen(argv[1])); close(recvfd); free(ptr); exit(0); }
运行结果如下:
root@ubuntu:/home/share/test# ./senddescribe & [1] 5547 root@ubuntu:/home/share/test# ./recvdescribe licaibiao root@ubuntu:/home/share/test# [1]+ Done ./senddescribe root@ubuntu:/home/share/test# root@ubuntu:/home/share/test# root@ubuntu:/home/share/test# cat /tmp/test1 I am Serverlicaibiao root@ubuntu:/home/share/test#可以看到,发送端打开一个文件之后写入 字符串: I am Server ,随后将该文件描述符发送出去,接收端接收到描述符之后,在该文件中输入字符串licaibiao,该程序测试成功。描述符传递看起来好像很简单,然而实际操作起来并不像看起来那样单纯。
有下面几个注意点:
1 需要注意的是传递描述符并不是传递一个 int 型的描述符编号,而是在接收进程中创建一个新的描述符,并且在内核的文件表中,它与发送进程发送的描述符指向相同的项。
2 在进程之间可以传递任意类型的描述符,比如可以是 pipe , open , mkfifo 或 socket , accept 等函数返回的描述符,而不限于套接字。
3 一个描述符在传递过程中(从调用 sendmsg 发送到调用 recvmsg 接收),内核会将其标记为“在飞行中”( in flight )。在这段时间内,即使发送方试图关闭该描述符,内核仍会为接收进程保持打开状态。发送描述符会使其引用计数加 1 。
4 描述符是通过辅助数据发送的(结构体 msghdr 的 msg_control 成员),在发送和接收描述符时,总是发送至少 1 个字节的数据,即使这个数据没有任何实际意义。否则当接收返回 0 时,接收方将不能区分这意味着“没有数据”(但辅助数据可能有套接字)还是“文件结束符”。
5 具体实现时, msghdr 的 msg_control 缓冲区必须与 cmghdr 结构对齐,可以看到后面代码的实现使用了一个 union 结构来保证这一点。