进程间通信:System V消息队列,pipe,FIFO
System V消息队列
基本概念
- 消息队列本质是在内核中存储的消息链表。和System V信号量/共享内存一样,首先需要使用
msgget
根据给定key
获取消息队列标识符qid
,在使用标识符链接到目标消息队列进行操作。 msgsnd
用于向消息队列添加新消息到队列尾部,msgrcv
可以实现以非先入先出的顺序从消息中取消息,可以按照消息的类型字段来取。
msgget
创建/获取一个消息队列IPC结构的标识符
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
- 通过给定
key
创建/获取消息队列成功,返回消息队列标识符qid
,供后续msgctl
/msgsnd
/msgrcv
- 类似于System V信号量,共享内存,一个消息队列IPC结构被创建后,内核会维护一下
msqid_ds
结构,保存对应消息队列的控制信息和权限,后续可以通过msgctl
进行修改:
struct msqid_ds {
struct ipc_perm msg_perm; /* 消息队列权限 */
time_t msg_stime; /* 最后一次msgsnd时间 */
time_t msg_rtime; /* 最后一次msgrcv时间 */
time_t msg_ctime; /* 创建时间/最后一次msgctl()修改时间 */
unsigned long __msg_cbytes; /* 当前队列字节数 */
msgqnum_t msg_qnum; /* 当前队列中消息数 */
msglen_t msg_qbytes; /* 队列中允许的最大字节数 */
pid_t msg_lspid; /* 最后一次msgsnd的进程pid */
pid_t msg_lrpid; /* 最后一次msgrcv的进程pid */
};
msgctl
执行消息队列的控制,修改操作
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
buf
用于接收msgctl
返回的控制结构,或者传入目标修改的控制结构,如果不需要设置置为NULL
即可,根据具体cmd
而定cmd
指定msgctl
具体的操作,Linux还有一些自己专有的cmd
,可以看man,这里只列出APUE上通用的:IPC_STAT
:拷贝当前消息队列的msqid_ds
到buf
中IPC_SET
:修改msgqid
中部分成员:msg_perm.uid
,msg_perm.gid
,msg_perm.mode
。这些成员只有由当前的有效用户进程或者sudo进程设置。此外msg_qbytes
只能由sudo用户才能增加超过系统参数MSGMNB
IPC_RMID
:立刻删除该消息队列,以及消息队列中所有数据,唤醒所有读写等待的进程,错误返回并将errno
设置为EIDRM
(和文件,共享内存相反,信号量相同,只有所有文件指针都关闭/共享内存链接都分离,文件/共享内存才会实际关闭/释放)。同样这个命令需要当前的有效用户进程为消息队列创建用户,或者sudo进程设置。
msgsnd
向消息队列写数据, msqid.msg_qnum
会递增,当消息队列已满,flag
没有设置的话默认阻塞写:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
对于msgsnd
传递的消息格式,一般由三部分组成:
- 正长整型(
long
)字段type
,表示消息类型 - 消息数据,如果
msgsz
指定为0,则没有消息数据 - 消息数据长度,就是参数
msgsz
,注意这里不是整个消息结构的长度,只是消息数据长度
因此一般msgp
指向的消息结构可以自定义如下:
struct mymesg {
long mtype; // 消息类型
char mtext[msgsz]; // 消息数据
};
关于消息类型mtype
需要注意,32位程序long
詹4字节,64位程序long
占8字节,32位向64位程序送数据没有问题,如果64位向32位程序送数据,如果mtype
大于INT_MAX
,会被截断。
关于flag
一般可以为0,或者指定IPC_NOWAIT
,这样发送队列如果已满,函数能够立即返回错误并设置errno
为EAGAIN
msgrcv
从消息队列中读出一个消息数据,msqid_ds::msg_qnum
递减,可以是非先入先出读,返回读取消息数据部分长度(但是我在测试程序的时候返回0。。。):
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
msgp
和msgsz
定义与msgsnd
相同,msgtype
指定读取哪一个消息:
msgtype == 0
:读取队首消息msgtype > 0
:读取相同type下第一个消息msgtype < 0
:读取type小于等于msgtype
绝对值的第一个消息,如果有多个则读取type最小的那个消息
关于flag
同样可以设置为IPC_NOWAIT
,读操作不阻塞,队列中没有消息可用时,msgrcv
返回错误,errno
设置为ENOMSG
pipe(匿名管道)
基本概念
#include <unistd.h>
int pipe(int fd[2]);
pipe
是半双工的,提供的fd[2]
中,fd[0]
仅用于读,fd[1]
仅用于写,fd[0]
的输出就是fd[1]
的输入。UNIX定义的系统调用IO可以正常对于pipe
的fd
操作pipe
只支持公共祖先的两个进程通信,比如父进程打开pipe
后,fork
子进程与其通信- 当读一个已关闭的管道时,所有数据被读取后,
read
返回0,表文件结束(和socket
读取一致) - 当往一个已关闭的管道写时,产生
SIGPIPE
信号(默认终止动作),忽略该信号或调用信号处理函数返回后,write
返回-1。可以看到写一个已关闭管道问题更严重,一般如果pipe
在一个进程内通信(比如同一事件源),最好先关fd[1]
,再关fd[0]
- 内核管道缓冲区大小由
PIPE_BUF
(系统限制值,默认一个页长4K)规定。通过fpathconf
或者pathconf
修改
popen / pclose
组合函数,将fork
,pipe
,exec
,标准输入输出重定向组合在一起的函数:
#include <stdio.h>
FILE* popen(const char *cmdstring, const char *type);
int pclose(FILE *fp);
popen
先fork
子进程,然后根据cmdstring
执行shell指令type
指定了cmdstring
重定向的IO,"r"
:指定了fd[0]
重定向到子进程的STDOUT_FILENO
(父进程fd[1]
<–子进程stdout
),子进程的输出直接作为父进程管道的输出"w"
:制定了fd[1]
重定向到子进程的STDIN_FILENO
(父进程fd[0]
–>子进程stdin
),父进程管道的输入直接作为子进程的输入
- 虽然
popen
会开启子进程,但是用户不需要在父进程中调用wait
/waitpid
,调用pclose
中会内部调用waitpid
回收子进程资源
FIFO(有名管道)
基本概念
FIFO
解决了pipe
通信双方必须是具有公共祖先这一限制。FIFO
需要提供路径名进行标识,FIFO
的路径名也是存在于UNIX文件系统中,创建一个FIFO
函数如下:
#include <sys/stat.h>
int mkfifo(const char *path, mode_t mode);
int mkfifoat(int fd, const char *path, mode_t mode);
FIFO
的创建类似于文件创建,mode
的指定和open
相同,在mkfifoat
的mode
中指定了AT_FDCWD
,path
以当前路径开始- 创建的FIFO需要使用
open
打开,当所有引用FIFO
路径的进程都关闭时,虽然FIFO
的路径还会留在文件系统中,但是数据已经被删除 mode
和O_NONBLOCK
的设置:- 没有设置
O_NONBLOCK
:只读FIFO
打开会阻塞到有其他进程写打开,只写FIFO
打开会阻塞到有其他进程读打开 - 设置
O_NONBLOCK
:只读FIFO
打开立即返回,只写FIFO
打开在没有其他读进程打开的情况返回-1设置errno
为ENXIO
- 没有设置
FIFO
适合有多个进程向管道写,读进程聚合数据的情况(pipe
一般用于单入单出),因此需要原子写保证多进程写数据不交叉,PIPE_BUF
指定了原子写FIFO
的最大数据量