Linux——进程间通信初解(匿名管道与命名管道)

进程间通信

进程间通信

进程通信的概念

在Linux中,进程间通信(IPC,Interprocess Communication)是指多个进程之间进行数据交换和同步的机制。由于进程拥有独立的地址空间,无法直接访问彼此的内存,因此需要借助操作系统提供的IPC机制来实现通信。

本质上通信就是让不同的进程看到同一份资源,然后对其进程读写操作获取或者写入数据。

进程间通信的主要方式

Linux中常见的进程间通信方式包括以下几种:

管道(Pipe)

匿名管道:用于具有亲缘关系的进程间通信(如父子进程)。数据单向流动,一端写,另一端读。

命名管道(FIFO):允许无亲缘关系的进程通过文件系统中的一个特殊文件进行通信。

消息队列(Message Queue)

消息队列允许进程通过发送和接收消息来通信,消息可以按类型区分,支持异步通信。

共享内存(Shared Memory)

多个进程共享同一块内存区域,通信速度最快,但需要同步机制(如信号量)来避免竞争条件。

信号量(Semaphore)

用于进程间的同步,通常与共享内存结合使用,防止多个进程同时访问共享资源。

信号(Signal)

用于通知进程发生了某种事件,是一种异步通信机制,常用于处理异常或中断。

套接字(Socket)

可用于不同主机或同一主机上的进程间通信,支持网络通信和本地通信。

文件(File)

进程可以通过读写文件来交换数据,但效率较低,通常用于简单的通信场景。

选择依据

选择IPC方式时,需考虑以下因素:

通信关系:是否有亲缘关系。

通信方向:单向还是双向。

数据量:大数据量适合共享内存或套接字。

同步需求:是否需要同步机制。

性能要求:共享内存性能最高,文件性能最低。

匿名管道

管道的概念

管道(Pipe)是Linux中一种最基本的进程间通信(IPC)机制,主要用于具有亲缘关系的进程(如父子进程)之间的通信。管道是单向的 ,数据只能从一端写入,从另一端读取。(重复:单向管道

管道的本质

管道的本质:内核中的环形缓冲区

匿名管道的本质是 内核中的一块内存缓冲区,这块缓冲区是一个 环形队列(Circular Buffer),用于存储进程间传递的数据。环形队列的特点是:

  • 数据以 先进先出(FIFO) 的方式存储和读取。

  • 缓冲区的大小是固定的(通常为 64KB)。

  • 内核通过维护 读指针 和 写指针 来管理数据的读写。

环形缓冲区的工作原理

写操作:

  • 数据从写指针位置开始写入。

  • 写指针向前移动,如果到达缓冲区末尾,则绕回到缓冲区开头。

  • 如果缓冲区已满,写操作会阻塞,直到有空间可用。

读操作:

  • 数据从读指针位置开始读取。

  • 读指针向前移动,如果到达缓冲区末尾,则绕回到缓冲区开头。

  • 如果缓冲区为空,读操作会阻塞,直到有数据可读。

文件描述符的本质

在 Linux 中,文件描述符(File Descriptor, FD) 是一个非负整数,用于标识一个打开的文件或 I/O 资源。每个进程都有一个文件描述符表,用于记录该进程打开的文件或资源。

管道的文件描述符

当调用 pipe(fd) 时,内核会:

创建一个环形缓冲区。

分配两个文件描述符:

  • fd[0]:读端。

  • fd[1]:写端。

将这两个文件描述符与环形缓冲区关联。

文件描述符的本质是对内核资源的引用,通过它可以访问内核中的缓冲区。

内核如何管理管道

内核数据结构

在内核中,管道是通过以下数据结构实现的:

struct pipe_inode_info:

这是管道的核心数据结构,包含环形缓冲区的元信息,如:

  • 缓冲区的大小。

  • 读指针和写指针的位置。

  • 等待队列(用于阻塞的读写操作)。

文件

	struct file:

每个文件描述符对应一个 struct file 对象,其中包含:

  • 指向 pipe_inode_info 的指针。

  • 文件的打开模式(读或写)。

内核的工作流程

创建管道:

调用 pipe(fd) 时,内核会:

1,分配一个 pipe_inode_info 结构。

2,创建两个 struct file 对象,分别关联到 fd[0] 和 fd[1]。

3,将这两个文件描述符返回给用户进程。

写数据:

当进程调用 write(fd[1], buffer, size) 时:

  • 内核检查环形缓冲区是否有足够的空间。

  • 如果有空间,将数据从用户空间拷贝到内核缓冲区。

  • 更新写指针。

  • 如果缓冲区已满,进程会阻塞,直到有空间可用。

读数据:

当进程调用 read(fd[0], buffer, size) 时:

  • 内核检查环形缓冲区是否有数据可读。

  • 如果有数据,将数据从内核缓冲区拷贝到用户空间。

  • 更新读指针。

  • 如果缓冲区为空,进程会阻塞,直到有数据可读。

关闭管道:

当进程调用 close(fd[0]) 或 close(fd[1]) 时:

  • 内核释放对应的文件描述符。

  • 如果没有进程再使用管道,内核会释放环形缓冲区。

管道的阻塞与非阻塞

阻塞模式

默认情况下,管道的读写操作是阻塞的:

  • 如果缓冲区为空,读操作会阻塞。

  • 如果缓冲区已满,写操作会阻塞。

非阻塞模式

可以通过 fcntl() 将文件描述符设置为非阻塞模式:

  • 如果缓冲区为空,读操作立即返回 -1,并设置 errno 为 EAGAIN。

  • 如果缓冲区已满,写操作立即返回 -1,并设置 errno 为 EAGAIN。

管道的局限性

单向通信:

  • 管道是单向的,只能一端写,另一端读。如果需要双向通信,需要创建两个管道。

亲缘关系:

  • 匿名管道只能用于具有亲缘关系的进程(如父子进程)。

缓冲区大小有限:

  • 管道的缓冲区大小通常为 64KB,超过后会阻塞写操作。

数据无格式:

  • 管道传输的是字节流,没有消息边界。如果需要结构化数据,需要额外处理。

代码实例

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main() {
    int fd[2]; // 文件描述符数组
    pid_t pid; // 进程ID
    char buffer[100]; // 用户空间缓冲区

    // 创建管道
    if (pipe(fd) == -1) {
        perror("pipe"); // 如果失败,打印错误信息
        return 1;
    }

    // 创建子进程
    pid = fork();
    if (pid < 0) {
        perror("fork"); // 如果失败,打印错误信息
        return 1;
    }

    if (pid == 0) { // 子进程
        close(fd[1]); // 关闭写端
        read(fd[0], buffer, sizeof(buffer)); // 从管道读取数据
        printf("Child process received: %s\n", buffer); // 打印数据
        close(fd[0]); // 关闭读端
    } else { // 父进程
        close(fd[0]); // 关闭读端
        write(fd[1], "Hello from parent process!", 26); // 向管道写入数据
        close(fd[1]); // 关闭写端
        wait(NULL); // 等待子进程结束
    }

    return 0;
}

命名管道

概念

命名管道(Named Pipe),也称为 FIFO(First In First Out),是一种特殊的文件类型,用于进程间通信(IPC)。与匿名管道不同,命名管道:

  • 有一个文件系统中的路径名,可以被无亲缘关系的进程访问。

  • 数据以先进先出的方式传输。

  • 既可以用于本地进程间通信,也可以用于网络通信(通过文件系统共享)。

命名管道的定义方式

(1)在 Bash 中创建命名管道

在 Bash 中,可以使用 mkfifo 命令创建命名管道:

bash 复制代码
mkfifo /tmp/my_fifo

/tmp/my_fifo 是命名管道的路径。

创建后,可以通过文件操作(如 cat、echo)使用命名管道。

示例:

bash 复制代码
# 终端 1:写入数据
echo "Hello from terminal 1" > /tmp/my_fifo


# 终端 2:读取数据
cat /tmp/my_fifo

(2)在代码中创建命名管道

在 C 代码中,可以使用 mkfifo() 函数创建命名管道:

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

mkfifo("/tmp/my_fifo", 0666);
  • /tmp/my_fifo 是命名管道的路径。

  • 0666 是权限模式,表示所有用户可读写。

命名管道的本质

(1)文件系统中的特殊文件

  • 命名管道在文件系统中表现为一个特殊文件。

  • 它不存储实际数据,而是作为进程间通信的桥梁。

(2)内核中的缓冲区

  • 命名管道在内核中也是一个环形缓冲区,与匿名管道类似。

  • 数据以先进先出的方式传输。

(3)文件描述符

  • 进程通过 open() 打开命名管道,获取文件描述符。

  • 通过文件描述符进行读写操作。

命名管道与匿名管道的不同

特性 命名管道(FIFO) 匿名管道(Pipe)
文件系统可见性 是,有一个路径名 否,仅存在于内核中
进程关系 可用于无亲缘关系的进程 仅用于具有亲缘关系的进程
创建方式 通过 mkfifo 命令或 mkfifo() 函数 通过 pipe() 系统调用
持久性 持久存在,直到被删除 随进程结束而销毁
使用场景 本地或网络进程间通信 父子进程或兄弟进程间通信
通信方向 单向或双向(需创建两个 FIFO) 单向
缓冲区大小 通常为 64KB 通常为 64KB
阻塞行为 默认阻塞,可设置为非阻塞 默认阻塞,可设置为非阻塞
内核实现 通过 struct inodestruct file 管理 通过 pipe_inode_info 管理
适用性 适用于无亲缘关系的进程 适用于有亲缘关系的进程
删除方式 使用 unlink()rm 命令删除 随进程结束自动销毁

命名管道的原理与底层解读

(1)内核数据结构

  • 命名管道在内核中通过 struct inode 和 struct file 管理。

  • 数据存储在环形缓冲区中,与匿名管道类似。

(2)读写操作
写操作:

  • 如果管道为空,写操作会阻塞,直到有进程读取数据。

  • 如果管道已满,写操作会阻塞,直到有空间可用。

读操作:

  • 如果管道为空,读操作会阻塞,直到有进程写入数据。

  • 如果管道有数据,读操作会立即返回数据。

(3)阻塞与非阻塞模式

  • 默认情况下,命名管道的读写操作是阻塞的。

  • 可以通过 open() 的 O_NONBLOCK 标志设置为非阻塞模式:

  • 非阻塞模式下,读操作立即返回(即使没有数据)。

  • 非阻塞模式下,写操作立即返回(即使没有空间)。

通信案例

(1)Bash 示例

终端 1:写入数据

bash 复制代码
echo "Hello from terminal 1" > /tmp/my_fifo

终端 2:读取数据

bash 复制代码
cat /tmp/my_fifo

(2)C 代码示例

写入进程(writer.c)

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    int fd;
    const char *fifo_path = "/tmp/my_fifo";
    const char *message = "Hello from writer process!";

    // 打开命名管道(写模式)
    fd = open(fifo_path, O_WRONLY);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // 向命名管道写入数据
    if (write(fd, message, strlen(message) + 1) == -1) {
        perror("write");
        close(fd);
        exit(EXIT_FAILURE);
    }

    printf("Writer: Data written to FIFO.\n");
    close(fd);
    return 0;
}

读取进程(reader.c)

c 复制代码
复制
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd;
    const char *fifo_path = "/tmp/my_fifo";
    char buffer[100];

    // 打开命名管道(读模式)
    fd = open(fifo_path, O_RDONLY);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // 从命名管道读取数据
    if (read(fd, buffer, sizeof(buffer)) == -1) {
        perror("read");
        close(fd);
        exit(EXIT_FAILURE);
    }

    printf("Reader: Data read from FIFO: %s\n", buffer);
    close(fd);
    return 0;
}

运行步骤:

(1)创建命名管道:

bash 复制代码
mkfifo /tmp/my_fifo

(2)编译代码:

bash 复制代码
gcc writer.c -o writer
gcc reader.c -o reader

(3)运行读取进程(会阻塞,等待数据):

bash 复制代码
./reader

(4)运行写入进程:

bash 复制代码
./writer

(5)输出:

Writer: Data written to FIFO.
Reader: Data read from FIFO: Hello from writer process!
相关推荐
GGGGGGGGGGGGGG.1 小时前
使用dockerfile创建镜像
java·开发语言
路由侠内网穿透2 小时前
本地部署资源聚合搜索神器 Jackett 并实现外部访问
linux·运维·服务器·网络协议·tcp/ip
兮动人2 小时前
SpringBoot加载配置文件的优先级
java·spring boot·后端·springboot加载配置
我爱Jack2 小时前
HttpServletRequest 和 HttpServletResponse 区别和作用
java·spring·mvc
yyueshen2 小时前
volatile 在 JVM 层面的实现机制
java·jvm
慕容魏2 小时前
入门到入土,Java学习 day16(算法1)
java·学习·算法
认真的小羽❅2 小时前
动态规划详解(二):从暴力递归到动态规划的完整优化之路
java·算法·动态规划
m0_748254662 小时前
Spring Boot 热部署
java·spring boot·后端
啥都想学的又啥都不会的研究生2 小时前
Redis设计与实现-服务器中的数据库
运维·服务器·数据库·redis·笔记·缓存·性能优化
mango02192 小时前
SpringMVC
java