进程通信
# 进程间通信介绍
# 概念
进程间通信简称 IPC(Interprocess communication),进程间通信就是在不同进程之间传播或交换信息。
# 目的
- 数据传输: 一个进程需要将它的数据发送给另一个进程。
- 资源共享: 多个进程之间共享同样的资源。
- 通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件,比如进程终止时需要通知其父进程。
- 进程控制: 有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
# 分类
按照通信方式分类:
- 管道
- 匿名管道
- 命名管道
- System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
- POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
按照通信类型分类:
- 单工通信:发送方只能发送数据,而接收方只能接收数据,两者不能互换角色。
- 半双工通信:两个设备可以交替地发送和接收数据,但不能同时发送和接收。
- 全双工通信:双工通信允许数据在同一时刻进行双向传输,即两个设备可以同时发送和接收数据。
# 本质
- OS 需要给通信双方的进程提供内存空间
- 通信的进程要看到一份公共的资源
因此,进程间通信的本质就是,让不同的进程看到同一份资源(内存,文件内核缓冲等)。 由于这份资源可以由操作系统中的不同模块提供,因此出现了不同的进程间通信方式。
# 管道
# 匿名管道
是内核管理的一块缓冲区,是一种半双工通信手段,通过让不同进程都能访问同一块缓冲区,来实现进程间通讯。它仅限于本地父子进程之间通信,结构简单,相对于命名管道,其占用小,实现简单。
# 作用
匿名管道用于进程间通信,且仅限于本地父子进程之间的通信。
# pipe 函数
pipe 函数用于创建匿名管道,pip 函数的函数原型如下:
#include <unistd.h>
int pipe(int pipefd[2]);
// pipefd[0] 读端 "嘴巴"
// pipefd[1] 写端 "铅笔"
2
3
4
pipe 函数调用成功时返回 0,调用失败时返回-1。

# 读写操作
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <cstdio>
#include <cstdlib>
using namespace std;
int main() {
// 创建管道
int fds[2];
if (pipe(fds) == -1) {
perror("pipe failed");
return 1;
}
cout << "fds[0] (read end) = " << fds[0] << endl; // 读端
cout << "fds[1] (write end) = " << fds[1] << endl; // 写端
// 父进程进行读取,子进程进行写入
// 创建子进程
pid_t id = fork();
if (id == -1) {
perror("fork failed");
return 1;
}
if (id == 0) {
// 子进程
close(fds[0]); // 关闭读端
int cnt = 0;
while (cnt < 5) { // 设置最大消息数为5
cnt++;
char buff[1024];
snprintf(buff, sizeof(buff), "%d:%d :msg from child process to father process", cnt, getpid());
write(fds[1], buff, strlen(buff)); // 向管道写端写入数据
sleep(3); // 每隔3秒发送一次消息
}
close(fds[1]); // 关闭写端
exit(0); // 子进程退出
} else {
// 父进程
close(fds[1]); // 关闭写端
while (true) {
char buff[1024];
ssize_t sz = read(fds[0], buff, sizeof(buff) - 1); // 从管道读端读取数据
if (sz > 0) {
buff[sz] = '\0'; // 添加字符串结束符
cout << "#" << getpid() << " | get message: " << buff << endl;
} else if (sz == 0) {
// 管道写端被关闭,退出循环
cout << "Child process closed the pipe. Exiting..." << endl;
break;
} else {
perror("read failed");
break;
}
}
close(fds[0]); // 关闭读端
waitpid(id, nullptr, 0); // 等待子进程退出
}
return 0;
}
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
输出结果:
fds[0] (read end) = 3
fds[1] (write end) = 4
#2533100 | get message: 1:2533101 :msg from child process to father process
#2533100 | get message: 2:2533101 :msg from child process to father process
#2533100 | get message: 3:2533101 :msg from child process to father process
#2533100 | get message: 4:2533101 :msg from child process to father process
#2533100 | get message: 5:2533101 :msg from child process to father process
Child process closed the pipe. Exiting...
2
3
4
5
6
7
8
# 管道的读写规则
- 当没有数据可读时
- O_NONBLOCK disable:read 调用阻塞,即进程暂停执行,一直等到有数据来到为止。
- O_NONBLOCK enable:read 调用返回-1,errno 值为 EAGAIN。
- 当管道满的时候
- O_NONBLOCK disable: write 调用阻塞,直到有进程读走数据
- O_NONBLOCK enable:调用返回-1,errno 值为 EAGAIN
- 如果所有管道写端对应的文件描述符被关闭,则 read 返回 0
- 如果所有管道读端对应的文件描述符被关闭,则 write 操作会产生信号 SIGPIPE,进而可能导致 write 进程退出
- 当要写入的数据量不大于 PIPE_BUF 时,linux 将保证写入的原子性。
- 当要写入的数据量大于 PIPE_BUF 时,linux 将不再保证写入的原子性
# 四种特殊情况
- 写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒。
- 读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。
- 写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。
- 读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么操作系统会将写端进程杀掉。
# 命名管道
# 作用
匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间的通信,通常,一个管道由一个进程创建,然后该进程调用 fork,此后父子进程之间就可应用该管道。 如果要实现两个毫不相关进程之间的通信,可以使用命名管道来做到。命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。
FIFO(命名管道)与 pipe(匿名管道)相同点:
- 都是半双工通信
- 都是在内核中维护固定大小的缓冲区
- 都是使用引用计数来管理生命周期
区别:
- pipe 在文件系统不存在,而 FIFO 有路径,是一个特殊的文件
- 创建与打开的方式不同,pipe 通过 pipe()自动获得,FIFO 还需要 open()打开
- pipe 用在本地父子进程,FIFO 用在本地任意进程
# mkfifo 函数
- 在命令行中创建命名管道
mkfifo namedepipe
- mkfifo 函数用于创建命名管道,mkfifo 函数的函数原型如下:
int mkfifo(const char *pathname, mode_t mode);
返回值 命名管道创建成功,返回 0;命名管道创建失败,返回-1
如果当前打开操作是为读而打开 FIFO 时,读操作会阻塞,直到有相应的进程为写而打开该 FIFO。这意味着,如果当前没有进程为写而打开 FIFO,那么尝试读取该 FIFO 的进程将被挂起,直到有写进程打开 FIFO。
如果当前打开操作是为写而打开 FIFO 时,写操作会阻塞,直到有相应的进程为读而打开该 FIFO。这意味着,如果当前没有进程为读而打开 FIFO,那么尝试写入该 FIFO 的进程将被挂起,直到有读进程打开 FIFO。
# 实现 serve&client 通信
client.cpp
#include <iostream>
//client.cpp
#include "comm.hpp"
int main() {
int fd = open(FILE_NAME, O_WRONLY); //以写的方式打开命名管道文件
if (fd < 0) {
perror("open");
return 1;
}
char msg[128];
while (1) {
msg[0] = '\0'; //每次读之前将msg清空
printf("Please Enter# "); //提示客户端输入
fflush(stdout);
//从客户端的标准输入流读取信息
ssize_t s = read(0, msg, sizeof(msg) - 1);
if (s > 0){
msg[s - 1] = '\0';
//将信息写入命名管道
write(fd, msg, strlen(msg));
}
}
close(fd); //通信完毕,关闭命名管道文件
return 0;
}
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
server.cpp
#include <iostream>
//server.cpp
#include "comm.hpp"
int main() {
umask(0); //将文件默认掩码设置为0
if (mkfifo(FILE_NAME, 0666) < 0) { //使用mkfifo创建命名管道文件
perror("mkfifo");
return 1;
}
int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件
if (fd < 0) {
perror("open");
return 2;
}
char msg[128];
while (1) {
msg[0] = '\0'; //每次读之前将msg清空
//从命名管道当中读取信息
ssize_t s = read(fd, msg, sizeof(msg) - 1);
if (s > 0) {
msg[s] = '\0'; //手动设置'\0',便于输出
printf("client# %s\n", msg); //输出客户端发来的信息
}
else if (s == 0) {
printf("client quit!\n");
break;
}
else {
printf("read error!\n");
break;
}
}
close(fd); //通信完毕,关闭命名管道文件
return 0;
}
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
comm.hpp
#pragma once
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#define FILE_NAME "namedpipe" //让客户端和服务端使用同一个命名管道
2
3
4
5
6
7
8
9
10
11
Makefile
.PHONY:all
all:server client
server:server.cpp
g++ -o $@ $^ -std=c++11 -g
client:client.cpp
g++ -o $@ $^ -std=c++11 -g
.PHONY:clean
clean:
rm -f server client
2
3
4
5
6
7
8
9
10
11
12
运行结果
先将 server 程序运行起来,再运行 client 程序
从客户端输入信息,从服务端输出信息
# System V 共享内存
# 共享内存的基本原理
共享内存让不同进程看到同一份资源的方式就是,在物理内存当中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存,是最快的一种通信方式。
shmid_ds 是 Linux 系统中用于共享内存的一个内核数据结构。它主要用于描述和管理一个共享内存段的元数据(即关于数据的数据,如权限、大小、时间戳等)
# 共享内存函数
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
2
- 参数
- 第一个参数 key,表示待创建共享内存在系统当中的唯一标识。
- 第二个参数 size,表示待创建共享内存的大小。
- 第三个参数 shmflg,表示创建共享内存的方式
其中 key 用于唯一性标识,一般使用 ftok 函数获取:
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
2
其中 shmflg 的取值:
- IPC_CREAT:表示如果共享内存不存在,则创建一个共享内存;存在的话,就继续使用这个共享内存。
- IPC_EXCL:表示如果共享内存已经存在,则 shmget 调用失败;不存在的话,就创建一个共享内存。
- 返回值
- shmget 调用成功,返回一个有效的共享内存标识符(用户层标识符)。
- shmget 调用失败,返回-1
# 创建和获取
comm.hpp
#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATHNAME "."
#define PROJ_ID 0x66
#define MAX_SIZE 1000
key_t getKey() {
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0) {
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(1);
}
return key;
}
int getShmHelper(key_t key, int flags) {
int shmid = shmget(key, MAX_SIZE ,flags);
if(shmid < 0) {
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(1);
}
return shmid;
}
int getShm(key_t key) {
return getShmHelper(key, IPC_CREAT);
}
int createShm(key_t key) {
return getShmHelper(key, IPC_CREAT | IPC_EXCL);
}
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
shm_server.cpp
#include "comm.hpp"
int main() {
std::cout << "server" << std::endl;
key_t key = getKey();
std::cout << "key = 0x" << key << std::endl;
int shmid = createShm(key);
std::cout << "shmid = " << shmid << std::endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
shm_client.cpp
#include "comm.hpp"
int main() {
std::cout << "client" << std::endl;
key_t key = getKey();
std::cout << "0x" << key << std::endl;
int shmid = getShm(key);
std::cout << "shmid = " << shmid << std::endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
运行:
[cen@VM-4-9-opencloudos shm]$ ./server
server
key = 0x1711371500
shmid = 1
[cen@VM-4-9-opencloudos shm]$ ./cilent
client
0x1711371500
shmid = 1
2
3
4
5
6
7
8
单独使用 ipcs 命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:
-q:列出消息队列相关信息 -m:列出共享内存相关信息 -s:列出信号量相关信息
[cen@VM-4-9-opencloudos shm]$ ipcs
------ Message Queues --------
key msqid owner perms used-bytes messages
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x660174ec 1 cen 0 1000 0
------ Semaphore Arrays --------
key semid owner perms nsems
2
3
4
5
6
7
8
9
10
11
# 控制
共享内存的控制通常使用 shmctl 函数来实现。这个函数的全称是"shared memory control",用于控制共享内存段的属性、状态以及执行一些管理操作。
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
2
cmd:控制命令,指定了要执行的操作。常见的命令包括:
- IPC_STAT:获取共享内存段的状态信息,并将其保存在 buf 中。
- IPC_SET:设置共享内存段的状态信息为 buf 中的值。
- IPC_RMID:删除共享内存段。因为每个共享存储段有一个连接计数 (shm_nattch 在 shmid_ds 结构中),所以除非使用该段的最后一个进程终止或与该段脱接,否则不会实际上删除该存储段。不管此段是否仍在使用,该段标识符立即被删除 ,所以不能再用 shmat 与该段连接。
# 释放
当我们的进程运行完毕后,申请的共享内存依旧存在,并没有被操作系统释放。实际上,管道是生命周期是随进程的,而共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放。
这说明,如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC 都是如此),同时也说明了 IPC 资源是由内核提供并维护的。
此时我们若是要将创建的共享内存释放,有两个方法,一就是使用命令释放共享内存,二就是在进程通信完毕后调用释放共享内存的函数进行释放。
- 使用指令
key 是在内核层面上保证共享内存唯一性的方式,而 shmid 是在用户层面上保证共享内存的唯一性,key 和 shmid 之间的关系类似于 fd 和 inode 之间的的关系
使用ipcrm -m shmid命令即可:
[cen@VM-4-9-opencloudos shm]$ ipcrm -m 1
[cen@VM-4-9-opencloudos shm]$ ipcs
------ Message Queues --------
key msqid owner perms used-bytes messages
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
------ Semaphore Arrays --------
key semid owner perms nsems
2
3
4
5
6
7
8
9
10
11
- 使用函数
void deleteShm(int shmid) {
if(shmctl(shmid, IPC_RMID, nullptr) == -1) {
std::cout << errno << strerror(errno) << std::endl;
exit(1);
}
}
2
3
4
5
6
# 关联和去关联
共享内存和进程地址空间的关联和去关联通过下列函数实现:
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
2
3
关联:
shmat 是 “shared memory attach” 的缩写,它的功能是将共享内存区域附加到指定的进程地址空间中。一旦附加成功,进程就可以像访问自己的内存一样访问共享内存。
- shmat 函数的参数说明:
- 第一个参数 shmid,表示待关联共享内存的用户级标识符。
- 第二个参数 shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为 NULL,表示让内核自己决定一个合适的地址位置。
- 第三个参数 shmflg,表示关联共享内存时设置的某些属性。
- shmat 函数的返回值说明:
- shmat 调用成功,返回共享内存映射到进程地址空间中的起始地址。
- shmat 调用失败,返回(void*)-1。
shmdt 函数的功能是将之前附加到进程的共享内存段从进程地址空间中分离。一旦分离,进程就不能再访问这块共享内存了。
- shmdt 函数的参数说明:
待去关联共享内存的起始地址,即调用 shmat 函数时得到的起始地址。
- shmdt 函数的返回值说明:
shmdt 调用成功,返回 0。 shmdt 调用失败,返回-1。
示例:
int main() {
std::cout << "server" << std::endl;
key_t key = getKey();
std::cout << "key = 0x" << key << std::endl;
// 创建
// int shmid = createShm(key);
int shmid = shmget(key, MAX_SIZE, IPC_CREAT | IPC_EXCL | 0666);
std::cout << "shmid = " << shmid << std::endl;
// 关联
char* start = (char*)attachShm(shmid);
sleep(5);
// 去关联
detchShm(start);
sleep(5);
// 释放
sleep(5);
deleteShm(shmid);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
运行结果:
[cen@VM-4-9-opencloudos shm]$ while :;do ipcs -m; sleep 1; echo "----------"; done
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
----------
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
----------
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
----------
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x660174ec 19 cen 666 1000 1
----------
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x660174ec 19 cen 666 1000 1
----------
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x660174ec 19 cen 666 1000 1
----------
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x660174ec 19 cen 666 1000 0
----------
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x660174ec 19 cen 666 1000 0
----------
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x660174ec 19 cen 666 1000 0
----------
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
----------
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
----------
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
----------
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# 共享内存实现 server&cilent 的通信
comm.hpp
#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#define PATHNAME "."
#define PROJ_ID 0x66
#define MAX_SIZE 1000
// 获key值
key_t getKey() {
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0) {
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(1);
}
return key;
}
int getShmHelper(key_t key, int flags) {
int shmid = shmget(key, MAX_SIZE ,flags | 0666);
if(shmid < 0) {
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(1);
}
return shmid;
}
int getShm(key_t key) {
return getShmHelper(key, IPC_CREAT);
}
// 创建共享空间
int createShm(key_t key) {
return getShmHelper(key, IPC_CREAT | IPC_EXCL);
}
// 释放共享空间
void deleteShm(int shmid) {
if(shmctl(shmid, IPC_RMID, nullptr) == -1) {
std::cout << errno << strerror(errno) << std::endl;
exit(1);
}
}
// 关联共享空间
void* attachShm(int shmid) {
void* mem = shmat(shmid, nullptr, 0);
if(mem == (void*)-1) {
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(1);
}
return mem;
}
// 去关联共享空间
void detchShm(void* start) {
if(shmdt(start) == -1) {
std::cout << "detchshm error" << std::endl;
return;
}
}
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
server 端:
#include "comm.hpp"
int main() {
std::cout << "server" << std::endl;
key_t key = getKey();
std::cout << "key = 0x" << key << std::endl;
// 创建
// int shmid = createShm(key);
int shmid = shmget(key, MAX_SIZE, IPC_CREAT | IPC_EXCL | 0666);
std::cout << "shmid = " << shmid << std::endl;
// 关联
std::cout << "attach success!" << std::endl;
char* start = (char*)attachShm(shmid);
sleep(10);
// 使用
while(true) {
std::cout << "cilent say:" << start << std::endl;
sleep(1);
}
// 去关联
std::cout << "attach exit!" << std::endl;
detchShm(start);
sleep(10);
// 释放
sleep(10);
deleteShm(shmid);
return 0;
}
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
client 端:
#include "comm.hpp"
int main() {
std::cout << "client" << std::endl;
key_t key = getKey();
std::cout << "0x" << key << std::endl;
int shmid = getShm(key);
std::cout << "shmid = " << shmid << std::endl;
// 关联
sleep(3);
std::cout << "attach success!" << std::endl;
char* start = (char*)attachShm(shmid);
const char* msg = "message from cilent";
int cnt = 1;
while(true) {
snprintf(start, MAX_SIZE, "%s[消息编号:%d]", msg, cnt++);
sleep(1);
}
// 去关联
sleep(3);
std::cout << "attach exit!" << std::endl;
detchShm(start);
return 0;
}
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