目录

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

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]) 时:

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

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

    https://i-blog.csdnimg.cn/direct/519e703f24114dca81d308ec885ca288.png

管道的阻塞与非阻塞

阻塞模式

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

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

非阻塞模式

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

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

管道的局限性

单向通信:

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

亲缘关系:

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

缓冲区大小有限:

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

数据无格式:

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

代码实例

#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 命令创建命名管道:

mkfifo /tmp/my_fifo

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

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

示例:

# 终端 1:写入数据
echo "Hello from terminal 1" > /tmp/my_fifo


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

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

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

复制
#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:写入数据

echo "Hello from terminal 1" > /tmp/my_fifo

终端 2:读取数据

cat /tmp/my_fifo

(2) C 代码示例

写入进程(writer.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)

复制
#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)创建命名管道:

mkfifo /tmp/my_fifo

(2)编译代码:

gcc writer.c -o writer
gcc reader.c -o reader

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

./reader

(4)运行写入进程:

./writer

(5)输出:

Writer: Data written to FIFO.
Reader: Data read from FIFO: Hello from writer process!