在进程间传送fd(文件描述符)能确保guest进程在不了解文件或者设备节点细节的情况下对于拿到的fd的I/O操作能力,这体现一种封装的思想,在实际使用中也非常有意义。
比如,我本人就曾经碰见一个需要类似技术的问题:
在开发的某款嵌入式设备中,应用程序主进程需要使用串口控制可能接入的外设,但是出厂测试程序作为一个单独的测试进程也需要打开串口节点对串口硬件是否正常进行测试,之前主进程对串口的使用原则是使用即打开,使用完关闭,所以因为两个进程同时使用串口的时机几乎没有,所以大体也相安无事。
但是各个使用处没有互斥,经常出问题,同时因为硬件差异跟软件逻辑耦合过高,导致往新的平台迁移工作量较大,所以在剥离硬件差异进行封装时对串口做了架构调整:串口只在开机时打开,同时每个串口配有一个状态和一把锁,fd不公开,对应用层只提供有限的read/write/select/ioctl几个函数指针,采用注册的方式,这样往新平台上迁移,改动就能集中到一个文件里,不同满工程里面改,同时各种宏弄得维护人员晕头转向,优化之后,相关问题得到有效解决。
但是,问题来了,产品部门过来找我,说他们产线生产的设备出厂测试全部失败,看了下,是因为设备节点已经被一个进程打开,所以另外一个open失败,当时知识储备不足,不懂进程间共享fd的相关知识,且时间比较紧急,所以采用了比较拙计的方式临时解决了问题,说出来很不光彩,就不多言了,免的被笑。
后来抽时间查了查,有更好的解决方案,今天分享出来。
*nix内核使用三种数据结构来表示打开的文件:
1,每个进程的进程表中都有一个记录项,其中包含一张打开的文件描述符表,包含一个文件描述符标志和一个指向文件表项的指针。
2,内核为所有的打开文件统一维护一张文件表,包含文件状态标志,当前文件偏移量,指向该文件i节点(unix为v节点,linux有两个i节点,一个依赖文件文件系统,一个独立于文件系统,此为类似unix的v节点的i节点)表项的指针。
3,每个打开文件都有一个i(v)节点,包含文件类型以及各种操作文件的函数指针,有的还包含该文件的i节点(此为文件系统i节点,包含文件所有者,长度,所在磁盘,数据块在磁盘中的位置等信息)
参见下图:
ps:图取自unix高级环境编程,所以图中有v节点
可以看到,每个进程中所使用的fd项实际上最终都是在内核中那张统一的文件表格中得到了统一,这也是进程间能传送文件描述符的基础。
发送进程实际上向接受进程传送一个指向文件表项的指针。该指针被分配存放在接受进程的第一个可用描述符项中,虽然名为传送描述符,但是发送和接受进程的实际使用描述符是几乎不可能相同的,
当发送进程传送给接收进程后,通常关闭该描述符,只是在描述符表中去掉该描述符,文件在全局文件表中仍然存在,相当于交出监护权吧。
为了更好演示相关传送,将apue中一段代码改了改,分享出来
发送端:
/*************************************************************
* file: unix_sock_fd_sender.c
* brief:文件描述符传送发送端
* yejing@2015.3.4 1.0 creat
*************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <fcntl.h>
#include <stddef.h>
/*********************************
struct msghdr{
void *msg_name;
socklen_t msg_namelen;
struct iovec *msg_iov;
int msg_iovlen;
void *msg_control;
socklen_t msg_controllen;
int msg_flags;
};
struct cmsghhdr {
socklen_t cmsg_len;
int cmsg_level;
int cmsg_type;
}
*********************************/
#define CONTROLLEN CMSG_LEN(sizeof(int))
static struct cmsghdr* cmptr = NULL;
int fd_sender(int fd, int fd_to_send){
struct iovec iov[1];
struct msghdr msg;
char buf[2];
iov[0].iov_base = buf;
iov[0].iov_len = sizeof(buf);
msg.msg_iov = iov;
msg.msg_iovlen = 1;
msg.msg_name = NULL;
msg.msg_namelen = 0;
if(fd_to_send < 0) {
msg.msg_control = NULL;
msg.msg_controllen = 0;
buf[1] = -fd_to_send;
if(0 == buf[1])
buf[1] = 1;
}
else {
if(cmptr == NULL && (cmptr = malloc(CONTROLLEN)) == NULL)
return -1;
cmptr->cmsg_level = SOL_SOCKET;
cmptr->cmsg_type = SCM_RIGHTS;
cmptr->cmsg_len = CONTROLLEN;
msg.msg_control = cmptr;
msg.msg_controllen = CONTROLLEN;
*(int *)CMSG_DATA(cmptr) = fd_to_send;
buf[1] = 0;
}
printf("%s->%d \n", __func__, __LINE__);
buf[0] = 0;
if(sendmsg(fd, &msg, 0) != 2)
return -1;
printf("waiting, input ctrl+c quit \n");
while(1);
return 0;
}
int main(int argc, char* argv[]){
int sockfd, size, fd_tosend;
struct sockaddr_un un;
un.sun_family = AF_UNIX;
strcpy(un.sun_path, "/ipcteam/yejingyf2/data/sth/tmp");
sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);
printf("%s->%d \n", __func__, __LINE__);
if(!sockfd)
return -1;
size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
printf("%s->%d \n", __func__, __LINE__);
if(bind(sockfd, (struct sockaddr *)&un, size) < 0)
printf("bind error \n");
printf("%s->%d \n", __func__, __LINE__);
fd_tosend = open("/ipcteam/yejingyf2/data/sth/tmp1", O_RDWR, O_CREAT | O_EXCL);
fd_sender(sockfd, fd_tosend);
printf("%s->%d \n", __func__, __LINE__);
return;
}
接收端:
/*************************************************************
* file: unix_sock_fd_receiver.c
* brief:文件描述符传送接受端
* yejing@2015.3.4 1.0 creat
*************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <fcntl.h>
#include <stddef.h>
/*********************************
struct msghdr{
void *msg_name;
socklen_t msg_namelen;
struct iovec *msg_iov;
int msg_iovlen;
void *msg_control;
socklen_t msg_controllen;
int msg_flags;
};
struct cmsghhdr {
socklen_t cmsg_len;
int cmsg_level;
int cmsg_type;
}
*********************************/
#define CONTROLLEN CMSG_LEN(sizeof(int))
static struct cmsghdr* cmptr = NULL;
// int fd_receiver(int fd, ssize_t (*userfunc)(int, const void*, size_t)) {
int fd_receiver(int fd){
int newfd, nr, status;
char *ptr;
char buf[1024];
struct iovec iov[1];
struct msghdr msg;
status = 1;
while(1){
iov[0].iov_base = buf;
iov[0].iov_len = sizeof(buf);
msg.msg_iov = iov;
msg.msg_iovlen = 1;
msg.msg_name = NULL;
if(!cmptr && !(cmptr = malloc(CONTROLLEN)))
return -1;
msg.msg_control = cmptr;
msg.msg_controllen = CONTROLLEN;
if((nr = recvmsg(fd, &msg, 0)) < 0) {
printf("recvmsg error\n");
}
else if(nr == 0) {
printf("connection closed by server\n");
return -1;
}
for(ptr = buf; ptr < &buf[nr]; ){
if(*ptr++ == 0){
if(ptr != &buf[nr - 1])
printf("message format error");
status = *ptr & 0xff;
if(status == 0){
if(msg.msg_controllen != CONTROLLEN)
printf("status = 0 but no fd \n");
newfd = *(int *)CMSG_DATA(cmptr);
}
else
newfd = -status;
}
nr -= 2;
}
// if(nr && (*userfunc)(STDERR_FILENO, buf, nr) != nr)
return -1;
if(status >= 0)
return newfd;
}
}
int main(int argc, char* argv[]){
int sockfd, size, fd_tosend;
if(!sockfd)
return -1;
struct sockaddr_un un;
un.sun_family = AF_UNIX;
strcpy(un.sun_path, "/ipcteam/yejingyf2/data/sth/tmp");
sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if(!sockfd)
return -1;
size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
if(bind(sockfd, (struct sockaddr *)&un, size) < 0)
printf("bind error \n");
//fd_receiver(sockfd, fd_tosend);
fd_receiver(sockfd);
return 1;
}