Linux进程间通信——管道

前面的内容中我们已经学习了解了Linux系统的底层相关的东西及其多进程的事情。既然有了多个进程,那么就非常有必要让这些进程互相联系起来。因此进程之间通信就十分重要。

相关代码已经上传至作者的个人gitee:楼田莉子/Linux学习喜欢请点个赞

目录

进程间通信介绍

前言

进程通信的目的

进程间通信的发展

进程间通信的分类

管道

[System V IPC](#System V IPC)

[POSIX IPC](#POSIX IPC)

管道

通信原理

匿名管道

pipe函数

fork共享管道的原理

理解管道

文件标识符角度

内核角度

匿名管道的特性

匿名管道几种情况

命名管道

命名管道与匿名管道的对比

补充说明表

选择建议表


进程间通信介绍

前言

进程之前具有独立性,因此要让两个进程看到同一份"资源"。

进程通信的目的

数据传输:一个进程需要将它的数据发送给另一个进程。

资源共享:多个进程之间共享同样的资源。

通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

进程控制:有些进程希望完全控制另一个进程的执行(如调试进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

进程间通信的发展

管道

System V进程间通信

POSIX进程间通信

进程间通信的分类

管道

匿名管道(pipe)

命名管道

System V IPC

System V 消息队列

System V 共享内存

System V 信号量

POSIX IPC

消息队列

共享内存

信号量

互斥量

条件变量

读写锁

管道

通信原理

管道是unix系统最古老的通信方式。我们把从⼀个进程连接到另⼀个进程的⼀个数据流称为⼀个"管道"。管道是内核级基于文件的通信方式

fork代码执行的时候file要进行拷贝

管道是单向通信

管道是一个内存级的文件,不需要打开文件路径,不需要打开磁盘文件,没有路径,不需要文件名,是匿名通道

匿名管道

pipe函数

cpp 复制代码
#include <unistd.h>
功能:创建⼀⽆名管道
原型:
int pipe(int fd[2]);
参数:
fd:⽂件描述符数组,其中fd[0]表⽰读端, fd[1]表⽰写端
返回值:成功返回0,失败返回错误代码

fork共享管道的原理

理解管道

文件标识符角度

内核角度

接下来我们写代码验证,要求实现:

1、创建管道文件

2、fork进程

3、关闭读写端,子进程w,父进程r

4、子进程合并到父进程

cpp 复制代码
// 例子:从键盘读取数据,写入管道,读取管道,写到屏幕
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

int main() 
{
    //创建管道
    int pipefd[2]={0};
    int n=pipe(pipefd);
    if(n<0)
    {
        perror("pipe failed");
        return 1;
    }
    printf("pipefd[0]:%d,pipefd[1]:%d\n",pipefd[0],pipefd[1]);
    //fork进程
    pid_t id =fork();
    if(id==0)
    {
        //child
        close(pipefd[1]);//关闭写端
        char *str="你好诗华";
        // write(pipefd[0],str,strlen(str));//管道也是文件
        int cnt=10;
        char outbuffer[1024];
        while(cnt--)
        {
            //sizeof(outbuffer)-1 留一个位置给字符串结束符\0
            snprintf(outbuffer, sizeof(outbuffer)-1, "f->c# %s %d %d\n", str, getpid(), cnt);//父进程传到子进程的消息
            write(pipefd[0],outbuffer,strlen(outbuffer));//管道也是文件
            sleep(1);
        }
        exit(0);
    }
    //父进程
    close(pipefd[1]);
    char buffer[1024]={0};
    char inbuffer[1024]={0};
    while(1)
    {
        //sizeof(outbuffer)-1 留一个位置给字符串结束符\0
        read(pipefd[0],inbuffer,sizeof(inbuffer)-1);
    }
    pid_t ret=waitpid(id,NULL,0);
    (void)ret;//防止编译器报警告
    
    return 0;
}

打开的文件生命周期是跟进程一样的。

匿名管道的特性

  1. 管道是只能单向通信,单工通信

  2. 匿名管道只能用来进行 具有血缘关系的进程之间 常用 父子通信(继承内核资源)

  3. 管道是面向字节流的。

  4. 管道的生命周期是随进程的!

  5. 管道通信,对于多进程而言,是自带互斥同步机制

匿名管道几种情况

管道通信,前提是让不同的进程,看到同一份资源。可以是文件、文件的内核缓冲区、内存块,属于父子进程共享的资源。但是这种并发就会导致互斥和同步的问题。

互斥就是任何时刻只允许一个进程访问资源

同步就是进程访问资源是有一定顺序性的

实际情况下会出现以下几种情况:

1、子进程写的慢父进程堵塞,要等管道有数据,父进程才能读

此时管道为空,读阻塞了,要给写机会。

2、子进程写的快父进程不读,管道被写满则父进程阻塞

此时管道满了,写阻塞了,要给读机会

3、读端在读写端的关闭,读端读完管道内剩余数据,接着就会读取空字符串,read函数返回值为0,表明管道读到结尾。

4、写端一直在写,读端不关闭fd。OS会直接杀掉写的进程。

我们可以利用进程池的方法来解决。

进程池相关的博客介绍:https://blog.csdn.net/2401_89119815/article/details/157219550?fromshare=blogdetail&sharetype=blogdetail&sharerId=157219550&sharerefer=PC&sharesource=2401_89119815&sharefrom=from_link

命名管道

进程间通信的本质是让不同进程间看到同一份资源。那么如果两个进程毫无关系,那么该如何通信呢?就依赖于命名管道。

如果多个进程打开同一份文件和资源,怎么保证他们打开的都是同一个呢?只需要让访问路径是一样的即可。因为文件名+路径=是唯一incode。而且文件一定有名字。

接下来我们就来尝试利用命名通道创建不同进程之间的通信手段。创建方式有mkfifo指令和mkfifo函数

bash 复制代码
#创建名为filename的FIFO的命名管道指令
mkfifo filename
cpp 复制代码
//创建名字为filename、权限为mode的FIFO文件
int mkfifo(const char *filename,mode_t mode);

我们先创建一个namepipe的FIFO文件(命名管道)。随后在多个进程下进程读和写操作。(图一写后必须等待图二读,否则会进程阻塞)

但是这样显得不够准确,所以我们接下来来编写一段代码,模拟一段通信服务,就像下面这张图一样。

代码内容如下:

Pipe.hpp

cpp 复制代码
//.hpp是专属于C++的特殊编写模式文件,类似于.h文件。可以将头文件和源文件一起写
#pragma once
#include<iostream>
#include<string>
#include<cstring>
#include <sys/stat.h>
#include <fcntl.h>
#include <cerrno>
#include <sys/types.h>
#include<unistd.h>   
#define ForRead 0
#define ForWrite 1

const std::string gcommfile="./fifo";
class FIFO
{
    private:
        std::string _commfile;
        mode_t _mode;
        int _fd;
        bool IsExist()
        {
            //命名管道文件用open打开了必须要读写,如果只读就会阻塞
            // int fd=open(_commfile.c_str(),O_RDONLY);
            // if(fd<0)
            // {
            //     return false;
            // }
            // else
            // {
            //     close(fd);
            //     return true;
            // }
            struct stat buf;
            int n=stat(_commfile.c_str(),&buf);
            if(n<0)
            {
                return false;   
            }
            else
            {
                return true;
            }
        }
    public:
        FIFO(const std::string &commfile=gcommfile)
            :_commfile(commfile),_mode(0666),_fd(-1)
        {}
        //创建命名管道
        void create()
        {
            if(IsExist())
            {
                return;
            }
            //设置文件权限掩码为0,确保创建的命名管道具有所需的权限
            umask(0);
            int n =mkfifo(_commfile.c_str(),_mode);
            if(n<0)//创建失败
            {
                std::cerr<<"mkfifo error!"<<strerror(errno)\
                    <<"\terrno"<<errno<<std::endl;
                //strerror用于返回错误码对应的错误信息
                exit(1);
            }
            std::cerr<<"mkfifo success!"<<std::endl;
            

        }
        //获取命名管道
        void Open(int mode)
        {
            //通信没有开始之前如果读端打开写端没有打开
            //读端open就会阻塞,直到write打开
            if(mode==ForRead)
            {
                _fd=open(_commfile.c_str(),O_RDONLY);
            }
            else if(mode ==ForWrite)
            {
                _fd=open(_commfile.c_str(),O_WRONLY);
            }
            if(_fd<0)
            {
                std::cerr<<"open error!"<<strerror(errno)\
                    <<"\terrno"<<errno<<std::endl;
                exit(2);
            }
            else 
            {
                std::cout<<"open "<<_commfile<<" success!"<<std::endl;
            }

        }
        //发送用引用,接受用指针!
        //发送数据
        void Send(const std::string &data)
        {
            ssize_t n=write(_fd,data.c_str(),data.size());
            (void)n;
        }
        //接收数据
        int Recv(std::string *data)
        {
            char buffer[128];
            ssize_t n = read(_fd, buffer, sizeof(buffer)-1); // 为什么要-1?
            if(n > 0)
            {
                buffer[n] = 0;
                *data = buffer;
                return n;
            }
            else if(n == 0)
            {
                return 0;
            }
            else
            {
                return -1;
            }
        }
        //销毁命名管道
        void destroy()
        {
            if(!IsExist())
            {
                return;
            }
            int n=unlink(_commfile.c_str());
            if(n<0)
            {
                std::cerr<<"unlink error!"<<strerror(errno)\
                    <<"\terrno"<<errno<<std::endl;
                exit(2);
            }
            (void)n;
            std::cerr<<"unlink "<<_commfile<<" success!"<<std::endl;
            
        }
        ~FIFO()
        {}
};

Server.cpp

cpp 复制代码
#include"Pipe.hpp"
int main()
{
    FIFO pipe;
    pipe.create();
    sleep(3);
    pipe.Open(ForRead);
    std::string data;
    while(1)
    {
        int n=pipe.Recv(&data);
        if(n<=0)
        {
            break;
        }
        else
        {
            std::cout<<"client send data:#"<<data<<std::endl;
        }
        
    }
    pipe.destroy();
    return 0;
}

client.cpp

cpp 复制代码
#include"Pipe.hpp"
int main()
{
    //客户端只需要打开命名管道进行读写操作
    FIFO filename;
    filename.Open(ForWrite);
    while(1)
    {
        std::cout<<"Please Enter@ ";
        std::string data;
        std::getline(std::cin,data);
        filename.Send(data);
    }
    return 0;
}

先运行./Server,然后运行./client,有以下结果:

除了管道外,system V也是进程间通信的重要部分。但是其内容量较大,这里就不展开说明了。后续会更新这部分内容。本期内容就到这里了,喜欢的话请点个赞谢谢

命名管道与匿名管道的对比

特性 命名管道(Named Pipe/FIFO) 匿名管道(Anonymous Pipe)
创建方式 mkfifo() 函数或 mkfifo 命令 pipe() 系统调用
文件系统可见性 ✅ 有实体文件(类型为p ❌ 仅存在于内存中
持久性 持久存在,直到被显式删除 随进程结束而销毁
进程关系限制 ❌ 无亲缘关系要求 ✅ 必须具有亲缘关系
通信进程范围 系统中任何有权限的进程 父子进程或兄弟进程
访问方式 通过文件路径名打开 通过继承的文件描述符
Shell中使用 可作为普通文件操作 使用 `
创建示例 mkfifo mypipemkfifo("path", mode) int pipe_fd[2]; pipe(pipe_fd);
标识符类型 文件路径名(字符串) 文件描述符(整数)
阻塞行为 读写端相互阻塞等待 读写端相互阻塞等待
使用示例 echo "data" > mypipe `cmd1
生命周期管理 需要手动创建和删除 随进程自动回收
权限控制 有文件权限位(rwx) 无权限控制,继承父进程
打开次数限制 可被多个进程同时打开 只能被创建它的进程及其子进程使用
典型应用场景 1. 无亲缘关系进程通信 2. 持久化通信端点 3. Shell脚本间通信 1. 父子进程通信 2. Shell命令管道 3. 简单进程间数据传递

补充说明表

维度 命名管道 匿名管道
创建后表现 显示为文件,ls -l看到p类型 不显示,仅进程内部可见
通信方向 半双工(单向) 半双工(单向)
多进程使用 支持多个读者/写者(需协调) 通常一对一
跨网络 ❌ 仅本地 ❌ 仅本地
缓冲区大小 系统默认(通常4K-64K) 系统默认(通常4K-64K)
I/O多路复用 ✅ 支持(select/poll/epoll) ✅ 支持(select/poll/epoll)
信号处理 可接收SIGPIPE等信号 可接收SIGPIPE等信号
阻塞模式 默认阻塞,可设为非阻塞 默认阻塞,可设为非阻塞
原子性 保证小于PIPE_BUF的写入是原子的 保证小于PIPE_BUF的写入是原子的
POSIX标准 ✅ 是 ✅ 是
Windows支持 ✅ 有(命名管道) ✅ 有(匿名管道)

选择建议表

场景 推荐选择 理由
Shell命令流水线 匿名管道 简单直接,`cmd1
无关进程间通信 命名管道 不需要亲缘关系
需要持久化通信端点 命名管道 文件系统可见,长期存在
父子/兄弟进程通信 匿名管道 简单高效,自动清理
需要权限控制的IPC 命名管道 可利用文件权限机制
临时数据传递 匿名管道 自动管理生命周期
复杂IPC架构 命名管道 更适合复杂进程拓扑

封面图自取:

相关推荐
June bug2 小时前
(#字符串处理)判断字符串是否为有效IPv4地址
服务器·网络·p2p
gettingolder2 小时前
haproxy的简单负载均衡实现
运维·服务器·负载均衡
理人综艺好会2 小时前
Web学习之网络通信
学习
科技林总2 小时前
【系统分析师】5.4 数据库设计与建模
学习
Fᴏʀ ʏ꯭ᴏ꯭ᴜ꯭.2 小时前
HAPROXY安装,双网卡负载均衡实战指南
运维·负载均衡
whale fall2 小时前
【雅思-口语】与豆包聊天:出国旅游日常聊天英文 + 中文对照合集
笔记·学习·旅游
济6172 小时前
linux 系统移植(第十五期)---Linux 内核移植(4)-- 修改 EMMC 驱动--- Ubuntu20.04
linux·嵌入式硬件
henujolly2 小时前
区块链p2p
服务器·区块链·p2p
礼拜天没时间.2 小时前
《Docker实战入门与部署指南:从核心概念到网络与数据管理》:初识Docker——概念与优势
linux·运维·网络·docker·容器·centos