Linux环境:C编程之进程传递文件描述符
在多进程编程时,会用到进程间传递文件描述符的情况,例如进程池编程通信时,在主进程中建立连接,然后把建立后的socket转给子进程来处理任务。接下来看一下如何在进程间传递文件描述符。
需要注意的是:
文件描述符的编号在进程间独立,每个进程都维护一个进程文件打开表,所以同一文件描述符在不同进程中有不同的含义。因此,传递文件描述符,其实是在传递文件描述符对应的指向该文件的引用。
具体流程:通过socketpair
建立unix域套接字,然后利用这对套接字进行通信,利用sendmsg
和recvmsg
函数传递文件描述符对应文件的辅助信息。
socketpair函数
- 作用:sockpair函数用来建立一对无名套接字,用于全双工通信,和无名管道相比,无名套接字可以全双工通信,并且支持主机和网络通信,功能更加强大。
- 函数原型:
int socketpair(int d, int type, int protocol, int sv[2]);
- 参数:前三个参数和socket函数一致,分别是通信域,类型和协议,第四个参数是传出参数,传出创建的一对描述符。
- 注意事项:
- 可以用于全双工通信,每一个套接字既可以读也可以写。例如,可以往sv[0]中写,从sv[1]中读;或者从sv[1]中写,从sv[0]中读;
- 如果往一个套接字(如sv[0])中写入后,再从该套接字读时会阻塞,只能在另一个套接字中(sv[1])上读成功;
- 读、写操作可以位于同一个进程,也可以分别位于不同的进程,如父子进程。如果是父子进程时,一般会功能分离,一个进程用来读,一个用来写。因为文件描述副sv[0]和sv[1]是进程共享的,所以读的进程要关闭写描述符, 反之,写的进程关闭读描述符。
sendmsg和recvmsg函数
- 函数原型:
size_t sendmsg (int s, const struct msghdr *msg, int flags);
int recvmsg(int s, struct msghdr *msg, unsigned int flags);
- 作用:
这一对发送函数可以替代之前的send,recv,sendto和recvfrom,向另一个套接口发送信息。
参数:第一个参数是要发信息或收信息的套接字描述符;第二个参数是发送的信息结构体头指针;第三个参数是标志位,和之前的sendto/recvfrom等函数一致。 - 返回值:返回实际接受或发送的字节数,失败返回-1
struct msghdr结构体
要理解sendmsg和recvmsg,需要弄清楚struct msghdr结构体。该结构体定义如下:
//有这么多成员,所以才能实现多种功能
struct msghdr {
/*下面两组是套接字的地址指针和长度,用来替代sendto和recvfrom中指明接收方或发送方的地址参数*/
void *msg_name;
socklen_t msg_namelen;
/*这一组是要发送的数据信息的缓冲区指针和长度*/
struct iovec *msg_iov;
size_t msg_iovlen;
/*这一组是发送的辅助信息,用来传递一些特殊信息,比如文件描述符的控制信息*/
void *msg_control;
size_t msg_controllen;
/*接收信息标记位*/
int msg_flags;
}
sendmsg和recvmsg能实现这么多功能就是因为这个结构体很复杂,内容很多,不过我们使用的时候只需要初始化能用到的信息就可以了。
需要注意的是,如果想利用它传递辅助信息,比如文件描述符,必须携带至少一个字节的真实数据,也就是iov指针指向的缓冲区要有数据,iovlen至少是1。
struct cmsghdr结构体
我们要传递的辅助信息需要是一个struct cmsghdr
结构体类型保存,然后用msg_control
指向它。该结构体的定义如下:
struct cmsghdr {
socklen_t cmsg_len; /* 该变量的长度,包含结构体头部 */
int cmsg_level; /* 原始协议,比如SOL_SOCKET代表套接字API层*/
int cmsg_type; /* 控制信息类型,比如SCM_RIGHTS代表了文件描述符 */
/* followed by unsigned char cmsg_data[]; */
};
该结构体比较复杂所以有一系列的宏来对其进行操作
- CMSG_FIRSTHDR() :接收一个msghdr指针,返回指向msghdr的控制信息缓冲区的第一个指针,前提是msghdr已经申请了缓冲区空间。
- CMSG_DATA() :接收一个cmsghdr指针,返回cmsghdr的数据区的第一个字节的指针。
- CMSG_LEN() :接受我们希望放置在附属数据缓冲区中的对象尺寸作为输入参数。返回cmsghdr头结构加上所需要的填充字符的字节长度。这个值用来设置cmsghdr对象的cmsg_len成员。
通过把要传递的文件描述符放在data区,内核就可以帮我们完成转换。
recvmsg接收时和上述一致。