进程间通信( Inter Process Communication, IPC ) 指在不同的进程之间传播或交换信息。常用的进程间通信方法有:管道、消息队列、共享内存、信号量、套接字等, 其中前4种主要用于同一台机器上的进程间通信,而套接字主要用于跨机器的网络通信。
管道
管道是一种两个进程间进行单向通信的机制。因为其单向性,管道又被称为半双工通信。
无名管道
无名管道具有以下特点:
- 数据只能从一个进程流向另一个进程(一个读管道、一个写管道);如要进行双工通信,需要建立两个管道。
- 管道只能用于具有亲缘关系(父子、兄弟进程)的进程间通信。
- 管道没有名字(无名管道)。
- 缓冲区大小受限,传输的是无格式的字节流,需管道通信双方事先约定好数据格式。
- 从本质上说,管道也是一种文件,但只存在于内存中。
无名管道由 pipe()
创建, 管道两端分别用描述符fd[0]
以及fd[1]
表示,前者为读端,后者为写段。一个进程用pipe()
创建管道后,fork
一个子进程,再通过管道实现两者之间的通信。要实现读写,只需将无关的两个文件描述符关闭即可。
1 |
|
有名管道
有名管道( FIFO ) 提供一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中。这样只需访问该路径,即可通过 FIFO相互通信。
有名管道有以下特点:
- 它可以使互不相关的两个进程间实现彼此通信
- 该管道可以通过路径名来指出,并且在文件系统中是可见的
- 在建立了管道之后,两个进程就可以把它当作普通文件一样进行读写操作,使用非常方便
- FIFO 严格地遵循先进先出规则,对管道及 FIFO 的读操作总是从开始处返回数据,对它们的写操作则是把数据添加到末尾 。
有名管道由 mkfifo()
函数来创建,一般文件的 I/O 函数均可用于 FIFO , 如 close()
、read()
、write()
等。
1 |
|
有名管道的安全性
由于有名管道支持一对多的形式,多个进程同时向一个 FIFO 文件中写数据,一个进程从其中读取数据,是可能发现数据块的相互交错的。
为了解决这个问题,Unix 提供了写操作的原子化:在一个以 O_WRONLY (堵塞只写)打开的 FIFO 中,如果写入的数据长度小于 PIPE_BUF (管道写入最大值),那么要么全部写入,要么一个不写。以此来保证数据不会交错。
通过客户端在向服务端发送信息前先建立以自己的进程号 ( pid ) 命名的读入管道,告知服务器将其作为回复数据的写入管道,可以以此实现一对多双向交流。
消息队列
消息队列是在系统内核中保存消息的队列,以消息链表的形式出现。链表中的节点以 msg 声明。
创建新队列或取得已存在消息队列:
1
int msgget(key_t key, int msgflg);
key 为端口号,可以由函数 ftok 生成
msgflg 如果等于 IPC_CREAT 若无该队列,则创建并返回新标识符,已存在则返回原标识符
msgflg 如果等于 IPC_EXCL 若无该队列,则返回 -1, 若存在,则返回 0向队列中读 / 写消息:
1
2
3
4
5
6
7
8
9
10
11// 缓冲区通用结构
struct msgstru {
long mtype; // 大于0
char mtext[512];
};
// 从队列中取用消息
ssize_t msgrcv (int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
// 将数据放到消息队列中
int msgsnd (int msqid, canst void *msgp, size_t msgsz ,int msgflg);
msqid 为消息队列标识符
msgp 为指向消息缓冲区的指针,用于暂时存储发送和接收的消息
msgsz 消息的大小
msgtyp 从消息队列中读取的消息形式,为0 则读取所有消息
msgflg 指明程序在队列没有数据时该采取的行动
- 设置消息队列属性;
1 | int msgctl (int msgqid, int cmd, struct msqid_ds *buf ); |
msgctl 系统调用对 msgqid 标识的消息队列执行 cmd 操作,系统定义了三种 cmd 操作:
IPC_STAT 用来获取对应的 msqid_ds 数据结构,并保存到 buf 指定的内存空间
IPC_SET 用来设置属性,要设置的属性存储在 buf 中
IPC_RMID 用来从内核中删除 msqid 标识的消息队列
相比有名管道,消息队列的优势在于:
- 消息队列独立于发送和接收进程而存在
- 可以同时发送消息,避免了命名管道的同步和阻塞问题
- 接收程序可以根据消息类型有选择地接收数据
消息管道是一种正逐渐被淘汰的通信方式。
共享内存
共享内存是允许两个不相关的内存访问同一块物理内存。进程可以将同一段共享内存连接到它们自己的地址空间里,就像是用函数 malloc 分配的内存一样。 但共享内存为提供同步机制,需要其他机制来同步对共享内存的访问。
1 |
|
key
为共享内存命名,shmget
函数运行成功返回一个与 key 相关的共享内存标识符,用于后续的共享函数,失败返回 -1。size
以字节为单位指定共享的内存容量。shmflg
为与 open 函数的 mode 参数一样的权限标志, 可与 IPC_CREAT 做或操作实现不存在时创建共享内存。
共享内存创建后,使用 shmat
将其连接到自己的地址空间
1 | void *shmat(int shmid, void *addr, int flag); |
shmid
为shmget
函数返回的标识符
shmdt
函数用于将共享内存从当前进程分离,不是删除。
1 | int shmdt(const void *shmaddr); |
shmaddr
是shmat
函数的返回指针,调用成功返回0,失败返回 -1.
使用共享内存的优缺点如下所述:
- 优点:使用共享内存进行进程间的通信非常方便,而且函数的接口也简单,数据的
共享还使进程间的数据不用传送,而是直接访问内存,也加快了程序的效率。同时,它也不
像无名管道那样要求通信的进程有一定的父子关系。 - 缺点:共享内存没有提供同步的机制,这使得在使用共享内存进行进程间通信时,
往往要借助其他的手段来进行进程间的同步工作。
信号量
多线程同步使用的信号量是 POSIX 信号量,而多进程同步使用的是 SYSTEM V 信号量。
常用函数如下:
1 |
|
semget
函数中,参数 key 是通过调用 ftok 函数得到的键值, nsems 代表创建信号量个数,如果只是访问而不创建则指定为0,一旦创建了该信号量就不能改变其个数。 semflg 指定信号量读写权限,不可加 IPC_CREAT。semop
函数用于改变信号量的值,sem_id 为semget
返回的信号量标识符,sembuf 结构如上。semctl
函数用于控制信号量信息,cmd 通常是 SETVAL 或 IPC_RMID,前者初始化信号量为一个已知的值,由 union semun 中的 val 成员设置。 IPC_RMID 用于删除一个信号量标识符。
共享内存是进程间通信的最快的方式,但是共享内存的同步问题自身无法解决(即进程该何时去共享内存取得数据,而何时不能取),但用信号量即可轻易解决这个问题。
学习资料
《后台开发 核心技术与应用实践 第11章 进程间通信》