【Linux】进程间通信 -- 管道

发布于:2024-04-17 ⋅ 阅读:(22) ⋅ 点赞:(0)

进程间通信 (Inter-Process Communication, 简称 IPC) 是多进程协作的基础, 当进程之间需要交互, 协同完成一件事时, 就需要进程通信.

进程间通信的目的:

  • 数据传输: 不同进程间进行数据传输.
  • 资源共享: 多个进程之间需要共享资源.
  • 事件通知: 一个进程向其他进程发送消息, 通知处理相关事件.
  • 进程控制: 有些进程希望完全控制另一个进程的执行, 此时控制进程希望能够拦截另一个进程的所有陷阱和异常, 并能够及时知道它的状态改变.

由于进程之间是相互独立的, 所以需要操作系统提供一份公共的空间或资源, 使得进程可以从公共空间或资源读写数据, 所以进程通信的本质是 操作系统提供一份不同的进程都可以访问的资源.

一. 管道

管道是一种古老且经典, 基于文件操作的通信方案;

管道根据它的特性命名, 是一个可以连接不同进程的数据流, 且数据流是半双工的(单向通信);
其本质就是操作系统在物理内存中创建的一份文件, 不同的进程通过写入或读取文件内容达到交互的目的;

管道又分为 匿名管道命名管道.

1. 匿名管道

匿名管道是一种只能用于具有亲缘关系的进程之间通信的管道, 常用于父子进程.

1.1. 匿名管道的创建

命令行创建

匿名管道可以使用命令创建和使用, 通常使用符号 “|” 来表示管道.

  • 例:
    cat log.txt 的内容被 wc -l (统计数据的行数)指令读取, 打印出 log.txt 文件内容的行数.
    在这里插入图片描述
    原理:
    在这里插入图片描述
pipe() 函数创建

pipe() 函数可以创建一个匿名管道

#include <unistd.h>

int pipe(int fd[2]);
  • 参数 fd:输出型参数, 文件描述符数组; 其中 fd[0] 表示读端, fd[1] 表示写端 .
  • 返回值: 若成功, 返回 0 ; 若失败, 返回-1, 并设置错误代码.

例:

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

int main()
{
    int fd[2];
    int n = pipe(fd);   // 创建匿名管道
    if (n == -1)        // 若失败
    {
        perror("pipe: ");
        return 0;
    }

    int pid = fork();   // 创建子进程
    if (pid == -1)      // 若失败
    {
        perror("fork: ");
        return 0;
    }

    // 子进程
    if (pid == 0)
    {
        close(fd[0]);   // 关闭读端
        
		char buffer[] = "i am son";
        for (int i=0; i<10; i++)
        {
            // 向管道写入
            ssize_t sz = write(fd[1], buffer, sizeof(buffer) - 1);
            if (sz == -1)	// 写入失败
            {
                perror("write: ");  
                break;
            }
            sleep(1);
        }

        close(fd[1]);   // 关闭写端
        return 0;
    }

    // 父进程
    close(fd[1]);       // 关闭写端

    // 从管道读取
    char buffer[64];
    while (1)
    {
        ssize_t sz = read(fd[0], buffer, sizeof(buffer));
        if (sz == -1)   // 读取失败
        {
            perror("read: ");
            break;
        }

        buffer[sz] = 0;
        if (sz)         // 读取成功
            cout << buffer << endl;
        else            // 写端关闭
            break;
    }

    close(fd[0]);       // 关闭读端
    wait(nullptr);      // 等待子进程

    return 0;
}

在这里插入图片描述

原理:
父进程使用 pipe() 函数创建并记录匿名管道文件的读写端, 子进程会通过 fork() 继承匿名管道信息, 之后父子进程分别关闭不需要的读或写端, 即可实现进程间的单向通信.
在这里插入图片描述
注: 匿名管道文件是一个由操作系统提供的内存文件, 不需要将数据刷新至磁盘中;

1.2 匿名管道的读写规则

  • 当管道为空时:
    O_NONBLOCK disable: read() 函数调用阻塞, 即进程暂停执行, 直至有数据来到为止;
    O_NONBLOCK enable: read() 函数调用返回 -1, errno 设置为 EAGAIN.
  • 当管道为满时:
    O_NONBLOCK disable: write() 函数调用阻塞, 直至有进程读走数据;
    O_NONBLOCK enable: write() 函数调用返回 -1, errno 设置为 EAGAIN.
  • 若管道的所有写端对应的文件描述符被关闭, 则 read() 函数返回 0;
    若管道的所有读端对应的文件描述符被关闭, 则 write() 函数会产生信号 SIGPIPE, 进而可能导致写端进程退出.
  • 当写入的数据量不大于 PIPE_BUF (管道的大小, defined in <limits.h>)时, Linux 将保证写入的原子性;
    当写入的数据量大于 PIPE_BUF 时, Linux 将不再保证写入的原子性.

也就是管道读写时会出现四种情况:

  • 当管道内容为空, 读端 读取时, 读端进程会被阻塞挂起, 直至管道被写入数据后才会被唤醒;

  • 当管道内容满后, 写端 写入时, 写端进程会被阻塞挂起, 直至管道数据被读取后才会被唤醒;

  • 当写端进程写完数据关闭写端, 读端进程将管道中的数据全部读取后, 再次读取并不会阻塞挂起, 而是直接返回 0 值, 表示管道为空, 并且不会再有数据写入, 应该结束读取了.

  • 当读端进程关闭读端后, 若写端进程再次写入数据, 操作系统会认为是无意义行为, 将其进程强制终止, 写端属于异常退出.

1.3 匿名管道的特点

  • 管道单向通信, 是半双工的一种特殊情况; 数据只能向一个方向流动, 若需要双方通信时, 则需要建立两个管道.
  • 只能使用于具有亲缘关系的进程之间进行通信, 管道信息需要继承获得.
  • 管道是面向字节流的, 数据的读取或写入数量由上层控制.
  • 管道的生命周期跟随进程的, 当进程终止时, 管道资源会被操作系统回收.
  • 管道自带同步与互斥机制, 可以在一定程度上协调进程的读写顺序;

2. 命名管道

命名管道不同于匿名管道, 命名管道实现任意进程的通信.

命名管道其本质和匿名管道相同, 依旧是内存文件, 只不过分配了 inode, 将其映射在磁盘上, 使进程可以通过路径和文件名找到;
不过命名管道并没有被分配 Data block, 所以数据依旧不需要刷新至磁盘中;

命名管道在磁盘上就是一个特殊文件, 类型为 p, 在 Linux 中文件名后有一个 “|” 标志, 并且文件大小永远为 0.
在这里插入图片描述

2.1 命名管道的创建

命令行创建
mkfifo file_name
  • 例:
    在这里插入图片描述
mkfifo() 函数创建
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
  • 参数:
    pathname: 表示命名管道文件的路径和文件名; 若 pathname 仅含文件名, 则默认将命名管道文件创建在当前路径中.
    mode: 表示命名管道文件的默认权限.

  • 返回值: 若文件创建成功, 返回 0; 若失败, 返回 -1, 并设置 erron;

例:
模拟服务器和客户端的数据交互

  • comm.hpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

#define FILE_NAME "fifo"
using namespace std;
  • pipe_server.cc
#include "comm.hpp"


int main()
{
    umask(0);
    int n = mkfifo(FILE_NAME , 0666);       // 创建命名管道文件
    if (n == -1)
    {
        cout << "mkfifo:" << strerror(errno) << endl;
        return 0;
    }

    int fd = open(FILE_NAME, O_RDONLY);     // 打开管道文件
    if (fd == -1)
    {
        cout << "open:" << strerror(errno) << endl;
        return 0;
    }

    // 从管道中读取数据
    char buffer[128];
    while (1)
    {
        int n = read(fd, buffer, sizeof(buffer)-1); 
        if (!n)             // 若写端关闭
        {
            cout << "server quit!" << endl;
            break;
        }
        else if (n == -1)   // 若读取失败
        {
            cout << "read:" << strerror(errno) << endl;
            return 0;
        }

        // 正常读取
        buffer[n] = 0;      
        cout << "message: " << buffer << endl;
    }

    unlink(FILE_NAME);	//删除管道文件

    return 0;
}
  • pipe_client.cc
#include "comm.hpp"

int main()
{
    int fd = open(FILE_NAME, O_WRONLY);     // 打开管道文件
    if (fd == -1)
    {
        cout << "open:" << strerror(errno) << endl;
        return 0;
    }

    // 写入数据
    string buffer;
    while (1)
    {
        cout << "send: ";
        cin >> buffer;
        if (buffer == "quit")   
        {
            cout << "client quit!" << endl;
            break;
        }

        int n = write(fd, buffer.c_str(), buffer.size());
        if (n == -1)
        {
            cout << "write:" << strerror(errno) << endl;
            return 0;
        }
    }


    return 0;
}

在这里插入图片描述

2.2 命名管道的读写规则和特点

命名管道的读写规则和特点与匿名管道相同;

但会多两种情况:

  • 当管道文件存在时
    若写端未 open() 打开管道, 读端 open() 管道时, 读端会在 open() 位置堵塞等待写端打开; 相反同理.
    在这里插入图片描述

FIFO(命名管道)与 pipe (匿名管道) 之间唯一的区别在于它们创建与打开的方式不同, 一旦这些工作完成之后, 它们具有相同的语义.