每个操作系统都有自己的进程间通信的方式,不过大都类似,这里主要讨论Linux下的几种进程间通信方式。

管道

大多数操作系统的管道是半双工的,某些系统有全双工管道。管道只能在具有公共祖先的两个进程中使用,通常有一个进程创建,另一个进程被fork出来,此时这两个进程就可以使用该管道。

01

在命令行中会以|的形式将两边程序进行管道通信,例如

1
ps aux | grep s | wc

在程序中,管道可以通过pipe函数进行创建

1
2
3
4
5
#include <unistd.h>

// fd[0]是管道的读口,f[1]是管道的写口
// 如果创建成功返回0,否则返回-1
int pipe(int fd[2]);

为了不混淆两个进程谁来读或谁来写,一般会创建一个管道,fork之后,父进程关闭一端,子进程关闭另一端,这样就形成了一个单向流动的管道。

02

具体用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <unistd.h>
#include <wait.h>

#include <cstdio>

int main() {
// 假设fd1是父进程流向子进程,fd2是子进程流向父进程
int fd1[2];
int fd2[2];
pipe(fd1);
pipe(fd2);
pid_t pid = fork();
if (pid == 0) {
close(fd1[1]); // 关闭子进程fd1的写口
close(fd2[0]); // 关闭子进程fd2的读口

// 从子进程通过fd2转数据到父进程
char s1[] = "Hello World";
write(fd2[1], s1, sizeof(s1));

sleep(1); // 睡眠一会等待父进程传来数据
char s2[50];
read(fd1[0], s2, 20);
printf("from father to son : %s\n", s2);
} else {
close(fd1[0]); // 关闭父进程fd1的读口
close(fd2[1]); // 关闭父进程fd2的写口

// 从父进程通过fd1传数据到子进程
char s1[] = "dlroW olleH";
write(fd1[1], s1, sizeof(s1));

sleep(1); // 睡眠一会等待子进程传来数据
char s2[50];
read(fd2[0], s2, 20);
printf("from son to father : %s\n", s2); // 输出子进程传来的消息

wait(&pid);
}
return 0;
}

FIFO

FIFO又被称为有名管道,未命名管道只能用在两个相关的进程,而FIFO可以使两个不相关的进程也能交换数据。

创建FIFO如同创建文件,该路径名称也存在于文件系统中

1
2
3
4
5
6
#include <sys/stat.h>

// path是文件路径,mode是所有权规则。成功返回0,否则返回-1
int mkfifo(const char *path, mode_t mode);
int mkfifoat(int fd, const char *path, mode_t mode);

FIFO有两种用途:

  1. shell命令使用FIFO将数据从一条管道传送到另一条时,无需创建中间临时文件。
  2. 客户端和服务端应用程序中,FIFO作为汇聚点,再客户端和服务端之间传递数据。

具体用法

send.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include <cstdio>

int main() {
mkfifo("a.txt", 0777);
int fd = open("a.txt", O_WRONLY);
char s[] = "Hello World";
write(fd, s, sizeof(s));
printf("send.cpp send a message...\n");
return 0;
}

recv.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include <cstdio>

int main() {
mkfifo("a.txt", 0777);
int fd = open("a.txt", O_RDONLY);
while (true) {
char s[20];
int ret = read(fd, s, sizeof(s));
if (ret > 0) {
printf("recv recieve a message: %s\n", s);
break;
}
}
return 0;
}

消息队列

消息队列是消息的链接表,存储在内核中。每个消息包含一个正的长整型的字段、一个非负的长度以及实际字节数。不一定需要按照先进先出的顺序取消息,可以按照消息的类型字段取消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>

// 创建新队列或打开一个存在的队列
int msgget(key_t key, int msgflg);

/**
* @brief 对队列执行多种操作
*
* @param msqid 消息队列句柄
* @param cmd 命令
* @param buf 一般为NULL
* @return int
*/
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

/**
* @brief 向消息队列发送消息
*
* @param msqid 消息队列句柄
* @param msgp 要发送的消息结构体指针
* @param msgsz 发送的消息大小
* @param msgflg 操作类型
* @return int
*/
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

/**
* @brief 从消息队列中接收消息
*
* @param msqid 消息队列句柄
* @param msgp 接收消息存放位置
* @param msgsz 接受的消息大小
* @param msgtyp 消息类型
* @param msgflg 操作类型
* @return ssize_t
*/
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

具体用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <unistd.h>
#include <wait.h>

#include <cstdio>
#include <cstdlib>
#include <cstring>

#define MAX_LEN 128

struct MessageType {
long type; // 消息类型,必须大于0
char content[MAX_LEN]; // 消息内容
};

int main() {
int msgid = msgget(1001, 0666 | IPC_CREAT);

pid_t pid = fork();
if (pid == 0) {
sleep(1);
system("ipcs -q");
MessageType recv_msg;
// 接收消息
msgrcv(msgid, &recv_msg, sizeof(recv_msg.content), 0, 0);
printf("child recieve message: %s\n", recv_msg.content);
} else {
// 设置发送的消息
MessageType send_msg;
send_msg.type = 1;
memset(send_msg.content, 0, sizeof(send_msg.content));
strcpy(send_msg.content, "Hello World");
// 发送消息
msgsnd(msgid, &send_msg, strlen(send_msg.content), 0);
// msgctl(msgid, IPC_RMID, NULL);
wait(&pid);
}
return 0;
}

信号量

它是一个计数器,用于为多个进程提供对共享数据对象的访问。

为了获取共享资源,进程需要执行以下操作:

  1. 测试控制该资源的信号量。
  2. 如果该信号量为正,则可以使用该进程资源,同时进程会将信号量减1,表示它使用了一个资源。
  3. 否则如果该信号量为0,则进入休眠状态,直到信号量大于0。进程被唤醒后返回第一步。

当进程不再使用资源时,该信号量加1,如果有进程正在休眠等待此信号,则唤醒他们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>

/**
* @brief 创建信号量
*
* @param key 一般通过ftok函数得到
* @param nsems 需要的信号量数目
* @param semflg 一组标志位
* @return int 成功返回相应信号标识符,失败返回-1
*/
int semget(key_t key, int nsems, int semflg);

/**
* @brief 改变信号量的值
*
* @param semid 信号量标识符
* @param sops 数组指针,每个结构体对应一个信号量操作
* @param nsops 数组中元素个数
* @return int
*/
int semop(int semid, struct sembuf *sops, size_t nsops);

/**
* @brief 对信号量要进行一些操作
*
* @param semid 信号量集标识符
* @param semnum 要操作的信号量的编号
* @param cmd 操作
* @param ...
* @return int
*/
int semctl(int semid, int semnum, int cmd, ...);

共享内存

共享内存允许两个或多个进程共享一个给定的存储区,因为数据不需要来回复制,所以是最快的一种IPC。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <sys/shm.h>

/**
* @brief 获得一个共享存储标识符
*
* @param key
* @param size 存储段长度
* @param shmflg 权限位
* @return int 成功返回存储id,否则返回-1
*/
int shmget(key_t key, size_t size, int shmflg);

/**
* @brief 对共享存储段执行多种操作
*
* @param shmid 存储标识符
* @param cmd 操作
* @param buf 根据cmd进行设置
* @return int
*/
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

/**
* @brief 将共享存储段连接到地址空间
*
* @param shmid 存储标识符
* @param shmaddr
* 如果为0,则此段连接到内核选择的第一个可用的地址上;如果非0,并且没有指定
* SHM_RND,则此段连接到addr所指定的地址上;如果非0,并且制定了SHM_RND,
* 则此段连接到(addr - (addr % SHMLBA))所在的地址上
* @param shmflg
* @return void*
*/
void *shmat(int shmid, const void *shmaddr, int shmflg);

/**
* @brief 删除某个共享存储段
*
* @param shmaddr 段首地址
* @return int
*/
int shmdt(const void *shmaddr);

shm和mmap

mmap是在磁盘上创建一个文件,在进程的地址空间和文件建立映射;shm没有创建文件,每个进程都会映射到同一块物理内存。