目录
- 一. 管道
- 1. 匿名管道
- 1.1. 匿名管道的创建
- 命令行创建
- pipe() 函数创建
- 1.2 匿名管道的读写规则
- 1.3 匿名管道的特点
- 2. 命名管道
- 2.1 命名管道的创建
- 命令行创建
- mkfifo() 函数创建
- 2.2 命名管道的读写规则和特点
进程间通信 (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 (匿名管道) 之间唯一的区别在于它们创建与打开的方式不同, 一旦这些工作完成之后, 它们具有相同的语义.