Linux 进程间通信 匿名管道_命名管道

1.程间通信目的


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

2.匿名管道

1.原理

 上图表示进程打开了一个文件,每一个进程都有一个文件描述符表struct files_struct,

根据文件的路径在磁盘中找到文件的属性和内容,建立文件内核缓冲区 和 文件描述符struct file 里面有指向inode(文件属性)和文件内核缓冲区的指针。然后把文件描述符指针按由小到大的顺序填入文件描述符表的fd_arry[]数组中最后返回下标。

那如果以该进程为父进程创建子进程会是什么样的?

fork()创建一个子进程,肯定会把task_struct struct files_struct这些进程部分复制下来,

struct file不属于进程的一部分,但父子进程需要独立读写文件,也就意味着struct file里面的pos不同pos 通常指的是文件的当前偏移量,即下一个读写操作将从文件的哪个位置开始。),需要子进程复制一份。inode对文件的管理一份就够了,文件内核缓冲区是系统提供的,系统服务每个进程。

那父子进程如何完成通信呢?

父子进程在同一个文件的文件内核缓冲区进行操作,那文件内核缓冲区的内容要不要刷新到磁盘呢?这个文件的作用就是为了让进程间相互通信,没必要保存到磁盘。

有没有一种特殊的文件只用于进程间的通信呢?

管道 

父进程读写打开管道,fork()创建子进程。父进程关闭读端,子进程关闭写端。父进程向管道写入数据,子进程可以随时从管道读取数据,而不会干扰彼此的读写操作。形成单向通信.

可以不关读端吗?

建议关,因为管道的设计就是为了单向通信父进程没必要去进行读操作,不关浪费数组资源,文件描述符泄漏。还有可能会进行误操作。

补充:关于重定向 >

./cout.c > test 表示把cout.c文件输出的内容重定向到test文件中。但比不上所有的打印信息都会重定向到test文件中,错误流的信息并不会重定向到test,而是打印到显示器上。

我们知道文件描述符 0输入流 1输出流 2错误流 ,

./cout.c > test 完整的表示是 ./cout.c 1>test 

如果想把错误流也重定向到test文件中

./cout.c &> test 这里的&表示将标准错误与标准输出一起处理。

或者

./cout.c 1>test 2>&1 

>&表示将文件描述符的目标重定向。

2>&1 的意思是将标准错误重定向到标准输出所指向的位置,也就是test文件

2.pipe()系统调用

pipe()系统调用创建一个管道。

#include <unistd.h>

int pipe(int fd[2]);

参数:int fd[2]是一个输出行参数,返回文件描述符,fd[0]以读方式打开管道(读端)

fd[1]以写方式打开管道(写端)

返回值:成功返回0 失败返回-1 error

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

int main()
{
    // 1.创建管道
    int fd[2] = {0};
    int n = pipe(fd);
    if (n == -1)
    {
        std::cerr << "pipe error" << std::endl;
        return -1;
    }
    // 2.创建子进程
    pid_t id = fork();
    if (id < 0)
    {
        std::cerr << "fork error" << std::endl;
        return -2;
    }
    if (id == 0)
    {
        int count = 0;
        // 子进程 关闭读端
        close(fd[0]);
        while (1)
        {
            close(fd[0]);
            std::string message = "hello ";
            message += std::to_string(getpid());
            message += ",";
            message += std::to_string(count++);
            // 写入管道
            int n = write(fd[1], message.c_str(), message.size());
            sleep(2);
        }
        exit(0);
    }
    else
    {
        char buff[1024];
        // 父进程 关闭写端
        close(fd[1]);
        // 从管道中读
        while (1)
        {
            int n = read(fd[0], buff, 1024);
            if (n > 0)
            {
                // 读到数据
                buff[n] = '\0'; // 系统字符串没有规定以0结尾
                std::cout << "子进程->父进程 message:" << buff << std::endl;
            }
            else if (n == 0)
            {
                // 写端关闭 读到结尾
                break;
            }
        }
        int status;
        pid_t rid = waitpid(id, &status, 0);
        if (rid == -1)
        {
            std::cerr << "waitpid error" << std::endl;
            return -3;
        }
        //int 16字节 前7位退出信号 后8位退出码
        std::cout<<"子进程pid"<<rid<<"退出码"<<((status<<8)&0xFF)
        <<"退出信号"<<(status&0x7F);
    }
    return 0;
}

上面是子进程写入,父进程读入并输出。

root@hcss-ecs-178e:~/dir1# ./pipe
子进程->父进程 message:hello 8128,0
子进程->父进程 message:hello 8128,1
子进程->父进程 message:hello 8128,2
子进程->父进程 message:hello 8128,3
子进程->父进程 message:hello 8128,4
子进程->父进程 message:hello 8128,5
子进程->父进程 message:hello 8128,6
root@hcss-ecs-178e:~# ps axj|head -1;ps axj|grep pipe
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
   7039    8127    8127    7039 pts/2       8127 S+       0   0:00 ./pipe
   8127    8128    8127    7039 pts/2       8127 S+       0   0:00 ./pipe
   7918    8132    8131    7918 pts/3       8131 S+       0   0:00 grep --color=auto pipe

子进程不断向管道内写入,父进程读入并输出。

3.匿名管道的四种情况

1.管道为空&&管道正常 read会阻塞

子进程写入的速度小于父进程读入的速度,read会阻塞。

2.管道为满&&管道正常 wirte会阻塞

父进程读入速度小于子进程写入的速度,wirte会阻塞。

子进程每次写入一个字符,re表示写入多少字符。

让父进程读入一次就停止,子进程不停写 直到写满管道 wirte阻塞。

65536/1024=64KB 可见管道一次性最大存入64KB数据,父进程读入,子进程才能进行写入。也就意味着父进程读过后的内容在管道内不会保存。

3.管道写端关闭,读端继续。读端读到0,表示读到文件结尾。

 子进程只写入一次

父进程读到0,buff为空,意味着管道为空,读到结尾。退出返回退出码 退出信号

4.写端正常,读端关闭。OS会直接杀死进程

子进程写入管道就是为了让父进程读,如果读端关闭,意味着管道就没有存在的意义,会被系统杀死。

父进程读一次就关闭读端

可以看到退出信号为13,它表示一个进程试图向一个已关闭的管道或套接字写入数据。这种情况下,操作系统会终止该进程,通常会导致进程接收到该信号。

管道的特性

  1. 单向通信管道通常是单向的,数据从一个进程流向另一个进程。可以使用两个管道实现双向通信。

  2. 缓冲区:管道有一个内置的缓冲区,可以暂时存储数据。写入操作不会立即导致读取操作,因此可以在某些情况下实现异步通信。同步互斥

  3. 匿名性:在 Unix 和类 Unix 系统中,匿名管道不具名,不需要在文件系统中创建实体。它们只存在于相关联的进程之间。

  4. 阻塞行为:默认情况下,管道的读写操作是阻塞的。写入端在缓冲区满时会阻塞,读取端在管道为空时会阻塞。这可以帮助协调两个进程的执行。

  5. 进程关系:通常,管道用于父进程与子进程之间的通信。子进程可以继承父进程创建的管道描述符。

  6. 资源释放:当管道的最后一个读取端被关闭时,所有写入该管道的进程都会收到 SIGPIPE 信号,这可能导致它们终止。生命周期随进程

  7. 大小限制:管道的缓冲区大小通常受到操作系统的限制,不同系统的大小可能不同。

3.进程池模拟实现

父进程有多个管道进行写入要执行的命令,每个管道对应一个子进程,子进程从管道中读,读到命令并执行。

makefile

BIN=processpool
CC=g++
FLAGS=-c -Wall -std=c++11 #-Wall显示全部警告
LDFLAGS=-o
#SRC=$(shell ls *.cc)
SRC=$(wildcard *.cc) #wildcard函数 获取当前目录下所有以 .cc 结尾的文件
                     # $()引用变量或函数,将右侧函数结果赋给SRC
OBJ=$(SRC:.cc=.o)	 #将 SRC 变量中的每个以 .cc 结尾的文件名替换为对应的以 .o 结尾的文件名

$(BIN):$(OBJ)
	$(CC) $(LDFLAGS) $@ $^
%.o:%.cc             #以 .cc 结尾的文件都可以被编译为以 .o 结尾的文件。
	$(CC) $(FLAGS) $< 

.PHONY:clean
clean:
	rm -f $(BIN) $(OBJ)

Task.hpp

#pragma once
#include <iostream>
#include <functional>
#include <unordered_map>
#include <ctime>
#include <cstdlib>
#include <sys/types.h>
#include <unistd.h>

using task_t=std::function<void()>;

void DownLoad()
{
    std::cout<<"我是下载任务 pid: "<<getpid()<<std::endl;
}
void Log()
{
    std::cout<<"我是日志任务pid: "<<getpid()<<std::endl;
}
void Sql()
{
    std::cout<<"我是数据块同步任务pid: "<<getpid()<<std::endl;
}
static int number=0;
class TaskManger
{
public:
    TaskManger()
    {
        srand(time(NULL));
        InsertTask(DownLoad);
        InsertTask(Log);
        InsertTask(Sql);
    }
    void InsertTask(task_t task)
    {
        _tasks[number++]=task;
    }
    int SelecTask()
    {
        return rand()%number;
    }
    void Excute(int number)
    {
        //找不到直接返回 找到调用对应的函数
        if(_tasks.find(number)==_tasks.end()) return;
        _tasks[number]();
    }
    ~TaskManger()
    {}
private:
    std::unordered_map<int,task_t> _tasks;

};
TaskManger tm;

processpool.cc

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

using work_t = std::function<void()>; // 定义一个不带参数的 void 类型函数

enum
{
    OK = 1,
    UsageError,
    PipeError,
    ForkError
};

// 对管道先描述,再组织
class Channel
{
public:
    Channel(int fd, pid_t id) : _wfd(fd), _pid(id)
    {
        _name = "Channel-" + std::to_string(_wfd) + "-" + std::to_string(_pid);
    }
    std::string Name()
    {
        return _name;
    }
    // 把任务写入管道
    void Send(int cmd)
    {
        write(_wfd, &cmd, sizeof(cmd));
    }
    void Close()
    {
        close(_wfd);
    }
    pid_t Id()
    {
        return _pid;
    }
    ~Channel()
    {
    }

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

void Usage(std::string proce)
{
    std::cout << "Usage:" << proce << "process_nums" << std::endl;
}

void Worker()
{
    while (1)
    {
        int cmd = 0;
        int n = read(0, &cmd, sizeof(cmd));
        if (n == sizeof(cmd))
        {
            tm.Excute(cmd);
        }
        else if (n == 0)
        {
            std::cout<<"读到末尾"<<std::endl;
            break;//读到0意味着结束
        }
        else
        {
        }
    }
}
// work_t work 回调方法
int InitProcessPool(int num, std::vector<Channel> &Channels, work_t work)
{
    for (int i = 0; i < num; i++)
    {
        // 先创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        if (n < 0)
            return PipeError;
        // 再创建子进程
        pid_t id = fork();
        if (id < 0)
            return ForkError;
        if (id == 0)
        {
            // 子进程
            close(pipefd[1]);
            //关闭历史wfd写端
            for(auto&o:Channels)
            {
                o.Close();
            }
            dup2(pipefd[0], 0); // 从标准输入读取数据改从管道中读取的。
            work();
            exit(0);
        }
        // 父进程
        // 保存写端
        Channels.emplace_back(pipefd[1], id);
        close(pipefd[0]);
    }
    return OK;
}

void DispatchTask(std::vector<Channel> &Channels)
{
    int num = 5;
    int who = 0;
    while (num--)
    {
        // a.选择一个任务 整数
        int task = tm.SelecTask();
        // b.选择一个子进程 Channel
        Channel &cur = Channels[who++];
        who %= Channels.size();
        std::cout << "#############################" << std::endl;
        std::cout << "send " << task << " to" << cur.Name() << "任务数:" << num << std::endl;
        std::cout << "#############################" << std::endl;
        // c.派发任务
        cur.Send(task);
        sleep(1);
    }
}

void DebugPrit(std::vector<Channel> &Channels)
{
    for (auto &o : Channels)
    {
        std::cout << o.Name() << std::endl;
    }
}
void CleanProcessPool(std::vector<Channel> &Channels)
{
    // version 3
    for (auto &c : Channels)
    {
        c.Close();
        pid_t rid = ::waitpid(c.Id(), nullptr, 0);
        if (rid > 0)
        {
            std::cout << "child " << rid << " wait ... success" << std::endl;
        }
    }
}
    int main(int argc, char *argv[])
    {
        // 使用错误 返回
        if (argc != 2)
        {
            Usage(argv[0]);
            return UsageError;
        }
        // 创建指定个数的进程
        int num = std::stoi(argv[1]);
        // 管理管道
        std::vector<Channel> Channels;
        // 1.初始化进程池
        InitProcessPool(num, Channels, Worker);
        // DebugPrit(Channels);
        // 2.分配任务
        DispatchTask(Channels);
        // 3.退出进程池
        CleanProcessPool(Channels);
        return 0;
    }

在进行退出进程池操作时,我们是通过关闭父进程的写端,然后等管道为空子进程读到0,break退出循环,最后exit()退出子进程。但前提是我们关闭了历史写端

什么意思?

1.父进程pipe()读端写端同时链接管道,fork()第一个子进程,父进程关闭读端,子进程关闭写端。此时父进程文件描述符表中下标为4指向写端,子进程下标3指向读端。

2.父进程pipe()后文件描述符表情况是 3新分配的读端 4第一个管道的写端 5新分配的写端。

fork()第二个子进程,子进程复制父进程的文件描述符表。

父进程关闭3读端,子进程关闭5写端。但是此时子进程4仍指向第一个管道的写端。

3.不断fork(),父进程从下标4开始增加写端,子进程下标3都是指向自己管道的读端,但后面下标分别指向第一个管道的写端 第二个管道的写端 ...

子进程有历史写端指向管道,只把父进程的写端关闭,子进程就会read一直在阻塞,不会退出。 

怎么解决呢?

1.我们可以从最后一个子进程开始退出最后一个子进程的管道只有父进程链接写端,最后一个子进程的文件描述符表从下标4开始就链接着第一个管道写端 第2个...。把它删掉前面所有管道的写端数-1。在此基础上再把最后一个子进程退出...

2.根本因为在于子进程复制父进程的文件描述符表时,会把指向其它管道写端的文件描述符拷贝下来。拷贝下来后,可不可以把它们都关闭了呢?

在fork完子进程也把父进程的文件描述符表拷贝下来

此时vector<Channel> Channels每个Channel中都有指向管道写端的文件描述符。

通过for(auto :)遍历Channels一个一个把指向管道写端的文件描述符删除。

4.命名管道

1.原理:匿名管道是因为子进程复制父进程文件描述符表,所以可以看到同一份资源

命名管道,是让不同进程,用同一个文件系统的路径标志同一份资源。

2.生命周期:匿名管道如果读端 写端都没有进程链接就会消失

命名管道就算没有进程链接,也会存在,需要用unlink删除

3.命名管道和匿名管道都不会向磁盘刷新,所以命名管道关闭虽然不会消失,但里面也不会存有数据。

4.都是进程间单项通信用的。

创建一个命名管道

1.从命令行中 mkfifo 

mkfifo filename

2.用函数

int mkfifo(const char *filename, mode_t mode);

参数

  1. filename:要创建的命名管道的路径名,必须是一个有效的字符串。
  2. mode:文件权限,指定新管道的访问权限,通常以八进制形式表示(如 0666 代表读写权限)。

返回值

  • 成功时,返回 0
  • 失败时,返回 -1,并设置 errno,可以通过 errno 获取错误原因。

如果一个进程以写端打开命名管道,而另一个进程没有打开读端,那么写端会被阻塞。

反之,一个进程以打开命名管道读端,而另一个进程没有打开写端,那么读端会被阻塞。

只有两端都有进程链接时才会进行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值