序言
进程间通信是操作系统中多个进程之间相互交换信息的方法。这篇文章介绍的通信方式为管道,大家在平时生活中肯定也知道管道吧,就比如自来水管道,从一端输送水源到另一端。并且一般都是单向传输,你不能把你家的水送回自来水厂吧。相信通过这篇的学习,你更能理解这种通信方式为何叫管道了。
1. 进程间通信
1.1 为什么需要进程间通信?
资源独立性
:在操作系统中,进程是独立的资源分配单元,每个进程拥有独立的资源空间,包括内存、文件描述符等。这种独立性意味着一个进程不能直接访问另一个进程的资源
。信息交互
:尽管进程的资源是相互独立的,但它们之间经常需要进行信息的交互和状态的传递
。例如,一个进程可能需要将它的数据处理结果发送给另一个进程,或者一个进程需要通知另一个进程某个事件的发生。
1.2 进程间通信的本质
进程间是独立的,但是需要让进程之间进行通信,说到底就是 让不同的进程看到同一份操作系统的资源(如文件,内存等)
。
有的人会感到疑惑,进程都是独立的,还能访问同一个资源吗?怎么不可以呢,比如不同的进程就可以访问同一个文件。进程的独立应该是,每个进程都包含自己的一个进程地址空间,不会被其他进程访问。
1.3 进程间通信的方式
管道(Pipe)
:一种最基本的IPC机制,用于连接一个读进程和一个写进程。管道可以是匿名的,也可以是命名的(FIFO),允许无亲缘关系的进程之间进行通信。信号(Signal)
:一种比较原始的进程间通信方式,用于通知接收进程某个事件已经发生。信号的处理方式可以是忽略、捕捉或默认处理。消息队列(Message Queue)
:允许一个或多个进程向它写入或从中读取消息。消息队列是消息的链接列表,存储在内核中并由消息队列标识符标识。共享内存(Shared Memory)
:允许多个进程访问同一块内存区域。进程可以直接读写这块内存,而不需要进行数据复制,从而提高了通信效率。但共享内存需要同步机制来避免数据竞争和冲突。
在本篇文章中将介绍第一种方式 — 管道(Pipe)
。
2. 匿名管道
该管道很特殊,只能支持具有血缘关系的进程之间的通信(如父子进程,爷孙进程)为什么呢?希望大家在下面找出答案。
2.1 匿名管道的原理
1. 创建 struct file
在文件系统的学习中,当我们的进程以某种方式打开一个文件时,操作系统会为该文件生成一个 struct file
,便于更好的管理。进程会将所使用的文件的 struct file
,存入自己的文件描述符表中。
但是请问大家一个问题,我以写方式打开的文件和以读方式打开文件是同一个 struct file
吗?答案是 — 不是!每个打开操作都会创建或重用一个 struct file
结构体实例来跟踪该打开操作的状态和上下文。
所以首先,我们控制一个进程以读写两个方式创建两个 struct file
,但是尽管包含了两个文件结构体,但是两者共享一个内核级别文件缓冲区:
2. 创建子进程
我们使用 fork()
时会为我们创建一个子进程,子进程会复制一些父类的资源其中就包括文件描述符表
。该文件描述符表中的内容都是相同的,所以现在的情况是:
3. 选择写端读端
我们对文件写入的内容,不会直接写到磁盘上,先要保存到缓冲区上,最后由缓冲区写入磁盘上。
在这里,其实我们就已经建立起了通信的桥梁了,我们在上面说过 — 通信本质是 让不同的进程看到同一份操作系统的资源(如文件,内存等)
。现在父子进程不就都能看到内核级别的缓冲区了吗?向缓冲区读写数据,不就实现通信了吗?
现在我们需要选择写端读端,如果父进程是写端,就关闭他的读端;子进程为读端,就关闭他的写端。为什么非要关闭呢?因为不能存在同读写这种情况,尝试在管道的一端同时读写,将会导致数据混乱或通信失败。
所以这是单向的,为了安全起见所以要关闭。
最终我们可以将我们的图像精简为:
4. 总结
现在大家知道为什么这种通信方式为 匿名管道
了吧。
匿名
代表该文件只需要内核级别的缓冲区就行,不需要真正存在于磁盘上。
管道
该通信是单向的,即数据只能从一个方向流动。
2.2 匿名管道系统调用
该种通信方式操作系统提供相应的系统调用接口:int pipe(int filedes[2])
:
filedes[2]
: 这是一个整数数组,用于存储创建管道时分配的两个文件描述符。filedes[0] 用于管道的读端
,filedes[1] 用于管道的写端。
- 返回值:成功时,
pipe
返回 0。出错时,返回-1
,并设置errno
以指示错误原因。
举个栗子:
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <iostream>
#include <cerrno>
#include <string>
#include <cstring>
#define MAXSIZE 1024
int main()
{
int pipefd[2];
int n = pipe(pipefd);
if (n == -1)
{
std::perror("pipe");
}
int id = fork();
if (id == -1)
{
std::perror("fork");
}
// 子进程逻辑
else if (id == 0)
{
// 子进程用于读
close(pipefd[1]);
char buf[MAXSIZE];
int n = read(pipefd[0], buf, MAXSIZE - 1);
if (n == 0)
{
std::perror("read");
}
else
{
buf[n] = '\0';
std::cout << "I get parents msg: " << buf << std::endl;
}
close(pipefd[0]);
exit(0);
}
// 父进程用于写
close(pipefd[0]);
std::string msg;
std::getline(std::cin, msg);
write(pipefd[1], msg.c_str(), msg.size());
close(pipefd[1]);
pid_t rid = waitpid(id, 0, 0);
if(rid == -1)
{
std::perror("waitpid");
}
return 0;
}
2.3 匿名管道的特点
总结一下匿名管道的特点:
单向通信
:匿名管道是一种单向通信通道
,数据只能在一个方向上流动。血缘关系间通信
:只能用于具有血缘关系的进程间通信,如父子进程,因为需要继承相关的文件描述符表同步互斥
:匿名管道内部已经实现了同步机制,能够确保在多个进程同时访问时数据的一致性和安全性
。(比如再写入的时候会阻塞读,避免读出的信息不完整)基于内存缓冲区
:匿名管道通过内存缓冲区来传输数据
。写入进程将数据写入缓冲区,而读取进程从缓冲区中获取数据。这种方式提高了数据传输的效率和速度。生命周期随进程
:匿名管道的生命周期与创建它的进程及其子进程的生命周期紧密相关。当这些进程终止时,匿名管道也会被销毁。
3. 命名管道
与匿名管道名字相反,并且该管道支持任何进程之间的通信!
3.1 命名管道的原理
经过匿名管道内容的铺垫,现在我们也可以推测命名管道用于进程间通信,所以他肯定也是一个内存级别的文件(不存储在磁盘上)。
但是命名管道有一个很大的区别,它可以支持任意进程间的通信。在匿名管道中,因为父子之间通过继承关系都有同一个文件的 struct file
,进而可以通过该文件的缓冲区进行信息传递,但是不同进程之间怎么做到呢?
不难,我们只需要约定好 地点
。比如,文件 A
对文件B
约定, A
在根目录下的 home
目录下哪个哪个位置有个啥文件,文件里面是给 B
的信息。文件 B
就照着这个地址找到了文件,也就实现了通信。
所以说命名管道也是这样,不同的进程之间通过具有 唯一标识符(通常是文件路径)的管道进行数据交换。
3.2 命名管道系统调用
命名管道在操作系统系统中也存在相应的系统调用:
int mkfifo(const char *pathname, mode_t mode)
pathname
:是命名管道的路径和名称(唯一标识符)。mode
:指定了创建的命名管道的权限,类似于open
函数的mode
参数。- 返回值: 创建成功返回 0,创建失败返回 -1。
匿名管道无需我们释放,而命名管道需要我们释放空间,这是因为,匿名管道没有文件系统中的表示(没有名字),命名管道在文件系统中有一个明确的表示(即文件名)。释放指令:
int unlink(const char *pathname);
pathname
:是命名管道的路径和名称(唯一标识符)。- 返回值: 释放成功返回 0,失败返回 -1。
举个栗子:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <iostream>
#include <string>
#include <cerrno>
#define MAXSIZE 1024
const std::string _Fifo_Path = "./Fifo_temp";
int main()
{
int rmk = mkfifo(_Fifo_Path.c_str(), 0664);
if(rmk == -1)
{
std::perror("mkfifo");
}
int fd = open(_Fifo_Path.c_str(), O_RDONLY);
if(fd == -1)
{
std::perror("open");
}
char buf[MAXSIZE];
ssize_t rrd = read(fd, buf, MAXSIZE - 1);
buf[rrd] = '\0';
std::cout << buf << std::endl;
unlink(_Fifo_Path.c_str());
return 0;
}
--------------------------------
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <iostream>
#include <string>
#include <cerrno>
#define MAXSIZE 1024
const std::string _Fifo_Path = "./Fifo_temp";
int main()
{
int rmk = mkfifo(_Fifo_Path.c_str(), 0664);
if(rmk == -1)
{
std::perror("mkfifo");
}
int fd = open(_Fifo_Path.c_str(), O_RDONLY);
if(fd == -1)
{
std::perror("open");
}
char buf[MAXSIZE];
ssize_t rrd = read(fd, buf, MAXSIZE - 1);
buf[rrd] = '\0';
std::cout << buf << std::endl;
unlink(_Fifo_Path.c_str());
return 0;
}
在这里一共两个程序,一个用于发送,一个用于接收。
3.3 命名管道的特点
非血缘关系进程间通信
:命名管道能够用于非亲缘(不具有共同祖先)进程之间的通信。这意味着任何两个或多个进程,只要它们能够访问同一个命名管道文件,就可以通过该管道进行通信,而无需考虑它们之间的血缘关系。文件系统存储
:命名管道以FIFO
文件的形式存储在文件系统中。这使得命名管道具有持久性(尽管它本身不存储数据,只是提供通信的通道),并且可以被多个进程通过路径名访问。阻塞/非阻塞模式
:命名管道可以设置阻塞或非阻塞模式。在阻塞模式下,如果管道的读端没有数据可读,则读操作会阻塞直到有数据可读;在非阻塞模式下,如果管道的读端没有数据可读,则读操作会立即返回一个错误。读写权限设置
:命名管道文件具有文件系统的权限属性,可以设置不同的读写权限来控制哪些进程可以访问管道进行读写操作。
4. 总结
在这篇文章中,主要介绍了进程间通信的一种方式,管道通信,并且讲解了原理和如何使用,希望大家有所收获。