进程通信
# 进程间通信介绍
# 进程间通信的概念
进程间通信简称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,此后父子进程之间就可应用该管道。 如果要实现两个毫不相关进程之间的通信,可以使用命名管道来做到。命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。
# mkfifo函数
- 在命令行中创建命名管道
mkfifo namedepipe
- mkfifo函数用于创建命名管道,mkfifo函数的函数原型如下:
int mkfifo(const char *pathname, mode_t mode);
- 返回值 命名管道创建成功,返回0;命名管道创建失败,返回-1
# 实现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程序
从客户端输入信息,从服务端输出信息
# 共享内存
# 共享内存的基本原理
共享内存让不同进程看到同一份资源的方式就是,在物理内存当中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存。
# 共享内存函数
#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
# 共享内存的释放
当我们的进程运行完毕后,申请的共享内存依旧存在,并没有被操作系统释放。实际上,管道是生命周期是随进程的,而共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放。
这说明,如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(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
- 使用函数
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
2
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函数的参数说明:
- 第一个参数shmid,表示待关联共享内存的用户级标识符。
- 第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置。
- 第三个参数shmflg,表示关联共享内存时设置的某些属性。
- shmat函数的返回值说明:
- shmat调用成功,返回共享内存映射到进程地址空间中的起始地址。
- shmat调用失败,返回(void*)-1。
- 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