进程间通信(IPC):
进程间通信(IPC)是操作系统为用户提供的几种进程间的通信方式。
主要因为进程间有独立性(每个进程都有自己的虚拟地址空间,访问的都是自己的虚拟地址,而不是直接访问物理内存),因此无法直接通信,所以才需要操作系统提供进程间的通信方式,实现进程间的通信。
操作系统针对不同的通信场景提供了多种不同的通信方式:数据传输(管道/消息队列);数据共享(共享内存);进程控制(信号量);
从unix而来的管道;systemV标准的共享内存、消息队列和信号量。
管道(主要用于数据传输):
本质:内核中的一块缓冲区;
原理:多个进程若访问同一个管道(同一块缓冲区)就可以实现通信。
半双工通信:可以选择方向的 单向通信。(不完全双向)
种类:匿名管道和命名管道
声明:Linux下一切接文件,所有的东西都是当做文件一样进行操作(包括管道),通过IO操作完成对管道的访问。
匿名管道:
当使用IO操作创建一个管道,就会返回一个管道的文件描述符,若交给pcb,进程就可以对这个管道进行访问。若这个进程创建了子进程,子进程也就有了这个管道的描述符,进而对管道进行操作。然后父子进程就可以进行通信了。
没有具体的标识符,无法被其他进程找到,只能用于父子进程通信,这就是匿名管道。
匿名管道:内核中的缓冲区没有具体的标识符;因此只能用于具有亲缘关系的进程间通信;
因为只能通过子进程复制父进程的方式获取到管道的操作句柄。因为父进程创建管道的时候操作系统会返回管道的操作句柄。
也正是因为这种特性,创建管道必须要在创建子进程之前。
操纵句柄:文件描述符
创建匿名管道:
int pipe(int pepefd[2]);
pipefd[2]:具有两个int型节点的数组的首地址,用于接收创建管道返回的操作句柄。
pipefd[0]:用于从管道中读取数据; pipefd[1]:用于向管道中写入数据。
特性:管道本身是一个单向的资源传输,自身并不确定资源的传输方向。所以管道是以一个半双工通信(可以选择方向的单向传输)。
使用时,要么只能读,要么只能写。如果不使用哪一端,关闭这一端就可以。
管道的读写特性:
1.若管道中没有数据,则调用read阻塞;
2.若管道中数据写满了,则调用write写入数据会阻塞;(管道是一块缓冲区,一个内存空间,并非无限大);
阻塞:为了完成一个功能,发起调用,若不具备调完成条件,则一直等待。
3.管道生命周期随进程(打开管道的所有进程退出,管道就会被释放)。
4.若管道的*所有*读端pidfd[0]被关闭,则继续调用write会产生异常导致进程退出。
5.若管道的*所有*写端pidfd[1]被关闭,则继续调用read,read读完管道中的所有数据后不再阻塞,而是返回0。
总结:
管道的所有特性,都围绕着管道本身的性质,他是一个生命周期随进程的文件,两头开口,不管存储数据,只管数据的流入流出。
命令行中管道符的实现:
ps -ef | grep pipe
ps -ef : 默认将结果打印到标准输出
grep pipe: 默认从标准输入读取数据进行过滤
管道符就是通过匿名管道实现的
命令的运行都是创建一个子进程,让他在shell中运行的,
ps -ef | grep pipe就是在shell中创建两个子进程
一个子进程程序替换ps
一个子进程替换grep
将ps进程的数据传输给grep,由于这俩是具有亲缘关系的,所以在创建子进程之前创建匿名管道,
两个进程都会有管道的fd,将标准输出重定向到管道写入端,程序替换ps;将标准输入重定向到管道读取端,程序替换gerp,grep就会从管道中读取数据。
ps和grep猜测
标准输入0,标准输出1,标准错误2
grep
while(1)
{scanf("%s",buf); //read(0,buf,1023); 就是从标准输入读取数据}
把0换成管道fd就是从管道中读取数据
ps
getprocessinfo
wirte(1,data) //就是把数据写入标准输出
把1换成管道fd就是向管道写入数据
10 #include<stdio.h>
11 #include<unistd.h>
12 #include<stdlib.h>
13 #include<string.h>
14 #include<wait.h>
15
16
17 int main()
18 {
19 int pipefd[2] = {-1};
20 if(pipe(pipefd)<0)
21 {
22 perror("pipe error");
23 return -1;
24 }
25 pid_t ps_pid = fork();
26 if(ps_pid == 0){
27 //ps子进程
28 dup2(pipefd[1],1); //将标准输出重定向到管道写入端,向1写入数据就相当于向管道写入数据
29 execlp("ps","ps","-ef",NULL);
30 exit(0);
31 }
32 pid_t grep_pid = fork();
33 if(grep_pid == 0)
34 {
35 //grep子进程
36 close(pipefd[1]); //关闭写端:ps进程一旦退出,所有的写段被关闭,grep读完数据后返回0
37 dup2(pipefd[0],0); //将标准输入重定向到管道读取端,从0读取数据就相当于从管道读取数据
38 execlp("grep","grep","pipe",NULL);
39 exit(0);
40 }
41
42 close(pipefd[0]);
43 close(pipefd[1]);
44 waitpid(ps_pid,NULL,0);
45 waitpid(grep_pid,NULL,0);
46
47 return 0;
48
49 }
命名管道:
命名管道:内核中的缓冲区,这块缓冲区具有标识符;
因此--可用于同一主机上的任意进程间通信。
这个标识符是一个可见于文件系统的管道文件,能够被其他进程找到。找到并打开管道文件就可以获得管道的操作句柄。
多个进程通过命名管道通信是通过打开管道文件访问同一块内核中的缓冲区实现通信。
命令: mkfifo 创建管道文件
ls -l 可以观察到文件属性 -prwxrw-rw- -p表示这是管道文件
代码中的操作:int mkfifo(char* filename,mode_t mode);
管道文件名称 管道文件权限
成功返回0,失败返回-1;
open打开命名管道的特性:
1.若管道中没有数据,则调用read阻塞;
2.若管道中数据写满了,则调用write写入数据会阻塞;(管道是一块缓冲区,一个内存空间,并非无限大);
3.管道生命周期随进程(打开管道的所有进程退出,管道就会被释放)。
若文件以只读打开,则会阻塞,直到文件被以写的方式打开。
若文件以只写打开,则会阻塞,直到文件被以读的方式打开。
比如用echo"管道" >> test.fifo ,程序不会退出,在另一个命令行使用cat test.fifo读取了,这边的也就退出了
另写一个.c文件复制过去:
Esc模式下: gg y G 全文复制
:vnew ./fifo_write.c 在当前目录下创建(vnew)一个新的文件
管道的特性总结:
1.管道是半双通信
2.读写特性:管道没有数据则read阻塞,管道数据满了则write阻塞
关闭所有读端write会触发异常;关闭所有写端read读完数据正常退出。
3.管道提供字节流服务:可靠的、有序的、基于连接的字节流服务(数据可以在缓冲区中堆积很多,收发数据灵活)
4.管道自带同步与互斥:
同步:通过条件判断实现对临界资源访问的合理性
--管道中没有数据则read会阻塞,管道中数据满了则write阻塞
先进先出的性质其实也可以理解为队列
互斥:通过唯一访问实现对临界资源访问的安全性
--管道的读写操作在PIPE_BUF大小(一般4096)以内保证操作的原子性
临界资源:大家都能访问到的资源
原子操作:不能被打断的操作,指的是一个操作要么一次性完成,要么就不操作。
5.不人为干预的情况下,管道的生命周期随进程,所有进程关闭后,管道资源释放。
共享内存(主要用于共享数据)
共享内存:
管道用于数据传输的话,用于实现数据共享。
特性: 共享内存是最快的进程间通信方式
生命周期随内核
注意: 共享内存并没有自带同步和互斥 -- 多个进程进行访问时存在安全问题。
本质原理: 在物理内存上开辟一块空间,多个进程可以将同一块物理内存映射到自己的虚拟地址空间,
通过自己的虚拟地址空间直接访问这块物理空间,通过这种方式实现数据共享。
共享内存的操作流程:
1.创建共享内存 -- 在物理内存上开辟空间
int shmget(ket_t key,size_t size,int shmflg);
key:内核中共享内存的标识符(通过标识符可以找到操作句柄) -- 多个进程通过相同的标识符才能打开同一个共享内存。
size:以内存页为单位进行分配
shmflg: IPC_CREAT- 存在则打开,不存在则创建 | IPC_EXCL 与 IPC_CREAT 一起使用,存在则报错,不存在则创建 | mode
返回值:返回一个非负整数 -- 共享内存的操作句柄。
通过inode节点号与projid合成一个key -- ket_t ftok(const char* pathname ,int proj_id);
2.进程将共享内存映射到自己的虚拟地址空间
void* shmat(int shmid,const void* shmaddr,int shmflg);
shmid:shmget返回的共享内存操作句柄
shmaddr:共享内存映射在虚拟地址空间的首地址 -- 通常赋NULL
shmflg:映射成功之后对共享内存可以进行的操作,(操作权限)SHM_RDONLY用于只读(前提是有读的权限)/ 0-默认可读可写。
返回值:返回共享内存映射在虚拟地址空间的首地址 -- 通过这个首地址进行后续的内存操作。 失败返回(void*)-1.
遵循字节对齐定律
3.基本的内存操作都可以对这块空间进程操作
4.不用了,解除虚拟地址空间与共享内存的映射关系
int shmdt(const void* shmaddr); -- 解除映射关系
shmaddr:映射在虚拟地址空间中的首地址
成功返回0,失败返回-1
5.释放(删除)共享内存资源
int shmctl(int shmid,int cmd,struct shmid_ds* buf);
shmid:共享内存操作句柄
cmd:对共享内存想要进行的操作IPC_RMID - 删除共享内存
buf:用于获取/设置共享内存信息的结构,不使用则置空。
共享内存删除的时候,并不会被删除,只是将状态置为被销毁状态,
移除标识--为了不让这个共享内存继续被其他进程映射链接,
然后等x到当前共享内存映射连接数为0的时候,才会真正删除这块共享内存。
systemV标准的三种查看进程间通信资源的命令:
ipcs 三种全部查看
-m 查看共享内存
-q 查看消息队列
-s 查看信号量数组
ipcrm 删除进程间通信资源
-m -q -s 和上面一样
消息队列和信号量
消息队列:
内核中的一个优先级队列,多个进程通过访问一个消息队列,进行添加节点或者获取节点实现通信(以及很少使用)
1.创建消息队列 -- 在内核中创建一个优先级队列 int msgget(key_t key,int msgflg);
2.进程可以向消息队列中获取或添加节点 int msgsnd()
3.删除消息队列:int msgctl(int msqid,int cmd, struct msqid_ds *buf);
特性:
1.自带同步与互斥
2.生命周期随内核
信号量:
posix标准的信号量,
实现进程间的同步与互斥,进程间的通信就这几种方式,它是使用共享内存实现的,多个进程都可以访问
实现线程间的同步与互斥,线程通信灵活,使用全局变量就可以实现通信。只要是临界资源就可以通信
systemV标准的信号量
本质是内核中的一个计数器
是用于实现进程间通信的同步与互斥的
(共享内存本身本身是不提供同步与互斥的,操作存在安全隐患,因此需要使用信号量保护对共享内存的操作)。
同步:通过条件判断实现临界资源访问的合理性
互斥:通过同一时间唯一访问实现临界资源访问的安全性
本质:一个内核中的计数器 + pcb等待队列
信号量这个计数器可以对数据资源进行计数,进程在访问资源之前,先通过计数判断能够访问资源;
计数>0才能访问;获取一个资源, 计数-1;
计数< =0不能访问;则计数-1;然后讲pcb状态置为可中断休眠状态,加入等待队列
其它进程产生了资源,1计数+1;若计数>0则从等待队列中唤醒一个pcb去获取资源
同步与互斥:
同步:资源能访问的时候,让你访问;不能访问的时候,让你等着;等到能够访问了再去唤醒你去访问
信号量通过自身的计数器+等待队列以及使-个进程等待以及唤醒一个进程的操作组成
互斥: 保证同一时间只有一个进程能够访问资源
只需要保证信号量的资源技术不会大于1就可以实现,假设有一个停车位:计数为1,我的车则-1,变为0;其他车就都无法停车,我的车出来了则+1;
信号量实现互斥的思想就是,资源只有一份,只有一个进程可以访问获取,用完了放回来下一个才可以获取。
同步的实现:
通过计数器对资源进行技术,这里的计数器表示有多少资源。在进程访问资源之前进程P操作,产生资源之后进行V操作
互斥的实现:
这里的计数器表示资源只有一个(计数器为1),进程访问资源之前进行P操作(减为0表示没有了,其他进程也就不能访问了),访问完毕之后进行V操作(再加为1,表示可以访问了)