【Linux】管道

目录

一、进程间通信

         1 .管道

         2 .父子进程实现通信

         3 . 匿名管道实例

         4 . 匿名管道进程池

         5 .命名管道

         6.命名管道实例

         7 .总结


进程间通信

什么是进程间通信?

每个进程各自有不同的用户地址空间(进程地址空间),任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要进行数据交换必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。

简单来说,进程间通信有以下作用:

数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止 时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变。

当然进程间通信有多种方式:

管道
 
System V 进程间通信
 
POSIX 进程间通信

今天主要说管道。 

         1 .管道

什么是管道
管道是 Unix 中最古老的进程间通信的形式。
 
我们把从一个进程连接到另一个进程的一个数据流称为一个 管道

匿名管道
#include <unistd.h>
 
功能 : 创建一个无名管道
 
原型
 
参数
 
fd :文件描述符数组 , 其中 fd[0] 表示读端 , fd[1] 表示写端
 
返回值 : 成功返回 0 ,失败返回错误代码

特征

1、管道的作用是在具有亲缘关系的进程之间传递消息,所谓有亲缘关系,是指有同一个祖先。

  
管道并不是只可以用于父子进程通信,也可以在兄弟进程之间还可以用在祖孙之间等,反正只要共同的祖先调用了pipe函数,打开的管道文件就会在fork之后,被各个后代所共享;

  
2、管道是字节流通信,没有消息边界,多个进程同时发送的字节流混在一起,则无法分辨消息,所有管道一般用于2个进程之间通信;

  
流:相当于水流,写入数据时,写多少字节和你自己有关系,读的时候没有 格式的要求,完全取决你自己;

  
字节流:以字节来读取和写入,字节数的大小完全取决于自己

  
3、管道的内容读完后不会保存;

  
4、管道是单向的,一边要么读,一边要么写,不可以又读又写,想要一边读一边写,那就创建2个管道

5、 管道是一种文件,可以调用read、write和close等操作文件的接口来操作管道。另一方面管道又不是一种普通的文件,它属于一种独特的文件系统:pipefs。

6、管道内部自带同步机制:子进程写一条,父进程读一条

7、当进程退出之时,管道也随之释放,与文件保持一致

         2 .父子进程实现通信

当父进程调用 pipe 函数时,他会产生下面的一个管道,这个时候管道的读写端已经加载到了文件描述符表(pipefd数组的下标0是读端,1是写端)这个时候也可以进行像管道中写入内容,但是这是自言自语,因为读写端都是自己。

然后父进程进行fork,在fork之后,子进程会继承父进程的内容和数据,会出现以下画面:

父进程的两个子进程之间如何利用管道通信?

父进程再次创建一个子进程B,子进程B就持有管道写入端,这时候两个子进程之间就可以通过管道通信了。

在fork之后,我们需要关闭父子进程各自不需要的文件描述符,这样就形成单一的管道了。

文件描述符视角理解管道:

内核角度理解管道:

所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了Linux一切皆文件思想

pipe函数使用及其注意点:

#include <unistd.h>
 int pipe(int pipefd[2]);

参数说明:

  • fd为文件描述符数组,其中fd[0]表示读端,fd[1] 表示写端

返回值:

  • 成功返回0,失败返回-1,并且设置errno。

注意点:

  • 1、管道内没有数据时,读端(read)发生阻塞,等待有效数据进行读取

2、管道容量被数据填满时,写端(write)发生阻塞,等待进程将数据读走再进行写入

3、如果所有管道写端对应的文件描述符被关闭read返回0,但会将之前管道里的数据读完

4、如果所有管道的读端对应的文件描述符被关闭write操作会产生信号,SIGPIPE,进而导致write进程退出

  • 5、当要写入的数据量不大于管道的容量(PIPE_BUF)时,linux将保证写入的原子性

  • 6、当要写入的数据量大于管道容量(PIPE_BUF)时,linux将不再保证写入的原子性

         3 . 匿名管道实例

这里写一个父子进程通信的实例代码:

#include <iostream>
#include <string>
#include <cerrno>  
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

// fork之后子进程是能拿到父进程的数据 但是他是写时拷贝,对方都看不到
// char buffer[1024]; // 创建的共享内存不能这样,他也会写时拷贝

const int size = 1024;  //全局设置大小 简化代码

std::string getOtherMessage()
{
    static int cnt = 0;
    std::string messageid = std::to_string(cnt); // stoi -> string -> int
    cnt++;
    pid_t self_id = getpid();
    std::string stringpid = std::to_string(self_id);

    std::string message = "messageid: ";
    message += messageid;
    message += " my pid is : ";
    message += stringpid;

    return message;
}

// 子进程进行写入
void SubProcessWrite(int wfd)
{
    int pipesize = 0;
    std::string message = "father, I am your son prcess!";
    char c = 'A';
    while (true)
    {
        std::cerr << "+++++++++++++++++++++++++++++++++" << std::endl;
        std::string info = message + getOtherMessage(); // 这条消息,就是我们子进程发给父进程的消息
        write(wfd, info.c_str(), info.size()); // 写入管道的时候,没有写入\0, 没有必要,因为\0是c语言规定的
        
        std::cerr << info << std::endl;
        
        // sleep(1); 
        // write(wfd, &c, 1);
        // std::cout << "pipesize: " << ++pipesize << " write charator is : "<< c++ << std::endl;
        // // if(c == 'G') break;

        // sleep(1);
    }

    std::cout << "child quit ..." << std::endl;
}

// 父进程进行读取
void FatherProcessRead(int rfd)
{
    char inbuffer[size]; 
    while (true)
    {
        sleep(2);
        std::cout << "-------------------------------------------" << std::endl;
        // sleep(500);
        ssize_t n = read(rfd, inbuffer, sizeof(inbuffer) - 1); // sizeof(inbuffer)->strlen(inbuffer);
        if (n > 0)
        {
            inbuffer[n] = 0; // == '\0'
            std::cout  << inbuffer << std::endl;
        }
        else if (n == 0)
        {
            // 如果read的返回值是0,表示写端直接关闭了,我们读到了文件的结尾
            std::cout << "client quit, father get return val: " << n << " father quit too!" << std::endl;
            break;
        }
        else if(n < 0)
        {
            std::cerr << "read error" << std::endl;
            break;
        }

        // sleep(1);
        // break;
    }
}

int main()
{
    // 1. 创建管道
    int pipefd[2];
    int n = pipe(pipefd); // 输出型参数,rfd, wfd
    if (n != 0)
    {
        std::cerr << "errno: " << errno << ": "
                  << "errstring : " << strerror(errno) << std::endl;
        return 1;
    }
    // pipefd[0]->0->r(嘴巴 - 读)  pipefd[1]->1->w(笔->写)
    std::cout << "pipefd[0]: " << pipefd[0] << ", pipefd[1]: " << pipefd[1] << std::endl;
    sleep(1);

    // 2. 创建子进程
    pid_t id = fork();
    if (id == 0)
    {
        std::cout << "子进程关闭不需要的fd了, 准备发消息了" << std::endl;
        sleep(1);
        // 子进程 --- write
        // 3. 关闭不需要的fd
        close(pipefd[0]);

        // if(fork() > 0) exit(0);

        SubProcessWrite(pipefd[1]);
        close(pipefd[1]);
        exit(0);
    }

    std::cout << "父进程关闭不需要的fd了, 准备收消息了" << std::endl;
    sleep(1);
    // 父进程 --- read
    // 3. 关闭不需要的fd
    close(pipefd[1]);
    FatherProcessRead(pipefd[0]);
    std::cout << "5s, father close rfd" << std::endl;
    sleep(5);
    close(pipefd[0]);

    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if (rid > 0)
    {
        std::cout << "wait child process done, exit sig: " << (status&0x7f) << std::endl;
        std::cout << "wait child process done, exit code(ign): " << ((status>>8)&0xFF) << std::endl;
    }
    return 0;
}

         4 . 匿名管道进程池

要将每一个进程管理起来,就需要先描述,再组织。我们可以先将每一个进程变成一个对象,然后用顺序表管理起来:

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


// master
class Channel
{
public:
    Channel(int wfd, pid_t id, const std::string &name)
        : _wfd(wfd), _subprocessid(id), _name(name)
    {
    }
    int GetWfd() { return _wfd; }
    pid_t GetProcessId() { return _subprocessid; }
    std::string GetName() { return _name; }
    void CloseChannel()
    {
        close(_wfd);
    }
    void Wait()
    {
        pid_t rid = waitpid(_subprocessid, nullptr, 0);
        if (rid > 0)
        {
            std::cout << "wait " << rid << " success" << std::endl;
        }
    }
    ~Channel()
    {
    }

private:
    int _wfd;
    pid_t _subprocessid;
    std::string _name;
};

// 形参类型和命名规范
// const &: 输出
// & : 输入输出型参数
// * : 输出型参数
//  task_t task: 回调函数
void CreateChannelAndSub(int num, std::vector<Channel> *channels, task_t task)
{
    // BUG? --> fix bug
    for (int i = 0; i < num; i++)
    {
        // 1. 创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        if (n < 0)
            exit(1);

        // 2. 创建子进程
        pid_t id = fork();
        if (id == 0)
        {
            if (!channels->empty())//将多余的读端关闭
            {
                // 第二次之后,开始创建的管道
                for(auto &channel : *channels) channel.CloseChannel();
            }
            // child - read
            close(pipefd[1]);
            dup2(pipefd[0], 0); // 将管道的读端,重定向到标准输入
            task();
            close(pipefd[0]);
            exit(0);
        }

        // 3.构建一个channel名称
        std::string channel_name = "Channel-" + std::to_string(i);
        // 父进程
        close(pipefd[0]);
        // a. 子进程的pid b. 父进程关心的管道的w端
        channels->push_back(Channel(pipefd[1], id, channel_name));
    }
}

// 0 1 2 3 4 channelnum
int NextChannel(int channelnum)//实现了进程的轮询使用
{
    static int next = 0;
    int channel = next;
    next++;
    next %= channelnum;
    return channel;
}

void SendTaskCommand(Channel &channel, int taskcommand)
{
    write(channel.GetWfd(), &taskcommand, sizeof(taskcommand));
}
void ctrlProcessOnce(std::vector<Channel> &channels)
{
    sleep(1);
    // a. 选择一个任务
    int taskcommand = SelectTask();
    // b. 选择一个信道和进程
    int channel_index = NextChannel(channels.size());
    // c. 发送任务
    SendTaskCommand(channels[channel_index], taskcommand);
    std::cout << std::endl;
    std::cout << "taskcommand: " << taskcommand << " channel: "
              << channels[channel_index].GetName() << " sub process: " << channels[channel_index].GetProcessId() << std::endl;
}
void ctrlProcess(std::vector<Channel> &channels, int times = -1)//控制执行次数
{
    if (times > 0)
    {
        while (times--)
        {
            ctrlProcessOnce(channels);
        }
    }
    else
    {
        while (true)
        {
            ctrlProcessOnce(channels);
        }
    }
}

void CleanUpChannel(std::vector<Channel> &channels)
{
    // int num = channels.size() -1;
    // while(num >= 0)
    // {
    //     channels[num].CloseChannel();
    //     channels[num--].Wait();
    // }

    for (auto &channel : channels)
    {
        channel.CloseChannel();
        channel.Wait();
    }
    // // 注意
    // for (auto &channel : channels)
    // {
    //     channel.Wait();
    // }
}

// ./processpool 5
int main(int argc, char *argv[])
{
    if (argc != 2)//判断传入参数个数是否有问题
    {
        std::cerr << "Usage: " << argv[0] << " processnum" << std::endl;
        return 1;
    }
    int num = std::stoi(argv[1]);
    LoadTask();//加载任务

    std::vector<Channel> channels;
    // 1. 创建信道和子进程
    CreateChannelAndSub(num, &channels, work1);

    // 2. 通过channel控制子进程
    ctrlProcess(channels, 5);

    // 3. 回收管道和子进程. a. 关闭所有的写端 b. 回收子进程
    CleanUpChannel(channels);

    return 0;
}

这里模拟几个任务。注意,这里使用了回调函数和dup2,将读端和写段重定向到了0——标准输入重定向。这样一来,就实现了程序的解耦。我们可以将任何工作内容做成函数,用回调函数的方式进行访问。 

#pragma once

#include <iostream>
#include <ctime>
#include <cstdlib>
#include <sys/types.h>
#include <unistd.h>

#define TaskNum 3

typedef void (*task_t)(); // task_t 函数指针类型

void Print()
{
    std::cout << "I am print task" << std::endl;
}
void DownLoad()
{
    std::cout << "I am a download task" << std::endl;
}
void Flush()
{
    std::cout << "I am a flush task" << std::endl;
}

task_t tasks[TaskNum];

void LoadTask()
{
    srand(time(nullptr) ^ getpid() ^ 17777);
    tasks[0] = Print;
    tasks[1] = DownLoad;
    tasks[2] = Flush;
}

void ExcuteTask(int number)
{
    if (number < 0 || number > 2)
        return;
    tasks[number]();
}

int SelectTask()
{
    return rand() % TaskNum;
}

void work()
{
    while (true)
    {
        int command = 0;
        int n = read(0, &command, sizeof(command));
        if (n == sizeof(int))
        {
            std::cout << "pid is : " << getpid() << " handler task" << std::endl;
            ExcuteTask(command);
        }
        else if (n == 0)
        {
            std::cout << "sub process : " << getpid() << " quit" << std::endl;
            break;
        }
    }
}

void work1()
{
    while (true)
    {
        int command = 0;
        int n = read(0, &command, sizeof(command));
        if (n == sizeof(int))
        {
            std::cout << "pid is : " << getpid() << " handler task" << std::endl;
            ExcuteTask(command);
        }
        else if (n == 0)
        {
            std::cout << "sub process : " << getpid() << " quit" << std::endl;
            break;
        }
    }
}

void work2()
{
    while (true)
    {
        int command = 0;
        int n = read(0, &command, sizeof(command));
        if (n == sizeof(int))
        {
            std::cout << "pid is : " << getpid() << " handler task" << std::endl;
            ExcuteTask(command);
        }
        else if (n == 0)
        {
            std::cout << "sub process : " << getpid() << " quit" << std::endl;
            break;
        }
    }
}

         5 .命名管道

匿名管道与命名管道的区别
匿名管道由 pipe 函数创建并打开。
   
命名管道由 mkfifo 函数创建,打开用 open
  
FIFO (命名管道)与 pipe (匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完 成之后,它们具有相同的语义。

不同于匿名管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存储于文件系统中。

他相当于是双方都到约定的一个文件中去存取数据(记录了文件的地址),而匿名管道是继承同一个文件的地址,去存取东西。 

  • 命名管道是一个文件,因此,即使进程与创建FIFO的进程不存在亲缘关系,只要可以访问该路径,就能够通过FIFO相互通信。
  • 值得注意的是,FIFO(first input first output)总是按照先进先出的原则工作,第一个被写⼊的数据将首先从管道中读出。

特征:

  • 1、 可以进行不相干进程间的通信
  • 2.、命名管道是一个文件,对于文件的相关操作对其同样适用
  • 3、 对于管道文件,当前进程操作为只读时,则进行阻塞,直至有进程对其写入数据
  • 4、 对于管道文件,当前进程操作为只写时,则进行阻塞,直至有进程从管道中读取数据

使用mkfifo函数:

命名管道和管道的使用方法法基本是相同的。

  • 只是使用命名管道时,必须先调用open()将其打开。因为命名管道是一个存在于硬盘上的文件,而管道是存在于内存中的特殊文件。

需要注意的是,调用open()打开命名管道的进程可能会被阻塞。

  • 但如果同时用读写方式( O_RDWR)打开,则一定不会导致阻塞;
  • 如果以只读方式( O_RDONLY)打开,则调用open()函数的进程将会被阻塞直到有写方打开管道;
  • 同样以写方式( O_WRONLY)打开也会阻塞直到有读方式打开管道。

         6.命名管道实例

命名管道是两个任意进程进程通信的管道,他不局限于同一个进程。这里我们可以将封装为一个类,这样打开关闭,只需要创建对象就行了。

#pragma once

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

const std::string comm_path = "./myfifo";
#define DefaultFd -1
#define Creater 1
#define User 2
#define Read O_RDONLY
#define Write O_WRONLY
#define BaseSize 4096

class NamePiped
{
private:
    bool OpenNamedPipe(int mode)
    {
        _fd = open(_fifo_path.c_str(), mode);
        if (_fd < 0)
            return false;
        return true;
    }

public:
    NamePiped(const std::string &path, int who)
        : _fifo_path(path), _id(who), _fd(DefaultFd)
    {
        if (_id == Creater)
        {
            int res = mkfifo(_fifo_path.c_str(), 0666);
            if (res != 0)
            {
                perror("mkfifo");
            }
            std::cout << "creater create named pipe" << std::endl;
        }
    }
    bool OpenForRead()
    {
        return OpenNamedPipe(Read);
    }
    bool OpenForWrite()
    {
        return OpenNamedPipe(Write);
    }

    int ReadNamedPipe(std::string *out)
    {
        char buffer[BaseSize];
        int n = read(_fd, buffer, sizeof(buffer));
        if(n > 0)
        {
            buffer[n] = 0;
            *out = buffer;
        }
        return n;
    }
    int WriteNamedPipe(const std::string &in)
    {
        return write(_fd, in.c_str(), in.size());
    }
    ~NamePiped()
    {
        if (_id == Creater)
        {
            int res = unlink(_fifo_path.c_str());
            if (res != 0)
            {
                perror("unlink");
            }
            std::cout << "creater free named pipe" << std::endl;
        }
        if(_fd != DefaultFd) close(_fd);
    }

private:
    const std::string _fifo_path;
    int _id;
    int _fd;
};

server.cc

#include "namedPipe.hpp"

// server read: 管理命名管道的整个生命周期
int main()
{
    NamePiped fifo(comm_path, Creater);
    // 对于读端而言,如果我们打开文件,但是写还没来,我会阻塞在open调用中,直到对方打开
    // 进程同步
    if (fifo.OpenForRead())
    {
        std::cout << "server open named pipe done" << std::endl;

        sleep(3);
        while (true)
        {
            std::string message;
            int n = fifo.ReadNamedPipe(&message);
            if (n > 0)
            {
                std::cout << "Client Say> " << message << std::endl;
            }
            else if(n == 0)
            {
                std::cout << "Client quit, Server Too!" << std::endl;
                break;
            }
            else
            {
                std::cout << "fifo.ReadNamedPipe Error" << std::endl;
                break;
            }
        }
    }

    return 0;
}

client.c

#include "namedPipe.hpp"

// write
int main()
{
    NamePiped fifo(comm_path, User);
    if (fifo.OpenForWrite())
    {
        std::cout << "client open namd pipe done" << std::endl;
        while (true)
        {
            std::cout << "Please Enter> ";
            std::string message;
            std::getline(std::cin, message);
            fifo.WriteNamedPipe(message);
        }
    }

    return 0;
}

形成两个可执行文件:

gcc -o client client.c ./client

gcc -o server server.c ./server

         7 .总结

题目:

题目:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李 四

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值