当前位置: 首页 > news >正文

进程间通信(IPC)与匿名管道

目录

一、进程间通信(IPC)概述

1. 核心概念

2. 核心目的

3. IPC分类

二、匿名管道

1. 什么是管道

示例:Shell中的管道

2. 匿名管道的原理

3. 匿名管道的实现

3.1 创建管道:pipe()函数

3.2 使用 fork 共享管道

3.3 站在文件描述符角度理解管道

3.4 示例代码 

4. 匿名管道的核心特性与规则

4.1 读写规则

4.2 四种特殊情况

🌵情况 1:写端不写,读端一直读

🌵情况 2:读端不读,写端一直写

🌵情况 3:写端关闭后

🌵情况 4:读端关闭后

4.3 五种特征

🌴特征1:血缘关系限制

🌴特征2:流式服务

🌴特征3:生命周期随进程

🌴特征4:内核同步与互斥

🌴特征5:半双工通信


一、进程间通信(IPC)概述

1. 核心概念

        进程间通信(Interprocess Communication, IPC)是操作系统提供的一种机制,用于不同进程之间的数据交换与协作。由于每个进程拥有独立的虚拟地址空间,直接访问对方内存不可行,因此需要通过操作系统提供的共享资源实现通信。其本质是让不同进程看到同一份资源(如内存缓冲区、文件或内核数据结构)

2. 核心目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程,如客户端与服务器间的请求响应。

  • 资源共享:多个进程共享同一文件或内存区域。

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

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

3. IPC分类

类型具体实现
管道匿名管道、命名管道(FIFO)
System V IPCSystem V 消息队列、System V 共享内存、System V 信号量
POSIX IPC消息队列、共享内存、信号量、互斥量、条件变量、读写锁
其他机制信号(Signal)、套接字(Socket)、内存映射文件(Memory-Mapped Files)

二、匿名管道

1. 什么是管道

        管道是 UNIX 中最古老的进程间通信方式。它是一种半双工的通信机制,本质是内核维护的环形缓冲区,数据只能单向流动。管道通过将一个进程的标准输出连接到另一个进程的标准输入,实现数据的传递。

示例:Shell中的管道

who | wc -l  # 统计登录用户数

who命令的输出通过管道传递给 wc处理,实现进程间协作。


2. 匿名管道的原理

        匿名管道用于本地父子进程之间的通信。其原理是让父子进程共享同一文件资源,通过操作系统维护的文件描述符实现通信。匿名管道的数据存储在内存中,不会写入磁盘。

  1. 父进程和子进程的 task_struct

    • 每个进程都有一个 task_struct,用于描述进程的状态和资源。

    • 父进程和子进程各自有自己的 task_struct,但它们可以通过共享的文件描述符表进行通信。

  2. 文件描述符表 files_structfd_array

    • files_struct 是与进程相关的文件描述符表,管理进程打开的文件或管道。

    • fd_array 是一个数组,存储了文件描述符(file descriptor),每个文件描述符对应一个打开的文件或管道。

    • 父进程和子进程的 fd_array 都指向了同一个 struct file,这表明它们共享了同一份文件资源(例如管道)。

  3. 文件结构 struct fileinode

    • struct file 是 Linux 内核中表示打开文件的结构体。

    • inode 是文件系统中用于存储文件元数据的数据结构,描述了文件在文件系统中的位置和属性。

  4. 内核级文件缓冲区

    • 内核中有一个文件缓冲区,用于缓存文件数据,提高文件读写效率。

    • 当进程通过文件描述符读写文件时,数据会先从磁盘加载到文件缓冲区,再由进程从缓冲区中读取或写入。

  5. 磁盘文件 file.txt

    • 磁盘上的文件 file.txt 是实际存储数据的地方。

    • 当进程通过文件描述符读写文件时,数据会先从磁盘加载到文件缓冲区,再由进程从缓冲区中读取或写入。

  6. 父子进程共享资源

    • 父进程和子进程通过共享的文件描述符和文件系统结构(如 struct fileinode)访问同一份数据。

    • 这种机制是管道通信的基础:父进程和子进程通过共享的管道文件描述符进行数据交换。

  7. 内核管理

    • 内核负责管理管道的缓冲区和同步机制,确保数据的正确传递。

    • 数据在内核缓冲区中传输,不需要刷新到磁盘,因此效率较高。

3. 匿名管道的实现

3.1 创建管道:pipe()函数

pipe 函数是用于创建匿名管道的系统调用,其函数原型如下:

#include <unistd.h>

int pipe(int pipefd[2]);  // pipefd[0]为读端,pipefd[1]为写端

🌻参数说明:

  • pipefd:这是一个指向整数数组的指针,数组必须能够容纳至少两个元素。pipe 函数会填充这个数组,使其包含两个文件描述符:

    • pipefd[0]:管道的读端文件描述符(用于从管道中读取数据)。

    • pipefd[1]:管道的写端文件描述符(用于向管道中写入数据)。


🌻返回值:

  • 成功:返回 0,并且 pipefd 数组被填充为两个有效的文件描述符。

  • 失败:返回 -1,表示创建管道失败。此时,pipefd 数组的内容是未定义的。可以通过 errno 获取具体的错误原因。


🌻注意事项:

  1. 文件描述符的使用

    • pipefd[0] 通常用于读取数据,应该在读端进程中使用。

    • pipefd[1] 通常用于写入数据,应该在写端进程中使用。

  2. 关闭不需要的文件描述符

    • 在父子进程通信时,父进程和子进程需要分别关闭不需要的文件描述符。例如,父进程关闭写端文件描述符(pipefd[1]),子进程关闭读端文件描述符(pipefd[0])。

  3. 文件描述符的继承

    • 当父进程调用 fork() 创建子进程时,子进程会继承父进程的文件描述符表,包括管道的两个文件描述符。

3.2 使用 fork 共享管道

1. 父进程调用pipe()创建管道。

2. 调用fork()创建子进程。

3. 父子进程分别关闭未使用的端(父关写端,子关读端)。

3.3 站在文件描述符角度理解管道

1. 父进程创建管道

2. 父进程 fork 出子进程

3. 父进程关闭 fd[0],子进程关闭 fd[1]

3.4 示例代码 

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

int main()
{
    int pipefd[2]; // 用于存储管道的读端和写端文件描述符
    pid_t pid;

    // 创建匿名管道
    if (pipe(pipefd) == -1)
    {
        perror("pipe");
        exit(1);
    }

    // 创建子进程
    pid = fork();
    if (pid == -1)
    {
        perror("fork");
        exit(1);
    }

    if (pid == 0)
    {
        // 子进程
        close(pipefd[0]); // 关闭读端
        const char *msg = "Hello from child!";
        int count = 5;
        while (count--)
        {
            write(pipefd[1], msg, strlen(msg) + 1); // 向管道写入数据
        }
        close(pipefd[1]); // 关闭写端
        exit(0);
    }
    else
    {
        // 父进程
        close(pipefd[1]); // 关闭写端
        char buffer[128];
        ssize_t n = read(pipefd[0], buffer, sizeof(buffer) - 1); // 从管道读取数据
        for (int i = 0; i < 5; i++)
        {

            if (n > 0)
            {
                buffer[n] = '\0'; // 确保字符串以 '\0' 结尾
                printf("Received from child: %s\n", buffer);
            }
            else if (n == 0)
            {
                printf("No data received.\n");
            }
            else
            {
                perror("read");
            }
        }
        close(pipefd[0]); // 关闭读端
        wait(NULL);       // 等待子进程结束
    }

    return 0;
}

代码解释: 

🌵创建匿名管道

if (pipe(pipefd) == -1) {
    perror("pipe");
    exit(1);
}
  • pipe(pipefd) 创建一个匿名管道,pipefd[0] 是读端文件描述符,pipefd[1] 是写端文件描述符。

🌵创建子进程

pid = fork();
if (pid == -1) {
    perror("fork");
    exit(1);
}
  • fork() 创建一个子进程。pid 为 0 表示子进程,pid 为正数表示父进程。

🌵子进程写入数据

if (pid == 0) {
    close(pipefd[0]); // 关闭读端
    const char *msg = "Hello from child!";
    write(pipefd[1], msg, strlen(msg) + 1); // 向管道写入数据
    close(pipefd[1]); // 关闭写端
    exit(0);
}
  • 子进程关闭读端文件描述符 pipefd[0],只保留写端文件描述符 pipefd[1]

  • 子进程通过 write() 函数将数据写入管道。

  • 写入完成后,子进程关闭写端文件描述符并退出。

🌵父进程读取数据

else {
    close(pipefd[1]); // 关闭写端
    char buffer[128];
    read(pipefd[0], buffer, sizeof(buffer)); // 从管道读取数据
    printf("Received from child: %s\n", buffer);
    close(pipefd[0]); // 关闭读端
    wait(NULL); // 等待子进程结束
}
  • 父进程关闭写端文件描述符 pipefd[1],只保留读端文件描述符 pipefd[0]

  • 父进程通过 read() 函数从管道中读取数据。

  • 读取完成后,父进程打印从子进程接收到的数据,并关闭读端文件描述符。

  • 父进程调用 wait(NULL) 等待子进程结束,确保子进程的资源被正确回收。

运行结果:


4. 匿名管道的核心特性与规则

4.1 读写规则

1. 当没有数据可读时

  • O_NONBLOCK 禁用(默认阻塞模式)

    • 如果管道中没有数据,read() 调用会阻塞,即进程会暂停执行,直到有数据可读为止。

  • O_NONBLOCK 启用(非阻塞模式)

    • 如果管道中没有数据,read() 调用会立即返回 -1,并设置 errnoEAGAIN,表示没有数据可读。

2. 当管道满的时候

  • O_NONBLOCK 禁用(默认阻塞模式)

    • 如果管道已满,write() 调用会阻塞,即进程会暂停执行,直到有空间可写为止。

  • O_NONBLOCK 启用(非阻塞模式)

    • 如果管道已满,write() 调用会立即返回 -1,并设置 errnoEAGAIN,表示没有空间可写。

3. 如果所有管道写端对应的文件描述符被关闭

  • 当所有写端文件描述符都被关闭时,read() 调用会返回 0,表示管道已经关闭,没有更多数据可读。

4. 如果所有管道读端对应的文件描述符被关闭

  • 当所有读端文件描述符都被关闭时,write() 操作会产生 SIGPIPE 信号,这通常会导致写进程退出。

5. 写入数据的原子性

  • 当写入的数据量不大于 PIPE_BUF

    • Linux 会保证写入操作的原子性,即写入的数据要么全部写入,要么都不写入,不会被其他进程的读写操作打断。

  • 当写入的数据量大于 PIPE_BUF

    • Linux 不再保证写入操作的原子性,数据可能会被拆分成多个部分写入,可能会被其他进程的读写操作打断。

🌴总结:

  • 阻塞模式默认情况下,read()write() 会阻塞,直到有数据可读或有空间可写。

  • 非阻塞模式启用 O_NONBLOCK 后,read()write() 会立即返回,不会阻塞。

  • 文件描述符关闭关闭写端文件描述符后,读端会返回 0;关闭读端文件描述符后,写端会产生 SIGPIPE 信号。

  • 原子性写入数据量不大于 PIPE_BUF 时,写入操作是原子的;大于 PIPE_BUF 时,写入操作可能不原子。

4.2 四种特殊情况

  1. 写端不写,读端一直读读端进程挂起,直到有数据可读。

  2. 读端不读,写端一直写写端进程挂起,直到有空间可写。

  3. 写端关闭后读端读取完数据后继续执行,不会挂起。

  4. 读端关闭后写端进程收到 SIGPIPE 信号,被操作系统终止。


🌵情况 1:写端不写,读端一直读

描述

  • 如果写端不写数据,而读端一直尝试读取数据,读端进程会挂起,直到有数据可读。

代码验证

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

int main()
{
    int pipefd[2];
    pid_t pid;

    if (pipe(pipefd) == -1)
    {
        perror("pipe");
        exit(1);
    }

    pid = fork();
    if (pid == -1)
    {
        perror("fork");
        exit(1);
    }

    if (pid == 0)
    {
        // 子进程:写端不写数据
        close(pipefd[0]); // 关闭读端
        close(pipefd[1]); // 关闭写端(不写数据)
        exit(0);
    }
    else
    {
        // 父进程:读端一直读
        close(pipefd[1]); // 关闭写端
        char buffer[128];
        printf("父进程尝试读取数据...\n");
        ssize_t n = read(pipefd[0], buffer, sizeof(buffer)); // 读取数据
        if (n > 0)
        {
            buffer[n] = '\0';
            printf("Received: %s\n", buffer);
        }
        else if (n == 0)
        {
            printf("没有数据可读,子进程关闭了写端。\n");
        }
        else
        {
            perror("read");
        }
        close(pipefd[0]); // 关闭读端
        wait(NULL);       // 等待子进程结束
    }

    return 0;
}

运行结果

解释

  • 子进程关闭了写端,没有写入数据。
  • 父进程尝试读取数据时,由于没有数据可读,read() 会阻塞,直到子进程关闭写端,父进程的 read() 返回 0。

🌵情况 2:读端不读,写端一直写

描述

  • 如果读端不读数据,而写端一直尝试写入数据,写端进程会挂起,直到有空间可写。

代码验证

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

int main()
{
    int pipefd[2];
    pid_t pid;

    if (pipe(pipefd) == -1)
    {
        perror("pipe");
        exit(1);
    }

    pid = fork();
    if (pid == -1)
    {
        perror("fork");
        exit(1);
    }

    if (pid == 0)
    {
        // 子进程:写端一直写
        close(pipefd[0]); // 关闭读端
        for (int i = 0; i < 5; i++)
        {
            char buffer[128];
            sprintf(buffer, "数据 %d\n", i + 1);
            write(pipefd[1], buffer, strlen(buffer) + 1); // 写入数据
            sleep(1);                                     // 模拟写入间隔
        }
        close(pipefd[1]); // 关闭写端
        exit(0);
    }
    else
    {
        // 父进程:读端不读
        close(pipefd[1]); // 关闭写端
        printf("父进程不读取数据...\n");
        sleep(6);         // 模拟父进程不读取数据
        close(pipefd[0]); // 关闭读端
        wait(NULL);       // 等待子进程结束
    }

    return 0;
}

运行结果

解释

  • 父进程关闭了读端,没有读取数据。
  • 子进程尝试写入数据时,由于管道已满,write() 会阻塞,直到父进程读取数据或关闭读端。

🌵情况 3:写端关闭后

描述

  • 如果写端关闭后,读端读取完数据后会继续执行,不会挂起。

代码验证

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

int main()
{
    int pipefd[2];
    pid_t pid;

    if (pipe(pipefd) == -1)
    {
        perror("pipe");
        exit(1);
    }

    pid = fork();
    if (pid == -1)
    {
        perror("fork");
        exit(1);
    }

    if (pid == 0)
    {
        // 子进程:写端写入数据后关闭
        close(pipefd[0]); // 关闭读端
        char buffer[128];
        sprintf(buffer, "子进程写入的数据\n");
        write(pipefd[1], buffer, strlen(buffer) + 1); // 写入数据
        close(pipefd[1]);                             // 关闭写端
        exit(0);
    }
    else
    {
        // 父进程:读端读取数据
        close(pipefd[1]); // 关闭写端
        char buffer[128];
        ssize_t n = read(pipefd[0], buffer, sizeof(buffer)); // 读取数据
        if (n > 0)
        {
            buffer[n] = '\0';
            printf("父进程读取到数据: %s\n", buffer);
        }
        else if (n == 0)
        {
            printf("写端已关闭,没有更多数据。\n");
        }
        else
        {
            perror("read");
        }
        close(pipefd[0]); // 关闭读端
        wait(NULL);       // 等待子进程结束
    }

    return 0;
}

运行结果

解释

  • 子进程写入数据后关闭写端。
  • 父进程读取数据后,read() 返回 0,表示写端已关闭,没有更多数据。

🌵情况 4:读端关闭后

描述

  • 如果读端关闭后,写端进程会收到 SIGPIPE 信号,导致写进程被操作系统终止。

代码验证

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

void handle_sigpipe(int sig)
{
    printf("收到 SIGPIPE 信号,写端被终止。\n");
    exit(1);
}

int main()
{
    int pipefd[2];
    pid_t pid;

    if (pipe(pipefd) == -1)
    {
        perror("pipe");
        exit(1);
    }

    signal(SIGPIPE, handle_sigpipe); // 捕获 SIGPIPE 信号

    pid = fork();
    if (pid == -1)
    {
        perror("fork");
        exit(1);
    }

    if (pid == 0)
    {
        // 子进程:写端写入数据
        close(pipefd[0]); // 关闭读端
        char buffer[128];
        sprintf(buffer, "子进程写入的数据\n");
        write(pipefd[1], buffer, strlen(buffer) + 1); // 写入数据
        close(pipefd[1]);                             // 关闭写端
        exit(0);
    }
    else
    {
        // 父进程:关闭读端
        close(pipefd[1]); // 关闭写端
        close(pipefd[0]); // 关闭读端
        wait(NULL);       // 等待子进程结束
    }

    return 0;
}

运行结果

解释

  • 父进程关闭了读端。
  • 子进程尝试写入数据时,由于读端已关闭,write() 会触发 SIGPIPE 信号,导致子进程被终止。

4.3 五种特征

  1. 血缘关系限制:只能用于具有共同祖先的进程。

  2. 流式服务:数据无明确分割,按顺序传输。

  3. 生命周期随进程:进程退出后,管道释放。

  4. 内核同步与互斥:内核保证管道操作的同步与互斥。

  5. 半双工通信:数据单向流动,双向通信需两个管道。


🌴特征1:血缘关系限制

含义

  • 匿名管道只能用于具有共同祖先的进程之间的通信,通常是父子进程。
  • 管道的文件描述符在创建时由父进程持有,子进程通过 fork() 继承父进程的文件描述符表,从而共享管道。

原因

  • 匿名管道的文件描述符只在创建它的进程及其子进程中有效。
  • 非亲缘进程无法共享匿名管道的文件描述符,因此无法使用匿名管道进行通信。

🌴特征2:流式服务

含义

  • 数据在管道中没有明确的分割,按顺序传输。
  • 读取数据时,数据会连续地从管道中流出,直到读取完毕。
  • 写入数据时,数据会连续地写入管道,直到写入完毕。

特点

  • 数据传输是连续的,没有固定的报文边界。
  • 读取数据时,可能一次读取多个数据块,直到管道中的数据被读取完毕。
  • 写入数据时,可能一次写入多个数据块,直到数据全部写入管道。

🌴特征3:生命周期随进程

含义

  • 匿名管道的生命周期与创建它的进程相关。
  • 当所有引用管道的进程退出或关闭文件描述符后,管道会被销毁。

特点

  • 管道的文件描述符在所有进程中关闭后,管道会被释放。
  • 如果父进程或子进程退出,管道的文件描述符会被关闭,管道会被销毁。

🌴特征4:内核同步与互斥

含义

  • 内核负责管理管道的同步与互斥,确保数据的正确传递。
  • 当多个进程访问同一管道时,内核会自动处理同步与互斥问题,防止数据竞争和不一致。

特点

  • 同步:确保数据按顺序传输,读取操作在写入操作之后进行。
  • 互斥:确保同一时间只有一个进程可以对管道进行读写操作,防止数据冲突。

🌴特征5:半双工通信

含义

  • 数据在管道中只能单向流动,即从写端到读端。
  • 如果需要双向通信,必须创建两个管道,每个管道负责一个方向的数据传输。

特点

  • 单向通信:数据只能从写端流向读端,不能反向传输。
  • 半双工:数据可以在两个方向上传输,但不能同时进行。需要两个管道来实现双向通信。

相关文章:

  • Java容器异常分析与恢复实战指南
  • 20250302小米13ultra删除照片后没有在回收站
  • OpenHarmony4.1-轻量与小型系统ubuntu开发环境
  • [原创](Modern C++)现代C++的关键性概念: 利用元素序列生成器(std::views::istream)提取字段
  • vulnhub靶场之【digitalworld.local系列】的bravery靶机
  • git命令学习记录
  • 第一章:5.前缀和
  • 基于大模型的脂肪栓塞综合征风险预测与综合治疗方案研究报告
  • unsloth报错FileNotFoundError: [WinError 3] 系统找不到指定的路径。
  • 从零开始:H20服务器上DeepSeek R1 671B大模型部署与压力测试全攻略
  • 2025付费进群系统PHP网站源码
  • HopRAG: Multi-Hop Reasoning for Logic-AwareRetrieval-Augmented Generation
  • 线程 -- 阻塞队列
  • UGUI 自动扩张的聊天气泡制作时的问题
  • 心智模式与企业瓶颈突破
  • 云原生(六十) | Web源码迁移部署
  • AI辅助学习vue第十四章
  • 从神经元到大语言模型及其应用
  • 【前端基础】1、HTML概述(HTML基本结构)
  • 系统架构设计师—计算机基础篇—文件管理
  • 如何建设网站安全管理制度/关键词优化精灵
  • 苏州代理记账/长沙靠谱的关键词优化
  • 缅甸做网站/线下推广渠道有哪些方式
  • 做网站的税率/湖南有实力seo优化
  • 东莞网站优化推广/网站代运营推广
  • 58同城网站官网/广告代发平台