在以上的动静态库的章节当中我们了解了库的制作和原理,了解到了.o文件以及动静态库的本质上都是FLF格式的文件,还了解了ELF文件各个的组成部分。接下来在本篇开始我们就将学习Linux当中又一重要的章节——进程间通信。在进程间通信的学习当中我们将了解到进程通信的原理,并且学习四大的基本进程通信方式,在本篇当中首先学习的是匿名管道和命名管道两种进程间的通信方式,在此了解通信的原理之后将学习对应的系统调用接口并且实现对应的demo代码,那么接下来就开始本篇的学习吧!!!
1.了解进程间通信
在此我们先要来了解进程间通信的原理以及进程通信的本质。
进程间通信的定义如下:
进程间通信(IPC)是操作系统提供的一种机制,用于在不同进程之间传递数据、共享资源或协调任务。
进程间进行通信目的如下所示:
• 数据传输:⼀个进程需要将它的数据发送给另⼀个进程
• 资源共享:多个进程之间共享同样的资源。
• 通知事件:⼀个进程需要向另⼀个或⼀组进程发送消息,通知它(它们)发⽣了某种事件(如进
程终止时要通知父进程)。
• 进程控制:有些进程希望完全控制另⼀个进程的执行(如Debug进程),此时控制进程希望能够
拦截另⼀个进程的所有陷⼊和异常,并能够及时知道它的状态改变。
其实进程通信的本质就是让不同的进程看到同一份资源。注意这里的资源不是通信中的任何一个进程提供的而是由操作系统单独的提供一部分资源来实现进程的通信,OS提供给对应的进程相关的系统调用,这样进程就可以统一设计的接口来实现进程的通信。
2. 环境切换
2.1 系统切换
以上我们就了解到了进程通信的基本概念,那么在接下来了解具体的通信方式之前先来做一些软件环境的准备工作。
在之前的Linux学习当中我们使用的都是Centos系统,当时由于Centos在不久之前就宣布停止维护了,那么从现在开始我们就将Linux的系统切换为Ubuntu。切换之前注意要将之前的代码进行保存,可以通过保存到Windows或者如果你有将代码同步到gitee上,那么这就只需要在新系统当中将仓库重新拉到系统即可。
切换系统的工作其实很简单,只需要进入到你的云服务器的管理页面当中,之后点击切换镜像即可
在系统的版本当中选择22.04
接下来只需要等待几分钟即可实现镜像的切换。 完成系统的重装之后接下来就只需要将gitee远端的仓库从新的同步到系统当中即可。
注:在Centos当中我们安装软件使用的是yum,而在Ubuntu当中使用的则是apt。
2.2 启用VScode
除了以上切换系统之外接下来我们还要开始使用VS code,VS code本质上就是一个编辑器,当时我们可以通过VS code来远程链接云服务器,这样就可以在Window当中顺畅的进行开发的工作,相比之前我们使用vim的方式编写,VS code当中可以支持语法的错误提醒,这就对我们的开发十分的友好了。
那么接下来首先就来了解VS code任何进行安装。
只需要进入到以下的链接当中就可以之后点击DownLoad即可进行安装
Visual Studio Code - Code Editing. RedefinedVisual Studio Code - Code Editing. Redefined
安装完之后打开VS code接下来就开始进行一些使用之前的配置工作。
一开始进入到VS code当中此时默认是英文的,要切换为中文就需要在扩展当中搜索Chinese,之后点击安装即可。
在插件当中搜索C/C++即可在VS code当中远程连接的时候进行C/C++代码的编写,编写过程当中会提供语法检查等功能,这就可以让在Linux当中编写代码也能拥有之前Windows当中VS类似的体验。
要实现VS code当中远程连接云服务器首先就需要在拓展当中安装Remote -SSH
安装了之后就就可以发现在现在的VS code侧边当中出现了一个远程资源管理器
进入到远程资源管理器之后点击+即可新建远程
出来了一个弹窗之后按照ssh 用户名 云服务器的公网IP 输入之后点击回车即可
接下来SSH配置文件选择你的家目录下的即可,正常来说是第一个
点击回车之后就会弹出配置成功的窗口
这时你就可以点击打开配置查看添加的主机,文件内就会保存你添加主机的公网ip和登录的用户名
其实该文件是可以在Windows 家目录当中找到的,具体的路径就在C盘当中的用户目录下,这时就可以看到一个.ssh的文件
在此就可以看到之前在VS code 当中看到的config文件。在此除该文件之外known_hosts是确保连接的服务器身份可信,known_hosts.old作为 known_hosts
的备份,提供回滚可能。
进行了远程主机的配置之后接下来就可以连接主机了,点击在新窗口连接之后只需要输入密码即可
当左下角显示你的主机的公网IP,这时就说明连接成功了
点击左上角的打开文件夹就可以看到Linux系统当中的家目录当中的文件
在此打开一个空的目录,接下来就来试着在VS code当中写一段简单的C++代码。
首先点击新建文件
在此我们创建两个文件分别是test.cc和makefile。
注:在创建makefile文件的时候,VScode会推荐你安装对应的插件,只需要点击确认即可。
两个文件的内容如下所示:
注:在此有两个VS code的使用技巧:
1.在VScode当中在编码的时候只需要点击鼠标的右键就可以看到格式化文档。
![]()
2.在VScode当中要让编写的代码同步到服务器上需要在编写完之后点击CTRL+s
完成以上代码的编写之后点击VScode右上角的切换面板,这时就可以将命令行调出。
并且这时使用pwd指令可以看到当前所处的就是当前的文件夹下。
这时使用make指令
接下来就可以像之前在Xshell一样使用命令行来运行对应的可执行程序。
注:在此再推荐一个插件:Fitten Code: Faster and Better AI Assistant ,这个插件其实就是大规模代码模型的人工智能代码辅助工具,可以在我们编写代码的时候提供代码的补全。不过不建议对该插件过度的依赖。
![]()
例如以下示例:
当我们要写一个快速排序的代码就可以直接写出注释接下来,接下来只需要一直按Tab键ai就会帮我们生成对应的代码。
2.3 VScode远程连接常见错误及解决方案
在我们使用Vscode进行远程连接的时候可能会出现以下的错误
1.管道过程写入不存在,可以试着将自己的config路径配置⼀下
2.其他异常登录问题,可以试着在对应登录用户的家目录下, ls -al , 找到.vscode-server隐藏目录,删掉这个目录,重新登录。
3.问题:云服务器ip地址没有变,在其他的电脑上也远程连接过,在现在电脑突然就显示连接失败了。
其实这是因为.ssh/known_hosts 是 SSH 客户端用于存储已验证过公钥的远程主机信息的文件。当你首次通过 SSH 连接到某个远程服务器时,SSH 客户端会将该服务器的公钥存储在 ~/.ssh/known_hosts 文件中。这样,在后续连接时,客户端会检查服务器的公钥是否known_hosts 中的记录匹配,以确保连接的安全性。
解决方法:把之前的known_的所有文件都删掉即可
3. 具体通信方式
以上我们就了解了VScode基本的使用方法,那么接下来我们就可以开始Linux具体的进程通信方式的学习,大体上我们需要学习管道和SystemV进程通信两个方式,而在管道当中又划分为匿名管道和命名管道;System通信方式我们会了解到共享内存、消息队列以及信号量。在本篇当中我们将学习管道通信部分的知识,其他的将在下一篇当中进行讲解。
3.1 管道
其实在之前的Linux的学习当中我们就了解到管道相关的概念,在指令当中也使用到了管道。实际上管道是Unix当中最古老的进程通信方式,在此我们将一个进程连接到另一个进程的数据流称为“管道”。
3.1.1 匿名管道
概念学习
实际上系统的开发者最开始在设计进程之间通信时是想着基于操作系统当中原来就有的功能之上设计的。因此实际上匿名管道就是基于文件操作上实现的,我们知道父进程在创建子进程的时候对应的子进程是会继承父进程的文件描述符表的,那么这时父子进程文件描述符表内的元素指针都是执行同一个file文件对象,而在该对象当中是存在一个缓冲区的。这时不就做到了进程通信之前的必要条件——让不同的进程看到同一份资源。
在此父子进程看到的同一份资源就是——内存,其实在此进行的操作都是内存级的,也就是当父子进程将数据写入到对应文件的缓冲区之后是不会再将数据进一步写入到磁盘当中的,这就说明匿名管道虽然是基于文件系统的,但实际上是内存级的并未设计到此磁盘的操作。
以上就了解到匿名管道的原理,那么在Linux该如何创建匿名管道呢?
在Linux当中提供了pipe系统调用。
#include <unistd.h>
功能:创建⼀⽆名管道
int pipe(int fd[2]);
参数
fd:⽂件描述符数组,其中fd[0]表⽰读端, fd[1]表⽰写端
返回值:成功返回0,失败返回错误代码
通过之前的学习我们知道使用fork能创建子进程,那么再配合以上的pipe系统调用就创建匿名管道实现两个父子进程之间的通信,在此注意再创建子进程之后要将父子进程两端的各关闭一个读端和一个写端,这是由于匿名管道实际上只支持单向通信,若未关闭未使用的读写端可能导致进程阻塞或资源泄漏。
以下是父进程读,子进程进行写的具体过程:
从内核的角度匿名管道本质如下所示:
接口使用
以上我们了解了pipe的使用方法,那么接下来就试着来使用匿名管道来实现父子进程之间的通信,在此创建pipe.cc文件内实现代码
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <cstring>
#include <sys/wait.h>
int main()
{
int fd[2] = {0};
pid_t ret = pipe(fd);
if (ret < 0)
{
perror("pipe error!\n");
}
// 创建子进程
pid_t pid = fork();
// 子进程
if (pid == 0)
{
// 关闭子进程写端
close(fd[1]);
char buffer[1024] = {0};
// 子进程从管道内读
read(fd[0], buffer, sizeof(buffer) - 1);
printf("father say:%s\n", buffer);
close(fd[0]);
}
// 父进程
// 关闭父进程读端
close(fd[0]);
const char *buffer = "i am father";
// 父进程向管道内写入
write(fd[1], (char *)buffer, strlen(buffer));
close(fd[1]);
// 回收子进程退出信息
waitpid(pid, nullptr, 0);
return 0;
}
makefike:
Pipe_test:pipe.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f Pipe_test
以上我们就使用了pipe来创建出对应的匿名管道之后再关闭子进程的写端和父进程的读端,接下来父子进程在管道内进程数据的写入的读出时,实际上使用的就是文件读写的系统调用。编译以上的程序之后运行程序结果如下所示:
使用了pipe接口之后接下来就可以来了解匿名管道的5大特性和4种通信情况了。
5大特性:
1.匿名管道只能用于具有血缘关系进程之间进行通信(例如父子进程)
只有两个进程是具有血缘关系的也就是两个进程对应的文件描述符表当中是有储存相同的file对象指针的才能实现匿名管道的通信。以上我们进行了父子进程之间的通信,除此之外还可以进行兄弟进程等之间的通信。
2.匿名管道具有同步机制
以下了解匿名管道的4种通信模式就能了解所谓的同步机制是什么。
3.匿名管道是面向字节流的。
同样的面向字节流也需要通过以下的4种通信模式来了解,但是想深入的了解需要学习到Linux网络部分才行。
4.匿名管道能实现单向通信,属于半双工通信。
通过以上的了解就可以发现匿名管道在同一时间内是只能由一个进程来进行写入而另外一个进程进行读,无法实现两个进程同时实现读写,实际这种通信方式是半双工通信,现实生活当中的对讲机就是使用这种方式。而在同一时间内能可以同时发和收的就是全双工通信方式。
5.匿名管道的生命周期随进程。
匿名管道当对应的进程结束时候管道也就销毁了,之后要学习的System V通信种的共享内存等又与管道不同。
接下来来继续了解匿名管道的4种通信情况。
1.写慢,读快
在此就通过以下的代码来来验证该情况下匿名管道通信实际情况是什么样的。
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
void Child_Write(int f)
{
int n=0;
char buffer[1024]={0};
while(1)
{
sleep(1);
std::cout<<"please input:";
std::cin>>buffer;
write(f,buffer,strlen(buffer));
}
}
void Parent_Read(int f)
{
char buffer[1024]={0};
while(1)
{
ssize_t ret=read(f,buffer,sizeof(buffer)-1);
if(ret>0)
printf("Chird say:%s\n",buffer);
}
}
int main()
{
int fds[2]={0};
int ret=pipe(fds);
if(ret<0)
{
std::cerr<<"pipe error"<<std::endl;
}
pid_t id=fork();
if(id==0)
{
//子进程
//子进程关闭读
close(fds[0]);
Child_Write(fds[1]);
close(fds[1]);
}
//父进程关闭写
close(fds[1]);
Parent_Read(fds[0]);
waitpid(id,nullptr,0);
close(fds[0]);
return 0;
}
以上的代码当中是通过匿名管道来将子进程写入数据读,在父进程当中从管道当中读出,以上子进程当中通过用户的输入进行写入,而父进程则在不断的读取管道当中的数据,那么这时运行程序就会发现只要子进程一写入数据父进程就会马上将数据读取到,当父进程未读取到数据时就会一直阻塞。
通过以上代码就可以总结出写慢读快的特点就是读端要被阻塞。
2.写快,读慢
在此就通过以下的代码来来验证该情况下匿名管道通信实际情况是什么样的。
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
void Child_Write(int f)
{
int n=0;
char buffer[1024]={0};
while(1)
{
snprintf(buffer,sizeof(buffer),"I am child,my pid is:%d,n:%d",getpid(),n++);
write(f,buffer,strlen(buffer));
}
}
void Parent_Read(int f)
{
char buffer[2048]={0};
while(1)
{
sleep(3);
ssize_t ret=read(f,buffer,sizeof(buffer)-1);
if(ret>0)
printf("Chird say:%s\n",buffer);
}
}
int main()
{
int fds[2]={0};
int ret=pipe(fds);
if(ret<0)
{
std::cerr<<"pipe error"<<std::endl;
}
pid_t id=fork();
if(id==0)
{
//子进程
//子进程关闭读
close(fds[0]);
Child_Write(fds[1]);
close(fds[1]);
}
//父进程关闭写
close(fds[1]);
Parent_Read(fds[0]);
waitpid(id,nullptr,0);
close(fds[0]);
return 0;
}
以上的代码当中子进程在不断的写入数据到管道当中,而父进程每隔3秒才从管道当中读取数据,以上的代码编译为程序执行,结果如下所示:
通过以上代码就可以总结出写快读满的特点就是写端要被阻塞。
3.写关闭,读继续
在此如果在匿名管道通信过程当中将写端关闭,读端继续读取那么又会发生什么呢?接下来就通过以下的代码试试看
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
void Child_Write(int f)
{
int n=0;
char buffer[1024]={0};
while(1)
{
snprintf(buffer,sizeof(buffer),"I am child,my pid is:%d,n:%d",getpid(),n++);
write(f,buffer,strlen(buffer));
return ;
}
}
void Parent_Read(int f)
{
char buffer[2048]={0};
while(1)
{
ssize_t ret=read(f,buffer,sizeof(buffer)-1);
if(ret>0)
{
printf("Chird say:%s\n",buffer);
}
else if(ret==0)
{
printf("读取结束!\n");
exit(0);
}
else{
perror("read");
exit(1);
}
}
}
int main()
{
int fds[2]={0};
int ret=pipe(fds);
if(ret<0)
{
std::cerr<<"pipe error"<<std::endl;
}
pid_t id=fork();
if(id==0)
{
//子进程
//子进程关闭读
close(fds[0]);
Child_Write(fds[1]);
close(fds[1]);
}
//父进程关闭写
close(fds[1]);
Parent_Read(fds[0]);
waitpid(id,nullptr,0);
close(fds[0]);
return 0;
}
以上的代码就是将管道的写端关闭之后观察读端的状况,将以上的代码编译为可执行程序之后,执行结果如下所示:
实际上正如上所示,在匿名管道写端关闭之后,管道的读端是会读到0,那么这时我们就可以让对应的读端进程结束。
4.读关闭,写继续
如果将管道当中的读端关闭,写端继续又会发生什么呢?接下来来看以下的代码。
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
void Child_Write(int f)
{
int n = 0;
char buffer[1024] = {0};
while (1)
{
snprintf(buffer, sizeof(buffer), "I am child,my pid is:%d,n:%d", getpid(), n++);
write(f, buffer, strlen(buffer));
}
}
void Parent_Read(int f)
{
char buffer[2048] = {0};
while (1)
{
ssize_t ret = read(f, buffer, sizeof(buffer) - 1);
if (ret > 0)
{
printf("Chird say:%s\n", buffer);
}
else if (ret == 0)
{
printf("读取结束!\n");
exit(0);
}
else
{
perror("read");
exit(1);
}
break;
}
}
int main()
{
int fds[2] = {0};
int ret = pipe(fds);
if (ret < 0)
{
std::cerr << "pipe error" << std::endl;
}
pid_t id = fork();
if (id == 0)
{
// 子进程
// 子进程关闭读
close(fds[0]);
Child_Write(fds[1]);
close(fds[1]);
}
// 父进程关闭写
close(fds[1]);
Parent_Read(fds[0]);
close(fds[0]);
int status = 0;
sleep(3);
int n = waitpid(id, &status, 0);
if (n > 0)
{
printf("子进程退出码:%d,退出信号:%d\n", (status >> 8) & 0xFF, status & 0x7F);
sleep(3);
}
return 0;
}
在以上的代码当中让对应的父进程在读取管道内的数据一次之后就停止读取,之后对应的管道就没有了读端,那么这时子进程是否还会向管道一直写入数据吗?以上代码运行的结果如下所示:
进程监控如下所示:
我们会发现子进程的退出信号是为13,当前我们还为学习Linux当中的信号,但在此可以告诉你实际上子进程是被“杀掉”的,在操作系统当中是不会做如何浪费时间和空间的事情的,那么在匿名管道当中如果子对应的读端都退出了那么这时写端就没有存在的意义了,这时操作系统就会发出对应的信号将写端的进程杀掉。
进程池实现
以上我们就了解了匿名管道对应的概念和性质,并且也试着使用了对应的系统调用,那么接下来就可以试着基于匿名管道来实现进程池,那么进程池又是什么呢?
父进程是可以同时创建多个子进程的,这时就可以通过匿名管道让每个父子进程都通过匿名管道来建立通信,父进程通过管道来给不同的进程派发相应的任务,在此这种通过父进程创建多个子进程从而进行任务处理的方式称为进程池。
接下来就来分析进程池这个小项目整体的结构是什么样的。
首先创建三个文件,在ProcessPool.hpp文件当中实现进程池的完整结构,在Task.hpp文件当中实现父进程具体派发的任务,在Main.cc当中实现进程池的调用。
注:在此提到的.hpp也是c++当中一种文件类型,该文件的特点是将函数的声明和实现同时进行,在其他文件当中要实现该文件内的方法可以引用该头文件。
在代码的设计当中使用面向对象的方式实现。
在ProcessPool.hpp当中实现的类如下:
#ifndef _PROCESS_POLL_HPP__
#define _PROCESS_POLL_HPP__
#include <iostream>
#include"Task.hpp"
//匿名管道实现与各个接口
class Channel
{
//……
};
//匿名管道管理
class ChannelManager
{
//……
};
//进程池实现
class ProcessPool
{
//……
};
#endif
Task.hpp内类结构如下:
#pragma once
#include<iostream>
//任务管理类
class TaskManager
{
//……
};
首先就来实现ProcessPool.hpp文件的代码,完整如下所示:
#ifndef _PROCESS_POLL_HPP__
#define _PROCESS_POLL_HPP__
#include <iostream>
#include <vector>
#include <unistd.h>
#include<cstdlib>
#include<string>
#include<sys/wait.h>
#include"Task.hpp"
//匿名管道属性与接口
class Channel
{
private:
// 管道的写端文件描述符
int _wfd;
// 子进程的ID
pid_t _subid;
// 管道的名称
std::string _name;
public:
Channel(int wfd, pid_t subid)
: _wfd(wfd), _subid(subid)
{
_name="channel-"+std::to_string(_wfd)+"-"+std::to_string(_subid);
}
~Channel()
{
}
// 获取管道的名称
std::string GetName()
{
return _name;
}
//进行任务发送
void Send(int code)
{
int n=write(_wfd,&code,sizeof(int));
(void)n;
}
// 关闭管道的写端
void Close()
{
close(_wfd);
}
// 进行等待子进程结束
void Wait()
{
int ret=waitpid(_subid,nullptr,0);
(void)ret;
}
};
// 管理所有的匿名管道
class ChannelManager
{
private:
// 存储所有的管道
std::vector<Channel> _channels;
// 下一个要使用的管道索引
int _next;
public:
ChannelManager():_next(0)
{
}
~ChannelManager()
{
}
//选择一个管道
//采用轮询的方式
Channel& Select()
{
auto & c=_channels[_next];
_next++;
_next%=_channels.size();
return c;
}
// 插入一个管道
void Insert(int wfd, pid_t pid)
{
_channels.emplace_back(wfd, pid);
}
// 打印所有管道的名称
void PrintChannel()
{
for(auto x:_channels)
{
std::cout<<x.GetName()<<std::endl;
}
}
// 关闭并等待所有管道
void CloseAndStopChannel()
{
for(auto& x:_channels)
{
x.Close();
std::cout<<"关闭:"<<x.GetName()<<std::endl;
x.Wait();
std::cout<<"回收:"<<x.GetName()<<std::endl;
}
}
// 关闭所有管道的写端
void CloseAll()
{
for(auto& x:_channels)
{
x.Close();
}
}
};
//默认的进程数量
const int gddefaultnum = 5;
//进程池类
class ProcessPool
{
private:
// 管理所有的匿名管道
ChannelManager _cm;
// 进程数量
int _process_num;
//任务管理器
TaskManager _tm;
public:
ProcessPool(int num)
: _process_num(num)
{
_tm.Register(Upload);
_tm.Register(DownLoad);
_tm.Register(PrintLog);
}
//子进程进行对应的工作
void work(int pipefd)
{
while(1)
{
int code=0;
ssize_t n=read(pipefd,&code,sizeof(int));
if(n>0)
{
if(n!=sizeof(int))
{
continue;
}
std::cout<<"子进程["<<getpid()<<"]收到一个任务码"<<code<<std::endl;
_tm.Execute(code);
}
else if(n==0)
{
std::cout<<"子进程退出"<<std::endl;
break;
}
else{
std::cout<<"读取错误"<<std::endl;
break;
}
// std::cout<<"我是子进程,我的pid:"<<getpid()<<"文件描述符:"<<pipefd<<std::endl;
// sleep(1);
}
}
// 调试函数,打印所有管道的名称
void Debug()
{
_cm.PrintChannel();
}
//启动进程池
bool start()
{
for (int i = 0; i < _process_num; i++)
{
// 1.创建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0)
{
std::cerr << "创建管道失败" << std::endl;
return false;
}
// 创建子进程
pid_t subid = fork();
if (subid < 0)
return false;
else if (subid == 0)
{
// 子进程
// 关闭不需要的文件描述符
_cm.CloseAll();
close(pipefd[1]);
work(pipefd[0]);
close(pipefd[0]);
exit(0);
}
else
{
// 父进程
// 关闭不需要的文件描述符
close(pipefd[0]);
_cm.Insert(pipefd[1],subid);
//close(pipefd[1]);
}
}
return true;
}
//给子进程发送任务
void Run()
{
int taskcode=_tm.code();
auto& c=_cm.Select();
std::cout<<"选了一个子进程"<<c.GetName()<<std::endl;
c.Send(taskcode);
std::cout<<"发送的一个任务码"<<taskcode<<std::endl;
}
//销毁进程池
//关闭所有管道的写端,并等待子进程结束
void Destory()
{
_cm.CloseAndStopChannel();
}
};
#endif
Task.hpp代码实现如下所示:
#pragma once
#include<iostream>
#include<vector>
#include<ctime>
//任务函数指针
typedef void (*task_t)();
//具体任务
//----------------------------------------------------------------
void PrintLog()
{
std::cout<<"我是一个打印日志的任务"<<std::endl;
}
void DownLoad()
{
std::cout<<"我是一个下载的任务"<<std::endl;
}
void Upload()
{
std::cout<<"我是一个上传的任务"<<std::endl;
}
//----------------------------------------------------------------
//任务管理器
class TaskManager
{
private:
std::vector<task_t> _task;
public:
TaskManager()
{
// 初始化随机数种子
srand(time(nullptr));
}
~TaskManager()
{
}
//生成一个随机任务代码
int code()
{
return rand()%_task.size();
}
//注册任务
//将任务函数指针添加到任务列表中
void Register(task_t t)
{
_task.push_back(t);
}
//执行任务
void Execute(int code)
{
if(code>=0 && code<_task.size())
{
_task[code]();
}
}
};
以上的代码就将设计完了,接下来就是来解释整体的设计思路以及一些设计细节的必要性,以上的进程池设计文件ProcessPool.hpp当中在Channel类当中存储对应管道的写端文件描述符以及子进程的pid,以及用于实现管道的各个接口,包括向指定进程发送任务号等。
而在ChannelManager类当中就使用vector将所有的管道对象Channel管理起来,在类的内部实现管理管道的插入以及关闭等工作。
在以上的代码当中子进程的创建的过程当中为什么要先关闭不需要的文件描述符呢?这里关闭的文件描述符又是哪些呢?
要解答以上的问题就需要分析当代码当中未包含以上的代码时会出现什么情况呢?
当前是通过fork来创建5个子进程,我们知道在创建子进程的过程当中子进程是会将父进程的文件描述符拷贝一份,在文件描述符表当中默认是0、1、2分别表示标准输入、输出以及标准错误的file文件对象,那么在创建第一个子进程的时候接下来再创建匿名管道后确实是满足我们的要求的。
但是接下来在创建第二个子进程的时候就会出现问题了,那就是第二个子进程在创建的时是对应的文件描述符表是会将此时父进程的进行拷贝的,但是目前父进程文件描述符表中下标为4已经有了元素,那么接下来子进程创建出来时其文件描述符表下标为4的位置也会执行对应的file文件对象。
那么在第二个子进程创建出来之后第一个匿名管道就会有两个指针指向其,接下来在创建5个子进程之后就会使得第一个file文件对象有5个指针指向其,其他进程依次递减。
![]()
以上就会造成当我们将进程运行起来的时候在销毁管道过程当中,销毁父进程第一个管道的写端之后由于其他子进程当中还存在该管道的写端这就使得第一个子进程通过管道收到0无法使得操作系统将第一个管道杀掉,那么父进程就会阻塞在第一个子进程的进程等待处。
![]()
所以综合以上就需要在创建每个子进程之后将子进程当中不需要的匿名管道关闭。
在ProcessPool类当中存在管理管道的类ChannelManager对象以及管理任务类的对象TaskManager,在该类当中实现进程的创建以及给各个进程分配具体任务的工作。
以上实现的任务处理方式是通过父进程给子进程通过管道传输对应的任务号,之后子进程通过任务号执行对应的进程,父进程分配任务是通过轮询的方式。你也可以自己试着使用其他的分配策列。
接下来就来测试子进程的创建是否成功,在Main.cc函数当中调用ProcessPool类当中的DeBug函数进行测试。
#include"ProcessPool.hpp"
int main()
{
ProcessPool pp(gddefaultnum);
pp.start();
pp.Debug();
return 0;
}
通过以上的测试发现/是没有问题的,那么接下来就可以来调用进程池。
3.1.2 命名管道
以上当我们了解了匿名管道之后就可以接着来了解命名管道,我们知道匿名管道是只能用于具有血缘关系之间进程的通信,当要进行通信的两个进程毫无关系的时候就可以使用到命名管道。
命名管道本质上也是使用FIFO文件来做这项工作。命名管道是⼀种特殊类型的文件。
那么此时我们就需要思考了,和匿名管道不同命名管道能实现无血缘关系之间进程的通道,但是两个进程这时都不相关了,又是如何进行通信的呢?
假设当前有两个进程A和进程B,这时若两个进程都打开的路径为/home/test/test.txt路径下的文件,那么这时该文件是否会在操作系统当中被打开两次呢?
我们知道操作系统是不会做如何浪费时间和空间的事,因此相同的文件只会在操作系统当中打开一次。
实际上每个进程当中都有一份独立的struct file的对象,之前在匿名管道当中为进行区分只是方便理解。以上打开同一个文件不久让不同的进程看到同一份资源了吗?
在此同一份资源就是对应文件的缓冲区,在操作系统当中就将这种只用于两个进程之间通信的文件称为管道文件。
和普通的文件不同的管道文件在使用的时候只需要打开而不需要将内核缓冲区当中的数据刷新到磁盘当中。
实际上以上就是不同的进程使用cout等输出时都是输出到显示器上的原理。
在命令行可以使用以下的指令创建命名管道:
mkfifo [文件名]
除此之外还可以使用函数来创建对应的命名管道。
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename,mode_t mode);
以上函数的第一个参数是要创建的命名管道的名称,第二个参数是创建的管道初始化时各个用户对应的权限。
关闭管道可以使用以下系统调用:
#include <unistd.h>
int unlink(const char *pathname);
那么接下来我们就可以试着使用命名管道来实现两个进程之间的通信。
在此创建三个文件,分别是在comm.hpp内保存公有的属性,在server.cc实现创建管道和从管道内读数据,在client.cc内实现向管道内写数据。
实现代码如下所示:
comm.hpp
#pragma once
#define FIFO_FILE "fifo"
server.cc
#include <iostream>
#include "comm.hpp"
#include <sys/types.h>
#include <sys/stat.h>
#include<unistd.h>
#include <fcntl.h>
int main()
{
umask(0);
// 新建管道
int n = mkfifo(FIFO_FILE, 0666);
if (n < 0)
{
std::cerr << "创建管道失败" << std::endl;
exit(1);
}
std::cout << "创建管道成功!" << std::endl;
// 打开管道
int f = open(FIFO_FILE,O_RDONLY);
if(f<0)
{
std::cerr<<"打开管道失败!"<<std::endl;
exit(1);
}
std::cout<<"打开管道成功"<<std::endl;
//读取管道内数据
while(1)
{
char buffer[1024];
int ret=read(f,buffer,sizeof(buffer));
if(ret>0)
{
buffer[ret]=0;
std::cout<<"client say:"<<buffer<<std::endl;
}
else if(ret==0)
{
std::cout<<"client quit!"<<std::endl;
return 1;
}
else{
std::cerr<<"read cerror"<<std::endl;
}
}
//关闭管道
close(f);
//销毁管道
unlink(FIFO_FILE);
return 0;
}
client.cc
#include <iostream>
#include "comm.hpp"
#include <sys/types.h>
#include <sys/stat.h>
#include<unistd.h>
#include <fcntl.h>
int main()
{
// 打开管道
int f = open(FIFO_FILE,O_WRONLY);
if(f<0)
{
std::cerr<<"打开管道失败!"<<std::endl;
exit(1);
}
//向管道内写数据
int cnt=1;
while(1)
{
std::string str;
std::cout<<"Please input:";
getline(std::cin,str);
write(f,str.c_str(),str.size());
if(str == "quit")
{
std::cout<<"client quit!"<<std::endl;
break;
}
}
//关闭管道
close(f);
//销毁管道
unlink(FIFO_FILE);
return 0;
}
makefile:
.PHONY:all
all:Server Client
Server:server.cc
g++ -o $@ $^ -std=c++11
Client:client.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f Server Client fifo
运行代码效果如下:
除了以上的方式之外还可以使用面向对象的方式来使用管道实现进程之间的通信。
代码如下所示:
comm.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#define NAME "fifo"
#define PATH "."
class Fifo
{
public:
Fifo(std::string path, std::string name)
: _name(name), _path(path)
{
_fifoname = _path + "/" + _name;
umask(0);
// 新建管道
_n = mkfifo(_fifoname.c_str(), 0666);
if (_n < 0)
{
std::cerr << "创建管道失败" << std::endl;
exit(1);
}
else
{
std::cout << "创建管道成功!" << std::endl;
}
}
~Fifo()
{
if (_n > 0)
// 销毁管道
unlink(_fifoname.c_str());
}
private:
std::string _path;
std::string _name;
std::string _fifoname;
int _n;
};
class Openfifo
{
public:
Openfifo(std::string path, std::string name)
: _name(name), _path(path)
{
_filename = path + "/" + name;
}
~Openfifo()
{
}
void Open_for_write()
{
// 打开管道
_fd = open(_filename.c_str(), O_WRONLY);
if (_fd < 0)
{
std::cerr << "打开管道失败!" << std::endl;
exit(1);
}
else
{
std::cout << "打开管道成功" << std::endl;
}
}
void open_for_read()
{
// 打开管道
_fd = open(_filename.c_str(), O_RDONLY);
if (_fd < 0)
{
std::cerr << "打开管道失败!" << std::endl;
exit(1);
}
else
{
std::cout << "打开管道成功" << std::endl;
}
}
void Write()
{
std::string str;
while (1)
{
std::cout << "Please input:";
getline(std::cin, str);
write(_fd, str.c_str(), str.size());
if(str=="quit")
{
std::cout<<"server quit!"<<std::endl;
break;
}
}
}
void Read()
{
while (1)
{
// 读取管道内数据
char buffer[1024];
int ret = read(_fd, buffer, sizeof(buffer));
if (ret > 0)
{
buffer[ret] = 0;
std::cout << "client say:" << buffer << std::endl;
}
else if (ret == 0)
{
std::cout << "client quit!" << std::endl;
return;
}
else
{
std::cerr << "read cerror" << std::endl;
}
}
}
void Close()
{
close(_fd);
}
private:
std::string _path;
std::string _name;
std::string _filename;
int _fd;
};
server.cc
#include "comm.hpp"
int main()
{
Fifo f(PATH, NAME);
Openfifo fo(PATH, NAME);
fo.open_for_read();
fo.Read();
fo.Close();
return 0;
}
client.cc
#include "comm.hpp"
int main()
{
Openfifo fo(PATH, NAME);
fo.Open_for_write();
fo.Write();
fo.Close();
return 0;
}
以上代码运行结果如下所示:
以上就是本篇的所有内容了,接下来在下一篇中我们将继续学习System V相关的进程通信方式,未完待续……