前言

  在 Linux 系统中,管道是一种非常常用的进程间通信机制,它简单、高效,并且易于使用。本文将深入介绍 Linux 管道的原理、分类、特点以及使用方法,帮助读者更好地理解和应用管道在系统编程中的重要性。

一、管道文件

1、基本概念

  在 Linux 中,管道文件(Pipeline)是一种特殊的文件类型,用于在进程之间进行通信。管道文件被用来将一个进程的标准输出和另一个进程的标准输入连接起来,实现这两个进程之间的数据传输。管道文件有两种类型:匿名管道命名管道

2、匿名(无名)管道

  匿名管道是最常见的一种管道,通过命令行中的竖线符号 | 创建。例如:command1 | command2,这样可以将 command1 的输出直接传递给 command2 的输入。匿名管道只存在于相关进程的生命周期内,进程结束后管道也会自动关闭。

  • 特点:

    • 单向通信,数据从一个进程流向另一个进程。
    • 临时性,只在相关进程生命周期内存在。
    • 通常用于连接两个相关进程,例如 command1 | command2。
  • 使用注意事项:

    • 一次性的,一旦相关进程结束,管道就会关闭,数据会丢失。
    • 只能用于相关进程之间的通信,不能用于不相关进程之间的通信。

  在 Linux 中,可以使用一些常见的命令来演示匿名管道的使用,以实现进程之间的数据传输。以下是一个简单的示例,使用 ls 命令列出当前目录下的文件和子目录,然后使用 grep 命令过滤出包含特定字符的结果。

ls | grep "txt"

  在这个示例中,ls 命令列出当前目录下的文件和子目录的信息,并将结果通过匿名管道传递给 grep 命令。grep 命令会过滤出包含 “txt” 字符串的结果,然后将结果输出到标准输出。这样就实现了通过匿名管道连接两个命令,实现数据传输和处理的功能。

  你可以尝试在终端中执行上述命令,查看输出结果。这种使用匿名管道的方式可以帮助简化多个命令之间的数据传递和处理,提高系统管理和脚本编写的效率。当然,除了 lsgrep,你还可以结合其他命令来展示匿名管道的强大功能和灵活性。

3、命名(有名)管道

  命名管道也称为 FIFO(First In, First Out),它是一种特殊类型的文件,通过 mkfifo 命令创建。命名管道可以持久存在于文件系统中,并允许不相关的进程之间进行通信。命名管道通过文件系统中的路径名进行访问,进程可以像访问普通文件一样读写数据。

  • 特点:

    • 单向或双向通信,可持久存在于文件系统中,多个进程之间进行通信。
    • 可用于不相关进程之间的通信。
    • 通过 mkfifo 命令创建。
  • 使用注意事项:

    • 需要注意文件权限和文件系统容量问题。
    • 小心处理管道阻塞的情况,避免死锁问题。

创建命名管道的步骤如下:

  • 使用 mkfifo 命令创建命名管道文件:mkfifo pipe_file
  • 使用不同的进程分别打开该管道文件进行读写操作

  命名管道通常用于需要持久存在的进程通信,比如两个独立的进程之间需要进行数据交换,但又不需要建立像 TCP/IP 连接那样的网络通信。

4、管道的特点

  1. 单向通信: 管道是一种单向通信方式,数据从一个进程流向另一个进程。在匿名管道中,数据只能从管道的写入端流向读取端;在命名管道中,数据也是单向传输的,但可以支持双向通信(半双工)。

  2. 进程之间通信: 管道主要用于实现进程之间的通信,通过将一个进程的标准输出重定向到管道,另一个进程的标准输入重定向到同一管道,实现数据的传递。

  3. 数据流传输: 管道是一种数据流传输方式,数据以流的形式从一个进程传输到另一个进程,适合于需要连续处理数据的场景。

  4. 临时性: 匿名管道是临时的,只存在于相关进程的生命周期内(一般用于父子进程);而命名管道是持久的,可以在文件系统中保留,允许不相关的进程之间进行通信。

  5. 有限缓冲: 管道的数据传输有限制缓冲区大小,当缓冲区满时,写入端会被阻塞,直到读端读取数据,释放缓冲区空间。

  6. 阻塞式通信: 当管道中没有数据可读时,读取进程会被阻塞;当管道满时,写入进程也会被阻塞,直到有空间写入数据。

  7. 操作简便: 通过管道可以快速简便地实现进程间的数据传输,无需繁琐的网络连接设置或文件读写操作。

  8. 常用于进程调用链: 管道经常用于构建进程调用链,将多个命令通过管道连接起来,实现复杂任务的处理。

  总的来说,管道是 Linux 系统中非常实用的进程间通信机制,具有简单、高效、快速的特点,适用于一些需要实时数据传输并且不需要长期存储的场景。通过合理地使用管道,可以有效提升进程之间的通信效率和数据处理能力。

5、思考:何时只能使用无名管道,何时又只能用有名管道?

无名管道(匿名管道)适用的情况:

  1. 用于父子进程通信: 无名管道通常用于在父子进程之间进行通信,因为无名管道是一种特殊的文件描述符,只能用于有亲缘关系的进程之间通信。
  2. 单向通信: 无名管道是单向的,只能在一个方向上传输数据,通常用于实现单向数据传输的场景,比如父进程向子进程传输数据。
  3. 临时传输数据: 无名管道通常被用作临时通道,进程间传输少量数据,常用于将一个命令的输出传递给另一个命令进行处理。

有名管道(命名管道)适用的情况:

  1. 用于任意进程间通信: 有名管道不仅可以用于有亲缘关系的进程通信,还可以用于任意进程间通信,只要它们可以访问同一个管道文件。
  2. 双向通信: 有名管道支持双向通信,进程可以通过同一个管道文件来实现双向数据传输。
  3. 永久传输数据: 有名管道创建后会生成一个文件节点,直到被明确删除前一直存在,因此适用于需要长期或者反复传输数据的场景。

综上所述,无名管道适合用于父子进程之间的单向临时通信,有名管道则适合用于任意进程之间的双向永久通信。根据具体的需求和场景选择合适的管道类型可以更有效地实现进程间的数据交换和通信。

二、相关API介绍

  在 Linux 系统中,管道文件相关的 API 主要包括 pipe()mkfifo()open()read()write() 等函数。以下是这些 API 的详细解释:

1. int pipe(int pipefd[2])

  • 功能: 创建一个匿名管道,并返回两个文件描述符,pipefd[0] 用于读取数据,pipefd[1] 用于写入数据。
  • 参数: pipefd 是一个整型数组,用于存放管道的文件描述符。
  • 返回值: 若成功,返回 0;若失败,返回 -1。
  • 示例:
    int fd[2];
    if (pipe(fd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
    

2. int mkfifo(const char *pathname, mode_t mode)

  • 功能: 创建一个命名管道(FIFO)。
  • 参数:
    • pathname 是要创建的命名管道的路径名。
    • mode 是文件的权限模式。
  • 返回值: 若成功,返回 0;若失败,返回 -1。
  • 示例:
    if (mkfifo("/tmp/myfifo", 0666) == -1) {
        perror("mkfifo");
        exit(EXIT_FAILURE);
    }
    

3. int open(const char *pathname, int flags)

  • 功能: 打开一个管道文件,返回文件描述符。
  • 参数:
    • pathname 是要打开的管道文件的路径名。
    • flags 指定打开文件的方式,如 O_RDONLY(只读)和 O_WRONLY(只写)。
  • 返回值: 若成功,返回一个新的文件描述符;若失败,返回 -1。
  • 示例:
    int fd = open("/tmp/myfifo", O_WRONLY);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }
    

4. ssize_t read(int fd, void *buf, size_t count)

  • 功能: 从管道文件中读取数据。
  • 参数:
    • fd 是管道文件的文件描述符。
    • buf 是用于存放读取数据的缓冲区。
    • count 是要读取的字节数。
  • 返回值: 若成功,返回实际读取的字节数;若到达文件末尾,返回 0;若失败,返回 -1。
  • 示例:
    char buffer[100];
    ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
    

5. ssize_t write(int fd, const void *buf, size_t count)

  • 功能: 向管道文件中写入数据。
  • 参数:
    • fd 是管道文件的文件描述符。
    • buf 是要写入的数据。
    • count 是要写入的字节数。
  • 返回值: 若成功,返回实际写入的字节数;若失败,返回 -1。
  • 示例:
    char *msg = "Hello, world!";
    ssize_t bytes_written = write(fd, msg, strlen(msg));
    

  以上是使用管道文件相关 API 的基本介绡,开发者在创建、打开、读写管道文件时可以结合这些函数来完成必要的操作。在实际编程过程中,需要注意处理错误情况,确保程序的稳定性和可靠性。

6、示例代码

以下是一个简单的示例代码,展示如何在两个进程之间使用管道文件进行通信。首先创建一个命名管道文件,然后通过 fork() 创建子进程,父进程向管道中写入数据,子进程从管道中读取数据,并打印输出。具体代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

#define FIFO_PATH "/tmp/myfifo"

int main() {
    // 创建命名管道文件
    if (mkfifo(FIFO_PATH, 0666) == -1) {
        perror("mkfifo");
        exit(EXIT_FAILURE);
    }

    pid_t pid = fork();

    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    
    if (pid > 0) {
        // 父进程写入数据到管道
        int fd = open(FIFO_PATH, O_WRONLY);
        if (fd == -1) {
            perror("open");
            exit(EXIT_FAILURE);
        }

        char *msg = "Hello, child process!";
        if (write(fd, msg, strlen(msg)) == -1) {
            perror("write");
            exit(EXIT_FAILURE);
        }

        close(fd);
    } else if (pid == 0) {
        // 子进程从管道读取数据
        int fd = open(FIFO_PATH, O_RDONLY);
        if (fd == -1) {
            perror("open");
            exit(EXIT_FAILURE);
        }

        char buffer[100];
        ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
        if (bytes_read == -1) {
            perror("read");
            exit(EXIT_FAILURE);
        }

        buffer[bytes_read] = '\0';  // 添加字符串结束符
        printf("Child process received: %s\n", buffer);

        close(fd);
    }

    return 0;
}

  在此示例中,父进程向管道文件写入字符串 “Hello, child process!”,子进程从管道文件读取数据并打印输出。这样就实现了父子进程之间的简单通信。在实际使用中,可以根据需求修改数据传输方式和内容,以满足不同场景下的进程间通信需求。

四、参考阅读

  初识linux之管道
  Linux——进程间通信——管道(文件)通信
  linux之《管道》

五、知识扩展

管道:
	Linux中每个进程独享地址空间,不同进程间要进行进程间通信必须通过内核;

	进程A虚拟地址空间 《——》内核空间缓冲区 《——》进程B虚拟地址空间

	匿名管道由于没有标识符,只能用于具有亲缘关系的进程间进行通信

	自带同步和互斥,生命周期跟随进程

共享内存:
	在物理内存上开辟一块空间,多个进程将同一块物理内存映射到自己的虚拟地址空间,通过自己
	的虚拟地址空间直接访问这块物理内存
	
	虚拟地址-》页表查询-》物理地址
	
	最快的进程间通信方式
	
	没有自带同步和互斥,存在并发安全问题
	
	共享内存周期并不会跟随进程的周期结束而销毁

消息队列:
	内核中有个优先级队列,多个进程通过访问同一个队列,进行节点添加或者获取节点实现通信。
	创建消息队列,就是在内核中创建一个优先级队列
	
	自带同步与互斥,生命周期随内核

信号量:
	实现进程间的同步和互斥,可与共享内存搭配使用
	
	组成:一个内核中的计数器+pcb等待队列+使进程等待/唤醒的接口
	
	本质:信号量通过自身的计数器实现对资源进行计数并在访问时进行条件判断
	
	同步流程:获取资源前进行P操作,合理则获取资源,不合理就阻塞,访问完资源后进行v操作;
	计数器>0表示可以访问,获取一个资源,计数器-1(P操作);计数器<=0表示不能访问,计数
	器-1,然后将pcb状态置为可中断休眠状态,加入等待队列中;当有进程释放资源后(V操作),
	从等待队列中唤醒一个pcb去获取资源

  欢迎大家指导和交流!如果我有任何错误或遗漏,请立即指正,我愿意学习改进。期待与大家一起进步!

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐