文章目录
- 前言:
- 进程间通信介绍
- 进程间通信目的
- 进程之间如何通信?
- 进程间通信分类
- 管道
- 什么是管道?
- 匿名管道
- 🧨尝试使用:
- 🍗处理细节问题:
- 🚀管道的4种情况和5种特征:
- 4种情况:
- 5种特征:
前言:
前面的学习中,我们已经可以初步的了解操作系统对文件I/O操作的具体实现,通过学习重定向认识到了文件fd以及操作系统是如何管理文件的,不管是管理打开的文件的还是存在磁盘的文件,我们都已经有过了解了。
接下来的内容,我们想要深入一点,之前我们总是研究单一的进程,而我们之前又学习过进程具有独立性,那么对一个计算机来说,同一时间段内存在特别多的进程在运行,有一些数据肯定会从一部分进程中读取,那么进程与进程之间又是如何构建起来通信的呢?我们下面就来聊聊,进程间的通信问题!!!
进程间通信介绍
进程间通信目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
进程之间如何通信?
由于进程具有独立性,因此两个进程之间并的通信是不容易的!**如果 A 进程想要直接将数据交给 B 进程,是 A 进程去访问 B 进程的一段内存区域将数据拷进去呢?还是 B 进程去直接读取 A 进程的内存区域将数据拷出来呢?**所以这是行不通的!
回顾一下我们以前有没有过上课写小纸条的经历呢?你和你的同学在课上由于各种原因不能直接进行交流,即你们都是一个独立的个体,那么你们实现交流就是在小纸条上面写写画画,要么是我写然后你读,也可是是你写然后给我读!进程之间的通信也是如此。
- 所以不管进程是如何进行通信的,本质就是让不同的进程看到同一份资源 (即同一个媒介),从操作系统的角度来说,就是在内存中找到同一份资源!
- 这个资源通常是由 OS 提供的,不能由 A / B 进程的任何一个提供, 但 A / B 进程可以去向 OS 申请。
以上也是进程间通信的前提!
进程间通信分类
管道
- 匿名管道pipe
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
管道
什么是管道?
- 管道是Unix中最古老的进程间通信的形式。
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道
匿名管道
我们先来回顾一下曾经学习的文件fd他们的结构是怎么样的:
以上就是操作系统管理文件的一个结构,针对于打开的文件的管理。
现在我想创建一个子进程,然后让子进程以写方式打开文件。
首先,在struct files_struct
内部,我们都知道会默认打开0 && 1 && 2号文件,子进程会继承父进程打开0 && 1 && 2号文件。最开始是父进程bash默认打开了0 && 1 && 2号文件,此后所以的所有进程都会默认打开0 && 1 && 2号文件。
那么对于这种情况来说,针对于同一个文件,我们其实没有必要再给“写方式”的struct file
再创建inode 和 内核级缓冲区
了,直接共用就好了!
至此我们就可以创建子进程了,所以我们会fork出一个子进程,这个子进程会创建一个新的PCB,然后继承父进程的struct files_struct
,与其说是继承,倒不如说是用着父进程的struct files_struct
的一份拷贝,也可以说是共享!
由于子进程是与父进程"共享"一个struct files_struct
,那么对于父进程以两种方式打开的两个struct file
,子进程通过struct files_struct
的文件描述符fd,也可以轻松找到。
(由于inode在这里没有作用,所以我把inode区域给擦掉了)
好了,那我们通过上图就可以发现一个神奇的地方,我们写数据会往内核级的缓冲区去写,而读数据也是在内核级缓冲区里读。那这个“缓冲区”不就将我们的父子进程连接起来了吗?父子进程都看到了内存中的同一片区域了呀!满足了前提,当然构成进程间的通信。
那这个“缓冲区”就是我们的——匿名管道
那既然这样,子进程完成写操作,父进程完成读操作,那两个进程各自都存在没有用的操作文件方式,因此我们可以将父进程的写操作关闭,子进程的读操作关闭
这就是匿名管道的由来!理解我们曾经所讲解的——”Linux中一切皆文件“,我们也可以得知管道本身就是一个文件,而这个文件拥有两个struct file
通过上述例子我们可以解释一种现象:
由于子进程创建的时候会继承父进程的struct files_struct
的 0 && 1 && 2 号文件,因此我们就算在子进程中关闭1号文件夹(显示器文件)也不会影响父进程,这也能侧方面反映出进程独立性!
int main()
{
pid_t pid = fork(); // 创建子进程
// 子进程
if (pid == 0)
{
close(1); // 关闭显示器文件
exit(1);
}
// 父进程
cout << "hello linux!" << endl;
return 0;
}
🧨尝试使用:
既然我们现在理解了管道的底层,那我现在想利用管道来实现父子进程间的通信,如果单纯是对一个文件进行以读方式和写方式打开,再关闭读方式的fd和写方式的fd,最后写入文件缓冲区中,这样的操作过于复杂,因此我们操作系统提供了一个函数 —— int pipe(int pipefd[2]);
用来创建管道。
作用:创建一个匿名管道,用来进程间通信;
参数:
int pipefd[2]这个数组是一个传出参数;
pipefd[0] 对应管道的读端;
pipefd[1] 对应管道的写端;
返回值:
成功 0;
失败-1;
代码如下:
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
int main()
{
// 创建管道
int pipefd[2];
int n = pipe(pipefd);
if (n < 0)
{
std::cerr << "pipe failed" << std::endl;
exit(1);
}
pid_t pid = fork(); // 创建子进程
if (pid == 0)
{
// 子进程
close(pipefd[0]);
std::cout << "子:我是子进程,我关闭了读文件的操作!" << std::endl;
static int count = 0;
while (1)
{
sleep(4);
std::cout << "写 -> 现在,我准备向管道里发送消息!" << std::endl;
std::string message = "Hi I am child process!";
pid_t id = getpid();
message += " pid: ";
message += std::to_string(id);
message += " count: ";
message += std::to_string(count);
int n = write(pipefd[1], message.c_str(), strlen(message.c_str()));
if (n < 0)
{
std::cerr << "failed to write" << std::endl;
exit(1);
}
std::cout << "发送成功!" << std::endl;
count++;
}
}
// 父进程
close(pipefd[1]);
std::cout << "父:我是父进程,我关闭了写文件操作!" << std::endl;
while (1)
{
std::cout << "读 -> 现在,我准备向管道里读取消息" << std::endl;
char file_buffer[1024];
ssize_t n = read(pipefd[0], file_buffer, sizeof(file_buffer) - 1);
if (n < 0)
{
std::cerr << "failed to read" << std::endl;
exit(1);
}
file_buffer[n] = 0;
std::cout << "读取内容:";
std::cout << file_buffer << std::endl;
std::cout << "-----------" << std::endl;
}
return 0;
}
🍗处理细节问题:
-
代码中,父子进程的状态?:
我们先让父子进程都连接至管道,然后各自关闭自己不要的fd,然后做到子进程往管道里写信息,父进程往管道里读信息。 -
为什么我的父子进程一定要关闭fd呢?可以不关闭吗?
当然可以不关闭啦,但是我们在这里还是建议关闭,
因为如果父子进程都保留读端和写端,则它们可以相互读写,这可能导致通信混乱。
每个打开的文件描述符都会占用系统资源。关闭不必要的文件描述符可以释放这些资源,供其他进程或操作使用。
如果不关闭不必要的文件描述符,在读写管道时可能会遇到未预期的阻塞或错误,尤其是在管道已满或为空的情况下。 -
既然父子进程要关闭不需要的fd,那为什么当初还要创建和链接呢?
操作系统这么做肯定是有他的考虑的,如果父进程不链接也不创建,那在创建子进程的时候又该如何让子进程继承父进程的fd和
struct file
呢?所以这么做其实是方便子进程继承罢了。
🚀管道的4种情况和5种特征:
4种情况:
-
如果管道内部是空的,且子进程的write的fd一直存在,那么父进程在读取的时候会被阻塞
其实我们编写代码时,我们只是在子进程的写文件的部分加入了
sleep(4)
,并没有在父进程读取的时候也加入sleep()
这个函数,就比如这个时候父进程把管道内部的信息全部读完了,那么就会进入阻塞状态,等待子进程写入。
我们可以输入指令:
while :; do ps ajx | head -1 && ps ajx | grep test |grep -v grep; echo "---------------------"; sleep 1;
来实现一个监视窗口,然后运行。
-
如果管道被写满,且父进程的读操作的fd不关闭,那么管道写满后子进程就会处于阻塞状态
现在我们让子进程不断往管道里写字符’A’,然后关闭父进程的读操作,最后看看管道满了是什么样子的,子进程代码修改成这样:
while (1) { char a[] = "A"; int n = write(pipefd[1], a, 1); if (n < 0) { std::cerr << "failed to write" << std::endl; exit(1); } count++; std::cout << count << std::endl; }
最后停留在了65536这个数字,正好是64KB,因此在Ubuntu22.04下的匿名管道大小为64KB。
-
管道一直在读,并且此时子进程的关闭了写操作的fd,那么读端read就会返回0,代表读到了文件末尾。
我们在子进程加入
close(pipep[1])
代表关闭了写操作。
在这里我是把while循环关了,所以才没有不断的往下打印。打开cgdb调试也不难看出:
-
若读文件的fd关闭了 && 写文件的fd一直在写会怎么样?
既然你一直在像一个无人知晓的空间写数据,那不就是无用功吗?这种方式显然是浪费时间的!因此OS不会允许这种时发生,所以会直接杀掉对应的进程,具体的操作是OS给进程发送异常信号(13号->SIGPIPE)。而对于这种情况的管道也叫 broken pipe。
我们也可以通过代码验证:
- 父进程关闭pipe[0]
- 让子进程不断往管道里写数据
- 让父进程关闭pipe[0]后,等待子进程
- 最后打印出信号码即可
int status = 0; pid_t rid = waitpid(pid, &status, 0); if(rid > 0) { std::cout << "等待成功!" << std::endl; std::cout << "退出信号:" << (status & 0x7F) << std::endl; std::cout << "退出码:" << ((status>>8) & 0xFF) << std::endl; }
5种特征:
-
匿名管道只能用于具有“血缘关系”的进程之间进行通信,常用于父子进程。
-
管道内部,自带进程之间的同步机制。在多执行流执行代码的时候,具有明显的顺序性。
-
文件(管道也是个文件)的声明周期是随进程的。
-
管道文件在通信的时候,是面向字节流的。
-
管道的通信模式,是一种特殊的**”半双工“**模式。