进程通信IPC

引言:进程通信!!

进程通信

IPC

IPC(Inner-Process Communication 进程间通信)

进程之间协作完成任务,必然涉及到其进程间的通信

为什么要进程间通信?

​ 多进程之间的空间是独立的,如果需要操作共享数据,就涉及到了通信;进程通信就是为了协作操作共享数据的(和进程同步的目的也差不多)


下面我们先从为什么需要通信开始讲起

进程的空间是完全独立的

先引入一个问题

OS为什么要将进程设计为独立的?

进程空间

1、虚拟内存

就是程序运行时提供的内存空间,进程运行的空间都是虚拟内存空间(此虚拟内存空间指,OS在物理内存上营造出来的空间)

2、虚拟地址

虚拟空间有自己的虚拟地址,并不和真实的物理地址一一对应

注意:CPU从PC取指令运行,PC存放的地址为虚拟地址,而不是真实地址

(之后会进行虚拟地址与真实地址的转换,将数据从真实地址中取出)

3、 进程空间中虚拟地址的编码范围

注意:每个进程空间中的虚拟地址范围都是一样的

对于32位OS来说,虚拟内存的虚拟地址的编码范围为:

1
2
3
4
5
4G-1: 1111 1111...1111 1111
4G-2: 1111 1111...1111 1110
...
1: 0000 0000 ... 0000 0001
0: 0000 0000 ... 0000 0000

注意:虚拟地址范围如此大,但是对应到物理内存,只有几M而已!

实际上,OS对进程的虚拟地址做了限制,只使用了其中一部分

4、虚拟内存都一样,进程间会不会相互干扰?

不会,类似于一楼编号1-20、二楼编号1-20,,1楼对1楼的操作,不会影响到二楼(这也是虚拟内存的意义)

5、进程间相互独立空间的好处与坏处

好处:

​ 可以保证安全,防止病毒的侵入(病毒也是一个进程,他和我的其他进程不共享数据,也就无法入侵)

缺点:

​ 增加了共享信息的难度,所以提出了IPC

6、广义与狭义

广义上来说,只要能够实现进程间通信的交换方式就是进程间通信

比如:A进程 -> 文件 -> B进程AB进程通过文件来交换数据、再比如AB进程通过数据库来进行数据交换

但是我们研究的都是OS来实现的狭义上的进程通信

管道

管道分为两种:无名管道、有名管道

无名管道

内核会开辟一个“管道”,通信的进程可以共享这个管道,从而实现通信

什么是管道?

其实管道就是OS内核在自己的物理内存空间开辟的一段缓存空间,并且提供对这块缓存区域对应的API,方便进程进行读写

如何操作管道?

通过文件描述符及相关writeread等IO函数对文件进行操作

(这里我们也能知道,其实管道也是一个文件,因为有文件描述符

为什么要叫无名管道?

管道其实也是一个文件,但是这个文件比较特殊,因为它没有文件名,所以叫无名管道


无名管道的实现原理

函数原型:

1
2
3
#include <unistd.h>
int pipe(int pipe[2]); // 等同于 int pipe(int *pipe)
// 只不过这样写可以让你知道要传几个参数

功能就是创建一个用于有亲缘关系的进程通信的管道

(注意:管道只能满足亲缘关系进程通信,例如父子、父孙进程之间的通信!)

有两个参数:

  • 元素[0]:放读管道的读文件描述符
  • 元素[1]:放写管道的写文件描述符

(这里的读写文件描述符是独立的,即并不是open函数得到的,创建无名管道时就会有它的读写文件描述符)

为什么无名管道只能用于亲缘进程间通信?

因为无名管道没有文件名,因此没有办法通过open打开文件,得到文件描述符,所以只有一种办法:

  1. 就是父进程先调用pipe函数创建出管道,得到无名管道的文件描述符
  2. 然后fork出子进程,让子进程通过继承父进程打开文件描述符,从而父子间操作同一个管道,进行通信

无名管道示意图

注意:

  1. 管道创建失败返回 -1,成功返回一个大于0的数
  2. 读管道时,如果管道没有数据,那么读操作会休眠(即阻塞)
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 <stdio.h>
#include <stdlib.h>
#include <unistd.h> // 使用pipe()
#include <strings.h> // 使用了bzero()

void print_error(char *str){
perror(str);
exit(-1);
}

int main(){
int ret = 0;
int pipefd[2] = {0};// 用来存放无名管道的文件描述符
// 赋值为0,是为了清除一下以前的数据

ret = pipe(pipefd);
printf("%d, %d \n", pipefd[0], pipefd[1]);
// 显示一下文件描述符是多少
// 其实猜也能猜到,是3, 4(因为0,1,2已经被占用了)
if(ret == -1){// 返回-1 代表管道创建失败
print_error("pipe_create_fail");
}
ret = fork();// 创建一个进程
if(ret > 0){// 父进程
while(1){
write(pipefd[1], "hello", 5);
// 三个参数,写端的文件描述符,写的内容,内容的长度
sleep(2);
// 每两秒写一次
}
}else if(ret == 0){// 子进程
while(1){
char buf[30] = {0};
bzero(buf, sizeof(buf));// 将缓存区清空
read(pipefd[0], buf, sizeof(buf));
printf("child process read: %s \n",buf);
}
}
}

运行结果如下:

1
2
3
4
5
6
[root@master learnIPC]# ./a.out 
3, 4
child process read: hello
child process read: hello
child process read: hello
...

无名管道是单向还是双向通信?

是单向通信的,只有一个写段,一个读端;

如果你用两个线程分别去读写,那么写线程会立即读到自己的写的内容

如何实现双向通信?

可以使用两个无名管道

无名管道的缺点

  • 无法用于无亲缘关系的进程
  • 不适合用于网状通信(因为会导致文件描述符变的异常复杂)(只适合单向双方通信)

有名管道

什么是有名管道

对比无名管道就是有了名字,即有了文件名的管道文件

这意味着我们可以通过名字open来打开这个文件,得到其文件描述符

有名管道的特点

  1. 能够用于非亲缘进程之间的通信
  2. 读管道时,管道无数据,读操作会阻塞
  3. 当进程写一个所有读端都被关闭的管道,进程会被返回SIGPIPE信号(如果不想管道被该信号终止,要忽略这个信号)

有名管道的创建步骤

  1. 使用mkfifo创建有名管道
  2. open打开有名管道
  3. read/write读写管道进行通信

函数原型

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

int mkfifo(const char *pathname, mode_t mode);

传入两个参数:

  • pathname:文件路径
  • mode:被创建的原始权限(一般为0664)必须包含读写权限

注意!有名管道也是单向通信,如果要实现双向通信,也需要使用两个有名管道

System V IPC提供了新的通信方式

什么是System V IPC?

管道是Unix提供的很原始的一种进程间通信方式,后来升级到版本5后,引入了新的进程通信方式:

  • 消息队列
  • 信号量
  • 共享内存

所以System V IPC指的就是这三种通信方式

System V 的通信原理

与管道的通信方式不同(管道的通信方式类似于文件操作)

System V IPC不再以文件的形式存在,而是通过一个标识符(完全可以认为这个标识符就是文件描述符的替代品)

消息队列

消息队列是什么?

消息队列就是一个用于存放消息的双向链表

通信的进程通过共享操作同一个消息队列,就能实现通信

消息是如何存放在消息队列中?

链表的每一个结点就是一个消息,结点应该有两部分内容:

  • 消息编号
  • 消息正文
1
2
3
4
struct msgbuf{
long mtype; // 消息编号
char mtext[msgsz]; //消息内容
}

消息队列的作用

消息队列很好的实现了网状通信!

(具体的创建过程等内容,过于硬件,不再介绍)

共享内存

什么是共享内存?

共享内存就是OS在内存区域开辟的一大段内存空间

适合于什么场景?

适合于传输大量的数据(如果用管道或者消息队列,将会很慢)是最快的IPC方式

如果要让很多个进程共享同一个空间,要加保护措施

实现方式

  1. 调用API,让OS内核在物理内存开辟出一大段空间
  2. 让进程与开辟的缓存建立映射关系

共享内存与管道的区别:

  1. 管道小,共享内存大
  2. 管道慢(管道每次调用都要进行系统调用),共享内存快
  3. 管道不需要自己管理,其会自己阻塞;而共享内存需要我们自己管理

信号量

信号量是一个锁机制,为什么会归类到IPC内?

因为其进行锁的原理,就是通知各个进程什么时候该做什么,所以也算是一种通信方式(只不过其通信的内容仅仅只是事情发生了没有,而不是事情是什么)

Socket

socket也可以实现进程间通信(Socket另外单独来讲)

信号

什么是信号?

​ 信号是一条小的消息,由内核或者其它进程生成并发送至目标进程,目标进程可以根据该信号来做出响应。

​ 信号可以由进程或者内核发出,例如:

  • 用户在Bash界面通过键盘对正在执行的进程输入Ctrl+CCtrl+\等信号命令,或者执行kill命令发送信号。
  • 进程执行出错,例如访问了一个非法的地址、除0运算,或者硬件发生故障,就会由内核向进程发送一个信号。
  • 进程执行kill命令向目标进程发送信号。

信号的传递步骤:

传送一个信号到目的进程主要有两个步骤

  • 发送信号内核通过更新目标进程上下文的某个状态,传递一个信号给目标进程
  • 接收信号:目标进程会被内核强制以某种方式对信号的发送做出反应,它就会接收到信号。(如果程序没有针对这种信号指定其处理方式,就会采用默认的处理策略,例如中止进程、忽略)

一个发出但没有接收的信号称之为待处理信号,在任何时刻,同一种类型的信号最多只会有一个待处理的信号

发送信号的方式

  1. 通过操作系统的bin/kill程序,向程序发送信号

例如:kill -9 pid向进程发送信号值为9的信号

  1. 用键盘发出信号

在命令行界面中,能有一个前台任务和多个后台任务

  1. kill函数发送信号

kill函数的定义位于头文件signal.h中:

1
2
3
4
5
6
int kill(pid_t pid, int sig);
/*
如果pid > 0,该函数会向PID为pid的进程发送信号值为sig的信号。
如果pid = 0,该函数会向当前进程隶属的进程组下所有的进程发出信号值为sig的信号,包括这个进程自己。
如果pid < 0,该函数会向进程组ID为-pid下所有的进程发出信号值为sig的信号
*/
  1. alarm函数发送SIGALRM信号

进程可以通过alarm函数向自己发送一个SIGALRM信号,该函数定义于头文件unistd.h中:

1
2
3
unsigned int alarm(unsigned int secs);
// 传入一个时间参数secs,单位为秒,
// 表示在secs秒后发送一个SIGALRM信号给当前线程