Bootstrap

Linux——进程间通信之管道

进程间通信之管道

在这里插入图片描述

1. 进程间通信

1.1 为什么要进行进程间的通信

对于这个问题,答案显而易见:一个进程必然不能解决所有的问题,系统中往往需要多个进程的协作来进行工作,而进程间的协作就需要进程之间进行信息的交互,这个过程也叫做进程间的通信

简单来说,进程间的通信可以实现以下功能:

  • 数据传输
  • 进程控制
  • 资源共享
  • 事件通知
  • ………………

1.2 如何进行进程间的通信

由于进程具有独立性,因此两个进程之间不能直接进行数据之间的传输(否则就会破坏进程的独立性)

因此我们需要一片公共的区域来供进程之间进行信息交流,而这片公共区域就是由OS操作系统提供

根据上面的分析,我们也可以得出关于进程间通信的本质:

进程间通信的实质上就是:让不同的进程看到同一份资源

1.3 进程间通信的方式

根据操作系统OS提供公共区域方式的不同,进程间的通信也会有不同的形式,例如:

  • 管道
  • 共享内存
  • 信号量
  • 消息队列

2. 管道

我们来假设这样一个场景:

一个进程同时以读写的方式打开一个文件,那么就会产生两个文件描述符fd:

在这里插入图片描述

此时,创建子进程,子进程会继承父进程属性:

在这里插入图片描述

我们可以发现,这时子进程和父进程就同时看到同一份文件file了。这时,如果我们关闭父进程的写端,再关闭子进程的读端:

在这里插入图片描述

这样,就可以通过子进程向公共文件写数据,父进程向公共文件读数据的方式,进行父子进程之间的通信了。


向上面这样的,基于文件的形式进行进程间通信的方式,叫做管道

通过上面的例子,我们也知道:管道只允许一端读,一端写,因此基于管道的通信都是单向的

管道可以分为匿名管道和命名管道

2.1 匿名管道

2.1.1 系统调用pipe()

系统不允许用磁盘文件当作进程间通信的公共资源,为此系统提供了系统调用pipe()来为我们创建进程通信需要的文件:

#include <unistd.h>
int pipe(int pipefd[2]);
  • 该系统调用会创建并以读写方式打开一个不存在于磁盘且不需要向磁盘刷新的,只存在于内存中的匿名文件(因为这个文件只需用来给两个进程进行通信,不需要持久的保存),这个文件就叫做匿名管道
  • 数组pipefd是一个输出型参数,用于存放匿名管道的fd,其中pipefd[0]存放的是读端;pipefd[1]存放的是写端

2.1.2 使用匿名管道进行通信

使用pipe()创建了系统调用后,我们再创建一个子进程,让子进程继承父进程的数据,此时父子进程就会同时以读写的方式指向同一个匿名管道了。最后只要确定数据传输方向,关闭父进程的读(写)端,关闭子进程的写(读)端,就可以正常进程进程间的通信

同时,我们应该清楚:

由于是通过创建子进程的方式,来让子进程继承父进程的数据,使其指向相同的匿名管道来进行通信,因此匿名管道只能用来进行血缘进程之间的通信(通常用于父子进程)

示例:

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

void my_write(int wfd)	//写端操作
{
    const char* str = "i am child process: ";
    int cnt = 0;
    char buf[128] = {0};
    pid_t id = getpid();
    while (cnt != 20)
    {
        snprintf(buf, sizeof(buf), "message: %s, id: %d, cnt: %d\n", str, id, cnt++);
        write(wfd, buf, strlen(buf));
        sleep(1);
    }
}

void my_read(int rfd)	//读端操作
{
    char buf[1024] = {0};
    int cnt = 20;
    while(cnt--)
    {
        read(rfd, buf, sizeof(buf) - 1); //-1:预留'\0',防止出现错误
        printf("%s\n", buf);
    }
}

int main()
{
    int pipefd[2] = {0};	
    int ret = pipe(pipefd);	//创建匿名管道
    if (-1 == ret)
    {
        perror("pipe:");
        return 1;
    }

    pid_t id = fork();
    if (0 == id)	//子进程
    {
        //chile(w)
        close(pipefd[0]);	//关闭读端
        my_write(pipefd[1]);	//进行写操作
        exit(-1);
    }

    //father(r)
    close(pipefd[1]);	//关闭写端
    my_read(pipefd[0]);	//进行读操作

    wait(NULL);	//等待子进程退出,防止出现僵尸进程

    return 0;
}

效果如图所示:

在这里插入图片描述

2.1.1 匿名管道四种情况

情况一:写端停止写入并且管道没有数据,那么读端就会阻塞,直到读到数据

例如,将上述代码的my_write()函数修改:

void my_write(int wfd)
{
	//写端不写
}

重新编译后,运行效果如图:

在这里插入图片描述

可以看到,如果写端不向读端写入数据,并且管道没有数据,读端就会陷入阻塞等待状态

情况二:写端一直在写直到管道写满,读端不读,那么写端就会阻塞等待,直到管道的数据被读取

例如,将上述代码的my_write()、my_read()函数修改:

void my_write(int wfd)
{
 int cnt = 0;
 char a = 'A';
 //每次只写入一个字符,并输出输入的字符个数
 while(1)
 {
     write(wfd, &a, 1);
     printf("cnt: %d\n", cnt++);
 }
}

void my_read(int rfd)
{
	//读端不读
}

重新编译后,运行效果如图:

在这里插入图片描述

可以看到:当读端不读时,写端向管道写入了65535 + 1个字符后,就进入了阻塞等待状态,这说明管道已经被填满了。同时也可以推出,在博主所用的系统中,默认的管道大小为65536Byte也就是64kB

如果我们再次修改代码,让读端每2秒读取一次管道:

void my_read(int rfd)
{
 char buf[1024] = {0};
 while(1)
 {
     sleep(2);
     int n = read(rfd, buf, sizeof(buf) - 1);
     if (0 == n)
         break;
     printf("%s\n", buf);
 }
}

重新编译后,运行效果如图:

在这里插入图片描述

可以总结:当管道的部分数据被读取后,写端有重新写入数据了

情况三:写端关闭,当读端读完管道的数据后,读端就会读到管道的末尾(相当于读到文件尾),自动关闭读端

例如,将上述代码的my_write()、my_read()函数修改:

void my_write(int wfd)
{
 const char* str = "i am child process: ";
 int cnt = 0;
 char buf[128] = {0};
 pid_t id = getpid();
 while (cnt != 5)
 {
     snprintf(buf, sizeof(buf), "message: %s, id: %d, cnt: %d\n", str, id, cnt++);
     write(wfd, buf, strlen(buf));
     sleep(1);
 }
}

void my_read(int rfd)
{
 char buf[1024] = {0};
 while(1)
 {
     int n = read(rfd, buf, sizeof(buf) - 1);
     if (0 == n)	//如果read的返回值为0,说明写端关闭了fd,读端读到了管道末尾
     {
         printf("no data be read, read exit\n");
         break;
     }

     printf("%s\n", buf);
 }
}

重新编译后,运行效果如图:

在这里插入图片描述

情况四:读端关闭,写端还在写,系统就会通过发送信号的方式强制终止写端(kill -13 child_pid)

将整体代码修改如图:

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

void my_write(int wfd)
{
 const char* str = "i am child process: ";
 char buf[128] = {0};
 pid_t id = getpid();
 while (1)
 {
     snprintf(buf, sizeof(buf), "message: %s, id: %d\n", str, id);
     write(wfd, buf, strlen(buf));
     sleep(1);
 }
}

void my_read(int rfd)
{
 char buf[1024] = {0};
 int cnt = 5;
 while(cnt--)
 {
     int n = read(rfd, buf, sizeof(buf) - 1);
     if (0 == n)
     {
         printf("no data be read, read exit\n");
         break;
     }
     printf("%s\n", buf);
 }
 printf("read will close\n");
 sleep(1);
 close(rfd);
}

int main()
{
 int pipefd[2] = {0};
 int ret = pipe(pipefd);
 if (-1 == ret)
 {
     perror("pipe:");
     return 1;
 }

 pid_t id = fork();
 if (0 == id)
 {
     //chile(w)
     close(pipefd[0]);
     my_write(pipefd[1]);

     printf("child close wfd\n\n");
     close(pipefd[1]);
     exit(-1);
 }

 //father(r)
 close(pipefd[1]);
 my_read(pipefd[0]);

 int status = 0;
 int rid = waitpid(id, &status, 0);
 if (rid == id)
 {
     printf("child process singal code: %d\n", status & 0x7f);
 }

 return 0;
}

重新编译后,运行效果如图:

在这里插入图片描述

2.1.2 匿名管道的五大特性

  • 同步机制:管道在处理数据读写时,确保数据的有序性和正确性的一种控制方式
  • 血缘进程通信:由于匿名管道的构建建立在进程继承的基础上,因此匿名管道只允许血缘进程的通信
  • 单向通信(半双工):同一时间,只允许一端读,一端写
  • 文件的生命周期是随进程的:父子进程退出,管道(文件)自动释放
  • pipe是面向字节流的

2.1.3 进程池

进程池的基本概念

进程池是一组预先创建好的进程集合,这些进程处于空闲等待状态,随时准备接收任务并进行处理。父进程可以在任意时候控制子进程的休眠、工作与退出。

进程池的优点

  • 可以复用进程,从而避免了频繁的调用系统函数,节省了资源开销与时间
  • 使用进程池有利于管理,并充分利用系统资源

实现进程池:

进程池程序myprocesspool的程序流程大致如下:

在这里插入图片描述

实现代码:

task.hpp:

#pragma once

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

typedef void(*work_t)(pid_t);
typedef void(*task_t)(pid_t);

void task_1(pid_t id)
{
 std::cout << "task_1" << std::endl;
}

void task_2(pid_t id)
{
 std::cout << "task_2" << std::endl;
}

void task_3(pid_t id)
{
 std::cout << "task_3" << std::endl;
}

task_t tasks[3] = {task_1, task_2, task_3};

void worker(int channel_index)
{
 while(1)
 {
     int task_index = 0;
     int n = read(0, &task_index, sizeof(int));
     if (0 == n)
     {
         std::cout << "write close, channel: " << channel_index << " closing…………" << std::endl; 
         break;
     }

     std::cout << "channel: " << channel_index << "  pid: " << getpid() << ": i am working: ";
     tasks[task_index](getpid());
 }
}

processpool.cc:

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctime>
#include "task.hpp"

#define TIME 5

//return value
enum
{
 USAGE_ERROR = 1,
 PROCESS_NUM_ERROR, 
 PIPE_ERROR,
 SELECT_CHANNEL_ERROR,
 SELEDT_TASK_ERROR
};

void usage()
{
 std::cout << "Usage: ./processpool [process_num]" << std::endl;
}

class channel
{
public:
 channel(std::string name, int rfd, int wfd, pid_t child_id)
 : _name(name), _rfd(rfd), _wfd(wfd), _child_id(child_id)
 {}

 const std::string& get_name()
 {
     return _name;
 }

 int get_rfd()
 {
     return _rfd;
 }

 int get_wfd()
 {
     return _wfd;
 }

 pid_t get_child_id()
 {
     return _child_id;
 }

 void close_fd()
 {
     close(_wfd);
 }
private:
 const std::string _name;
 const int _rfd;
 const int _wfd;
 const pid_t _child_id;
};

class processpool
{
public:
 processpool(int process_num)
 : _process_num(process_num)
 {}

 //创建管道
 int create_channel()
 {
     std::vector<int> fd;
     for (int i = 0; i < _process_num; i++)
     {
         int pipefd[2] = {0};
         int ret = pipe(pipefd);
         if (-1 == ret)
         {
             perror("pipe:");
             return PIPE_ERROR;
         }

         pid_t id = fork();
         if (id == 0)
         {
             //child: read
             close(pipefd[1]);
             dup2(pipefd[0], 0);

             //关闭除第一个子进程外的多个写端
             if (!fd.empty())
             {
                 for (auto k : fd)
                     close(k);
             }

             //work
             worker(i);

             std::cout << "channel " << i << " pid: " << getpid() <<  " exit" << std::endl;
             exit(-1);
         }

         //father: write
         close(pipefd[0]);

         std::string name = std::string("channel: ") + std::to_string(i);
         channel c = channel(name, pipefd[0], pipefd[1], id);
         _channels.push_back(c);

         fd.push_back(pipefd[1]);
     }

     return 0;
 }    

 int select_channel()
 {
     static int c = 0;
     int ret = c;
     c = (++c) % _process_num;
     return ret;
 }

 int select_task()
 {
     return rand() % 3;
 }

 int control_child()
 {
     int cnt = TIME;
     while(cnt--)
     {
         //选择一个进程(管道),一个任务
         int channel_index = select_channel();
         int task_index = select_task();
         if (channel_index >= _process_num)
         {
             std::cout << "select channel error" << std::endl;
             return SELECT_CHANNEL_ERROR;
         }
         if (task_index >= 3)
         {
             std::cout << "select task error" << std::endl;
             return SELEDT_TASK_ERROR;
         }

         //发送任务
         int wfd = _channels[channel_index].get_wfd();
         write(wfd, &task_index, sizeof(int));

         sleep(2);
     }

     return 0;
 }

 void PrintDebug()
 {
     for (auto channel: _channels)
     {
         std::cout << channel.get_name() << ": rfd: " << channel.get_rfd() << ": child_id: " << channel.get_child_id() << std::endl;
     }
 }

 void clean_wait()
 {
     //关闭所有的写端
     for (auto channel : _channels)
     {
         channel.close_fd();

         pid_t id = waitpid(channel.get_child_id(), nullptr, 0);
         if (-1 == id)
         {
             perror("waitpid:");
         }
         if(id == channel.get_child_id())
         {
             std::cout << "wait child: " << channel.get_child_id() << " success\n" << std::endl;
         }
     }
 }
private:
 const int _process_num;
 std::vector<channel> _channels;
};

//  ./process num
int main(int argc, char* argv[])
{
 if (1 == argc)
 {
     usage();
     return USAGE_ERROR;
 }

 int process_num = std::stoi(argv[1]);

 if (process_num <= 0)
 {
     std::cout << "process_num should be grearter than 0" << std::endl;
     return PROCESS_NUM_ERROR;
 }

 srand((unsigned int)time(nullptr));

 std::cout << "process nums: " << process_num << std::endl;

 //创建进程池
 processpool* processpool_1 = new processpool(process_num);

 //创建管道
 int ret = processpool_1->create_channel();   
 if (0 != ret)
 {
     return ret;
 }
 std::cout << "create channels complete" << std::endl; 

 //工作
 ret = processpool_1->control_child();
 if (0 != ret)
 {
     return ret;
 }

 //回收 
 processpool_1->clean_wait();
 return 0;
}

2.2 命名管道

上文提到,匿名管道只适用于血缘进程之间的通信,那么为了解决没有关系进程间通信的问题,操作系统提供了命名管道

2.2.1 创建命名管道

使用命令创建命名管道:

mkfifo [filename]

例如:

在这里插入图片描述

可以看到,通过命令mkfifo fifo在当前目录创建了一个名为fifo的管道文件,同时可以看到,这个管道文件的文件类型为p(pipe)

使用系统调用创建命名管道:

int mkfifo(const char *pathname, mode_t mode);
  • pathname是管道文件的文件名
  • mode是管道文件的权限
  • 返回值:成功返回0;失败返回-1

2.2.2 利用命名管道进行进程间通信

知道了系统调用后,我们就可以利用管道来进行进程间的通信了:

我们用下面的代码进行演示:

头文件:

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define PATH "FIFO"	//默认文件名
#define MODE 0666	//默认权限

//命名管道类
class fifo
{
public:
    fifo(const std::string& name)	//构造函数创建管道文件
    : _name(name)
    {
        int ret = mkfifo(_name.c_str(), MODE);
        if (-1 == ret)
        {
            std::cerr << "mkfifo error, " << "errno: " << errno << ", errorstring: " << strerror(errno) << std::endl;
            exit(-1);
        }
        std::cout << "fifo made success" << std::endl;
    }
    ~fifo()
    {
        unlink(_name.c_str());	//进程结束时,利用系统调用unlink()删除管道文件
    }
private:
    const std::string _name;
};

服务端server:读

#include "common.hpp"

int main()
{
    std::cout << "I am server" << std::endl;	

    fifo named_pipe(PATH);	//服务端创建管道文件,读端
    int rfd = open(PATH, O_RDONLY);	
    
    char buffer[1024] = {0};
    while (1)
    {
        int n = read(rfd, buffer, sizeof(buffer) - 1);
        if (0 == n)	//返回值为0,说明读到文件尾,即写端关闭
        {
            std::cout << "client exit, server also exit" << std::endl;
            break;
        }
        else if (-1 == n)
        {
            std::cerr << "read error, " << "errno: " << errno << ", errorstring: " << strerror(errno) << std::endl;
            exit(-1);
        }
        else
        {
            buffer[n] = 0;
            std::cout << buffer << std::endl;
        }
    }

    close(rfd);
    return 0;
}

客户端client:写

#include "common.hpp"

int main()
{
    std::cout << "I am client" << std::endl;

    int wfd = open(PATH, O_WRONLY);	//客户端,写

    std::string buffer;
    while(1)
    {
        printf("message # ");
        std::getline(std::cin, buffer);
        if ("quit" == buffer)
        {
            printf("client exit\n");
            break;
        }

        int n = write(wfd, buffer.c_str(), buffer.size());
        if (n != buffer.size())
        {
            std::cerr << "write error, " << "errno: " << errno << ", errorstring: " << strerror(errno) << std::endl;
            exit(-1);
        }
    }

    close(wfd);
    return 0;
}

效果如图:

在这里插入图片描述

2.2.3 命名管道的一个特性

如果我们在服务端server.cc代码中加一行代码:

#include "common.hpp"

int main()
{
    std::cout << "I am server" << std::endl;

    fifo named_pipe(PATH);
    int rfd = open(PATH, O_RDONLY);

    std::cout << "open success" << std::endl;	//查看客户端是否成功打开管道文件
    
    //……………………
    
}

并运行服务端(读端)一段时间后再打开客户端(写端):

在这里插入图片描述

可以看到:在服务端(读端)打开到客户端(写端)未打开的这段时间中,服务端(读端)并没有打开管道文件,而是等客户端(写端)启动后,再打开的管道。

通过这个现象,我们可以得出命名管道的一个特性:

当写端未打开而读端打开时,读端会阻塞,直至写端也打开


本篇完

;