进程间通信recvmsg与sendmsg函数
DPDK中的进程间通信使用了recvmsg与sendmsg函数。
1、函数定义
1 2 3 4 | #include <sys/types.h> #include <sys/socket.h> ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags); ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags); |
sendmsg和recvmsg函数是一对相对底层的套接字发送、接受函数,可以用来在进程间传递描述符。这两个函数支持一般数据的发送和接收,还支持多缓冲区的报文发送和接收,还可以在报文中带辅助数据。这些功能是常用的send、recv等接口无法完成的。
2、数据结构定义
struct msghdr结构体的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 | struct iovec { /* Scatter/gather array items */ void *iov_base; /* Starting address 指向要发送的数据 */ size_t iov_len; /* Number of bytes to transfer 要发送的数据的长度 */ }; struct msghdr { void *msg_name; /* optional address 发送地址 */ socklen_t msg_namelen; /* size of address 发送地址的长度 */ struct iovec *msg_iov; /* scatter/gather array 发送数据缓冲区的指针 */ size_t msg_iovlen; /* # elements in msg_iov 发送数据的长度 */ void *msg_control; /* ancillary data, see below 附加数据,控制数据 */ size_t msg_controllen; /* ancillary data buffer len 附加数据/控制数据缓冲区长度 */ int msg_flags; /* flags on received message 接收到的消息的标记 */ }; |
在发送一个数据包之前,要先把要发送的数据以及相关控制信息填写到这个结构体中。
msg_name 主要用于保存当前使用的协议的地址,比如使用了tcp协议、udp协议、UNIX domain协议等,每种地址都存在一定的差异,比如unix domain的地址就是(AF_UNIX, file_path)。这样就使得该接口更加的通用,针对各种类型的协议都是有效的。如果无需指明协议地址(如对于 TCP 套接字或已连接 UDP 套接字),msg_name 应置为空指针。
msg_namelen指明了p这个套接字地址的长度。
msg_iov是一个struct iovec *类型的io向量,多缓冲区的发送和接收处理就是通过一个struct iovec的数组,每个成员的io_base都指向了不同的buffer的地址。struct iovec中的iov_len是指该buffer中的数据长度。
struct msghdr中的msg_iovlen是指buffer缓冲区的个数,即iovec数组的长度。
msg_control通常指向一个控制消息头部,其结构体如下:
1 2 3 4 5 6 | /* Structure used for storage of ancillary data object information. */ struct cmsghdr { size_t cmsg_len; /* 类型本来应该是socklen_t,但内核不兼容该定义 */ int cmsg_level; /* Originating protocol. 具体协议标识 */ int cmsg_type; /* Protocol specific type. 协议类型 */ }; |
其中,
cmsg_len表示辅助数据的字节数,包含这个cmsghdr结构体头部大小,加上cms_data数据区域的长度,由CMSG_LEN()宏计算得到。
cmsg_level主要包含IPPROTO_IP(IPv4)、IPPROTO_IPV6(IPv6)、SOL_SOCKET(Unix Domain)。
cmsg_type是根据上述的类型有分别有不同的内容,比如SOL_SOCKET中主要包含以下两个:
- SCM_RIGHTS(发送接收描述符),
- SCM_CREDS(发送接收用户凭证,是一个包含证书信息的结构)。
该cmsghdr是辅助数据的数据头部,在recvmsg的msg_control中可能携带多个单独的辅助数据,每一个辅助数据之前都有一个cmsghdr结构, 可以采用对应的宏协助处理:
CMSG_FIRSTHDR(), CMSG_NXTHDR(), CMSG_DATA(), CMSG_LEN(), CMSG_SPACE()
最后的字段是msg_flags,主要用于在接收消息的过程中用来获取消息的相关属性。
Linux中提供了一系列宏来简化对这些数据结构的操作:
(1)CMSG_FIRSTHDR()
得到第一个辅助数据的cmsghdr结构。如果不存在辅助数据,则返回NULL。
1 2 3 | #define CMSG_FIRSTHDR(mhdr) \ ((size_t) (mhdr)->msg_controllen >= sizeof (struct cmsghdr) \ ? (struct cmsghdr *) (mhdr)->msg_control : (struct cmsghdr *) 0) |
(2)CMSG_NXTHDR()
得到当前辅助数据的下一个cmsghdr结构。如果不存在下一个,则返回NULL。
1 | #define CMSG_NXTHDR(mhdr, cmsg) __cmsg_nxthdr (mhdr, cmsg) |
(3)CMSG_ALIGN()
1 2 | #define CMSG_ALIGN(len) (((len) + sizeof (size_t) - 1) \ & (size_t) ~(sizeof (size_t) - 1)) |
(4)CMSG_LEN()
计算cmsghdr及其附属数据的大小。
1 | #define CMSG_LEN(len) (CMSG_ALIGN (sizeof (struct cmsghdr)) + (len)) |
(5)CMSG_SPACE()
计算cmsghdr及其附属数据的大小,以及对其字段和可能的结尾填充字段的大小。
这个宏与CMSG_LEN()的区别是,CMSG_LEN()计算得到的值并不包含结尾的填充字段大小。
CMSG_SPACE()宏对于确定所需要的缓存大小非常有用。
需要注意的是,如果有多个辅助数据,一定要同时使用多个CMSG_SPACE()宏进行计算。
1 | #define CMSG_SPACE(len) (CMSG_ALIGN (len) + CMSG_ALIGN (sizeof (struct cmsghdr))) |
以下示例可以体现两者的差别:
1 2 | printf("CMSG_SPACE(sizeof(short))=%d\n", CMSG_SPACE(sizeof(short))); // 返回16 printf("CMSG_LEN(sizeof(short))=%d\n", CMSG_LEN(sizeof(short))); // 返回14 |
(6)CMSG_DATA()
返回cmsghdr头后面的辅助数据的地址。
1 | #define CMSG_DATA(cmsg) ((unsigned char *) ((struct cmsghdr *) (cmsg) + 1)) |
3、sendmsg()与recvmsg()函数
两函数的定义如下:
1 2 3 4 | #include <sys/types.h> #include <sys/socket.h> ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags); ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags); |
对于 recvmsg 和 sendmsg,必须区别它们的两个标志变量:
- 一个是传递值的 flags 参数;
- 另一个是所传递 msghdr 结构的 msg_flags 成员,它传递的是引用,因为传递给函数的是该结构的地址。
- 只有 recvmsg 使用 struct msghdr结构体中的msg_flags 成员。recvmsg 被调用时,flags 参数被复制到 msg_flags 成员,并由内核使用其值驱动接收处理过程。内核还依据 recvmsg 的结果更新 msg_flags 成员的值。
- sendmsg 则忽略struct msghdr 结构体中的 msg_flags 成员,因为它直接使用 flags 参数驱动发送处理过程。这一点意味着如果想在某个 sendmsg 调用中设置 MSG_DONTWAIT 标志,那就把 flags 参数设置为该值,把 msg_flags 成员设置为该值不起作用。
使用两个函数传递描述符的过程如下:
在两个进程之间创建一个unix domain socket套接字,然后调用sendmsg跨这个套接字发送一个特殊的消息,该消息由内核进行特殊的处理,从而把打开的描述符从发送进程传递到接收进程(使用recvmsg接收)。分析主要包含如下几个过程:
(1) 创建一个字节流或者数据报的unix domain socket套接字,
- 1) 在父子进程之间可采用socketpair实现流管道,类似于pipe的实现,两个进程中分别有一个socket套接字
- 2) 在无关系的进程之间采用基本的domain socket过程(服务器Bind, listen, accpet返回的套接字, 客户端connect返回的套接字),将两个进程关联起来。
(2) 发送进程通过调用返回描述符的任一unix函数打开一个描述符(也就是返回fd类型的函数打开一个描述符)。
(3) 发送进程创建一个msghdr结构,其中将待传递的描述符作为辅助数据发送,调用sendmsg跨越(1)中获得的套接字发送描述符。在发送完成以后,在发送进程即使关闭该描述符也不会影响接收进程的描述符,发送一个描述符导致该描述符引用计数加1。
(4) 接收进程调用recvmsg在(1)中获取的套接字上接收该描述符,该描述符在接收进程中的描述符号不同于在发送进程中的描述符号是正常的,也就是说如果在发送进程中描述符号是20,而在接收进程中对应的描述符号可能被使用,该进程会分配一个不一样的描述符号(如open对同一个文件进行多次打开,每一次的fd返回值都不一样是一个道理),此时就是说两个进程同时指向一个描述符。
注意:在发送过程中由于没有报文,在接收的过程中会分不清是文件已经结束还是只是发送了辅助数据,因此通常在发送辅助数据的时候会传输至少一个字节的数据,该数据在接收过程中不做任何的处理。
4、函数参数与返回值
(1)sendmsg()函数
flags可以为0,或者包含以下标记:
- MSG_CONFIRM:告诉链路层,从对端收到一个成功的响应。如果链路层没有获取到该消息,则它会重新探测neighbor(如通过广播ARP包)。该标记只支持SOCK_DGRAM和SOCK_RAW类型的套接字有效,且目前只支持IPv4和IPv6。
- MSG_DONTROUTE:表示不通过路由发送数据,只有在直连网络的情况下才发送。该标记通常只在诊断模式或者路由程序中使用。该标记只对路由家族协议定义,数据包socket不使用。
- MSG_DONTWAIT:启用非阻塞操作。如果操作可能阻塞,则返回EAGAIN 或 EWOULDBLOCK。该选项也可以使用函数fcntl()使用O_NONBLOCK标记实现。
- MSG_EOR:终止一条记录(如果该标记支持,则对应的套接字类型为SOCK_SEQPACKET)。
- MSG_MORE:调用者有更多数据要发送,该标记与TCP套接字一起使用,可以达到TCP_CORK类型套接字选项相同的效果,不同的是该标记可以在每次调用时才设置。从linux内核2.6开始,该标记也支持UDP套接字,会通知内核将所有通过该标记发送的数据打包成一个datagram,当函数调用没有指定该标记时,一次性把这些数据发送出去。(可以参考UDP_CORK套接字类型)
- MSG_NOSIGNAL:当另一端断开链接时,当面向流的套接字发生错误时,请求不发送SIGPIPE信号。EPIPE错误仍被返回。
- MSG_OOB:在支持该选项套接字上(如SOCK_STREAM类型)发送Out-of-band数据,底层的协议也必须支持out-of-band数据。
如果函数执行成功,则返回发送的字节数;如果发送失败,则返回-1,并设置errno。
(2)recvmsg()函数
recvmsg()函数用来从指定socket接收数据,不管这个socket是不是面向连接的。
返回值 :
如果接收成功,则返回接收到的消息的长度。如果接收的数据长度超过缓存的大小,则多出来的消息可能被丢弃,取决于socket的类型。
如果socket上没有数据,则函数会一直等待,除非socket被设置为非阻塞(通过fcntl函数)。
在非阻塞模式下,如果socket没有数据,函数会返回-1,并将errno设置为EAGAIN或EWOULDBLOCK。
如果对端关闭,则函数返回0。
其他errno值:
- EAGAIN或EWOULDBLOCK:如果socket被标记为非阻塞,而接收端产生了阻塞;或者设置了一个timeout值,但在数据返回之前就超时。POSIX.1-2001允许返回以上任意一个错误值,且不要求这两个常量的值相同,因此程序需要同时检查这两个值。
- EBADF:表示sockfd是一个无效描述符。
- ECONNREFUSED:表示对端拒绝网络连接(通常是因为没有运行需要的服务)。
- EFAULT:表示接收缓存指针指向的地址超出程序的范围。
- EINTR:表示在接收到数据之前,被中断信号所中断。
- ENOMEM:表示无法为recvmsg()分配内存。
- ENOTCONN:表示socket关联到一个面向连接的协议,且当前没有连接。
- ENOTSOCK:表示参数sockfd没有关联到任何socket。
5、参考
https://blog.csdn.net/dfwseq/article/details/25653373
https://www.cnblogs.com/jimodetiantang/p/9190958.html
https://wenku.baidu.com/view/7a8a522158fb770bf78a55cf.html
————————————————————
原创文章,转载请注明: 转载自孙希栋的博客
本文链接地址: 《进程间通信recvmsg与sendmsg函数》