文章目录
- 💐专栏导读
- 💐文章导读
- 🐧进程间通信的目的
- 🐧如何进行进程间通信
- 🐧进程间通信的分类
- 🐧管道
- 🐦什么是管道
- 🐦管道原理
- 🐧实例代码
- 🐧管道的特点
- 🐧代码拓展 - 通过管道实现进程控制
💐专栏导读
🌸作者简介:花想云 ,在读本科生一枚,C/C++领域新星创作者,新星计划导师,阿里云专家博主,CSDN内容合伙人…致力于 C/C++、Linux 学习。
🌸专栏简介:本文收录于 Linux从入门到精通,本专栏主要内容为本专栏主要内容为Linux的系统性学习,专为小白打造的文章专栏。
🌸相关专栏推荐:C语言初阶系列、C语言进阶系列 、C++系列、数据结构与算法。
💐文章导读
本章我们将深入学习如何通过管道进行进程间通信。
🐧进程间通信的目的
- 数据传输:一个进程需要将它的数据发送给另一个进程;
- 资源共享:多个进程之间共享同一份资源;
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件(如进程终止时要通知父进程);
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截一个进程所有陷入和异常,并能够及时知道它的状态改变;
🐧如何进行进程间通信
首先进程间进行通信有最大的一个难题:
进程是具有独立性的
;
也就是说,我们无法做到让两个进程之间访问彼此的资源。因此进程间通信的首要任务是
让两个进程看到同一份资源
;然后让一方写入,另一方进行读取
;
这也是主流的我们将要的介绍的几种进程间通信的方法的核心思想。
🐧进程间通信的分类
进程间通信经过漫长的发展,逐渐衍生出以下几种方式:
管道
- 匿名管道(pipe);
- 命名管道;
System V IPC
- System V 消息队列;
- System V 共享内存;
- System V 信号量;
POSIX IPC
- 消息队列;
- 共享内存;
- 信号量;
- 互斥量;
- 条件变量;
- 读写锁;
本章节我们将介绍管道的进程间通信方式。
🐧管道
🐦什么是管道
在不久之前我们应该都见过Linux中的管道,例如:
在 myfile.txt 中写入五行文字:
$ echo "hello world" >> myfile.txt
$ echo "hello world" >> myfile.txt
$ echo "hello world" >> myfile.txt
$ echo "hello world" >> myfile.txt
$ echo "hello world" >> myfile.txt
$ cat myfile.txt
hello world
hello world
hello world
hello world
hello world
当我们想统计出 myfile.txt 中有几行文字时:
$ cat myfile.txt | wc -l
5
- wc -l 用来统计文本中的行数;
在Linux中,"管道"是一种机制,允许将一个命令的输出直接传递给另一个命令作为输入。这个机制通过竖线符号|
来实现。通过使用管道,你可以将一个命令的输出作为另一个命令的输入,从而实现多个命令的协同工作,形成一个命令链。
例如,假设你有两个命令:command1 和 command2。你可以使用管道将它们连接在一起,使 command2 处理 command1 的输出。命令的形式如下:
$ command1 | command2
这会将 command1 的输出传递给 command2,而不是将其打印到终端。这种机制使得在Linux系统上可以轻松地组合和重用命令,从而实现更复杂的操作。
在计算机领域中,管道是一个比较大的概念,Linux指令中的 | 是管道的一种形式。
🐦管道原理
我们首先要明确,管道是一个文件
。要做到让两个进程看到同一份资源,说明这一份资源不独属于任何一个进程。
当我们创建一个进程,该进程会拥有自己的tast_struct
结构体,在这个结构体中管理着 struct files_struct
字段,files_struct中又管理着该进程的文件描述符表
(struct file* fd_array[]),如下图所示:
在该进程中,分别用读方式与写方式打开一个文件;然后 fork 创建子进程,此时子进程会继承来自父亲的与父亲相同的文件描述符表;
此时父进程与子进程指向了同一个文件,接下来结合实际场景,例如,我们想让父进程进行写入,让子进程负责读取,这时父子进程需要关闭不需要的文件描述符,父进程将 4 关闭,子进程将 3 关闭,这时父子进程就可以根据需要来收发数据了。
🐧实例代码
现在有一个需求:子进程向管道中发送数据,父进程每隔一秒读取一次管道中的内容。
在代码的编写过程中,我们需要用到一个接口——pipe为我们生成管道文件。它的基本原型如下:
#include <unistd.h>
int pipe(int pipefd[2]);
这里,pipefd
是一个整型数组,有两个元素,分别表示管道的两个端口。pipefd[0] 代表读取端口
,pipefd[1] 代表写入端口
。成功调用 pipe 函数后,这两个文件描述符将被用于在两个相关进程之间传递数据。
#include <iostream>
#include <iostream>
#include <string>
#include <cerrno>
#include <cassert>
#include <cstring>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
int main()
{
int pipefd[2] = {0};
// 1. 创建管道
int n = pipe(pipefd);
if (n < 0)
{
cout << "pipe error" << errno << ": " << strerror(errno) << endl;
return 1;
}
cout << "pipfd[0]" << pipefd[0] << endl;
cout << "pipfd[1]" << pipefd[1] << endl;
// 2.创建子进程
pid_t id = fork();
assert(id >= 0);
if(id == 0) // 子进程
{
// 3.关闭不需要的fd
close(pipefd[0]);
// 4.开始通信
const string namestr = "我是子进程";
int cnt = 1;
char buffer[1024];
while(true)
{
snprintf(buffer, sizeof buffer, "%s : %d, pid : %d\n", namestr.c_str(), cnt++, getpid());
write(pipefd[1], buffer, strlen(buffer));
}
close(pipefd[1]);
exit(0);
}
// 父进程
close(pipefd[1]);
// 4.开始通信
char buffer[1024];
int cnt = 0;
while(true)
{
int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
if(n > 0)
{
cout << "我是父进程,我读到了信息:\n" << buffer << endl;
}
else if(n == 0)
{
cout << "我是父进程,我读到了文件末尾" << buffer << endl;
break;
}
else
{
cout << "我是父进程,读取异常" << buffer << endl;
break;
}
sleep(1);
}
return 0;
}
🐧管道的特点
在管道的使用中,我们经常能遇到以下四种场景:
- 当我们read读取完毕所有的管道数据,如果写端不再继续写入数据,那读端只能等待;
- 当写端将管道全部写满了之后(管道文件的容量是有上限的)就不能在继续写入数据了;
- 当写端将数据全部写入并退出后,读端读取完毕管道数据后,read就会返回0,代表读到了文件末尾;
- 当写端一直写入数据,而读端关闭,这时写端所作的事情是无意义的,OS此刻会杀死一直写入数据的进程,OS会通过发送信号来终止该进程;
由四种场景,我们可以总结出管道具有的特点:
- 管道是单向通信的(半双工)。管道并不能读取和写入同时进行;
- 管道的生命周期随进程,当进程退出时,管道释放;
- 管道通信经常是由具有“血缘关系”的进程间进行的,例如父子进程;
- 在管道通信中,读与写的次数并不是强相关的,因为管道提供的是流式服务;
- 管道具有一定的系统能力,让读端与写端能够按照一定的步骤进行通信;
🐧代码拓展 - 通过管道实现进程控制
接下来,我们就基于管道进行一个简单的设计——父进程向不同的子进程写入特定的消息,唤醒子进程,并让子进程去执行特定的命令。
/* ctrlProcess.cpp */
#include "Task.hpp"
using namespace std;
Task t;
const int gnum = 3;
class EndPonit
{
public:
EndPonit(int id, int fd)
: _child_id(id),
_write_fd(fd)
{}
~EndPonit()
{}
public:
pid_t _child_id; // 子进程id
int _write_fd; // 写端fd
};
// 子进程要执行的方法
//void WaitCommand(int _write_fd)
void WaitCommand()
{
while (true)
{
int command = 0;
int n = read(_write_fd, &command, sizeof(int));
if (n == sizeof(int))
{
t.Execute(command);
}
else if (n == 0)
{
break;
}
else
{
break;
}
}
}
void creatProcess(vector<EndPonit> *end_points)
{
for (int i = 0; i < gnum; i++)
{
// 1.创建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n == 0);
(void)n;
// 2.创建进程
pid_t id = fork();
assert(id != -1);
// 子进程
if (id == 0)
{
// 3.关闭不要的fd
close(pipefd[1]);
// 输入重定向
dup2(pipefd[0], 0);
// 子进程等待获取命令
WaitCommand();
//WaitCommand(pipefd[0]);
close(pipefd[0]);
exit(0);
}
// 父进程
// 关闭不要的fd
close(pipefd[0]);
// 4.将新的子进程和它的管道写端,构建对象
end_points->push_back(EndPonit(id, pipefd[1]));
}
}
int main()
{
vector<EndPonit> end_points;
creatProcess(&end_points);
int num = 0;
while (true)
{
// 1.选择任务
int command = COMMAND_FUNC1;
// 2.选择进程
int index = rand() % end_points.size();
// 3.下发任务
write(end_points[index]._write_fd, &command, sizeof(int));
sleep(1);
}
return 0;
}
/* Task.hpp */
#pragma once
#include <iostream>
#include <vector>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <assert.h>
using namespace std;
typedef void (*func_t)();
void Func1()
{
cout << "pid:" << getpid() << "Func1...." << endl;
}
void Func2()
{
cout << "pid:" << getpid() << "Func2...." << endl;
}
void Func3()
{
cout << "pid:" << getpid() << "Func3...." << endl;
}
#define COMMAND_FUNC1 0
#define COMMAND_FUNC2 1
#define COMMAND_FUNC3 2
class Task
{
public:
Task()
{
_tasks.push_back(Func1);
_tasks.push_back(Func2);
_tasks.push_back(Func3);
}
void Execute(int command)
{
if (command >= 0 && command < _tasks.size())
_tasks[command]();
}
~Task()
{
}
private:
vector<func_t> _tasks;
};
运行效果展示
本章的内容到这里就结束了!如果觉得对你有所帮助的话,欢迎三连~