使用Unix域套接字在进程间传递文件描述符
下面的实例展示了如何使用Unix域套接字在进程间传递文件描述符
参考文献:1) 《Unix网络编程》
2)
最近学习了使用Unix域套接字在进程间传递文件描述符,仿照参考资料,自己也写了简单的程序来实践这种技术。
其他不多说了,具体理论知识参见参考资料,开始我自己的程序介绍(在OpenSolaris 2009.06平台上测试):
1 程序作用说明:父进程,子进程以及另外一个进程向同一个文件的文件描述符向这个文件中写内容。
具体如下:
1)父进程指定要打开的文件名,打开权限,打开模式;
2)fork一个子进程;
3)子进程调用execl函数来执行程序openfile:该新程序取得指定文件的文件描述符;向指定文件中写入“openfileprog write test”;向父进程返回该文件描述符;
4)父进程收到该文件描述符后,向文件中写“paraent process write ppp”;
5)父进程作为server端建立域socket等待客户端进程连接;
6)客户端进程连接父进程;
7)父进程向该客户端进程返回从子进程得到的文件描述符;
8)客户端进程收到该文件描述符后使用它在文件中写“this is client process ccc”;
其中父子进程传递文件描述符通过建立的一对套接字管道,父进程和客户端进程传递文件描述符通过Unix域套接字。
2 具体代码说明:
1) 首先看openfile程序,这时子进程通过调用execl执行的。调用方法如下:
execl("./openfileprog", "openfileprog", permit, mode, argsockfd, (char *)NULL);
其中参数1: openfile程序路径;
参数2: openfile程序名;
参数3: 待打开文件的权限;
参数4: 待打开文件模式;
参数5: 父进程建立的一对套接字管道的其中之一;
作为openfile程序,主要按照execl传的参数,打开指定文件,取得文件描述符;向该文件中写入内容;然后调用func_send_fd函数通过argsockfd把取得的文件描述符传给父进程。该程序代码如下:
int main(int argc, char *argv[]) /* openfileprog */
{
int i, fd, ret;
ssize_t size;
size_t buflen;
char data[10];
char buf[] = "openfileprog write test\n"; /* 向文件中写入的内容 */
/* execl("./openfileprog", permit, mode, argsockfd, (char *)NULL); */
fd = -1;
if((fd = open("./file", atoi(argv[1]), atoi(argv[2]))) < 0)
{
printf("in openfileprog, open failed\n");
exit(-1);
}
size = -1;
buflen = sizeof(buf);
if((size = write(fd, buf, buflen)) <= 0)
{
printf("in openfileprog, write failed\n");
}
/* 把设定的data信息也传给父进程 */
ret = 'a';
for(i = 0; i < sizeof(data); i++, ret++)
{
data[i] = ret;
}
data[sizeof(data) - 1] = '\0';
ret = -1;
if(0 > (ret = func_send_fd(atoi(argv[3]), fd, data, 10)))
{
printf("in openfileprog, func_send_fd failed\n");
}
close(fd);
return 0;
}
func_send_fd函数负责把取得的文件描述符传出去:
int func_send_fd(int send_sock, int send_fd, void *data, int bytes)
{
struct msghdr msghead;
struct iovec passdata[1];
int ret;
/* 填充msghead结构 */
msghead.msg_accrights = (caddr_t)&send_fd;
msghead.msg_accrightslen = sizeof(send_fd);
msghead.msg_name = NULL;
msghead.msg_namelen = 0;
passdata[0].iov_base = data;
passdata[0].iov_len = bytes;
msghead.msg_iov = passdata;
msghead.msg_iovlen = 1;
/* 发送信息 */
if(0 > (ret = sendmsg(send_sock, &msghead, 0)))
{
printf("in func_send, send_fd is %d, sendsock is %d, sendmsg failed,errno is %d\n", send_fd,send_sock,errno);
return -1;
}
return ret;
}
在上述两个函数之前,加上以下必要头文件和宏:
#include
#include
#include
#include
#include
#include
#include
#define SLEEPTIME 3
#define ARGLEN 20
以上作为一个c文件。
2)然后看父进程代码
下面是父进程程序:
int main(int argc, char *argv)
{
int status,sockfd[2];
char permit[ARGLEN];
char mode[ARGLEN];
char argsockfd[ARGLEN];
int recvfd;
char data[20];
int bytes;
int ret,i;
ssize_t size;
int buflen;
pid_t pid,chldpid;
/* 以下几行是使用域套接字必要变量 */
int fdsock, fdaccept;
struct sockaddr_un addr_server;
int len;
const char path[] = "/export/home/temp/test/other_prog/fengxianzhong";
/* 以下是父进程写入文件的内容 */
char buf[] = "paraent process write ppp\n";
/* 父进程同时向处理向client发送的数据 */
char datasend[] = "send by myopen\n";
memset(permit, '\0', sizeof(permit));
memset(mode, '\0', sizeof(mode));
memset(argsockfd, '\0', sizeof(argsockfd));
memset(data, '\0', sizeof(data));
printf("now it is parent process,now will fork a child process\n");
sleep(SLEEPTIME);
/* 设置文件权限和打开模式 */
snprintf(permit, sizeof(permit), "%d",PERMIT);
snprintf(mode, sizeof(mode), "%d",MODE);
// printf("in myopen %s, %s\n", permit, mode);
/* 建立和子进程通信的socket套接字管道 */
ret = socketpair(AF_UNIX,SOCK_STREAM,0,sockfd);
if(0 > ret)
{
printf("socketpair failed,errno is %d \n",errno);
}
/* fork 子进程 */
if(0 == (chldpid = fork())) /* child process */
{
printf("now it is child process, sendsock is %d\n",sockfd[1]);
close(sockfd[0]);
snprintf(argsockfd, sizeof(argsockfd), "%d", sockfd[1]);
/* 子进程中执行新程序openfile */
execl("./openfileprog", "openfileprog", permit, mode, argsockfd, (char *)NULL);
printf("execl failed, perimit is %s, mode is %s\n",permit, mode);
exit(-1);
}
/* paraent process start to write the file opened by child process */
printf("now it is parent process\n");
close(sockfd[1]);
bytes = sizeof(data);
/* 等待子进程结束 */
pid = wait(&status);
if((status = WEXITSTATUS(status)) == 0) /* child process terminate */
{
printf("child %d process terminate,now parent will write file ...\n",pid);
}
/* 从子进程取得文件描述符 */
recvfd = -1;
// printf("recv sock is %d\n", sockfd[0]);
ret = func_recv_fd(sockfd[0], &recvfd, data, bytes);
if(ret < 0)
{
printf("paraent recv failed\n");
}
/*
else
{
printf("fd %d paraent recv %d bytes data is %s\n", recvfd,strlen(data),data);
}
*/
/* 向文件写入数据 */
size = -1;
buflen = sizeof(buf);
if((size = write(recvfd, buf, buflen)) <= 0)
{
printf("in openfileprog, write failed\n");
}
/* 父进程作为server建立域套接字,等待client连接 */
printf("parent write over! Accept other process ......\n");
fdsock = socket(AF_UNIX, SOCK_STREAM, 0);
if(-1 == fdsock)
{
printf("myopen creat socket error!errno is %d\n", errno);
}
unlink(path);
memset(&addr_server, 0, sizeof(addr_server));
addr_server.sun_family = AF_UNIX;
strcpy(addr_server.sun_path, path);
len = sizeof(struct sockaddr_un);
ret = bind(fdsock, (struct sockaddr*)&addr_server, len);
if(-1 == ret)
{
printf("in myopen bind error, errorno is %d\n",errno);
close(fdsock);
unlink(path);
}
ret = listen(fdsock,1);
if(-1 == ret)
{
printf("in myopen listen error, errorno is %d\n",errno);
close(fdsock);
unlink(path);
}
fdaccept = accept(fdsock, (struct sockaddr*)&addr_server, &len);
if(-1 == ret)
{
printf("in myopen accept error, errorno is %d\n",errno);
close(fdsock);
unlink(path);
}
/* 向已经连接的client传递该文件描述符 */
ret = func_send_fd(fdaccept, recvfd, datasend, sizeof(datasend));
if(0 > ret)
{
printf("in myopen, func_send_fd failed\n");
}
printf("send fd over! Will sleep 10s \n");
sleep(10);
exit(0);
}
func_recv_fd函数负责从子进程接受文件描述符:
int func_recv_fd(int recv_sock, int *recvfd, void *data, int bytes)
{
struct msghdr msghead;
struct iovec passdata[1];
int ret;
int temp;
int newfd;
struct cmsghdr *msgptr1;
struct cmsghdr *msgptr = NULL;
memset(&msghead, 0, sizeof(msghead));
/* 同func_send_fd ,填充所需要的结构 */
msghead.msg_accrights = (caddr_t)&newfd;
msghead.msg_accrightslen = sizeof(recvfd);
msghead.msg_name = NULL;
msghead.msg_namelen = 0;
passdata[0].iov_base = data;
passdata[0].iov_len = bytes;
msghead.msg_iov = passdata;
msghead.msg_iovlen = 1;
/* 接收信息(文件描述符 )*/
if(0 > (ret = recvmsg(recv_sock, &msghead, 0)))
{
printf("in func_recv_fd, recvmsg failed\n");
return -1;
}
if(msghead.msg_accrightslen == sizeof(recvfd))
{
*recvfd = newfd; /* 文件描述符 */
}
return ret;
}
其中父进程向client进程发送文件描述符也使用了func_send_fd函数,该函数在此c文件中重新写了一遍;其实没有必要这样重复。我们可以把它作为一个库来使用;不过这里暂且这样使用。函数代码参考上面所写的。
在这个c文件中还要加入以下头文件和宏定义:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SLEEPTIME 3
#define ARGLEN 20
#define MODE S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IROTH /* -rwxr--r-- */
#define PERMIT O_RDWR | O_APPEND | O_CREAT /* if the file not exit ,creat it , data written to it append */
3)最后看client进程代码:
int main()
{
int sockfd, recvfd,ret;
struct sockaddr_un addr_client;
int length,buflen;
char data[10];
ssize_t size;
const char path[] = "/export/home/temp/test/other_prog/fengxianzhong";
char buf[] = "this is client process ccc\n" ;
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
if(-1 == sockfd)
{
printf("client creat socket error!errno is %d\n", errno);
}
addr_client.sun_family = AF_UNIX;
strcpy(addr_client.sun_path, path);
length = sizeof(addr_client.sun_family) + sizeof(addr_client.sun_path);
ret = connect(sockfd, (struct sockaddr*)&addr_client, length);
if(-1 == ret)
{
printf("in client connect error, errorno is %d\n",errno);
close(sockfd);
}
ret = func_recv_fd(sockfd, &recvfd, data, sizeof(data));
if(-1 == ret)
{
printf("in client func_recv_fd failed\n");
close(sockfd);
}
size = -1;
buflen = sizeof(buf);
if((size = write(recvfd, buf, buflen)) <= 0)
{
printf("in openfileprog, write failed\n");
}
printf("client write over!\n");
exit(0);
}
其中同样调用了func_recv_fd函数。在这里,我们对它的处理同func_send_fd函数。
不要忘了再加上以下头文件和宏定义:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SLEEPTIME 3
#define ARGLEN 20
以上3个部分代码分别作为3个c文件编译即可。
总结:上面的函数如func_recv_fd,func_send_fd代码被重复使用了,这样显得很多余。就像上面所说的可以使用其他方法来避免如此重复。但是我这里重点实践文件描述符的传递,其余的,就不去多追究了。
参考资料中以下几段话值得我们去理解:
进程可以用任何返回描述符的UNIX函数打开一个描述符:例如open()、pipe()、mkfifo()、socket()或者accept()。可以在进程间传递任何类型的描述符。
(3)发送进程建立一个msghdr结构,其中包含要传递的描述符。在POSIX中说明该描述符作为辅助数据发送,但老的实现使用msg_accright成员。(这里,在Opensolaris上,我使用的就是老的成员)发送进程调用sendmsg()通过第一部得到的UNIX域套接字发出套接字。这时这个描述符是在飞行中的。即使在发送进程调用sendmsg()之后,但在接受进程调用recvmsg()之前将描述符关闭,它仍会为接收进程保持打开状态。描述符的发送导致它的访问统计数加1。
(4)接收进程调用recvmsg()在UNIX域套接字上接收套接字。通常接收进程收到的描述符的编号和发送进程中的描述符的编号不同,但这没有问题。传递描述符不是传递描述符的编号,而是在接收进程中建立一个新的描述符,指向内核的文件表中与发送进程发送的描述符相同的项。