Unix域协议编程:任意进程间描述符的传递

    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 结构来保证这一点。




猜你喜欢

转载自blog.csdn.net/li_wen01/article/details/55048551