●个人主页:你帅你先说.
●欢迎点赞关注收藏
●既选择了远方,便只顾风雨兼程。
●欢迎大家有问题随时私信我!
●版权:本文由[你帅你先说.]原创,CSDN首发,侵权必究。
在系统中,进程之间可能会存在特定的协同工作的场景。一个进程要把自己的数据交付给另一个进程,让其进行处理。两个进程要互相通信其实并不容易,因为进程具有独立性,想要通信必须先看到一份公共资源(一段内存),而这个资源不可能属于任何一个进程,这样就违背独立原则,而是由操作系统管理,所以进程间通信的本质:其实是由OS参与,提供一份所有通信进程都能看到的公共资源。
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
- 管道
- System V进程间通信
- POSIX进程间通信
管道
- 匿名管道pipe
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
在OS中,父进程创建出子进程,它们共享同一份代码和数据,直至发生写时拷贝,才各自拥有一份数据,不知道大家有没有想过,当父进程创建出一个文件时,子进程是否也会创建出一个文件,实际上并不会,因为父子进程是共享代码和数据,而文件不属于进程,所以父子进程会共同访问这个文件,当父进程向文件写入数据时,数据会保存在文件的内核缓冲区,此时子进程可以读取到这段数据,这样进程间就建立起了通信,这就是管道。
图示的管道就是刚刚我们所说的文件的内核缓冲区
。管道是一个只能单向通信的通信信道
#include
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
匿名管道特点
只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
管道提供流式服务。
一般而言,进程退出,管道释放,所以管道的生命周期随进程。
一般而言,内核会对管道操作进行同步与互斥。
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。
管道是有大小的,是64KB
。
为什么写满64KB后,不能再写了,虽然我们写满了,但我们可以覆盖数据,这样可以继续写。因为我们需要给读的人来读,如果写满就覆盖了,那么读的人读到的数据就不完整了。当管道满了后,当读的人读了一定的数据后(在Linux下是4KB),管道会继续写入,所以管道有同步机制
。
Linux中是这样描述这种现象的
当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。
在使用管道进行读写操作时,我们通常会遇到以下四种情况:
1.读端不读或读的慢,写端要等读端
2.读端关闭,写端收到SIGPIPE信号直接终止
3.写端不写或写的慢,读端要等写端
4.写端关闭,读端读完pipe内部的数据然后再读,会读到0,表明读到文件结尾。
- 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
- 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
- 命名管道是一种特殊类型的文件
- 命名管道的数据不会刷新到磁盘,为了效率
指令
mkfifo 管道名称
成功创建则返回0,失败返回-1。
这是一个以p开头的文件
我们之前说过,进程是具有独立性的,所以进程间通信的成本是比较高的,想要让进程间通信就必须让不同的进程先要看到同一份资源。
接下来我们用代码来实现进程间的通讯。
这个时候就可以进行两个进程间的通讯了。
学了匿名管道和命名管道,不知你是否有疑问为什么我们之前的pipe叫做匿名管道,为什么现在的fifo叫做命名管道呢?
为了保证不同的进程看到同一个文件,所以命名管道必须有名字。
而匿名管道是通过父子继承的方式,看到同一份资源,不需要名字来标识同一个资源。
在OS层面专门为进程间通信设计的一个方案。
System V 进程通信,一定会存在专门用来通信的接口(system call)
System V的IPC资源的生命周期是随内核的。(即只能通过程序员手动释放或OS重启)
共享内存
1.通过某种调用,在内存中创建一份内存空间
2.通过某种调用,让参与通信的进程"挂接"到这份新开辟的内存空间上。
满足以上两个条件的就是共享内存。
需要知道的是:
1.共享内存在系统中可能存在多份。
2.共享内存有标识唯一性的ID,方便让不同的进程能识别同一个共享内存资源。
创建共享内存
int shmget(key_t key,size_t size,int shmflg)
//size的大小建议是4KB
//若申请不到4KB,则分配4KB,若申请超过4KB,则向上取整。例如申请4097个字节,即4096(4KB)+1,操作系统会给你8KB的空间。
先来说说shmflg标志位
如果单独使用IPC_CREAT
,或者flag为0:不存在则创建一个共享内存,若存在则直接返回当前已经创建好的共享内存。
IPC_EXCL(单独使用没有意义)
IPC_CREAT | IPC_EXCL:如果不存在共享内存,则创建。如果已经有了共享内存,则返回出错。(如果创建成功,我得到的一定是一个最新的,没有被使用过的共享内存)
再来说说参数key,我们刚刚说了共享内存有标识唯一性的ID让不同进程看到同一份资源,这个key就是这个ID,这个ID需要我们自己手动设置,但实际中我们不太方便自己去设定,而是用ftok
函数帮我们去设定
key_t ftok(const char* pathname,int proj_id);
//pathname:自定义路径名
//自定义项目ID
shmctl(int shmid,int cmd,struct shimd_ds * buf);
//cmd是控制的命令
关联/取消关联共享内存
若关联成功函数会返回一个地址,我们所学的所有的函数返回的地址指的都是虚拟地址。
shmdt并不是释放共享内存,而是取消当前进程和共享内存的关联。
删除共享内存
ipcrm -m shmid
:
key值只是用来在系统层面进行标识唯一性的,不能用来管理共享内存
shmid:是OS给用户返回的id,用来在用户层进行关系内存管理。
代码实现共享内存的过程
我们发现key值都是一样的,说明两个进程访问的都是同一块内存
我们刚刚在通过共享内存进行通信时并没有用到read()或write(),所以共享内存一旦建立好并映射进自己进程的地址空间,该进程就可以直接看到该共享内存,就如同malloc的空间一般,不需要任何系统调用接口。(共享内存是所有进程间通信中速度最快的)共享内存不提供任何同步或者互斥机制,需要程序员自行保证数据的安全。
我们来看看共享内存的内核数据结构
我们来看看消息队列的内核数据结构
再来看看信号量的内核数据结构
你们会发现这三个的数据结构的第一个类型都是完全一样的。这是为什么?
因为所有的System V标准的ipc资源都是通过数组组织起来的,xxxid_ds 结构体的第一个成员都是struct ipc_perm
类型的。
内核中会有一个struct ipc_perm* ipc_id_arr[64]
的指针数组,有人会有疑惑,类型不一致怎么存?可以通过强制类型转换,如下
ipc_id_arr[0] = (struct ipc_perm*)&shmid_ds
若要访问这个结构体的其它内容,如下
(shmid_ds*)ipc_id_arr[0]->其它属性。
这个操作似乎有些熟悉,熟悉C++的人就会知道这有点像C++的切片技术。
什么是信号量:
管道(匿名和命名)、共享内存、消息队列都是以传输数据为目的的,信号量不是以传输数据为目的的,而是通过共享"资源"的方式来达到多个进程的同步和互斥的目的。
信号量的本质:是一个计数器,类似于count++/--
这样计数。
什么是临界资源:凡是被多个执行流同时能够访问的资源就是临界资源!同时向显示器打印、进程间通信的时候、管道、共享内存、消息队列等都是临界资源。
什么是临界区:进程的代码可是有很多的,其中,用来访问临界资源的代码,就叫做临界区。
什么是原子性:一件事情要么不做,要么就做到最好,没有中间态,就叫做原子性。
喜欢这篇文章的可以给个一键三连
点赞关注收藏