【Linux系统篇】:从匿名管道到命名管道--如何理解进程通信中的管道?

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh--CSDN博客
✨ 文章所属专栏:Linux篇--CSDN博客

文章目录

一.进程通信

进程通信概念

进程通信(Inter-Process Communication, IPC)是操作系统提供的机制,允许两个或多个独立的进程实现数据层面的交互。

因为进程独立性的存在,导致进程之间进行通信的成本比较高;而通信成本,主要就是用来打破进程独立性。

1.进程间通信的本质,必须让不同的进程看到同一份"资源"!这个"资源",本质上就是一个特定形式的内存空间,相当于两个独立的进程共享一个特殊的内存空间,然后进行通信。

2.至于这个"资源"由谁提供?肯定是由操作系统提供

那为什么不是进行通信的两个进程中的其中一个呢?

假设这个"资源"是其中一个进程提供,这个"资源"本质上是属于提供的这个进程的,还是由该进程独有,而进程之间具有独立性,一旦这个"资源"由其中某个进程提供,就会破环该进程的独立性,所以这个"资源"一定是个第三方空间,由操作系统提供的。

3.这个"资源"由操作系统提供,当进程访问这个空间进行通信时,本质就是访问操作系统 !而进程代表的是用户,相当于是用户在访问操作系统;用户访问操作系统只能通过系统调用来实现

这个"资源"从创建到使用再到释放都是通过系统调用接口来实现!

4.一般操作系统会有一个独立的通信模块:IPC通信模块---隶属于文件系统;而进程间通信是有标准的---system Vposix(现如今的两种通信标准),如果没有统一的标准,就无法实现不同设备间的通信。

进程通信目的

  • 数据传输

    一个进程需要将他的数据发送给另一个进程。

  • 资源共享

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

  • 通知事件

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

  • 进程控制

    有些进程希望完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时直到他的状态改变。

进程通信分类

  • 管道:匿名管道和命名管道
  • System V IPCSystem V消息队列;System V共享内存;System V信号量
  • Posix IPC:消息队列;共享内存;信号量;互斥量;条件变量;读写锁

二.管道

管道是Unix中最古老的进程间通信的形式

一个文件可以被多个进程所访问,文件本身就是一种公共资源,如果一个进程写,另一个进程读,就可以实现进程间通信。管道就是一种基于文件形式的通信方式

其中管道分为匿名管道和命名管道

匿名管道

1.原理实现

文件无论是读还是写,都要先将磁盘上的内容加载到文件对应的页缓冲区。而对于通信来说,只需要两个进程间通过管道文件来实现数据传输即可,并不需要将数据刷新到磁盘上。这种文件属于内核级文件,不考虑页缓冲区和磁盘之间的刷新

每个进程都有一个文件描述符表,表示的是该进程打开的文件描述列表。当父进程通过fork创建子进程后,子进程要拷贝父进程的内核数据结构,所以子进程会有一份和父进程相同的文件描述符表

但是对于父进程打开的文件,并不会为子进程创建一份相同的文件再打开(文件属于文件系统,和进程管理不相连,进程的内核数据结构可以拷贝,但是文件不会再拷贝一份)。

而父子进程的文件描述符表中的内容相同,存放的都是相同的struct file*,所以最后要指向相同的文件(当其中某个进程关闭该文件时,因为其他进程还在使用,引用计数不为0,所以不会关闭,并不影响另一个)。

如果操作系统给父进程创建一个管道文件(内核级文件),最后子进程通过继承也会和父进程指向同一个内核级文件,这样父子进程就可以看到同一份"资源",就可以实现进程间通信了。

但是如果父进程以读方式打开管道文件,子进程继承父进程后,也会是读方式打开,并不是写方式打开。而进程间通信,肯定是一个读,另一个写;所以这样两个都读就不能实现进程间通信。

所以父进程会以读和写方式打开管道文件两次,子进程继承下来也会以读和写方式打开两次 。读和写方式打开两次,就会为该进程创建两个strcut file,但是两个struct file指向的是同一个文件页缓冲区,只是读写方式不同而已。

创建子进程后,父子进程指向两个相同的struct file*,最后也就指向同一个页缓冲区,父子进程都有读写方式,但是只能进行单向通信

父进程读取,子进程写入;父进程就要关闭写端,子进程就要关闭读端

子进程读取,父进程写入;子进程就要关闭写端,父进程就要关闭读端

注意是哪个不用关哪个

如果想要进行双向通信,可以建立多个管道;但是不能在一个管道中双向通信

2.接口函数与编码实现

pipe函数是Unix/Linux系统中的一个关键系统调用,用于创建管道以实现进程间通信。

1.函数原型

cpp 复制代码
#include <unistd.h>

int pipe(int fd[2]);
  • 成功返回0,失败返回-1,并设置errno
  • 参数fd[2]是一个输出型参数,系统为进程创建管道后,会将读写方式打开的两个文件描述符填入到参数fd[2]中,函数调用结束后带出来,让用户使用;其中fd[0]为读端,fd[1]为写端。

2.单向通信

  • 管道是单向通信的,数据从写端流向读端。
  • 若需双向通信,需创建两个管道。

3.文件描述符管理

  • 创建管道后,父进程通过fork函数创建子进程,子进程会继承父进程的文件描述符。
  • 通信时,应关闭不需要的端,避免资源泄露或阻塞。

编码实现

单向通信(子进程写入,父进程读取):

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<cstdlib>
#include<cstdio>
#include<string>
#include<cstring>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;

#define N 2
#define NUM 1024

void Write(int wid){
    string str = "hello, I am child";
    pid_t pid = getpid();
    int number = 0;

    //创建一个应用缓冲区
    char buffer[NUM];
    while (true){
        //缓冲区当字符串使用,每次清空
        buffer[0] = 0;
        //格式化写入到缓冲区中---字符串处理
        snprintf(buffer, sizeof(buffer), "%s-%d-%d", str.c_str(), pid, number++);

        //发送/写入给父进程
        write(wid, buffer, strlen(buffer));     //注意这里不用+1,只是写入内容,不考虑最后的'\n'
        //每次休眠1秒
        sleep(1);
    }
}

void Read(int rid){
    //创建一个应用缓冲区
    char buffer[NUM];

    while(true){
        buffer[0] = 0;
        ssize_t n = read(rid, buffer, sizeof(buffer));
        if(n>0){
            buffer[n] = '\0';
            cout << "father get a message[" << getpid() << "]:" << buffer << endl;
        }
    }
}

int main(){
    int pipefd[N] = {0};
    int n = pipe(pipefd);
    if(n<0)
        return 1;

    // cout << "pipefd[0]: " << pipefd[0] << ", pipefd[1]: " << pipefd[1] << endl;

    // child-> w, father-> r
    pid_t id = fork();
    if(id < 0)
        return 2;
    if(id == 0){
        //child
        //关闭读端
        close(pipefd[0]);

        //IPC code
        Write(pipefd[1]);

        //关闭写端
        close(pipefd[1]);
        exit(0);
    }
    //father
    //关闭写端
    close(pipefd[1]);

    //IPC code
    Read(pipefd[0]);

    //父进程回收子进程
    pid_t rid = waitpid(id, nullptr, 0);
    if(rid<0)
        return 3;
    
    // 关闭读端
    close(pipefd[0]);

    return 0;
}

3.管道的4种读写情况

  • 1.读写端正常,管道如果为空,读端就要阻塞

    以上面演示的代码为例,让子进程写入时,每次休眠一秒,而父进程读取时没有限制,最后看到的现象就是父进程读取同样也是每间隔一秒读取到一次内容。

    子进程每一秒写入一次,父进程读取完一次后,管道为空,父进程读端就会阻塞,等待子进程下一次的写入。

  • 2.读写端正常,管道如果写满,写端就要阻塞

    让子进程每次写入时没有限制,父进程每次读取时先休眠五秒,最后看到的现象就是每次读取时都是读取到管道中的全部内容。

    子进程每次写入没有限制,在父进程休眠的前5秒,子进程将管道中全部写满,此时父进程还没有进行读取,子进程写端就会阻塞,等待父进程将管道中的内容读取后才能继续写入。

  • 3.读端正常,写端关闭,读端就会读到0,表示读取到文件末尾,不会被阻塞

    子进程每隔一秒写入一次,写入五次后关闭不在写入,写端关闭;父进程每隔一秒读取一次,读取5次后,之后再次读取每次都是读取到0;虽然子进程的写端关闭,但是父进程的读端并不会阻塞,依然在读取,只不过每次都是读取到0,表示读取到文件末尾。

    如果读取到文件末尾后不想再继续读取,可以根据read函数的返回值设置对应的判断语句:

    cpp 复制代码
    void Read(int rid){
        //创建一个应用缓冲区
        char buffer[NUM];
    
        while (true)
        {
            //sleep(5);
            buffer[0] = 0;
            ssize_t n = read(rid, buffer, sizeof(buffer));
            if(n>0){
                buffer[n] = '\0';
                cout << "father get a message[" << getpid() << "]:" << buffer << endl;
            }
            //如果读取到文件末尾,就结束
            else if(n==0){
                cout << "father read file done" << endl;
                break;
            }
            else{
                break;
            }
            cout << n << endl;
            sleep(1);
        }
    }
  • 4.写端正常写入,读端关闭,操作系统会杀掉正在写入的进程

    子进程写端正常写入,但是父进程读端读取5次后,关闭读端,此时子进程就没必要再继续写入了,因为没有其他进程来读取,操作系统不会做低效,浪费等类似工作的,如果做了,就是操作系统的bug;因此操作系统就会杀掉正在写入的子进程。

    如何杀掉正再写入的进程?

    通过信号来杀掉!

    通过代码测试:子进程写端修改成每隔一秒写入一次:

    父进程读端修改成读取5次后关闭:

    父进程回收子进程后,输出子进程的退出信息:

    最后看到的现象就是,子进程收到的异常信号为13,因为代码没有跑完,所以退出码为0。


4.管道的5种特征

1.对于匿名管道需要具有血缘关系的进程才可以进行进程间通信(比如父子进程,兄弟进程,爷孙进程),而这个管道是一个内核级别的文件,不需要有路径和文件名,因此又叫做匿名管道。

2.管道只能单向通信,不能双向通信,如果要双向通信,需要建立两个管道。

3.进程间通信时是会进程协同的(比如上面管道4种读写情况中的前两种),同步与互斥--用来保护文件的数据安全(后面讲解多线程的时候会讲解,现在只需了解即可)。

4.管道是面向字节流的(后面讲解网络部分会讲解,现在只需了解即可)。

5.管道是基于文件的,而文件的生命周期是随进程的,当进程结束后,系统会自动释放管道文件资源。

匿名管道补充内容

匿名管道的其中一个应用场景就是指令中的管道|

测试:

父进程都是bash命令行解释器,是具有血缘关系的进程。

命名管道

匿名管道的一个限制就是只能在具有血缘关系(具有共同祖先)的进程间通信。如果想在不相关的进程之间进行通信,可以使用FIFO文件来做这项工作,他经常被称为命名管道

1.原理实现

对于两个不同的进程,如果想要进行通信,也是需要先看到同一份"资源",也就是打开同一个文件。对于具有血缘关系的进程,通过继承可以打开同一个文件;而不相关的进程要想打开同一个一个文件,只能通过同路径下同一个文件名来实现。

路径+文件名具有唯一性,两个不相关的进程就可以根据不同的读写方式打开同一个文件,一个读,一个写,这样就可以实现进程间通信了。

对于两个不相关的进程打开同一个文件,在内核中,系统也是只打开一个文件(页缓冲区只有一个,但是struct file有两个,一个以读方式打开,一个以写方式打开),和匿名管道通信的原理大致相同。

2.指令和接口函数

1.命名管道可以从命令行上创建/删除,命令行方法是使用下面这个命令

shell 复制代码
# 创建
mkfifo filename

# 删除
rm/unlink filename

2.命名管道可以从程序里创建/删除,相关函数

cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>

// 创建
int mkfifo(const char *pathname, mode_t mode);
// 删除
int unlink(const char *pathname);
  • 参数
    • pathname:FIFO文件(命名管道)的路径名。
    • mode:文件权限(八进制数字,比如0666;需结合进程的umask值:0002,实际权限为mode & ~umask
  • 返回值
    • 成功返回0,失败返回-1,并设置errno

特性

  • 文件系统中的可见性

    • FIFO文件会持久性在文件系统中(需手动删除)。
    • 可通过指令ls-l查看,类型为p(如:prw-r--r--)。
  • 阻塞行为

    默认情况下,打开FIFO时会阻塞,直到另一端也被打开:

    其中一个进程以只读(O_RDONLY)模式打开,会阻塞直到另一个进程以写模式(O_WRONLY)模式打开;

    其中一个进程以写模式(O_WRONLY)模式打开,会阻塞直到另一个进程以只读(O_RDONLY)模式打开;

3.示例--编码实现

用命名管道实现server&client通信

comm.hpp文件:

通过一个Init类封装命名管道的创建和删除

cpp 复制代码
#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <cerrno>
#include <cstring>
#include <fcntl.h>
#include <string>

#define FIFO_FILE "./myfifo"
#define MODE 0664
#define SIZE 1024

enum
{
    FILE_CREATE_ERR = 1,
    FILE_DELETE_ERR,
    FILE_OPEN_ERR,
    FILE_READ_ERR,
};

class Init
{
public:
    Init(){
        // 创建管道
        int n = mkfifo(FIFO_FILE, MODE);
        if (n == -1){
            perror("mkfifo");
            exit(FILE_CREATE_ERR);
        }
    }
    ~Init(){
        // 删除管道
        int m = unlink(FIFO_FILE);
        if (m == -1){
            perror("unlink");
            exit(FILE_DELETE_ERR);
        }
    }
};

server.cc文件:

server进程用来读取client进程写入的信息

cpp 复制代码
#include "comm.hpp"
using namespace std;

int main(){
    Init init;
    
    // 打开管道
    int fd = open(FIFO_FILE, O_RDONLY);
    if(fd < 0){
        perror("open");
        exit(FILE_OPEN_ERR);
    }
    // 等待写入端打开文件之后,读取端才会打开文件,然后继续执行,否则会阻塞等待
    cout << "server open file done! " << endl;

    // 开始通信
    while(true){
        char buffer[SIZE];
        int x = read(fd, buffer, sizeof(buffer));
        if(x > 0){
            buffer[x] = 0;
            cout << "client say@ " << buffer << endl;
        }
        else if(x == 0){
            cout << "client quit, me too!" << endl;
            break;
        }
        else{
            perror("read");
            exit(FILE_READ_ERR);
            break;
        }
    }

    close(fd);
    return 0;
}

client.cc文件:

client进程用来写入信息发送给server进程

cpp 复制代码
#include "comm.hpp"
using namespace std;

int main(){
    // 打开管道
    int fd = open(FIFO_FILE, O_WRONLY);
    if(fd < 0){
        perror("open");
        exit(FILE_OPEN_ERR);
    }
    cout<<"client open file done! "<<endl;

    // 开始通信
    string line;
    while (true){
        cout << "Please Enter# ";
        getline(cin, line);
        write(fd, line.c_str(), line.size());
    }

    close(fd);
    return 0;
}

实现效果

当server进程以只读模式打开时会阻塞,直到client进程以写入模式打开:

两种管道总结对比


1. 创建方式与可见性

特性 匿名管道 命名管道
创建方式 通过 pipe() 系统调用创建,返回两个文件描述符(读端 fd[0] 和写端 fd[1])。 通过 mkfifo() 系统调用创建,生成一个 FIFO 文件(如 ./myfifo)。
可见性 仅存在于内存中,没有文件系统实体。 以文件形式存在于文件系统中,可通过 ls -l 查看(类型标记为 p)。

2. 使用范围

特性 匿名管道 命名管道
通信范围 仅限具有亲缘关系的进程(如父子进程、兄弟进程)。 允许任意进程(即使无亲缘关系)通过文件名访问。
典型场景 简单的单向通信(如父子进程间传递数据)。 跨进程通信(如独立运行的进程间协作)。

3. 生命周期

特性 匿名管道 命名管道
生命周期 随进程结束自动销毁。 需手动调用 unlink() 删除文件,否则持久存在于文件系统中。
数据持久性 数据仅存在于内核缓冲区中,进程结束后消失。 数据同样暂存于内核缓冲区,但文件名持久化。

4. 打开与阻塞行为

特性 匿名管道 命名管道
打开行为 创建后两端(读/写)已同时存在,无需等待。 默认以阻塞模式打开: 以 O_RDONLY 打开会阻塞,直到有进程以 O_WRONLY 打开; 以 O_WRONLY 打开会阻塞,直到有进程以 O_RDONLY 打开。
关闭行为 一端关闭后,另一端读取会返回 EOF,写入会触发 SIGPIPE 信号。 行为与匿名管道一致。

5. 权限与安全性

特性 匿名管道 命名管道
权限控制 无文件系统权限,仅通过进程继承文件描述符共享。 受文件系统权限控制(如 mode 参数和 umask)。
安全性 仅能被创建管道的进程以及具有血缘关系的进程访问。 需确保文件名唯一且路径安全,避免冲突或恶意访问。

6. 双向通信实现

特性 匿名管道 命名管道
双向通信 需创建两个管道(一个读一个写)。 同样需创建两个 FIFO 文件。

以上就是关于进程通信中管道的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!

相关推荐
cwywsx7 分钟前
Linux:进程控制2
linux·运维·算法
熙曦Sakura7 分钟前
【Linux网络】 HTTP cookie与session
linux·网络·http
南棱笑笑生11 分钟前
20250512给NanoPi NEO core开发板在Ubuntu core20.04系统下重新编译boot.img
linux·运维·ubuntu
Ha-gd25 分钟前
Linux基础开发工具一(yum/apt ,vim)
linux·服务器
charlie1145141911 小时前
内核深入学习3——分析ARM32和ARM64体系架构下的Linux内存区域示意图与页表的建立流程
linux·学习·架构·内存管理
Caron_xcb2 小时前
大数据——解决Matplotlib 字体不足问题(Linux\mac\windows)
大数据·linux·matplotlib
水水沝淼㵘2 小时前
嵌入式开发学习日志(数据结构--顺序结构单链表)Day19
linux·服务器·c语言·数据结构·学习·算法·排序算法
愚润求学2 小时前
【Linux】基础 IO(一)
linux·运维·服务器·开发语言·c++·笔记
what_20182 小时前
分布式链路跟踪
java·运维·分布式
大白的编程日记.3 小时前
【Linux学习笔记】理解一切皆文件实现原理和文件缓冲区
linux·笔记·学习