进程间通信 (匿名管道)

一、进程间通信的概念

        进程间通信是一个进程把自己的数据交给另一个进程,它可以帮助我们进行数据传输、资源共享、通知事件和进程控制。

        进程间通信的本质是让不同的进程看到同一份资源。因此,我们要有:

        1、交换数据的空间。2、这个空间不能由通信双方任意一方提供。(要有一个独立的空间)

二、匿名管道 

  1、匿名管道的基本使用

        基于文件的,让不同进程看到同一份资源的通信方式,叫做管道。

        匿名管道通常用于具有血缘关系的进程间进行通信。例如:父子进程间通信

        匿名管道就是通过系统调用创建出一份管道文件,然后给调用的进程返回读端、写端的文件描述符,然后创建子进程,子进程会继承父进程的相关属性信息,也可以拿到读端和写端,然后父子进程就可以进行通信了。

        例如父进程写,子进程读。只要父进程关闭读端,然后往写端写数据,子进程关闭写端,往读端读数据,就可以实现父子进程间的通信。

接口:

        参数:输出型参数,传入一个大小为2的int类型数组,就会返回读端和写端的文件描述符。  例如传入的数组名位pipefd,读端的文件描述符:pipefd[0],写端的就是pipefd[1]。

        使用pipe()创建匿名管道,读端和写端默认都是打开的,所以我们应该根据要求关闭读端或写端。

        返回值:成功返回 0;失败返回 -1,并设置错误码。  

示例代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

void mywrite(int wfd)
{
	char message[1024] = {0};
	int i = 1;
	while (1)
	{
		// 自定义设置写入的内容
		snprintf(message, sizeof(message), "send a message to father, mypid is : %d, i = %d\n", getpid(), i);
		++i;
		write(wfd, message, strlen(message));

        // 方便观察
        sleep(1);
	}
}

void myread(int rfd)
{
	char message[1024] = {0};

	while (1)
	{
        // 读
		ssize_t n = read(rfd, message, sizeof(message) - 1);
		message[n] = '\0';
		printf("%s", message);

        // 方便观察
        sleep(1);
	}
}

int main()
{
	// 子进程写,父进程读
	int pipefd[2] = {0};
	int pret = pipe(pipefd);
	if (pret < 0)
	{
		printf("create pipe fail, errno is %d, errinfo is %s\n", errno, strerror(errno));
		return errno;
	}
	pid_t id = fork();
	if (id == 0)
	{
		// 子进程关闭读端
		close(pipefd[0]);
		mywrite(pipefd[1]);
		close(pipefd[1]);
		exit(0);
	}
	// 父进程关闭写端
	close(pipefd[1]);

	myread(pipefd[0]);

	close(pipefd[0]);
	// 等待,防止僵尸
	wait(NULL);

	return 0;
}

        可以看到子进程不断写,父进程不断读,并打印。 

        小细节:pipe()函数必须在 fork 之前,因为如果 fork 之后再创建管道,就是父子进程都会创建管道,父子进程拿不到同一份管道资源,就无法进行通信。

  2、进程池

        我们可以利用匿名管道,让父进程给多个子进程派发任务,也就是父进程写任务,然后多个子进程读任务。

创建多个子进程,并用read使它们阻塞,等待父进程派发任务

// 创建 sp_num 个子进程
void CreateSubProcess(int sp_num, vector<ChildP> &ChildPs)
{
    for (int i = 0; i < sp_num; ++i)
    {
        // 创建管道
        int pipefd[2] = {0};
        pipe(pipefd);

        pid_t id = fork();
        if (id < 0)
        {
            // 创建子进程失败
            printf("fork fail, errno is %d, errstr is %s\n", errno, strerror(errno));
        }
        else if (id == 0)
        {
            // 子进程读取任务
            // 关闭写端
            close(pipefd[1]);

            // 读
            ReadTask(pipefd[0], getpid());

            exit(0);
        }
        // 父进程派发任务,关闭读端
        close(pipefd[0]);
        // 父进程需要记录每个父进程的写端wfd。为了方便查看,顺便记录名字和pid
        string name = "process " + to_string(i);
        ChildPs.push_back(ChildP(pipefd[1], id, name));
    }
}

 读任务函数

void ReadTask(int rfd, int pid)
{
    while (true)
    {
        char buffer[200];
        ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n] = '\0';
            printf("子进程: %d 正在执行:> %s\n", pid, buffer);
        }
        // 写端关闭,读端读到0表示结束
        else if (n == 0)
        {
            printf("子进程: %d 退出...\n", pid);
            break;
        }
        // n < 0表示出错
        else
        {
            printf("read fail, errno is %d, errstr is %s\n", errno, strerror(errno));
            return;
        }

        sleep(1);
    }
}

记录子进程相关信息的类

class ChildP
{
public:
    ChildP(int wfd, pid_t pid, const string &name)
        : _wfd(wfd), _pid(pid), _name(name)
    {
    }

    int getwfd() { return _wfd; }
    pid_t getpid() { return _pid; }
    string getname() { return _name; }

private:
    int _wfd;     // 父进程的写端
    pid_t _pid;   // 子进程的pid
    string _name; // 子进程的名字
};

不断往不同的子进程派送任务

void WriteTask(ChildP &cp)
{
    char buffer[1024];
    static int i = 1;
    snprintf(buffer, sizeof(buffer) - 1, "Task %d", i);
    ++i;

    write(cp.getwfd(), buffer, strlen(buffer));

    // 打印确认信息
    cout << "Aleady Send a Task to " << cp.getname() << " ,pid is " << cp.getpid() << endl;
}

// 发送 TaskNum 个任务
void SendTask(vector<ChildP> &ChildPs, int sp_num, int TaskNum)
{
    // PNode 表示子进程在数组内的编号,为 0 - (sp_num-1)
    int PNode = 0;
    while (TaskNum--)
    {
        // 指派指定的子进程执行任务
        WriteTask(ChildPs[PNode]);
        sleep(1);
        PNode = (PNode + 1) % sp_num;
    }
}

主函数:

int main()
{
    int sp_num = 5;
    vector<ChildP> ChildPs;

    CreateSubProcess(sp_num, ChildPs);

    int TaskNum = 7;

    SendTask(ChildPs, sp_num, TaskNum);

    for(auto& cp : ChildPs)
    {
        // 关闭写端
        close(cp.getwfd());
    }

    for(auto& cp : ChildPs)
    {
        // 阻塞式等待
        waitpid(cp.getpid(), nullptr, 0);
        cout << "wait successfully: " << cp.getname() << " ,pid is " << cp.getpid() << endl;
    }

    return 0;
}

运行结果:

文件描述符关闭时要注意的问题:

        按照上面的代码,有多个子进程时,当我们关闭第一个子进程的写端时,正常来说写端关闭,读端就会读到0退出,但第一个子进程并不会退出。为什么呢?这是因为其他子进程还有该管道的写端并且没关。

        其他子进程的为什么会有第一个子进程的写端呢?

        因为在父进程创建第一个子进程后,只关闭了读端,因此,到创建第二个子进程时,子进程继承了父进程的写端,所以子进程2不仅打开了自己的读端,还打开了子进程1的写端。由此类推,子进程3打开了子进程1和子进程2的写端以及自己的读端 ......因此,当最后一个子进程的写端关闭时,才能一步步回退,把所有子进程关闭。

 

        由于这种问题的存在,当我们只想结束某一个子进程时,如果该子进程不是最后一个,那就会出错。

        因此,我们可以做出改进:在创建子进程时,保存父进程的写端,然后在创建新的子进程后关闭。

改进后的创建子进程代码:

void CreateSubProcess(int sp_num, vector<ChildP> &ChildPs)
{
    // 记录父进程的写端
    vector<int> f_wfd;
    for (int i = 0; i < sp_num; ++i)
    {
        // 创建管道
        int pipefd[2] = {0};
        pipe(pipefd);

        pid_t id = fork();
        if (id < 0)
        {
            // 创建子进程失败
            printf("fork fail, errno is %d, errstr is %s\n", errno, strerror(errno));
        }
        else if (id == 0)
        {
            // 关闭父进程指向其他管道的写端
            for(int e : f_wfd)
            {
                close(e);
            }

            // 子进程读取任务
            // 关闭写端
            close(pipefd[1]);

            // 读
            ReadTask(pipefd[0], getpid());

            exit(0);
        }
        // 父进程派发任务,关闭读端
        close(pipefd[0]);
        // 父进程需要记录每个父进程的写端wfd。为了方便查看,顺便记录名字和pid
        string name = "process " + to_string(i);
        ChildPs.push_back(ChildP(pipefd[1], id, name));
        f_wfd.push_back(pipefd[1]);
    }
}

感谢大家观看!

  • 30
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值