《Linux系统编程》15.进程间通信-管道

💡Yupureki:个人主页

✨个人专栏:《C++》 《算法》《Linux系统编程》《高并发内存池》《MySQL数据库》

《个人在线OJ平台》


🌸Yupureki🌸的简介:


目录

[1. 进程间通信的概念](#1. 进程间通信的概念)

[1.1 进程间通信目的](#1.1 进程间通信目的)

[1.2 常见的进程间通信](#1.2 常见的进程间通信)

[2. 匿名管道](#2. 匿名管道)

[2.1 什么是管道?](#2.1 什么是管道?)

[2.2 匿名管道的原理](#2.2 匿名管道的原理)

[2.3 接口介绍](#2.3 接口介绍)

[2.3.1 pipe() --- 创建管道](#2.3.1 pipe() — 创建管道)

[2.3.2 read() 和 write() --- 读写管道](#2.3.2 read() 和 write() — 读写管道)

[2.3.3 close() --- 关闭管道端](#2.3.3 close() — 关闭管道端)

[2.4 测试用例](#2.4 测试用例)

[2.5 管道读写规则](#2.5 管道读写规则)

[3. 命名管道](#3. 命名管道)

[3.1 什么是命名管道?](#3.1 什么是命名管道?)

[3.2 命名管道的原理](#3.2 命名管道的原理)

[3.3 接口介绍](#3.3 接口介绍)

[3.3.1 创建 FIFO](#3.3.1 创建 FIFO)

[3.3.2 打开 FIFO](#3.3.2 打开 FIFO)

[3.3.3 读写与关闭](#3.3.3 读写与关闭)

[3.3.4 删除 FIFO](#3.3.4 删除 FIFO)

[3.3.5 C++封装fifo](#3.3.5 C++封装fifo)

[3.3.6 测试用例](#3.3.6 测试用例)


1. 进程间通信的概念

1.1 进程间通信目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程)此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

1.2 常见的进程间通信

常见的Linux进程间通信方式主要有以下几种:

  • 管道 :分为匿名管道 (用于父子进程或具有亲缘关系的进程,如 shell 中的 |)和命名管道(通过文件系统中的管道文件,允许无亲缘关系的进程通信)。特点是简单,遵循先进先出原则,适合流式数据传输。

  • 信号 :一种软件中断机制,用于通知进程发生了某个事件。它是异步的,常用于进程控制(如 SIGKILL 终止进程、SIGSTOP 暂停进程)或异常处理,传输的信息量非常有限(仅传递信号编号)。

  • 消息队列:内核中的一个消息链表。进程可以将数据块(消息)添加到队列中,另一个进程可以读取指定类型的消息。相比管道,它允许随机读取(不必先进先出),且每条消息都有类型,能承载一定结构化的数据。

  • 共享内存效率最高的方式。它允许多个进程将同一块物理内存区域映射到自己的虚拟地址空间中,进程可直接像访问普通内存一样读写数据,无需经过内核拷贝。但缺点是需要额外的同步机制(如信号量)来防止数据竞争。

  • 信号量 :本质上是一个计数器,主要用于实现进程间的同步与互斥。它并非用于传输数据,而是用于保护共享资源(如共享内存),防止多个进程同时访问导致冲突。

  • 套接字最通用的方式 ,支持不同主机间通过网络通信,也支持本机通信。它通过 socket 接口实现,在Linux中通常使用 AF_UNIX 域来实现本机高效的IPC,AF_INET 用于网络通信。

2. 匿名管道

2.1 什么是管道?

管道是Unix中最古老的进程间通信的形式。

我们把从一个进程连接到另一个进程的一个数据流称为一个"管道"

两个甚至多个进程通过管道传递数据

2.2 匿名管道的原理

匿名管道为什么"匿名"?

这里的"匿名",你可以理解为不需要定义管道的名字,不需要自定义管道的各项属性,因为匿名管道是被继承出来的

所谓要继承,那么需要父进程创建匿名管道。这个管道本质也是文件 ,因此返回这个管道,即文件的文件描述符(读写fd),用readwrite通过文件描述符fd向管道内输入输出数据

然后其子进程继承了父进程的几乎所有的资源 ,其中包括匿名管道的文件描述符,这样父进程就可以与子进程互相通信了,一气呵成

当然这也有局限性,即只能父进程和子进程通信

具体流程( C语言)

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

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

  3. 在父进程中关闭不需要的一端(例如关闭读端,只保留写端)。

  4. 在子进程中关闭另一端(例如关闭写端,只保留读端)。

  5. 父进程向写端写入数据,子进程从读端读取数据。

  6. 通信结束后,关闭各自持有的文件描述符。

2.3 接口介绍

2.3.1 pipe() --- 创建管道

cpp 复制代码
#include <unistd.h>

int pipe(int pipefd[2]/*输出型参数*/);

功能

创建一个匿名管道,并返回两个文件描述符:

  • pipefd[0]:读端(read end)

  • pipefd[1]:写端(write end)

数据从写端写入内核缓冲区,从读端读出。

返回值

  • 成功:返回 0

  • 失败:返回 -1,并设置 errno

常见错误:

  • EMFILE:进程已打开的文件描述符过多

  • ENFILE:系统打开文件总数已达上限

  • EFAULTpipefd 无效指针

2.3.2 read()write() --- 读写管道

管道是一种特殊的文件描述符,使用标准的 readwrite 系统调用操作。

读取数据

cpp 复制代码
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
  • fd 是管道读端时,从管道缓冲区中读取数据。

  • 如果管道为空:

    • 阻塞模式 (默认):read 会一直等待,直到有数据写入或所有写端关闭。

    • 非阻塞模式 (通过 fcntl 设置 O_NONBLOCK):立即返回 -1errnoEAGAIN

  • 如果所有写端已关闭,read 返回 0(表示读到文件结束)。

写入数据

cpp 复制代码
ssize_t write(int fd, const void *buf, size_t count);
  • fd 是管道写端时,将数据写入管道缓冲区。

  • 如果管道已满:

    • 阻塞模式write 会阻塞,直到有空间可用。

    • 非阻塞模式 :立即返回 -1errnoEAGAIN

  • 如果所有读端已关闭,写操作会触发 SIGPIPE 信号,进程默认终止;也可以忽略信号,此时 write 返回 -1errnoEPIPE

2.3.3 close() --- 关闭管道端

cpp 复制代码
#include <unistd.h>

int close(int fd);
  • 每个管道端需要独立关闭。

  • 当所有写端关闭后,读端 read 会返回 0

  • 当所有读端关闭后,写端 write 会引发 SIGPIPE

  • 良好实践:在 fork 后,父子进程应关闭自己不需要的一端,避免意外的文件描述符残留。

2.4 测试用例

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

int main()
{
    int fd[2];
    pipe(fd);

    if (fork() == 0) 
    { // 子进程:向管道内写入数据
        close(fd[0]);// 关闭读端
        char buffer[] = "hello world";
        write(fd[1],buffer,sizeof(buffer));
        return 1;
    }
    else
    {// 父进程:从管道内读取数据
        sleep(1);//等待子进程写入完毕
        close(fd[1]);//关闭写端
        char buffer[1024];
        read(fd[0],buffer,sizeof(buffer) - 1);
        printf("child say:%s\n",buffer);
    }
    waitpid(-1,NULL,0);
    return 0;
}

2.5 管道读写规则

  • 当没有数据可读时
    • O_NONBLOCKdisable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
    • O_NONBLOCKenable:read调用返回-1,errno值为EAGAIN。
  • 当管道满的时候
    • O_NONBLOCKdisable:write调用阻塞,直到有进程读走数据
    • O_NONBLOCKenable:调用返回-1,errno值为EAGAIN
  • 如果所有管道写端对应的文件描述符被关闭,则read返回0
  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

3. 命名管道

3.1 什么是命名管道?

命名管道(FIFO)是匿名管道的扩展,解决了匿名管道只能用于亲缘进程的限制。它通过文件系统中的特殊文件(FIFO文件)为任意进程提供通信能力,依然遵循先进先出的流式数据传输,但可以像普通文件一样被不同进程打开和使用。

3.2 命名管道的原理

命名管道在磁盘上表现为一个特殊类型的文件 (类型标识为 p),但数据并不存储在磁盘上,而是内核维护的内存缓冲区。文件系统只起到"标识"作用,供进程通过路径名找到并打开同一个管道。

  • 内核缓冲区:与匿名管道类似,是一个环形缓冲区,默认大小通常为 64KB(可调整)。

  • 阻塞/非阻塞 :默认情况下,open 一个 FIFO 时,如果以只读方式打开,会阻塞直到另一个进程以只写方式打开(反之亦然)。这种机制确保了通信双方同时就绪。

  • 单向通信:也是半双工,数据流只能从一端流向另一端。若需要双向通信,需创建两个 FIFO。

  • 生命周期:与文件系统绑定,即使所有打开它的进程都退出,FIFO 文件依然存在,可以后续再次使用(除非显式删除)。

3.3 接口介绍

3.3.1 创建 FIFO

bash 复制代码
mkfifo [选项] 名称

示例:mkfifo mypipe 创建名为 mypipe 的 FIFO 文件。

C 语言函数:mkfifo() / mkfifoat()

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

int mkfifo(const char *pathname, mode_t mode);
  • pathname:FIFO 文件路径。

  • mode:权限,如 0666,受 umask 影响。

  • 返回值:成功 0,失败 -1。

mkfifoat() 类似,但基于目录文件描述符。

3.3.2 打开 FIFO

使用标准 open() 系统调用,但要注意阻塞行为:

c

复制代码
#include <fcntl.h>
int fd = open("mypipe", O_RDONLY);  // 只读打开,阻塞直到有写者
int fd = open("mypipe", O_WRONLY);  // 只写打开,阻塞直到有读者
  • 若以 O_RDONLY 打开且没有写者,open 会阻塞。

  • 若以 O_WRONLY 打开且没有读者,open 会阻塞。

  • 使用 O_NONBLOCK 标志可避免阻塞:

    • open("mypipe", O_RDONLY | O_NONBLOCK) 立即成功,即使没有写者。

    • open("mypipe", O_WRONLY | O_NONBLOCK) 如果没有读者,立即失败,errnoENXIO

3.3.3 读写与关闭

与匿名管道完全一致,使用 read()write()close()

3.3.4 删除 FIFO

通过 unlink()rm 命令删除文件,释放管道在文件系统中的入口。

3.3.5 C++封装fifo

cpp 复制代码
#pragma once

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

#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)

#define PATH "."//默认路径
#define FIFONAME "fifo"//默认管道名称
#define MAXNUM 1024

class Fifo
{
public:
    Fifo(std::string path = PATH, std::string fifo_name = FIFONAME)
    {
        //创建fifo文件
        _path = path;
        _fifo_name = fifo_name;
        _path_name = _path + "/" + _fifo_name;
        if (mkfifo(_path_name.c_str(), 0666))
            ERR_EXIT("mkfifo");
    }

    std::string get_path()
    {
        return _path;
    }

    std::string get_fifo_name()
    {
        return _fifo_name;
    }
    ~Fifo()
    {
        unlink(_path_name.c_str());
    }
private:
    std::string _path;
    std::string _fifo_name;
    std::string _path_name;
};

//封装命名管道接口
class NamedPipe
{
public:
    NamedPipe(std::string path = PATH, std::string pipe_name = FIFONAME)
    {
        _path_name = path + "/" + pipe_name;
    }
    bool change_path_name(std::string path, std::string pipe_name)
    {
        _path_name = path + "/" + pipe_name;
        return true;
    }

    int ReadForFifo()
    {//fifo读端
        fd = open(_path_name.c_str(), O_RDONLY, 0666);
        if (fd == -1)
            ERR_EXIT("open");
        return fd;
    }
    int get_fd()
    {
        return fd;
    }
    int WriteForFifo()
    {//fifo写端
        if (fd != -1)
            close(fd);
        fd = open(_path_name.c_str(), O_WRONLY, 0666);
        if (fd == -1)
            ERR_EXIT("open");
        return fd;
    }
    std::string Read()
    {//从fifo读取数据
        char buffer[MAXNUM] = {0};
        read(fd,buffer,sizeof(buffer));
        std::string str(buffer);
        return str;
    }
    bool Write(std::string str)
    {//向fifo写入数据
        int n = write(fd,str.c_str(),str.size());
        if (n == -1)
        {
            ERR_EXIT("write");
            return false;
        }
        return true;
    }
    ~NamedPipe()
    {
        if (fd != -1)
            close(fd);
    }
private:
    std::string _path_name;
    int fd = -1;
};

3.3.6 测试用例

client.cpp:写端

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

int main()
{
    NamedPipe named_pipe;
    named_pipe.WriteForFifo();
    std::string buffer = "hello world ";
    for(int i = 1;;i++)
    {
        named_pipe.Write(buffer+std::to_string(i));
        sleep(1);
    }

}

server.cpp:读端

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

int main()
{
    Fifo fifo;
    NamedPipe named_pipe;
    std::string buffer;
    int fd = named_pipe.ReadForFifo();
    while (true)
    {
        buffer = named_pipe.Read();
        std::cout << buffer << std::endl;
    }
}
相关推荐
Yupureki2 小时前
《Linux系统编程》14.库的制作与原理
linux·运维·服务器·c语言·开发语言·c++
2301_822782822 小时前
嵌入式C++调试技术
开发语言·c++·算法
正点原子2 小时前
瑞芯微工业级芯加持,正点原子RK3562J开发板/核心板解锁嵌入式开发新可能!
linux·ubuntu·嵌入式
2301_776508722 小时前
实时信号处理库
开发语言·c++·算法
路溪非溪2 小时前
Linux下wifi子系统的数据流
linux·arm开发·驱动开发
feng68_2 小时前
MySQL集群主从复制
linux·运维·数据库·mysql·adb
三三有猫2 小时前
爬虫代理基础知识:为什么用与怎么用
开发语言·c++·爬虫
QWQ___qwq2 小时前
AutoDL服务器NLTK语料包下载失败(卡死/404)完美解决方案
运维·服务器
志栋智能2 小时前
预算有限?超自动化安全运维的普惠解决方案
运维·网络·人工智能·安全·自动化