【Linux】如何实现单机版QQ,来看进程间通信之管道(下)

简介: 【Linux】如何实现单机版QQ,来看进程间通信之管道(下)

我们在Task类中先定义一个vector,vector中存放的类型为函数指针类型,每一个函数指针都指向一个函数方法,这样我们就可以通过指令来调用不同的方法,对于指令command来讲我们之前也提到过其实就是位图,我们定义三个宏值分别代表三种打印方法,相要调用哪个方法就让command参数&上对应的宏值就完成了。我们在.cc文件中加上头文件,然后定义一个全局的Task类:


5f3580acc03f44cc94e3b6aae1f69648.png


接下来我们继续实现子进程任务函数:


7b4c8b91040d4a60b80621d487d6bf69.png


在子进程函数中我们默认指令为int类型,然后通过read函数的返回值判断是否读取成功,如果返回值等于4字节说明读到了指令那么就调用对应的函数方法,如果返回值等于0就说明当前的子进程要退出了直接break,剩下的就是读取失败也是直接退出,当然我们不能只执行一次,所以我们加个循环让子进程周而复始的去执行任务:


de291a7180184e6eb0ec6c0d0cf7b739.png


那么接下来的父进程还需要干什么呢?父进程需要选择任务,选择进程然后下发任务,下面我们来实现:


a38fc2840b764e14a99a2672881eaca4.png


这里的任务是可以随机选择的,但是我们为了测试就只用了LOG任务,然后选择进程的时候直接用随机函数选择即可,下发任务也就是让父进程写入,写入的是指令对应的方法,然后我们将之前答应的代码加上getpid,把子进程的pid也打印出来:


25578782cd044170bad6069fb9a010e8.png


以下是运行起来后的代码:

9a76402485b841d29bb178204d7a5095.png45a6d64eb8894cbf844a7e09657395e2.png


这样我们就完成了要实现的功能,现在我们再把相应的函数优化一下,让选择任务的时候可以是交互式的,首先第一步让用户选择任务,这里我们先实现一个选择面板:


f14d12c4ccfb4b40a80fe09974c687bd.png


将选择面板搞定后我们就可以实现让用户选择对应的任务了:


5709064847004ff0b110dd594f41fbcc.png


下面我们让选择进程的功能变成按顺序的,这样就不会出现每次随机让一个进程执行任务的情况:

03b07e5a02b944428a082d08fa897eb9.png

下面我们我们再实现一下每次拿到任务后打印正在执行哪个任务:


41d2debde3b245b1b0f24810639386b8.png1942fe1f61d84d1aac402567c46d7d9d.png


 我们先创建一个静态的int变量用来充当第几个进程,然后我们在类外将number初始化为0,string类型的进程名接收缓冲区里的字符,函数name可以返回一个进程名。然后我们在选择任务那块也打印一下:


fd3c76e1122e40a8999bd1e0aca6a3b0.png


然后如果进程退出的话也要有提示,所以我们在子进程那块加上打印提示:


e5b7d90c955a4b66b1362424cf3af3ce.png


当n等于0说明父进程将写端关闭了,随之就打印一下告诉用户。然后我们再将代码简化一下,把刚刚main函数中写的控制函数代码都放到ctrlProcess函数中:


beaaa5629bef4defa24697350bae667b.pngd871deb5908b496583e4205d7dc1c91e.png


写到这里发现124行有个报错,是因为返回的name函数需要加上const让静态变量也能使用(因为我们的ctrlProcess函数的参数是const对象)

3544cd513e4a4461a4ca9a540259b3f8.png


下面我们运行一下程序:


3f6b98786d9d44babcf33681ab2066ea.png


程序运行起来后我们发现输入3退出不了,并且输入其他指令程序也没有打印动作,所以我们用if语句判断一下:


a0445a9166e34c4798084863ddbab703.png


除了这个问题我们还要解决子进程退出问题,还记得我们之前说过的僵尸进程吗,如果子进程没有被父进程等待那么子进程就变成了僵尸状态,为了防止这种情况我们再写一个进程等待函数函数:


c76ad450ee6c46848bea82d486d18c7f.png45b5f6cd4adc4104b4717356e2417571.png


回收子进程之前我们需要将父进程的所有写端都关了,然后等待10秒后我们让父进程把每个子进程都回收了,这样就完成了简单的回收子进程函数。

1efe648abc1f4568923b952d2509d29c.png


当我们运行起来后发现可以完成我们的需求,并且在退出后也能正常的回收子进程。


不知道有没有人会有疑问,为什么我们刚刚在同一个循环内不能直接边关闭边回收子进程呢,就像下图这样:


71af155348da4019b4e55e1b31c016d8.png


我们先用这样的代码运行一下:


11f3f1f85f9f4d4f9e0986c29afa0654.png


我们发现这样写后程序就卡在了回收进程处,这是为什么呢?其实是和子进程有关,我们早就说过子进程会继承父进程打开的文件描述符(这就是管道的原理),这里的坑就在于当第二次及第二次以后的创建子进程时,这个子进程会继承前面所有父进程的端口,也就是说越往后的子进程继承的端口越多,如下图所示:


972ea8b06aab4319886296e8ea426b81.png


也就是说刚刚卡主的原因是写端没有全部关完就回收了子进程,就造成了阻塞。而我们用一个循环单独关闭父进程的端口时是不会出现这个问题的,因为我们是先关闭了父进程的写端,都关闭后才去回收子进程。


由于上面的讲解都是通过图片的形式,下面我们将此次的实例代码发出来:


首先是ctrlProcess.cc文件中的:


#include <iostream>
#include <string>
#include <cassert>
#include <unistd.h>
#include <vector>
#include "task.hpp"
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
const int gnum = 3;
Task t;
class EndPoint
{
private:
     static int number;
public:
     pid_t _child_id;
     int _write_fd;
     std::string processname;
     EndPoint(int id,int fd)
          :_child_id(id)
          ,_write_fd(fd)
     {
         char namebuffer[64];
         snprintf(namebuffer,sizeof(namebuffer),"process-%d[%d:%d]",number++,_child_id,_write_fd);
         processname = namebuffer;
     }
     std::string &name()
     {
        return processname;
     }
     ~EndPoint()
     {
     }
};
int EndPoint::number = 0;
//子进程要执行的方法
void WaitCommand()
{
    while (true)
    {
        int command = 0;
        int n = read(0,&command,sizeof(int));
        if (n==sizeof(int))
        {
            t.Execute(command);
        }
        else if(n==0)
        {
            std::cout<<"父进程让我退出,我就退出了: "<<getpid()<<std::endl;
            break;
        }
        else 
        {
            break;
        }
    }
}
void CreatProcess( vector<EndPoint>* end_points)
{
     //1.先进行构建控制结构,父进程写入,子进程读取
    for (int i = 0;i<gnum;i++)
    {
        //1.1创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        assert(n==0);
        (void)n;
        //1.2创建进程
        pid_t id = fork();
        assert(id!=-1);
        //返回值等于0一定是子进程
        if (id==0)
        {
            //先关闭不要的fd
            close(pipefd[1]);
            //我们期望所有的子进程读取指令的时候,都从标准输入读取
            //1.3.1输入重定向
            dup2(pipefd[0],0);  //读0就像读管道一样
            //1.3.2让子进程开始等待获取命令
            WaitCommand();
            close(pipefd[0]);
            exit(0);
        }
        //一定是父进程
        //1.3关闭不要的fd
        close(pipefd[0]);
        //1.4将新的子进程和他的管道写端,构建对象
        end_points->push_back(EndPoint(id,pipefd[1]));
    }
}
int ShowBoard()
{
    std::cout<<"################################"<<std::endl;
    std::cout<<"##0. 执行日志任务 1. 执行数据库任务##"<<std::endl;
    std::cout<<"##2.执行请求任务  3.退出#########"<<std::endl;
    std::cout<<"################################"<<std::endl;
    std::cout<<"请选择#";
    int command = 0;
    std::cin>>command;
    return command;
}
void ctrlProcess(vector<EndPoint>& end_points) 
{
    //2.1我们可以写成自动化的,也可以写成交互式的
    int cnt = 0;
    int num = 0;
    while (true)
    {
        //1.选择任务
        int command = ShowBoard();
        if (command==3)
        {
            break;
        }
        if (command<0|| command>2)
        {
            continue;
        }
        //2.选择进程
        int index = cnt++;
        cnt%=end_points.size();
        std::cout<<"选择了进程:"<<end_points[index].name()<<" | 处理任务"<<command<<std::endl;
        //3.下发任务
        write(end_points[index]._write_fd,&command,sizeof(command));
        //sleep(1);
    }
}
void ExitProcess()
{
    exit(0);
}
void waitProcess(const vector<EndPoint>& end_points)
{
     //1.我们需要让子进程全部退出 -- 只需要让父进程关闭所有的写端
     //for (const auto &ep:end_points)
     for (int end = end_points.size()-1;end>=0;end--)
     {
        std::cout<<"父进程让所有的子进程全部退出"<<std::endl;
        //先关闭最后一个写端倒着一直关闭,因为子进程会继承父进程的文件描述符所以
        //后面的子进程都会链接到第一个父进程的写端,如果关第一个无法全部关闭,会造成
        //阻塞
        close(end_points[end]._write_fd);
        std::cout<<"父进程回收了所有的子进程"<<std::endl;
        waitpid(end_points[end]._child_id,nullptr,0);
     }
     //std::cout<<"父进程让所有的子进程全部退出"<<std::endl;
     sleep(10);
     //2.父进程要回收子进程的僵尸状态
     //for (const auto& ep:end_points)
     //{
     //   waitpid(ep._child_id,nullptr,0);
     //}
     //std::cout<<"父进程回收了所有的子进程"<<std::endl;
     //sleep(10);
}
int main()
{
    //1.先进行构建控制结构,父进程写入,子进程读取
    vector<EndPoint> end_points;
    CreatProcess(&end_points);
    //2.我们得到了什么?
    ctrlProcess(end_points);
    //3.处理所有的退出问题
    waitProcess(end_points);
    return 0;
}


下面是头文件task.hpp中的:


#include <iostream>
#include <vector>
#include <unistd.h>
typedef void(*fun_t)();  //函数指针
void PrintLog()
{
    std::cout<<"pid:"<<getpid()<<",打印日志任务,正在被执行..."<<std::endl;
}
void InsertMySQL()
{
    std::cout<<"执行数据库任务,正在被执行..."<<std::endl;
}
void NetRequest()
{
    std::cout<<"执行网络请求任务,正在被执行..."<<std::endl;
}
#define COMMAND_LOG 0
#define COMMAND_MYSQL 1
#define COMMAND_REQUEST 2
class Task
{
public:
    Task()
    {
        funcs.push_back(PrintLog);
        funcs.push_back(InsertMySQL);
        funcs.push_back(NetRequest);
    }
    ~Task()
    {
    }
    void Execute(int command)
    {
        if (command >= 0&& command < funcs.size())
        {
            funcs[command]();
        }
    }
public:
    std::vector<fun_t> funcs;
};


总结



管道读写规则:


当没有数据可读时:

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,进而可能导致writ进程退出

当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。

当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。


管道特点:

只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。

管道提供流式服务

一般而言,进程退出,管道释放,所以管道的生命周期随进程

一般而言,内核会对管道操作进行同步与互斥

管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

目录
相关文章
|
8月前
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
310 67
|
7月前
|
Web App开发 Linux 程序员
获取和理解Linux进程以及其PID的基础知识。
总的来说,理解Linux进程及其PID需要我们明白,进程就如同汽车,负责执行任务,而PID则是独特的车牌号,为我们提供了管理的便利。知道这个,我们就可以更好地理解和操作Linux系统,甚至通过对进程的有效管理,让系统运行得更加顺畅。
230 16
|
7月前
|
Unix Linux
对于Linux的进程概念以及进程状态的理解和解析
现在,我们已经了解了Linux进程的基础知识和进程状态的理解了。这就像我们理解了城市中行人的行走和行为模式!希望这个形象的例子能帮助我们更好地理解这个重要的概念,并在实际应用中发挥作用。
147 20
|
6月前
|
监控 Shell Linux
Linux进程控制(详细讲解)
进程等待是系统通过调用特定的接口(如waitwaitpid)来实现的。来进行对子进程状态检测与回收的功能。
135 0
|
6月前
|
存储 负载均衡 算法
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
209 0
|
6月前
|
存储 Linux Shell
Linux进程概念-详细版(二)
在Linux进程概念-详细版(一)中我们解释了什么是进程,以及进程的各种状态,已经对进程有了一定的认识,那么这篇文章将会继续补全上篇文章剩余没有说到的,进程优先级,环境变量,程序地址空间,进程地址空间,以及调度队列。
137 0
|
6月前
|
Linux 调度 C语言
Linux进程概念-详细版(一)
子进程与父进程代码共享,其子进程直接用父进程的代码,其自己本身无代码,所以子进程无法改动代码,平时所说的修改是修改的数据。为什么要创建子进程:为了让其父子进程执行不同的代码块。子进程的数据相对于父进程是会进行写时拷贝(COW)。
187 0
|
9月前
|
存储 Linux 调度
【Linux】进程概念和进程状态
本文详细介绍了Linux系统中进程的核心概念与管理机制。从进程的定义出发,阐述了其作为操作系统资源管理的基本单位的重要性,并深入解析了task_struct结构体的内容及其在进程管理中的作用。同时,文章讲解了进程的基本操作(如获取PID、查看进程信息等)、父进程与子进程的关系(重点分析fork函数)、以及进程的三种主要状态(运行、阻塞、挂起)。此外,还探讨了Linux特有的进程状态表示和孤儿进程的处理方式。通过学习这些内容,读者可以更好地理解Linux进程的运行原理并优化系统性能。
364 4
|
9月前
|
Linux Shell
Linux 进程前台后台切换与作业控制
进程前台/后台切换及作业控制简介: 在 Shell 中,启动的程序默认为前台进程,会占用终端直到执行完毕。例如,执行 `./shella.sh` 时,终端会被占用。为避免不便,可将命令放到后台运行,如 `./shella.sh &`,此时终端命令行立即返回,可继续输入其他命令。 常用作业控制命令: - `fg %1`:将后台作业切换到前台。 - `Ctrl + Z`:暂停前台作业并放到后台。 - `bg %1`:让暂停的后台作业继续执行。 - `kill %1`:终止后台作业。 优先级调整:
741 5
|
9月前
|
Linux 应用服务中间件 nginx
Linux 进程管理基础
Linux 进程是操作系统中运行程序的实例,彼此隔离以确保安全性和稳定性。常用命令查看和管理进程:`ps` 显示当前终端会话相关进程;`ps aux` 和 `ps -ef` 显示所有进程信息;`ps -u username` 查看特定用户进程;`ps -e | grep &lt;进程名&gt;` 查找特定进程;`ps -p &lt;PID&gt;` 查看指定 PID 的进程详情。终止进程可用 `kill &lt;PID&gt;` 或 `pkill &lt;进程名&gt;`,强制终止加 `-9` 选项。
149 3