【Linux】匿名管道

匿名管道

管道通信是一种单向的通信方式,一方负责写数据,一方负责读数据,这样就达到了通信的目的。

但我们知道,进程之间是相互独立的,那么独立的两个进程之间是如何进行通信的呢?

当一个进程以读和写的方式进行打开文件的时候如下图所示。

在当前进程下分配文件描述符,然后同时指向这个文件,因此,这个文件的引用计数也就变为了2。

那么如何让另外一个进程也能够看到这一份文件呢?

可以通过fork()创建子进程来达到这个目的,我们知道,子进程是会继承父进程的PCB的大部分内容的,也就是说子进程的PCB是以父进程的PCB为模板来创建的。因此,子进程肯定会继承父进程的文件描述符表。

虽然子进程不会直接继承父进程的文件,但是继承了文件描述符表就可以通过文件描述符来找到文件,也就是间接的“继承了父进程的文件”。

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include<sys/wait.h>
#include <fcntl.h>
#include <string.h>
using namespace std;
int main()
{
    
    
    int fd1 = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); //以写方式打开
    int fd2 = open("log.txt", O_RDONLY);                           //以读方式打开
    pid_t id = fork();
    if (id == 0)
    {
    
    
        //子进程
        //子进程的3号文件描述符对应写端,4号对应读端
        close(3); //子进程关闭写端
        char buffer[1024] = {
    
    0};
        while (true)
        {
    
    
            //子进程尝试一直读取内容
            ssize_t num = read(4, buffer, sizeof(buffer) - 1);
            if (num != 0)
            {
    
    
                buffer[num] = 0;
                printf("子进程的pid是%d,ppid是%d,子进程从log.txt中读取到了内容,是%s\n", getpid(), getppid(), buffer);
            }
        }
        cout << "---------------------" << endl;
        close(4); //子进程关闭4号文件描述符
        exit(0);  //子进程读取完毕退出
    }
    close(4); //父进程关闭读端
    //父进程向log.txt中写入数据
    int cnt = 0;
    int num = 10;
    char message[1024] = {
    
    0};
    while (num--)
    {
    
    
        snprintf(message, 1024, "%s %d", "父进程发送消息", cnt++);
        write(3, message, strlen(message));
        sleep(1);
    }
    close(3);      //父进程关闭写端
    wait(nullptr); //父进程尝试回收子进程
    //当父子进程都退出时,系统会回收log.txt的struct file内核数据结构
    return 0;
}

这种直接使用磁盘上的文件进行通信会有以下缺点:

1.父进程的写入比较慢的话,子进程在内核缓冲区读取不到内容就会返回0。

2.当父进程把写端关闭之后,子进程依旧会继续读取内核缓冲区当中的内容,但由于不再往缓冲区中写入东西了,子进程也read不到内容,就会不断返回0。

3.父进程在尝试回收子进程的时候,子进程依然会不断的尝试从log.txtstruct file中读取内容,因此,父进程的回收不会成功,而且由于子进程还与log.txtstruct file有关联,系统也无法回收log.txt的内核数据结构资源,可能导致内存泄漏。

pipe

因此,操作系统提供了一个pipe接口来解决这一问题。

int pipe2(int pipefd[2], int flags);

pipefd[2]是一个输出型的参数,pipefd[0]对应的是读取端,pipefd[1]对应的是写入端。

创建成功返回0,失败返回-1并且会被errno接收。

匿名管道是一个纯内存级别的文件,没有相应的inode,也不会把缓冲区内容刷新到磁盘当中去。

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <sys/wait.h>
#include <cerrno>
using namespace std;

int main()
{
    
    
    int pipefd[2]={
    
    0};
    int ret=pipe(pipefd);
    if(ret==-1)
    {
    
    
        perror("创建匿名管道失败");
        return -1;
    }
    int id=fork();
    if(id==0)
    {
    
    
        //子进程
        close(pipefd[1]);//关闭子进程的写端
        char buff[1024]={
    
    0};
        while (true)
        {
    
    
            ssize_t num=read(pipefd[0],buff,sizeof(buff)-1);
            if(num>0)
            {
    
    
                //读取到了内容
                buff[num]=0;
                cout<<"子进程读取到的内容:"<<buff;
            }else{
    
    
                break;
            }
        }
        //关闭子进程的读端
        close(pipefd[0]);
        exit(0);//退出进程
    }
    //一定是父进程
    close(pipefd[0]);//关闭读端
    //进行内容的写入
    char message[]="hello Linux\n";
    int num=10;
    while(num--)
    {
    
    
        write(pipefd[1],message,sizeof(message));
        sleep(1);
    }
    //关闭写入端
    close(pipefd[1]);
    //回收子进程
    int res=waitpid(id,nullptr,0);
    if(res==-1)
    {
    
    
        cout<<"子进程回收失败"<<endl;
    }else
    {
    
    
        cout << "成功回收子进程,子进程的pid是" << res << endl;
    }
    return 0;
}

管道的特点

1.管道是半双工的一种特殊情况

  • 单工通信(Simplex Communication):单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。
  • 半双工通信(Half Duplex):半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。
  • 全双工通信(Full Duplex):全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。

2.管道的生命周期是随进程的

因为管道的本质还是文件,不过是内存级别的文件,而文件描述符fd的生命周期是随进程的,因此,管道的生命周期也是随进程的。

管道的四种特殊情况

  1. 如果read完了管道内的所有数据,如果写端没有继续写入数据,那么读取端就只能继续等待。
  2. 如果写端把管道写满了就不能继续写入了。
  3. 如果我关闭了写入端,读取完了管道内的数据,继续读就会返回0,表示读取到了文件的结尾。
  4. 写端一直写入,但是把读端关闭,操作系统会直接杀死一直在写入的进程,并且关闭管道,操作系统会通过信号来终止进程13)SIGPIPE
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    
    
	int fd[2] = {
    
     0 };
	if (pipe(fd) < 0){
    
     //使用pipe创建匿名管道
		perror("pipe");
		return 1;
	}
	pid_t id = fork(); //使用fork创建子进程
	if (id == 0){
    
    
		//child
		close(fd[0]); //子进程关闭读端
		//子进程向管道写入数据
		const char* msg = "hello father, I am child...";
		int count = 10;
		while (count--){
    
    
			write(fd[1], msg, strlen(msg));
			sleep(1);
		}
		close(fd[1]); //子进程写入完毕,关闭文件
		exit(0);
	}
	//father
	close(fd[1]); //父进程关闭写端
	close(fd[0]); //父进程直接关闭读端(导致子进程被操作系统杀掉)
	int status = 0;
	waitpid(id, &status, 0);
	printf("child get signal:%d\n", status & 0x7F); //打印子进程收到的信号
	return 0;
}

管道的最大容量为4096个字节。

多个匿名管道的控制

上面的代码都是一个父进程控制一个子进程,然后通过管道进行通信。

那么能否一个父进程控制10个20个子进程,然后创建10个或者20个管道进行通信呢?当然是可以的。

问题1:如何去管理这些管道呢?

答:可以创建一个vector数组,每一次创建管道就将它的读写端文件描述符写入数组当中。

这个数组可以设置为一个自定义类型,创建一个结构体,里面设置自己想要的数据。

process.cc文件

#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <unistd.h>
#include "Task.hpp"
#include<sys/types.h>
#include<sys/wait.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;
public:
    EndPoint(int id, int fd) : _child_id(id), _write_fd(fd)
    {
    
    
        //process-0[pid:fd]
        char namebuffer[64];
        snprintf(namebuffer, sizeof(namebuffer), "process-%d[%d:%d]", number++, _child_id, _write_fd);
        processname = namebuffer;
    }
    std::string name() const
    {
    
    
        return processname;
    }
    ~EndPoint()
    {
    
    
    }
};
int EndPoint::number = 0;

// 子进程要执行的方法
void WaitCommand(int fd)
{
    
    
    while (true)
    {
    
    
        int command = 0;
        int n = read(fd, &command, sizeof(int));
        if (n == sizeof(int))
        {
    
    
            t.Execute(command);
        }
        else if (n == 0)
        {
    
    
            break;
        }
        else
        {
    
    
            break;
        }
    }
}

void waitProcess(const vector<EndPoint>& end_points)
{
    
    
    for(auto& ep:end_points)
    {
    
    
        std::cout<<"父进程让子进程退出: "<<ep._child_id<<std::endl;
        close(ep._write_fd);//关闭写入端
        
        waitpid(ep._child_id,nullptr,0);//等待子进程并且进进行回收
        std::cout<<"父进程回收了子进程: "<<ep._child_id<<std::endl;
        sleep(5);
    }
}

void createProcesses(vector<EndPoint> *end_points)
{
    
    
    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);
        // 一定是子进程
        if (id == 0)
        {
    
    
            // 1.3 关闭不要的fd
            close(pipefd[1]);
            // 我们期望,所有的子进程读取"指令"的时候,都从标准输入读取
            // 1.3.1 输入重定向
            //dup2(pipefd[0], 0);           
            // 1.3.2 子进程开始等待获取命令
            WaitCommand(pipefd[0]);
            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(const vector<EndPoint> &end_points)
{
    
    
    // 2.1 我们可以写成自动化的,也可以搞成交互式的
    int num = 0;
    int cnt = 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::string name = end_points[index].name();
        std::cout<< " | 处理任务: " << command << std::endl;

        //3. 下发任务
        write(end_points[index]._write_fd, &command, sizeof(command));

        sleep(1);
    }
}

int main()
{
    
    
    // 1. 先进行构建控制结构, 父进程写入,子进程读取 , bug?
    vector<EndPoint> end_points;

    createProcesses(&end_points);
    // 2. 我们的得到了什么?end_points
    ctrlProcess(end_points);

    waitProcess(end_points);
    return 0;
}

task.hpp文件

#pragma once

#include <iostream>
#include <vector>
#include <unistd.h>
#include <unordered_map>

// typedef std::function<void ()> func_t;

typedef void (*fun_t)(); //函数指针

void PrintLog()
{
    
    
    std::cout << "pid: "<< getpid() << ", 打印日志任务,正在被执行..." << std::endl;
}

void InsertMySQL()
{
    
    
    std::cout << "执行数据库任务,正在被执行..." << std::endl;
}

void NetRequest()
{
    
    
    std::cout << "执行网络请求任务,正在被执行..." << std::endl;
}

// void ExitProcess()
// {
    
    
//     exit(0);
// }

//约定,每一个command都必须是4字节
#define COMMAND_LOG 0
#define COMMAND_MYSQL 1
#define COMMAND_REQEUST 2

class Task
{
    
    
public:
    Task()
    {
    
    
        funcs.push_back(PrintLog);
        funcs.push_back(InsertMySQL);
        funcs.push_back(NetRequest);
    }
    void Execute(int command)
    {
    
    
        if(command >= 0 && command < funcs.size()) funcs[command]();
    }
    ~Task()
    {
    
    }
public:
    std::vector<fun_t> funcs;
    // std::unordered_map<std::string, fun_t> funcs;
};

程序bug

但这段程序有一个bug,就是无法正常回收子进程。

那么这是为什么呢?

我们知道,如果关闭了管道的读端,那么操作系统会强制把子进程杀掉,并且关掉管道,那么是什么原因导致的操作系统没有杀掉子进程呢?

这是由于子进程会继承父进程的文件描述符表,子进程也会多出一份指向父进程读端的文件描述符,因此,父进程的读端就会被多次指向,即使关掉了父进程的读端,子进程还有一次指向,就会导致子进程杀不掉的原因。

那么如何进行解决呢?

解决方法一:可以从最后一个子进程开始关闭,因为最后一个子进程对应的管道的读端一定只会被指向一次,关掉读端就会杀掉子进程了,顺便也把子进程中指向父进程的读端也关掉了。

解决方法二:可以记录上一次创建管道的读端的文件描述符,然后在子进程中对被记录的所有的文件描述符进行关闭即可,这样就不会造成重复指向了。

为什么写端一直是3

由于每次父进程都关闭3,会导致再次开启管道时会使得3被重复开启,也就导致子进程每次都会以3为读端。

#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include "Task.hpp"
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;
public:
    EndPoint(int id, int fd) : _child_id(id), _write_fd(fd)
    {
    
    
        //process-0[pid:fd]
        char namebuffer[64];
        snprintf(namebuffer, sizeof(namebuffer), "process-%d[%d:%d]", number++, _child_id, _write_fd);
        processname = namebuffer;
    }
    std::string name() const
    {
    
    
        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 createProcesses(vector<EndPoint> *end_points)
{
    
    
    vector<int> fds;
    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);
        // 一定是子进程
        if (id == 0)
        {
    
    
            for(auto &fd : fds) close(fd);

            // std::cout << getpid() << " 子进程关闭父进程对应的写端:";
            // for(auto &fd : fds)
            // {
    
    
            //     std::cout << fd << " ";
            //     close(fd);
            // }
            // std::cout << std::endl;
            
            // 1.3 关闭不要的fd
            close(pipefd[1]);
            // 我们期望,所有的子进程读取"指令"的时候,都从标准输入读取
            // 1.3.1 输入重定向,可以不做
            dup2(pipefd[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]));

        fds.push_back(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(const vector<EndPoint> &end_points)
{
    
    
    // 2.1 我们可以写成自动化的,也可以搞成交互式的
    int num = 0;
    int cnt = 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::string name = end_points[index].name();
        std::cout << "选择了进程: " <<  name << " | 处理任务: " << command << std::endl;

        //3. 下发任务
        write(end_points[index]._write_fd, &command, sizeof(command));

        sleep(1);
    }
}

void waitProcess(const vector<EndPoint> &end_points)
{
    
    
    // 1. 我们需要让子进程全部退出 --- 只需要让父进程关闭所有的write fd就可以了!
    // for(const auto &ep : end_points) 
    // for(int end = end_points.size() - 1; end >= 0; end--)
    for(int end = 0; end < end_points.size(); end++)
    {
    
    
        std::cout << "父进程让子进程退出:" << end_points[end]._child_id << std::endl;
        close(end_points[end]._write_fd);

        waitpid(end_points[end]._child_id, nullptr, 0);
        std::cout << "父进程回收了子进程:" << end_points[end]._child_id << std::endl;
    } 
    sleep(10);

    // 2. 父进程要回收子进程的僵尸状态
    // for(const auto &ep : end_points) waitpid(ep._child_id, nullptr, 0);
    // std::cout << "父进程回收了所有的子进程" << std::endl;
    // sleep(10);
}


// #define COMMAND_LOG 0
// #define COMMAND_MYSQL 1
// #define COMMAND_REQEUST 2
int main()
{
    
    
    vector<EndPoint> end_points;
    // 1. 先进行构建控制结构, 父进程写入,子进程读取 , bug?
    createProcesses(&end_points);

    // 2. 我们的得到了什么?end_points
    ctrlProcess(end_points);

    // 3. 处理所有的退出问题
    waitProcess(end_points);
    return 0;
}

猜你喜欢

转载自blog.csdn.net/2301_79516932/article/details/133436069