Linux 中的管道:进程间数据传输的利器

个人主页:chian-ocean

文章专栏-Linux

前言

**进程间通信(Inter-Process Communication, IPC)**是指在操作系统中,不同进程之间交换数据或信息的方式。由于每个进程都有自己的地址空间,直接访问另一个进程的数据是不被允许的,因此需要使用IPC机制来实现进程间的通信

进程间通信

进程间通信的目的

进程间通信(IPC)的主要目的是让不同进程能够交换数据、协作工作和同步执行。由于每个进程有独立的内存空间,IPC提供了一种安全的方式让进程之间共享信息和资源。它的作用包括:

  • 数据共享:让多个进程能够共享数据。
  • 进程同步:协调进程的执行顺序,避免资源冲突。
  • 控制与协作:让进程之间进行协作或控制执行。
  • 高效并发:提高系统的处理能力和资源利用率。
  • 模块化设计:简化系统设计,通过独立的进程模块进行工作。

进程间通信的分类

Linux 提供了多种进程间通信机制,以适应不同的应用场景:

  • 管道、消息队列、共享内存:适用于同一系统内的进程间通信。
  • 信号量、套接字:用于进程同步与资源访问控制。
  • 信号和D-Bus:用于事件通知和消息传递。
  • 内存映射文件:用于高效的大数据交换。

管道

管道(Pipe)是一种常见的进程间通信(IPC)机制,用于不同进程之间传递数据。它本质上是一个数据缓冲区,允许一个进程将数据写入其中,另一个进程从管道中读取数据。管道是一种单向通信的方式,但可以通过创建多个管道来实现双向通信。

管道的类型:

  1. 匿名管道(Anonymous Pipe)
    • 匿名管道是无名的、临时的,通常用于父子进程之间的通信。
    • 由于匿名管道没有文件名,它们只能在创建它们的进程间使用。
    • 通过 pipe() 系统调用可以创建匿名管道,常用于同一系统内的进程间通信。
  2. 命名管道(Named Pipe,FIFO)
    • 命名管道是带有名字的管道,可以跨进程甚至跨系统使用。
    • 命名管道在文件系统中有一个路径,进程可以通过路径访问该管道。
    • 通过 mkfifo() 系统调用创建命名管道,适用于没有父子关系的进程间通信。

管道原理

写入数据 读取数据 父进程 管道 子进程

管道用于进程间的通信,特别是用于父子进程之间的简单数据传输。父进程创建管道,并通过管道的写端向管道写入数据;子进程从管道的读端读取数据。由于管道的特性,父子进程共享这对文件描述符,因此它们可以通过该管道进行通信。

  1. 父进程创建管道 :父进程通过 pipe() 创建管道,得到一对文件描述符。
  2. 创建子进程 :父进程调用 fork() 创建子进程,子进程会继承父进程的文件描述符。
  3. 父子进程通信:
    • 父进程通过写端写入数据到管道。
    • 子进程通过读端读取管道中的数据。
  4. 关闭管道:在数据传输完毕后,父子进程都应关闭管道的文件描述符,防止资源泄露。

匿名管道

匿名管道(Anonymous Pipe) 是一种用于父子进程间进行通信的机制,它允许数据在两个进程之间传输,而无需显式的文件名或路径。匿名管道的特点是它没有名字,并且通常只能用于具有亲缘关系的进程(即父进程与子进程)

匿名管道的特性

  1. 具有血缘关系的进程可以进行匿名管道通信。
  2. 管道只能进行单向通信。
  3. **父子进程会进行协同:**父进程和子进程之间的协同工作不仅可以在数据处理上进行配合,还可以在任务执行上互相协作。
  4. **管道是面向字节流的:**这意味着管道在传输数据时并不会关心数据的具体类型,而是以字节流的方式进行传输。
  5. 管道是基于文件,文件基于进程的生命周期。

匿名管道的SysCall

c 复制代码
int pipe(int pipefd[2])

参数:

  • pipefd[2] :一个整数数组,长度为 2。pipefd[0] 用于读取数据(读端),pipefd[1] 用于写入数据(写端)。

返回值:

  • 成功 :返回 0
  • 失败 :返回 -1,并设置 errno 以指示错误原因。

功能:

pipe()创建一个管道并将两个文件描述符放入 pipefd 数组中:

  • pipefd[0] 是管道的读端,用于从管道中读取数据。
  • pipefd[1] 是管道的写端,用于向管道中写入数据。

这些文件描述符可以用于与其他进程或线程进行数据通信。

匿名管道示例

cpp 复制代码
#include<iostream>
#include<string>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<cstdio>
#include<cassert>
#include<cstdlib>
#include<cstring>

#define NUM 2  // 定义管道数组的大小
#define SIZE 1024  // 定义缓冲区大小

// Reader函数:读取管道中的数据并打印出来
void Reader(int fd)
{
    char buffer[SIZE];  // 缓冲区用于存储读取的数据
    while (true)
    {
        buffer[0] = 0;  // 清空缓冲区的第一个字节

        // 从管道读取数据
        ssize_t n = read(fd, buffer, sizeof(buffer));
        if (n > 0)
        {
            buffer[n] = '\0';  // 添加字符串结束符
            std::cout << "father say:[" << "PID : " << getpid() << "]: " << buffer << std::endl;
        }

        // 可以通过 sleep(1) 暂停 1 秒,避免 CPU 过度使用(目前注释掉)
        //sleep(1);
    }
}

// Writer函数:向管道中写数据
void Writer(int fd)
{
    char buffer[SIZE];  // 缓冲区用于存储要写入的数据
    std::string msg = "hello I am child";  // 子进程发送的消息
    int num = 1;  // 用于编号消息

    while (true)
    {
        buffer[0] = 0;  // 清空缓冲区的第一个字节
        snprintf(buffer, sizeof(buffer), "child-PID: %d-%s-%d", getpid(), msg.c_str(), num);  // 格式化消息
        num++;  // 自增消息编号

        // 写入管道
        write(fd, buffer, strlen(buffer));
        sleep(1);  // 每次写入后暂停 1 秒
    }
}

int main()
{
    int pipefd[NUM] = {0};  // 创建一个管道文件描述符数组

    // 创建管道
    int n = pipe(pipefd);
    if (n < 0) return -1;  // 如果创建管道失败,返回 -1

    // 创建子进程
    pid_t id = fork();
    if (id < 0) return -2;  // 如果创建子进程失败,返回 -2

    if (id == 0)  // 子进程部分
    {
        close(pipefd[0]);  // 关闭读端,因为子进程只需要写入数据

        // 调用 Writer 函数向管道中写数据
        Writer(pipefd[1]);
        exit(0);  // 子进程结束
    }

    // 父进程部分
    close(pipefd[1]);  // 关闭写端,因为父进程只需要读取数据

    // 调用 Reader 函数从管道中读取数据
    Reader(pipefd[0]);

    // 等待子进程结束
    pid_t ret = waitpid(id, NULL, 0);
    if (ret == 0)
    {
        std::cout << "success return" << std::endl;  // 子进程正常退出
    }

    return 0;
}

代码流程:

  1. 创建管道
    • 使用 pipe() 创建一个管道,并通过 pipefd[0]pipefd[1] 分别访问读端和写端。
  2. 创建子进程
    • 使用 fork() 创建子进程。
    • 在子进程中,关闭管道的读端,调用 Writer() 函数向管道写数据。
    • 在父进程中,关闭管道的写端,调用 Reader() 函数从管道读取数据。
  3. 管道通信
    • 子进程通过写端 (pipefd[1]) 向管道写数据。
    • 父进程通过读端 (pipefd[0]) 从管道读取数据并打印。
  4. 进程同步
    • 使用 waitpid() 等待子进程的结束。

主要功能:

  • 父子进程的管道通信:父进程读取子进程写入管道的数据并输出。
  • 无限循环的读写操作:子进程不断向管道写入数据,而父进程不断从管道读取数据。

管道的4种情况(用代码自己验证)

  1. 读端正常,写端关闭:读端会挂起阻塞。
  2. 读端正常,写端写满:写端会阻塞。
  3. 读端正常,写端关闭:读端就会读到0(表示读到了尽头)
  4. 写端正常,读端关闭:OS会通过操作系统杀掉写端。(13号信号:管道信号);

命名管道

Linux 中的命名管道(Named Pipe,通常称为 FIFO)是一种特殊类型的文件,它用于在不同进程之间进行数据交换。它与匿名管道(Anonymous Pipe)相比,具有一些独特的特性。

命名管道的特性

  • 跨进程通信:命名管道不仅限于父子进程之间的通信,它可以用于任何两个在系统中运行的进程之间,甚至是属于不同用户的进程。
  • 阻塞行为 :(这一点类似于匿名管道)
    1. 写操作阻塞:如果管道的缓冲区已满,写入数据的进程会被阻塞,直到有进程从管道中读取数据释放出空间。
    2. 读操作阻塞:如果管道为空,读取数据的进程会被阻塞,直到有进程写入数据。

命名管道的创建和销毁

创建

bash下
bash 复制代码
mkfifo name
  • p开头的是管道文件。
C++代码中
cpp 复制代码
int mkfifo(const char *pathname, mode_t mode);
  • pathname:指定要创建的FIFO文件的路径。

  • mode:设置FIFO文件的权限,通常使用标准的文件权限值(例如 0664)。

销毁

bash
bash 复制代码
unlink name

C++代码下

cpp 复制代码
int unlink(const char *pathname);
  • pathname:要删除的文件路径。

命名管道的示例

  • 提供server端进行读取管道。
  • 提供client端进行写入管道。

server

cpp 复制代码
#include "piped.hpp"

int main()
{
    // 创建命名管道(FIFO文件),进行进程间通信
    int n = mkfifo(FIFO_FILE, MODE);  // 使用 mkfifo 系统调用创建命名管道,FIFO_FILE 为管道文件路径,MODE 为权限模式
    if (n < 0) {
        perror("mkfifo");  // 如果创建管道失败,打印错误信息
        exit(FIFO_CREAT_ERR);  // 退出程序并返回自定义的错误码 FIFO_CREAT_ERR
    }

    // 打开命名管道,进行读取操作
    int fd = open(FIFO_FILE, O_RDONLY);  // 使用 open 系统调用打开管道文件进行只读操作
    if (fd < 0) {
        return FIFO_OPEN_ERR;  // 如果打开管道失败,返回自定义的错误码 FIFO_OPEN_ERR
    }

    // 不断读取管道中的数据
    while (true)
    {
        char buffer[SIZE] = {0};  // 创建一个字符数组 buffer 来存放从管道读取的数据
        int n = read(fd, buffer, sizeof(buffer));  // 使用 read 系统调用从管道读取数据,读取的最大字节数为 buffer 的大小
        if (n == -1) {
            perror("read");  // 如果读取数据失败,打印错误信息
            exit(FIFO_READ_ERR);  // 退出程序并返回自定义的错误码 FIFO_READ_ERR
        }
        else if (n > 0) {
            buffer[n] = 0;  // 确保读取的数据是以 null 字符 '\0' 结束的,以方便处理为字符串
            std::cout << "PID :" << getpid() << " client said # " << buffer << std::endl;  // 打印读取的数据以及进程的 PID
        }
        else {
            break;  // 如果没有数据可读,退出循环
        }
    }

    close(fd);  // 关闭管道文件描述符
    return 0;  // 程序正常结束
}
代码解释
  1. 创建命名管道:

    使用 mkfifo() 创建一个命名管道,该管道的路径为 FIFO_FILE,其权限由 MODE 定义。如果创建失败,则通过 perror() 输出错误信息并退出。

  2. 打开命名管道:

    使用 open() 打开刚刚创建的命名管道文件,使用只读模式 (O_RDONLY)。如果管道打开失败,则退出并返回错误码。

  3. 读取数据:

    程序通过 read() 从管道中读取数据,每次读取的数据存放到 buffer 中,并且确保数据以 null 字符结束。读取到的数据会被打印出来,显示发送数据的客户端信息。

  4. 处理管道读取:

    如果读取操作成功且有数据,程序会打印客户端的消息。如果读取操作返回 0(表示管道关闭),则退出循环。

  5. 关闭文件描述符:

    当管道的数据读取完毕后,程序会通过 close() 关闭管道文件描述符。

cilent

cpp 复制代码
#include "piped.hpp"  

int main()
{
    // 打开命名管道文件(FIFO),以写模式打开
    int fd = open(FIFO_FILE, O_WRONLY);  // 使用 open 系统调用以写模式打开管道文件 FIFO_FILE
    if (fd == -1) {
        perror("open");  // 如果打开失败,打印错误信息
        exit(FIFO_OPEN_ERR);  // 退出程序并返回自定义的错误码 FIFO_OPEN_ERR
    }
    std::cout << "client open file done" << std::endl;  // 输出确认消息,表示管道文件已成功打开
    
    // 不断读取用户输入并将其写入管道
    while (true)
    {
        std::string str;  // 定义一个字符串来存储用户输入
        std::cout << "Please Enter@";  // 提示用户输入
        getline(std::cin, str);  // 从标准输入读取一行数据并存储到 str 中
        std::cout << str << std::endl;  // 打印用户输入的内容
        
        // 将输入的字符串写入管道
        int n = write(fd, str.c_str(), str.size());  // 使用 write 系统调用将输入字符串写入管道
        if (n == -1) {
            perror("write");  // 如果写入失败,打印错误信息
            exit(FIFO_WRITE_ERR);  // 退出程序并返回自定义的错误码 FIFO_WRITE_ERR
        }
    }

    close(fd);  // 关闭管道文件描述符
    return 0;  // 程序正常结束
}
代码解释:
  1. 打开管道文件:
    • 使用 open() 函数以写模式(O_WRONLY)打开命名管道文件 FIFO_FILE。如果管道文件打开失败,程序会打印错误信息并退出。
  2. 用户输入并写入管道:
    • 程序通过 std::getline(std::cin, str) 获取用户的输入,并将其存储在 str 变量中。
    • 输入的字符串会被通过 write() 系统调用写入管道,写入的数据是通过 str.c_str() 获取的字符串指针,str.size() 获取的字符串大小。
  3. 错误处理:
    • 如果 write() 函数返回错误(即返回 -1),程序会打印出错误信息,并退出,返回错误码 FIFO_WRITE_ERR
  4. 关闭管道文件:
    • 在写入完成后,程序会通过 close() 关闭管道文件描述符。

头文件包含以及宏定义hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define FIFO_FILE "./mypiped"
#define MODE 0664
#define SIZE 1024

enum 
{
    FIFO_CREAT_ERR = 1 ,
    FIFO_OPEN_ERR      ,
    FIFO_READ_ERR      ,
    FIFO_WRITE_ERR
};

管道文件描述符。

头文件包含以及宏定义hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define FIFO_FILE "./mypiped"
#define MODE 0664
#define SIZE 1024

enum 
{
    FIFO_CREAT_ERR = 1 ,
    FIFO_OPEN_ERR      ,
    FIFO_READ_ERR      ,
    FIFO_WRITE_ERR
};
相关推荐
MiniFlyZt几秒前
使用Redis如何实现分布式锁?(超卖)
数据库·redis·分布式
世转神风-2 分钟前
ubuntu-linux-系统用户界面无法显示-弹出报警框!
linux
20242817李臻18 分钟前
项目开发 1-确定选题,制作原型
linux
天草二十六_简村人20 分钟前
kong搭建一套微信小程序的公司研发环境
java·后端·微信小程序·小程序·kong
菜鸟一皓28 分钟前
告别XML模板的繁琐!Word文档导出,easy!
java·word
让子弹飞021 小时前
10.2linux内核定时器实验(详细编程)_csdn
linux·驱动开发·ubuntu·定时器·stm32mp157·自旋锁
charlie1145141911 小时前
Linux驱动开发框架基础——新旧字符设备驱动笔记整理(1)
linux·驱动开发·笔记·学习·操作系统·教程
别惊鹊1 小时前
hadoop集群配置-xsync脚本同步环境变量
大数据·linux·hadoop
Hum8le1 小时前
工具介绍《Awsome-Redis-Rogue-Server 与 redis-rogue-server》
网络·redis·安全·web安全
2401_884810742 小时前
maven笔记
java·笔记·maven