Linux进程间通信
进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。
IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。
一、管道
管道,通常指的是无名管道(匿名管道), 是UNIX 系统IPC最古老的形式。
1、特点
- **半双工通信:**管道是一种半双工的通信方式(即数据只能在一个方向上流动),具有固定的读端和写端;
- 亲缘关系:通常情况下,管道只能在具有亲缘关系的进程之间使用,例如父子进程或兄弟进程;
- 字节流:管道中的数据以字节流的形式传输,没有消息边界。写入管道的数据会被缓冲,直到读取端读取为止;
- 同步:管道的读取和写入是同步的,即读取操作会阻塞,直到有数据可供读取;写入操作会阻塞,直到有足够的空间可供写入;
- 有限容量:管道具有一定的容量限制,如果写入的数据超过了管道的容量,写入操作可能会阻塞或导致错误。
- **特殊文件:**它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
2、函数原型
#include <unistd.h>
int pipe(int fd[2]);
// 返回值:若成功返回0,失败返回-1
当一个管道建立时,它会创建两个文件描述符:fd[0]
用于读取管道,fd[1]
用于写入管道。如下图:
3、通信案例
单个进程中的管道几乎没有任何用处。通常,进程会先调用 pipe ,接着调用 fork,这样就创建了父进程与子进程之间的 IPC 通道。
fork之后要确定数据流的方向,若要数据流从父进程流向子进程,则关闭父进程的读端(fd[0]
)与子进程的写端(fd[1]
);反之,则可以使数据流从子进程流向父进程。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int fd[2]; // 用于存储管道的文件描述符
pid_t pid; // 用于存储子进程的 PID
char *writebuf = "hello chiled,i'm father"; // 要写入的字符串
char readbuf[128]; // 用于存储读取的数据
// 创建管道
if (pipe(fd) == -1) {
perror("pipe"); // 如果创建管道失败,输出错误信息
exit(0);
}
// 创建子进程
if ((pid = fork()) < 0) {
perror("fork"); // 如果创建子进程失败,输出错误信息
exit(0);
}
// 父进程
if (pid > 0) {
sleep(1); // 休眠一秒,让子进程先运行
printf("this is father\n");
close(fd[0]); // 关闭读端
write(fd[1], writebuf, strlen(writebuf)); // 向管道中写入数据
wait(NULL); // 等待子进程结束
} else if (pid == 0) {
printf("this is chiled\n");
close(fd[1]); // 关闭写端
read(fd[0], readbuf, sizeof(readbuf)); // 从管道中读取数据
printf("from father: %s\n", readbuf); // 打印读取到的数据
exit(0);
}
}
/*运行结果:
this is chiled
this is father
from father: hello chiled,i'm father*/
二、FIFO(命名管道)
FIFO,也称为命名管道,是一种特殊的文件类型。
1、特点
- 先进先出:数据以先进先出的方式在FIFO中排队,先写入的数据先被读取;
- 无缓冲区:FIFO本身不提供缓冲区,操作系统会为每个打开的FIFO文件描述符分配缓冲区;
- 文件系统对象:FIFO在文件系统中表现为一个特殊类型的文件,可以被任何有权限的进程访问;
- 非阻塞性:默认情况下,FIFO是阻塞的,但可以通过设置使其变为非阻塞;
- 进程间通信:FIFO允许不具有亲缘关系的进程之间进行通信;
- 数据流:FIFO支持数据流的传输,数据以字节流的形式存在。
2、函数原型
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
// 返回值:成功返回0,出错返回-1
- pathname:指定要创建的 FIFO 的路径名。这是一个字符串,包含了 FIFO 的名称和路径。
- mode: 指定 FIFO 的权限模式,是一个八进制数,类似于文件的权限设置。参数与open函数中的 mode 相同,一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。当 open 一个FIFO时,是否设置非阻塞标志(
O_NONBLOCK
)的区别:- 若没有指定
O_NONBLOCK
(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。 - 若指定了
O_NONBLOCK
,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。
- 若没有指定
3、创建FIFO
#include <sys/stat.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
int main()
{
int fd;
fd = mkfifo("./file",0666);
if(fd == -1){
perror("why");
}
close(fd);
//unlink("./file");//删除fifo文件
return 0;
}
运行结果:创建了一个名为file的特殊格式文件。
4、编程实现FIFO数据通信
- fifo_write:写数据
#include <sys/stat.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
int main()
{
int fd; // 文件描述符
char *writeBuf = "fifo write success"; // 要写入的字符串
// 以只写方式打开文件
fd = open("./file", O_WRONLY);
if (fd == -1) {
// 如果打开失败
perror("open"); // 输出错误信息
}
printf("open success\n"); // 打印打开成功的信息
// 将字符串写入文件
write(fd, writeBuf, strlen(writeBuf));
close(fd); // 关闭文件描述符
return 0;
}
- fifo_read:读数据
#include <sys/stat.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
int main()
{
int ret; // 用于存储函数返回值
int fd; // 文件描述符
int n_read; // 读取的字节数
char readBuf[128] = {
'\0'}; // 用于存储读取的数据
// 创建命名管道
ret = mkfifo("./file", 0600);
if ((ret == -1) && (errno!= EEXIST)) {
// 如果创建失败且错误码不是 EEXIST(文件已存在)
perror("mkfifo"); // 输出错误信息
}
printf("fifo ok\n"); // 打印创建成功的信息
// 以只读方式打开命名管道
if ((fd = open("./file", O_RDONLY)) < 0) {
perror("open"); // 输出打开文件失败的错误信息
}
printf("open ok\n"); // 打印打开文件成功的信息
// 从命名管道中读取数据
n_read = read(fd, readBuf, sizeof(readBuf));
printf("read %d byte, content: %s\n", n_read, readBuf); // 打印读取的字节数和内容
close(fd); // 关闭文件描述符
unlink("./file"); // 删除命名管道
return 0;
}
运行结果:默认情况下创建FIFO后的mode是阻塞状态,所以先运行只读程序会阻塞在"fifo ok"等待写入数据,等到执行写入程序后只读程序会执行完毕,读到写入程序的写入内容。
三、消息队列
- 消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识;
- 消息被发送到队列中。“消息队列”是在消息的传输过程中保存消息的容器。队列的主要目的是提供路由并保证消息的传递;如果发送消息时接收者不可用,消息队列会保留消息,直到可以成功地传递它。
1、特点
- 异步性:发送和接收消息是异步进行的;
- 持久性:消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除;
- 消息顺序:在同一个消息类型中,消息队列通常保证消息的顺序,即先发送的消息先被接收。在不同的类型或优先级中,允许接收者根据类型或优先级处理消息;
- 多生产者和多消费者:多个进程可以同时向同一个消息队列发送或接收消息;
- 解耦生产者和消费者:消息队列允许生产者和消费者在不同的时间点独立工作,从而降低了系统组件之间的耦合度。
2、相关API介绍
#include <sys/msg.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
// 通常用于生成消息队列、信号量或共享内存的键值:成功返回生成的键值,失败返回-1
int msgget(key_t key, int flag);
// 创建或打开消息队列:成功返回队列ID,失败返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 添加消息:成功返回0,失败返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 读取消息:成功返回消息数据的长度,失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
// 控制消息队列:成功返回0,失败返回-1
(1)ftok
ftok函数用于将一个路径名和一个项目标识符转换为一个键值,通常用于生成消息队列、信号量或共享内存的键。
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
//返回值:成功返回生成的键值,失败返回-1,并设置error错误码
/*在一般的UNIX实现中,是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。
如指定文件的索引节点号为65538,换算成16进制为0x010002,而你指定的ID值为38,换算
成16进制为0x26,则最后的key_t返回值为0x26010002。*/
参数说明:
- pathname:指向文件路径的指针,这个文件必须是已经存在且可以访问的。
- 一般使用当前目录,如key = ftok(“.”,1),"."代表当前目录;
ls -i
:可以查找文件的索引节点号。
- proj_id:一个 8 bit的整数,虽然是int类型,不过值的范围通常是 1 到 255。
(2)msgget
msgget函数用于获取一个已存在的消息队列的标识符,或者在 IPC_CREAT
标志被设置的情况下创建一个新的消息队列。通常,msgget函数与其他消息队列操作函数(如msgsnd和msgrcv)一起使用,用于实现进程间通信。
#include <sys/msg.h>
int msgget(key_t key, int flag);
// 返回值:成功返回队列ID,失败返回-1,并设置error错误码
参数说明:
- key:用于指定消息队列的唯一标识符。如果使用
IPC_PRIVATE
作为key
,将创建一个私有的消息队列,只对当前进程可见。 - flag:标志位,用于指定消息队列的访问权限和操作方式。常见的标志位有:
IPC_CREAT
:如果消息队列不存在,则创建它。IPC_EXCL
:与IPC_CREAT
一起使用,如果消息队列已经存在,则返回错误。IPC_NOWAIT
:如果消息队列已满或为空,立即返回错误,而不是阻塞等待。
(3)msgsend
msgsend函数用于将消息发送到指定的消息队列中。
#include <sys/msg.h>
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 返回值:成功返回0,失败返回-1,并设置error错误码
参数说明:
-
msqid:消息队列的标识符,通过
msgget
函数获取。 -
ptr:指向要发送的消息的指针。消息通常是一个结构体,包含一个
long mtype
类型的消息类型字段和一个char mtext[1]
类型的字符数组,用于存储消息的实际内容。 -
size:消息的大小,不包括消息类型的长度。
-
flag:标志位,用于指定发送消息的方式。常见的标志位有:
-
0
:正常发送消息; -
IPC_NOWAIT
:如果消息队列已满,立即返回错误,而不是阻塞等待。
-
(4)msgrcv
msgrcv函数用于从消息队列中接收消息。
#include <sys/msg.h>
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 返回值:成功返回消息数据的长度,失败返回-1,并设置error错误码
参数说明:
- msqid:消息队列的标识符,通过
msgget
函数获取。 - ptr::指向用于存储接收消息的缓冲区的指针。
- size:要接收的消息的大小(不包括消息类型的长度)。
- type:指定要接收的消息类型。type值非 0 时用于以非先进先出次序读消息,也可以把 type 看做优先级的权值。可以是以下几种情况之一:
type == 0
:接收队列任意类型的第一条消息;type > 0
:接收队列中消息类型为 type 的第一个消息;type < 0
:接收队列中消息类型值小于或等于 type 绝对值的消息,如果有多个,则取类型值最小的消息。
- flag:标志位,用于指定接收消息的方式。常见的标志位有:
IPC_NOWAIT
:如果没有满足条件的消息,立即返回错误,而不是阻塞等待。MSG_NOERROR
:如果接收到的消息长度大于msgsz
,则截断消息。
(5)msgctl
msgctl函数用于对消息队列进行控制操作,例如获取消息队列的信息、删除消息队列等。
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
//返回值:成功返回0,失败返回-1,并设置error错误码
参数说明:
- msqid:消息队列的标识符,通过
msgget
函数获取。 - cmd:控制命令,可以是以下几种值之一:常用
IPC_RMID
IPC_STAT
:获取消息队列的信息,并将其存储在buf
指向的结构体中。IPC_SET
:设置消息队列的属性,使用buf
指向的结构体中的值。IPC_RMID
:删除消息队列。
- buf:指向一个
msqid_ds
结构体的指针,用于存储或设置消息队列的信息,通常设置为NULL。
3、编程实现消息队列收发消息
- msg_read:读取消息队列的消息,然后通知发送端已经收到消息
#include <stdio.h>
#include <sys/msg.h>
#include <string.h>
// 定义消息结构体
struct msgbuf {
long mtype; // 消息类型
char mtext[128]; // 消息内容
};
int main() {
int ret; // 用于存储函数返回值
int msgID; // 消息队列标识符
key_t key; //键值
struct msgbuf readBuf; // 用于存储接收的消息
struct msgbuf sendBuf = {
999, "tankyou for read"}; // 要发送的消息
// 使用 ftok 函数生成键值
key = ftok(".",'a');//第二个参数id可以用字符,系统会根据Ascll码转换成相应数字
if (msgID == -1) {
perror("ftok"); // 如果键值生成失败,输出错误信息
}
printf("key = %x\n", key); // 输出生成的键值
// 获取消息队列,IPC_CREAT 表示如果不存在则创建,0777 表示权限
msgID = msgget(0x1234, IPC_CREAT | 0777);
if (msgID == -1