【Linux 系统】进程间通信(匿名管道 & 命名管道)-- 详解(上)https://developer.aliyun.com/article/1515599?spm=a2c6h.13148508.setting.19.11104f0e63xoTy
4、管道的特点
(1)管道只能用于具有血缘关系的进程进行通信,它常用父子进程通信
通常,一个管道由一个进程创建,然后该进程调用 fork(),此后父子进程之间就可应用该管道。
(2)管道为了让进程间协同,提供了访问控制(管道自带同步机制)
父进程写完数据后休眠 1s,而子进程却没有,子进程一瞬间就把数据读完了,在父进程休眠的那 1s 内,子进程在干什么呢?
管道和显示器都是一个文件。为什么之前父子进程同时往显示器打印写入时没有出现这样的情况?这种情况称为缺乏访问控制,而当前这个问题具有访问控制。
此时管道中并没有数据,子进程在进行等待管道内部有数据就绪,这需要写端造成。
如果此时父进程一直往管道里写,而子进程休眠上 20s 呢?
父进程写到 3972 次时就没再继续写了。换而言之,如果管道里写端已经写满了,此时是不能再继续写入的,而写端就在等待管道内部有空闲空间,这需要读端造成。
综上而言,通信双方在管道中,如果其中一方不写了,另一方把数据读完后就必须等待对方写入才可以继续读;反之如果一方写满了,另一方不读,那么一方就必须等待另一方读取后才可以继续写。这种特性就叫做进程间同步,它们两方必须得通过某种同步机制来保证数据安全:管道是内存空间。如果一方不写,另一方还在那读,那么读到的数据肯定是垃圾数据;同样,如果一方一直写入,但另外一方却不读,那就可能会导致原来的数据被覆盖。
进程间同步其实是一种保护临界资源的一种处理方案,后面会再详细介绍。
(3)管道提供面向流式的通信服务 —— 面向字节流
这里先简单理解一下,更进一步理解需要后面学习到网络部分。
流是什么?
下面是一段缓冲区,那么一定要有人去缓冲区中写入和读取。流就是想按几个字节就按几个字节写,想按几个字节读就按几个字节读。像这样的缓冲区对于读和写而言,就是字节流。
(4)管道是基于文件的
一般而言,进程退出,管道释放,所以管道的生命是随进程的,文件的生命周期也是随进程的。
(5)管道是单向通信的
管道是单向通信的,其本质是半双工通信的一种特殊情况。
管道是半双工的,数据只能向一个方向流动;当需要双方通信时,需要建立起两个管道。
举一个生活中的例子:人与人之间交流时一般是半双工(一个人说,一个人听),而在吵架时可能就是全双工(两个人都在说,也都在听)。
(6)管道能够保证一定程度的数据读取的原子性
如果往管道写 hello world,刚准备写 world,而 hello 就被读走了,此时就不能保证原子性。这里的一定程度一般指的是 4kb。
原子性的详细介绍主要是在后面的多线程部分。
4、管道的读写规则
当没有数据可读时:
- 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 将不再保证写入的原子性。
⚪验证部分特性
a. 读写端测试
如果读端关闭,写端一直写,肯定是没有意义的,其本质就是在浪费资源,所以这个时候写进程会立马被 OS 通过发送信号的方式终止。而写进程是子进程,此时父进程就可以 waitpid,从而知道子进程退出的原因。这里可以看到唯一还没有研究的正是 write 端写和 read 端不读&关闭,这也就是为什么要让子进程写,父进程读的原因,因为它更适合测试。
此时子进程不断的写,而父进程读取一次后就关闭读,子进程再写,OS 就发送 13 号信号终止了进程。
b. 单机版的负载均衡
运行效果:
(1)管道的大小
a. 子进程一直写,每次写一个字节,然后计数器统计,父进程不要读。可以看到结果是 65536 byte,也就是管道的大小是 64kb,不过操作系统不同数据可能不一样
b. ulimit -a 查看系统资源
这里通过计算器算出来结果其实也才 4 kb,而实践出来却是 64kb。这里的 64kb 是当前云服务器管道的最大容量,而这里的 4kb 只是以原子性写入管道中的单元大小(可以通过 man 7 pipe 手册进行查看,可以看到 PIPE_BUF 是 4096 byte(4kb),只要在这个范围内就都是原子的)。
5、命名管道 fifo
命名管道是一种特殊类型的文件。
命名管道是供毫不相关的进程进行进程间通信。命名管道一般叫做 fifo,fifo 一定不陌生,因为数据结构中队列就是这种特性。
(1)理解命名管道的原理
要让两个毫不相干的进程进行通信,首先一定是要保证这两个进程可以看到同一份资源。因为需要相同的文件路径,所以让进程 1 和 进程 2 分别以读写的方式打开同一路径下的文件,此时内存中一定会包含 struct file 结构体,以及该文件所对应的缓冲区。那么此时进程 2 把数据写到缓冲区中,进程 1 就可以进行读取。
命名管道也是管道,它也遵守管道的面向字节流,同步机制,单向通信等特点。唯一和匿名管道不同的是它可以和不相关的进程进行通信。
对于普通文件来说,是需要将数据刷新到磁盘上持久化存储的,所以它就应该要把写入的数据刷新到磁盘上。换而言之,进程 2 把文件打开写数据到磁盘然后关闭,进程 1 再从磁盘读取,那么这当然可以通信,但是数据放在磁盘上的效率就太低了,便没有什么价值。
所以系统中就存在一种特殊的文件 —— 管道文件,虽然它也有路径标识,但是系统不会把它对应的内存数据刷新到磁盘上,既然它是文件,那么它就一定有自己的名字,且它一定在系统路径中。而路径是唯一的,那么双方进程就可以通过管道文件的路径看到同一份资源。
(2)创建命名管道
a. 命令行创建
mkfifo filename
b. 在程序里创建
int mkfifo(const char *filename,mode_t mode);
(3)用命名管道实现 server & client 通信
A. mkfifo name_pipe
创建完管道文件之后,向管道里面写入内容,但是因为对方还没打开,此时处于阻塞状态。
一方写入,另一方读取,就可以看到所写入的内容了。这就叫作一个进程向另一个进程写入消息的过程。
name_pipe 就是一个管道文件,此时往文件中写入数据后,可以发现它的大小依旧是 0,因为数据只会在内存中,不会往磁盘刷。
B. 一边不断的往管道里写数据,另一边以管道作为标准输入然后输出重定向到 cat,最后显示出来
也可以说是 cat 从管道中把数据读取出来,这就完成了两个进程之间的通信。
C. 代码
a. 准备工作
想要 make 后一次生成两个不相关的可执行程序,需要我们在开头的时候定义 all 伪目标,它依赖的是两个可执行程序,没有依赖方法(因为它有依赖关系,所以 makefile 会推导 client 和 server 是怎么形成的)。
虽然 makefile 这样的技术已经很老了,但是它很稳定,几乎是现在主流的各种各样的工具的基础。虽然实际在公司并不会自己写 makefile(除非自己写测试代码),公司一般都有很多工具来自动生成 makefile,但是必要的 makefile 编写还是需要我们了解的,因为上层的工具和 makefile 有关系。
b. mkfifo 函数
mkfifo 既是命令,也是一个库函数。
第一个参数是命名管道的路径,第二个参数是命名管道的权限。成功返回 0,失败返回 -1。
c. 实现管道通信
此时代码中的 mkfifo 和命令中的 mkfifo 达到的效果是一样的。
下面 client.c 中以写打开管道文件,然后从键盘读取数据到 buffer,然后在往管道中写入 buffer 中的数据。然后 server.c 以读打开管道文件,把数据往 buffer 中读,然后再打印 buffer 中的数据。
【单个进程实现方式】
结果显示:
【多个子进程实现方式】
显示结果:
当前只有一个子进程:
一个管道可以有多个读端,它是单向通信的。
6、命名管道的打开规则
如果当前打开操作是为读而打开 FIFO 时:
- O_NONBLOCK disable:阻塞直到有相应进程为写而打开该 FIFO。
- O_NONBLOCK enable:立刻返回成功。
如果当前打开操作是为写而打开 FIFO 时:
- O_NONBLOCK disable:阻塞直到有相应进程为读而打开该 FIFO。
- O_NONBLOCK enable:立刻返回失败,错误码为 ENXIO。
7、匿名管道与命名管道的比较
- 匿名管道是供具有血缘关系的进程进行进程间通信;命名管道可供非具有血缘关系的进程进行进程间通信。
- 让不同进程看到同一份资源的手段不一样,匿名管道是通过子进程继承的方式(父子共享文件的特征让进程看到同一份资源);命名管道是通过打开同一目录的方式(命名管道是文件路径具有唯一性的特征)。
- pipe 创建的管道文件因为没有名字,所以它只能在在内存上;fifo 创建的管道文件有名字,所以它在磁盘上,只不过不会把数据写到磁盘上。
- FIFO(命名管道)与 pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。匿名管道由 pipe 函数创建并打开,命名管道由 mkfifo 函数创建,打开用 open。