12【进程间通信——管道】

发布于:2025-07-02 ⋅ 阅读:(35) ⋅ 点赞:(0)

1 IPC是什么 & 为什么要通信

1.1 什么是进程间通信

  进程间通信(Inter-Process Communication,IPC)是指两个或多个进程之间实现数据层面的交互。由于进程的天然独立性,通信的成本相对较高。

1.2 既然成本高为什么还要通信

因为很多场景下,进程之间必须协同工作,例如:

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

1.3 通信的本质:共享一块资源

  进程间通信的核心问题是:如何让两个进程看到同一块“资源”

  这块资源为什么不能由其中一个进程来提供?——> 因为这样会破坏进程独立性,这块资源必须由操作系统提供!!!

  本质上进程在访问 IPC,就是在访问操作系统,因为操作系统不信任用户进程,所以通信的整个生命周期 —— 创建、读写、释放 ——都必须通过系统调用接口来完成!

1.4 通信方式的分类

类型 方式 说明
基于文件的通信 匿名管道、命名管道 最基础、最常见的通信方式
System V IPC 消息队列、共享内存、信号量 较老但仍在广泛使用的传统机制
POSIX IPC 消息队列、共享内存、信号量、互斥量、条件变量、读写锁 接口更标准、更现代化、更易用

2 管道文件

2.1 管道的原理

  我们知道,一个文件可以被多个进程同时打开,这就具备了“公共资源”的特性。理论上,一个进程写、另一个进程读,就可以实现进程间通信。

  但是,如果我们用普通文件作为通信媒介,数据必须写入外设(如磁盘),这将带来严重的效率问题!

  于是,出现了“内存级文件”的概念——它像文件一样出现在文件系统中,但实际上只存在于内存,不会被刷写到磁盘。这类文件就是我们所说的:管道(pipe)。

  管道本质上是一种内核缓冲区(内存空间),通过“文件接口”封装出来,用户进程可以读写它,但数据只存在于内存中,从而避免频繁访问外设,提高通信效率。

2.2 匿名管道

问题1:为什么要用“父子进程”?
  在 Linux 中,子进程通过 fork() 从父进程复制而来,它会自动继承父进程的文件描述符表(fd表)。这就为“共享一块通信资源”提供了最简单的实现路径。

问题2:为什么不能用 open() 打开一个普通文件作为通信通道

  1. open()打开的是真实存在于磁盘上的文件,会有刷盘动作
  2. 普通文件不能只存在于内存中
  3. 操作系统无法区分你是“为了存储”还是“为了通信”

所以 Linux 提供了专门的接口 pipe(),用于创建一个 匿名管道(内存级文件)

匿名管道的特点:

  • pipe(int fd[2]) 创建,会自动生成两个文件描述符:
    • fd[0]: 读端(只读权限)
    • fd[1]: 写端(只写权限)
  • 是单向通信通道,默认不支持读写共用
  • 管道没有路径,不存在于磁盘中,只存在于内核内存
  • 通常在 fork() 之后,将一个端口交给子进程,一个端口保留在父进程,实现通信

直接看代码

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


#define N 2


using namespace std;

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;
    // VScode里面快速注释是ctrl + /
	// pipefd[0]: 3, pipefd[1]: 4
    return 0;
}

makefile文件

testPipe:testPipe.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f testPipe

main.c文件:

#include <iostream>
#include <unistd.h>    // pipe 、close、fork、write的头文件
#include <stdlib.h>    // exit 的头文件
#include <string.h>    // 字符串的头文件
#include <stdio.h>     // snprintf的头文件
#include <sys/types.h> // wait的头文件
#include <sys/wait.h>


#define N 2
#define NUM 1024

using namespace std;

// child
void Writer(int wfd)
{
    string s = "hello, I am child";
    pid_t self = getpid();
    int number = 0;

    char buffer[NUM];   // 定义的缓冲区
    while(true)
    {
        // 构建发送字符串
        buffer[0] = 0;  // 字符串清空,提醒阅读代码的人,我把这个数组当字符串了

        // int snprintf(char *str, size_t size, const char *format, ...);
        // 向哪个字符串写,写多大,什么形式写进去
        // s.c_str():将 std::string 转为 C 风格字符串(const char*)
        snprintf(buffer, sizeof(buffer),"%s-%d-%d", s.c_str(), self, number++);
        // cout << buffer << endl;

        // 发送/写入给父进程    man 2 write 查2号手册write
        // ssize_t write(int fd, const void *buf, size_t count);
        write(wfd, buffer, strlen(buffer));   // 这里不加一,\0不需要往里面写入

        sleep(1);
    }
}

// father
void Reader(int rfd)
{
    char buffer[NUM];

    while(true)
    {
        buffer[0] = 0;
        // ssize_t read(int fd, void *buf, size_t count);  查2号手册 man 2 read
        ssize_t n = read(rfd, buffer, sizeof(buffer));
        if(n > 0)
        {
            buffer[n] = 0;   // 我是当字符串用,必须\0结尾,这里0等于 '\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;

    pid_t id = fork();
    if(id < 0) return 2;

    // chile -> w  father -> r
    if(id == 0)
    {
        // 子进程
        close(pipefd[0]);

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


        // 写完后,写窗口也要关掉
        close(pipefd[1]);

        exit(0);  // 子进程写完后退出
    }
    // father
    close(pipefd[1]);

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

    pid_t rid = waitpid(id, nullptr, 0);  // 回收子进程

    if(rid < 0) return 3;

    // 读完成后,读窗口也关闭
    close(pipefd[0]);
    return 0;
}

在这里插入图片描述

2.3 匿名管道的特征

  1. 血缘关系限定:只能用于父子(或兄弟)进程通信
  2. 单向通信:一个管道只能实现单向流动;要双向通信需建立两个管道
  3. 进程协同机制:Linux 自动实现同步和互斥,保证管道数据安全
  4. 字节流特性:管道是面向字节流的,系统不关心你写了几次,它只看缓冲区里的字节
  5. 原子性写入保证:系统会确保在写入量不超过 PIPE_BUF 时,父进程不会来读
  6. 生命周期与进程绑定:匿名管道与进程生命周期一致,进程退出后管道自动销毁。

2.4 管道中的四种关键状态

情况 描述
① 读写正常,管道空 读端阻塞,防止读到无效数据
② 读写正常,管道满 写端阻塞,防止数据覆盖
③ 写端关闭,读端读 读到返回值为 0,代表 EOF,不阻塞
④ 读端关闭,写端写 写操作被操作系统发送 SIGPIPE 信号终止

2.5 命名管道

  匿名管道只能用于有血缘关系的进程,而两个毫无关系的进程通信就需要命名管道。Linux 提供的命名管道(Named Pipe / FIFO),创建方式如下:

mkfifo /tmp/myfifo

命名管道的特点:

  1. 具有路径名,出现在文件系统中
  2. 使用 open() 打开,进程通过同一路径访问同一个管道文件
  3. 同样是内存级通信,不会刷盘
  4. 适合非父子进程之间的通信
  5. 内核内部只开辟一块缓冲区,不论多少进程打开,只维护一份数据

问题1:两个无血缘进程如何确保访问的是同一个命名管道?
答:只要访问的路径+文件名相同,就会访问同一个命名管道。

问题 2:多个进程同时打开同一个命名管道,操作系统会创建多个副本吗?
答:不会。操作系统内部只维护一个文件对象 + 一个缓冲区


网站公告

今日签到

点亮在社区的每一天
去签到