Linux创建多个管道,通过多管道来控制多进程中的问题

引言

        最近学习到Linux使用匿名管道进行进程间通信的相关内容,准备写点代码来巩固这部分知识,同时巩固一下文件描述符和进程控制方面的内容。大概的框架就是让进程创建一批管道和子进程,使用这一批管道来控制子进程。父进程向管道写内容下发任务,子进程读取管道内容执行父进程下发的任务。在代码中我使用for循环创建了5个匿名管道,对应5个子进程。在最后回收子进程的时候通过关闭子进程中管道对应的文件描述符的读写端(父进程关闭管道写端,子进程读到0,就跳出执行任务,然后子进程关闭自己对应管道的读端),然后通过父进程waitpid()进行资源回收。由于有多个子进程,所以继续使用了循环来回收所有子进程。在资源回收时出现了bug,无法回收相关资源(为简化代码,以下所有代码均未进行子进程的回收,只体现了创建理想多管道控制多进程模型的代码)。作为新手将该问题以博客的方式记录下来,方便后续知识巩固。文中或有错误还请各位不吝指正。

问题原因分析

        我的目的是创建多个管道对应多个进程来控制对应进程。在父进程的文件描述符表中存放所有管道的写端文件描述符,而每个子进程的文件描述符表中存放对应管道的读端文件描述。其结构示意图如下图所示(此处只列出了管道相关的文件描述符,标准输入、输出、错误对应的文件描述符没有列出):

图1. 多管道控制多进程的理想模型 

#include <iostream>
#include <unistd.h>
#include <cassert>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    //让父进程写入,子进程读取
	int i = 0; 
    for(i = 1; i <= 5; i++)
    {
        int fd[2] = {0};
        //创建管道
        pipe(fd);
        //创建子进程
        pid_t id = fork();
        assert(id >= 0);
        if(id == 0)
        {
        //子进程
            //关闭不需要的管道读写端
            close(fd[1]);
            printf("我是%d号子进程,pid:%d,ppid:%d,我已被成功创建!\n", i, getpid(), getppid());
            while(1)
            {
                //执行目标任务
            }
            close(fd[0]);
            exit(0);
        }
        //父进程
            //关闭不需要的管道读写端
        close(fd[0]);
        sleep(1);
    }
    while(1)
    {
        //下发任务
        std::cout << "我是父进程,pid:" << getpid() << ",所有管道创建完毕,开始下发任务......" << std::endl;
        sleep(10);
    }
    //回收子进程
	return 0;
}

 图2. ctrl_process相关进程

 图3. 运行结果

        从图2和图3可知for循环确实成功创建出了对应的管道和进程 。

 图4. 各进程对应的文件描述符表

         但是在我打印出如图4所示各进程的文件描述符表(第1个为父进程的文件描述符表,下面接着一次是子进程1-5的文件描述符表)之后得出了结论。我忽略了一个非常重要的问题。在创建子进程时,子进程会继承父进程的文件描述符表,所以在第一次创建完成子进程,并且建立好管道通信之后,第二次for循环创建子进程时,子进程继承了父进程的文件描述符表,故子进程2的文件描述符表会有一个指向管道1写端的文件描述符,按照这样下去,越靠后创建的子进程,对应文件描述符表中就会有越多的文件描述符,并且这些文件描述符指向前面创建管道的写端,故我们通过for循环创建出的多管道控制多进程的实际模型如下图5所示:

 图5. 通过for循环创建的多管道控制多进程的实际模型

        由代码可得,每次是先在父进程创建管道,管道创建完成后再创建子进程,进入子进程首先关闭对应管道写端,然后在父进程中关闭对应管道的读端。根据文件描述符的分配规则,父进程先创建管道,故父进程对应的3、4号文件描述符对应管道1的读写端,然后创建子进程1,进入子进程1后,子进程1继承父进程的文件描述符表,故子进程1的3、4号文件描述符对应管道1的读写端。而后子进程1关闭了管道1的写端,故子进程1的4号文件描述符空出来了。在父进程中关闭了管道1的读端,故父进程的3号文件描述符空了出来。根据文件描述符由空闲文件描述符从小到大的分配规则,创建管道2时,管道2读端分配给父进程的3号文件描述符,写端分配给父进程的5号文件描述符。接着创建子进程2,子进程2继承父进程的文件描述符表,接着子进程2关闭管道2的写端,故子进程的5号文件描述符空了出来。在父进程中关闭了管道2的读端,故父进程的3号文件描述符空了出来。以此类推得出图5中代码实际创建的多管道控制多进程的结构。

        因此我们在使用在父进程关闭对应管道写端,子进程判断读管道内容是否返回0,返回0就跳出循环,结束子进程的方法就无法正常回收子进程,因为有多个进程的文件描述符指向同一个管道的写端,要将所有指向对应管道写端的文件描述符全部关闭才可以让管道的写端真正关闭,才能回收资源。

        那么我们如何解决这个问题呢?基于我们实际创建出的图5的模型,我们可以倒着回收资源就可以了,但这种方法有点取巧,我们回收资源不是最终目的。我们的最终目的是要创建图1那样的理想模型。

        在这种情况下,我们如何创建出图1那样的理想模型呢?我们可以发现所有进程包括子进程,它们所指向的同一个管道写端的文件描述符是一样的。那么,我们可以将父进程所指向的管道写端文件描述符放在一个vector中管理起来,在船舰子进程之后,在子进程最开始将所有从父进程那里继承来的指向管道写端的文件描述符关闭。这样就可以创建出图1,那样的理想模型了。我们来看代码和运行结果。

#include <iostream>
#include <unistd.h>
#include <cassert>
#include <stdio.h>
#include <stdlib.h>
#include <vector>

using namespace std;

int main()
{
    std::vector<int> fds;
    //让父进程写入,子进程读取
	int i = 0;
    
    for(i = 1; i <= 5; i++)
    {
        int pipefd[2] = {0};
        //创建管道
        pipe(pipefd);
        //创建子进程
        pid_t id = fork();
        assert(id >= 0);
        if(id == 0)
        {
        //子进程
            //关闭不需要的管道读写端
            for(auto fd : fds) close(fd);
            close(pipefd[1]);
            printf("我是%d号子进程,pid:%d,ppid:%d,我已被成功创建!\n", i, getpid(), getppid());
            while(1)
            {
                //执行目标任务
            }
            close(pipefd[0]);
            exit(0);
        }
        //父进程
            //关闭不需要的管道读写端
        close(pipefd[0]);
        fds.push_back(pipefd[1]);//将父进程指向管道写端的文件描述符放入fds管理起来
        sleep(1);
    }
    while(1)
    {
        //下发任务
        std::cout << "我是父进程,pid:" << getpid() << ",所有管道创建完毕,开始下发任务......" << std::endl;
        sleep(10);
    }
    //回收子进程  
	return 0;
}

 图6. ctrl_process相关进程(改进代码后)

 图7. 各进程对应的文件描述符表(改进代码后)

        由上图可见理想的多管道控制多进程的理想模型建立完成。值得一提的是上述方法让我们很迷惑,子进程在继承父进程的文件描述符表的时候除了要指向同一个文件,还要有一摸一样的文件描述符。那么如果子进程有其他打开的文件呢,根据文件描述符的分配规则,从空闲的文件描述符中由小到大分配,那么子进程如果有其他打开的文件那继承的文件描述的值不就可能会不一样了么。那就要从创建子进程的时候说起了,创建子进程时,内核会创建子进程的PCB,这个PCB包含了进程的各种属性,其中有一部分属性是子进程独有的,如PID等,其他大部分属性是从父进程那里继承来的。也就是说从子进程创建开始内核就开始维护子进程的PCB了,而PCB中包含了进程对应的文件描述符表,即子进程在创建后立即继承了父进程的文件描述符表,这时候不可能在子进程中有其他文件在这之前被打开,打开的其他文件一定是在子进程继承父进程的文件描述符表之后打开的。根据文件描述符的分配规则,子进程从父进程继承过来的文件描述符表中指向同一文件的文件描述符一定是相同的。

结论

        子进程从父进程那里继承的文件描述符表中指向同一文件的文件描述符的值一定是一样的,并且这些从父进程那里继承来指向同一文件的文件描述符一定在文件描述符表的前面部分。当子进程创建完成后,父子进程再打开同一个文件所分配到的文件描述符就不一定一样了,这是因为这时父子进程的文件描述符表是独立的,在这期间父子进程可能打开或关闭其中的文件,根据文件描述符的分配规则,父子进程打开的同一文件的文件描述符可能就不一样了,但是它们都是指向同一个文件的。

        文件描述符的分配规则是从文件描述符表中空闲的位置的编号中从小到大分配。在两个进程中会出现指向同一个打开文件的文件描述符不一样的情况,也会出现指向同一个文件的文件描述符一样的情况,要根据具体情况来分析。

        文件描述符表属于进程的属性,不属于数据,所以子进程从父进程那里继承文件描述符表是立即从父进程中拷贝文件描述符表到子进程,而不是写时拷贝。

        文件描述符实质是数组的下标,正常情况下进程会默认会打开0、1、2号文件描述符对应的标准输入、输出、错误。

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值