以前学习Unix套接字的时候,了解与TCP、UDP网络套接字有一个很特别的不同点,就是Unix套接字能够通过辅助数据传输文件描述符。通过这个特性,原本没有父子关系的进程也能使用匿名资源(例如匿名管道),或者访问到进程原本没有权限访问的资源。
最近学习Google在SOSP 2019发表的论文《Snap: A Microkernel Approach to Host Networking》收获很多,其中提到如何高效地在进程间通信,Snap在控制通道使用Unix套接字通信,如果要进行数据通信,则会使用共享内存技术,其中基于tmpfs的文件描述符通过Unix套接字的RPC响应中交换。打算分两篇文章去展开,第一篇先介绍通过Unix套接字传递文件描述符,第二篇介绍共享内存的部分。
系统调用API
发送消息的系统调用主要有send、sendto和sendmsg三个,其中sendmsg可以发送辅助控制信息。类似得,接收消息的系统调用主要有recv、recvfrom和recvmsg三个,其中recvmsg可以接收辅助控制信息。
ssize_t
其中,msg结构体的组成如下所示:
struct
msg_name和msg_namelen用于指定接收地址,一般用在无连接的套接字,有连接的套接字指定为NULL和0即可。msg_iov和msg_iovlen用于指定发送或接收的(多个)消息缓冲,虽然不是本文主要讨论范围,如果消息本身有一定的块结构(例如消息头部和消息体),这个机制可以减少用户态中内存的拷贝。msg_control和msg_controllen用于传输辅助数据,本文下面再展开。msg_flags在recvmsg中可以一些额外的行为。
辅助控制数据
根据cmsg(3)文档的介绍,辅助控制数据可以用于:
- 接收方获取接收报文的网络接口(IP_PKTINFO选项,仅适用于面向datagram的套接字)
- 接收IP等协议的头部信息,例如IP_RECVTTL选项获取IP_TTL信息和IP_RECVTOS选项获取IP_TOS信息
- 接收扩展的错误描述,主要指的是IP_RECVERR选项
- 发送或接收文件描述符,使用SCM_RIGHTS消息类型,下面展开讨论
- 发送或接收机密信息(指进程的uid/gid等),使用SCM_CREDENTIALS消息类型,用于认证访问的进程
其中,前面3个在开启指定IP套接字选项后就能通过recvmsg获取对应的辅助信息,后面2个仅适用于Unix套接字,并且cmsg_level为SOL_SOCKET(下面介绍)。
辅助控制数据实际上是cmsghdr结构体的序列,序列中结构体的数量对应msghdr的msg_controllen,而序列的首地址对应msghdr的msg_control字段。cmsghdr结构体的形式如下:
struct
按照cmsg(3)文档的介绍,由于结构体中有内存对齐的需求,建议通过宏进行操作:
#include
接收辅助信息后,可通过CMSG_FIRSTHDR和CMSG_NXTHDR顺序获取cmsghdr结构体,通过CMSG_DATA获取具体的数据指针。
发送方构造辅助信息,最重要知道缓冲所需长度。首先用CMSG_SPACE根据payload大小计算出单条消息所需的空间,然后把所有消息的Space大小加起来,使用CMSG_ALIGN计算出内存对齐后的缓冲大小。
char
由于这些宏是常量表达式,因此可以有上面这种编译时指定缓冲大小的用法。发送方指定cmsghdr的cmsg_len字段时,还需要用到CMSG_LEN宏。
传递文件描述符
说到这里,应该可以理解传递文件描述符的过程就是发送辅助信息和接收辅助信息。具体来说,cmsg_level设置为SOL_SOCKET,cmsg_type设置为SCM_RIGHTS,而cmsg_data设置为文件描述符数组(即int数组)。
有一点值得注意,发送方的fd和接收方得到的fd不需要相同的值,例如发送方的fd是3,而接收方得到的fd是4。可以认为接收方新增了一个fd,因此也受到文件描述符资源限制等。
Unix套接字地址
由于实现的时候踩过坑,也在这里记录一下Unix套接字地址的注意事项。Unix套接字地址分为传统基于文件系统路径的地址和abstract抽象地址。无论哪种地址,切记指定大小的时候(例如bind)不要使用sockaddr_un结构体的全部大小。
对于基于文件系统路径的地址,sun_path字段使用NULL结尾字符串,所以总体的地址大小就是sun_path字段之前的大小加上字符串的长度(包括NULL):
offsetof
对于抽象地址,sun_path数组第0字节需要置为NULL,后面的字符串不需要NULL结尾(因此不要使用strlen)。因此,总体地址大小是sun_path字段之前的大小加上sun_path字段用到的字节数。
抽象地址的好处是该地址的生命周期与应用的生命周期一致,不需要额外做一些删除的动作。
小结
本文介绍了Unix套接字如何发送和接收辅助控制数据,特别是文件描述符。通过这个机制,进程可以访问到匿名资源或没有直接权限的资源。在Snap中,服务进程将内存fd传递到客户进程,实现按需的内存共享机制,具体方式在下一篇文章中展开,敬请期待!