🌇前言
命名管道通信属于 IPC
的其中一种方式,作为管道家族,命名管道的特点就是 自带同步与互斥机制、数据单向流通,与匿名管道不同的是:命名管道有自己的名字,因此可以被没有血缘关系的进程看到,意味着命名管道可以实现毫不相干的两个独立进程间通信
🏙️正文
1、什么是命名管道
简单,给匿名管道起个名字就变成了命名管道
那么如何给 匿名管道 起名字呢?
- 结合文件系统,给匿名管道这个纯纯的内存文件分配
inode
,将文件名与之构建联系,关键点在于不给它分配Data block
,因为它是一个纯纯的内存文件,是不需要将数据刷盘到磁盘中的
可以将命名管道理解为 “挂名” 后的匿名管道,把匿名管道加入文件系统中,但仅仅是挂个名而已,目的就是为了让其他进程也能看到这个文件(文件系统中的文件可以被所有进程看到)
因为没有 Data block,所以命名管道这个特殊文件大小为 0
1.1、创建及简单使用
命令管道的创建依赖于函数 mkfifo
,函数原型如下
#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode);
关于 mkfifo
函数
组成部分 | 含义 |
返回值 int |
创建成功返回 0 ,失败返回 -1 |
参数1 const char *pathname |
创建命名管道文件时的路径+名字 |
参数2 mode_t mode |
创建命令管道文件时的权限 |
对于参数1,既可以传递绝对路径 /home/xxx/namePipeCode/fifo
,也可以传递相对路径 ./fifo
,当然绝对路径更灵活,但也更长
对于参数2,mode_t
其实就是对 unsigned int
的封装,等价于 uint32_t
,而 mode
就是创建命名管道时的初始权限,实际权限需要经过 umask
掩码计算
不难发现,mkfifo
和 mkdir
非常像,其实 mkfifo
可以直接在命令行中运行
创建一个名为 fifo
的命名管道文件
mkfifo fifo • 1
成功解锁了一种新的特殊类型文件:p
管道文件
这个管道文件也非常特殊:大小为 0,从侧面说明 管道文件就是一个纯纯的内存级文件,有自己的上限,出现在文件系统中,只是单纯挂个名而已
可以直接在命令行中使用命名管道:
echo
可以进行数据写入,可以重定向至fifo
cat
可以进行数据读取,同样也可以重定向于fifo
- 打开两个终端窗口(两个进程),即可进行通信
当然也可以通过程序实现两个独立进程 IPC
思路:创建 服务端 server
和 客户端 client
两个独立的进程,服务端 server
创建并以 读
的方式打开管道文件,客户端 client
以 写
的方式打开管道文件,打开后俩进程可以进程通信,通信结束后,由客户端关闭 写端
(服务端 读端
读取到 0
后也关闭并删除命令管道文件)
注意:
- 当管道文件不存在时,文件会打开失败,因此为了确保正常通信,需要先运行服务端
server
创建管道文件 - 服务端启动后,因为是读端,所以会阻塞等待 客户端(写端)写入数据
- 客户端写入数据时,因为
'\n'
也被读取了,所以要去除此字符 - 通信结束后,需要服务端主动删除管道文件
unlink 命令管道文件名 //删除管道文
为了让服务端和客户端能享有同一个文件名,可以创建一个公共头文件 common.h
,其中存储 命名管道文件名及默认权限等公有信息
公共资源
common.h
#pragma once #include <iostream> #include <string> std::string fifo_name = "./fifo"; //管道名 uint32_t mode = 0666; //权限
服务端
server.cc
#include <iostream> #include <cstring> #include <cerrno> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include "common.h" using namespace std; int main() { // 服务端 // 1、创建命名管道文件 int ret = mkfifo(fifo_name.c_str(), mode); if (ret < 0) { cerr << "mkfifo fail! errno: " << errno << " | " << strerror(errno) << endl; exit(0); } // 2、以读的方式打开文件 int rfd = open(fifo_name.c_str(), O_RDONLY); if (rfd < 0) { cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl; exit(0); } // 3、读取数据 while (true) { char buff[64]; int n = read(rfd, buff, sizeof(buff) - 1); buff[n] = '\0'; if (n > 0) { cout << "Server get message# " << buff << endl; } else if (n == 0) { cout << "写端关闭,读端读取到0,终止读端" << endl; break; } else { cout << "读取异常" << endl; break; } } close(rfd); unlink(fifo_name.c_str()); //删除命名管道文件 return 0; }
客户端
client.cc
#include <iostream> #include <cstring> #include <cerrno> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include "common.h" using namespace std; int main() { // 客户端 // 1、打开文件 int wfd = open(fifo_name.c_str(), O_WRONLY); if (wfd < 0) { cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl; exit(0); } // 2、写入数据,进行通信 char buff[64] = {0}; while (true) { cout << "Client send message# "; fgets(buff, sizeof(buff) - 1, stdin); buff[strlen(buff) - 1] = '\0'; // 去除 '\n' if (strcasecmp("exit", buff) == 0) break; write(wfd, buff, strlen(buff)); } close(wfd); return 0; }
注:strcasecmp
是一个字符串比较函数,无论字符串大小写,都能进行比较
运行效果:
所以 挂了名之后的命名管道是如何实现独立进程间 IPC
的呢?
1.2、命名管道的工作原理
把视角拉回文件系统:当重复多次打开同一个文件时,并不会费力的打开多次,而且在第一次打开的基础上,对 struct file
结构体中的引用计数 ++
,所以对于同一个文件,不同进程打开了,看到的就是同一个
- 具体例子:显示器文件(
stdout
)只有一个吧,是不是所有进程都可以同时进行写入? - 同理,命名管道文件也是如此,先创建出文件,在文件系统中挂个名,然后让独立的进程以不同的方式打开同一个命名管道文件,比如进程
A
以只读的方式打开,进程B
以只写的方式打开,那么此时进程B
就可以向进程A
写文件,即IPC
因为命名管道适用于独立的进程间 IPC
,所以无论是读端和写端,进程 A
、进程 B
为其分配的 fd
是一致的,都是 3
- 如果是匿名管道,因为是依靠继承才看到同一文件的,所以读端和写端
fd
不一样
所以 命名管道 和 匿名管道 还是有区别的
1.3、命名管道与匿名管道的区别
不同点:
- 匿名管道只能用于具有血缘关系的进程间通信;而命名管道不讲究,谁都可以用
- 匿名管道直接通过
pipe
函数创建使用;而命名管道需要先通过mkfifo
函数创建,然后再通过open
打开使用 - 出现多条匿名管道时,可能会出现写端
fd
重复继承的情况;而命名管道不会出现这种情况
在其他方面,匿名管道与命名管道几乎一致
- 两个都属于管道家族,都是最古老的进程间通信方式,都自带同步与互斥机制,提供的都是流式数据传输
2、命名管道的特点及特殊场景
命名管道的特点及特殊场景与匿名管道完全一致,这里简单回顾下,详细内容可跳转至 《Linux进程间通信【匿名管道】》
2.1、特点
可以简单总结为:
- 管道是半双工通信
- 管道生命随进程而终止
- 命名管道任意多个进程间通信
- 管道提供的是流式数据传输服务
- 管道自带 同步与互斥 机制
2.2、四种特殊场景
四种场景分别为
- 管道为空时,读端阻塞,等待写端写入数据
- 管道为满时,写端阻塞,等待读端读取数据
- 进程通信时,关闭读端,
OS
发出13
号信号SIGPIPE
终止写端进程 - 进程通信时,关闭写端,读端读取到
0
字节数据,可以借此判断终止读端
3、命名管道实操
以下是一些使用命名管道实现的简单小程序,主要目的是为了熟悉命名管道的使用
3.1、实现文件拷贝
下载应用的本质是在下载文件,将服务器看作写端,自己的电脑看作读端,那么 下载 这个动作本质上就是 IPC
,不过是在网络层面实现的
我们可以利用 命名管道实现不同进程间 IPC
,即进程从文件中读取并写入一批数据,另一个进程一次读取一批数据并保存至新文件中,这样就实现了文件的拷贝
目标:利用命名管道,向空文件 target.txt
中写入数据,即拷贝源文件 file.txt
公共资源
common.h
#pragma once #include <iostream> #include <string> std::string fifo_name = "./fifo"; //管道名 uint32_t mode = 0666; //权限
服务端(写端)
server.cc
提供文件拷贝服务
#include <iostream> #include <cstring> #include <cerrno> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include "common.h" using namespace std; int main() { // 服务端 // 1、打开文件 int wfd = open(fifo_name.c_str(), O_WRONLY); if (wfd < 0) { cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl; exit(0); } // 2、打开源文件 FILE *fp = fopen("file.txt", "r"); if (fp == NULL) { cerr << "fopen fail! errno: " << errno << " | " << strerror(errno) << endl; exit(0); } // 3、读取源文件数据 char buff[1024]; int n = fread(buff, sizeof(char), sizeof(buff), fp); //IPC区域 // 4、写入源文件至命名管道 write(wfd, buff, strlen(buff)); cout << "服务端已向管道写入: " << n << "字节的数据" << endl; //IPC区域 fclose(fp); fp = nullptr; close(wfd); return 0; }
客户端(读端)
client.cc
从服务端中拷贝文件(下载)
#include <iostream> #include <cstring> #include <cerrno> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include "common.h" using namespace std; int main() { // 客户端 // 1、创建命名管道文件 int ret = mkfifo(fifo_name.c_str(), mode); if (ret < 0) { cerr << "mkfifo fail! errno: " << errno << " | " << strerror(errno) << endl; exit(0); } // 2、以读的方式打开管道文件 int rfd = open(fifo_name.c_str(), O_RDONLY); if (rfd < 0) { cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl; exit(0); } // 3、打开目标文件 FILE *fp = fopen("target.txt", "w"); if (fp == NULL) { cerr << "fopen fail! errno: " << errno << " | " << strerror(errno) << endl; exit(0); } //IPC区域 // 4、读取数据 char buff[1024]; int n = read(rfd, buff, sizeof(buff) - 1); buff[n] = '\0'; if (n > 0) cout << "客户端已从管道读取: " << n << "字节的数据" << endl; else if (n == 0) cout << "写端关闭,读端读取到0,终止读端" << endl; else cout << "读取异常" << endl; //IPC区域 //5、写入目标文件,完成拷贝 fwrite(buff, sizeof(char), strlen(buff), fp); cout << "客户端已成功从服务端下载(拷贝)了文件数据" << endl; fclose(fp); close(rfd); unlink(fifo_name.c_str()); // 删除命名管道文件 return 0; }
拷贝结果:成功拷贝
此时 服务端是写端,客户端是读端,实现的是 下载服务;当 服务端是读端,客户端是写端时,实现的就是 上传服务,搞两条管道就能模拟实现简单的 数据双向传输服务
注意: 创建管道文件后,无论先启动读端,还是先启动写端,都要阻塞式的等待另一方进行交互
3.2、实现进程控制
在 Linux
匿名管道 IPC
中,我们实现了一个简易版的进程控制程序,原理是通过多条匿名管道实现父进程对多个子进程执行任务分配
匿名管道用于有血缘关系间 IPC
,命名管道也可以
所以我们可以把上一篇文章中的 匿名管道换为命名管道,一样可以实现通信
任务池
Task.hpp
#include <iostream> #include <string> #include <functional> #include <unordered_map> #include <unistd.h> using namespace std; void PrintLOG() { cout << "PID: " << getpid() << " 正在执行打印日志的任务…" << endl; } void InsertSQL() { cout << "PID: " << getpid() << " 正在执行数据库插入的任务…" << endl; } void NetRequst() { cout << "PID: " << getpid() << " 正在执行网络请求的任务…" << endl; } class Task { public: Task() { // 装载任务 _tt = {{"打印日志", PrintLOG}, {"数据库插入", InsertSQL}, {"网络请求", NetRequst}}; } // 展示任务 void showTask() { cout << "目前可用任务有:["; for (auto e : _tt) cout << e.first << " "; cout << "]" << endl; cout << "输入 退出 以终止程序" << endl; } // 执行任务 void Execute(const string &task) { if (_tt.count(task) == 0) { cerr << "没有这个任务:" << task << endl; } else { _tt[task](); // 函数对象调用 } } private: unordered_map<string, function<void(void)>> _tt; };
控制程序
namePipeCtrl.cc
包括进程、管道创建,任务执行与进程等待
#include <iostream> #include <string> #include <vector> #include <cassert> #include <cstdlib> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/stat.h> #include <fcntl.h> #include "Task.hpp" using namespace std; enum { NAME_SIZE = 64 }; // 子进程基本信息类 class ProcINfo { public: ProcINfo(pid_t pid = pid_t(), int wfd = int()) : _pid(pid), _wfd(wfd), _num(_cnt++) { char buff[NAME_SIZE] = {0}; snprintf(buff, NAME_SIZE, "Process %d | pid:wfd [%d:%d]", _num, _pid, _wfd); _name = string(buff); } pid_t _pid; int _wfd; int _num; string _name; static int _cnt; }; int ProcINfo::_cnt = 0; // 进程控制类 class ProcCtrl { public: ProcCtrl(int num = 3, mode_t mode = 0666) : _num(num), _mode(mode) { // 根据 _num 创建命名管道及子进程 CreatPipeAndProc(); } ~ProcCtrl() { waitProc(); } // 创建管道及进程 void CreatPipeAndProc() { // 因为是继承的,所以也要注意写端重复继承问题 vector<int> fds; for (int i = 0; i < _num; i++) { // 步骤:创建管道,存入 _vst char pipeNameBUff[NAME_SIZE]; // 管道名缓冲区 snprintf(pipeNameBUff, NAME_SIZE, "./fifo-%d", i); int ret = mkfifo(pipeNameBUff, _mode); assert(ret != -1); (void)ret; _vst.push_back(string(pipeNameBUff)); // 创建子进程,让子进程以只读的方式打开管道文件 pid_t id = fork(); if (id == 0) { // 子进程内 // 先关闭不必要的写端 for (auto e : fds) close(e); // 打开管道文件,并进入任务等待默认(读端阻塞) int rfd = open(_vst[i].c_str(), O_RDONLY); assert(rfd != -1); (void)rfd; waitCommand(rfd); close(rfd); // 关闭读端 exit(0); } // 父进程以写打开管道,保存 fd 信息 int wfd = open(_vst[i].c_str(), O_WRONLY); assert(wfd != -1); (void)wfd; // 注册子进程信息 _vpt.push_back(ProcINfo(id, wfd)); fds.push_back(wfd); } } // 子进程等待任务派发 void waitCommand(int rfd) { while (true) { char buff[NAME_SIZE] = {0}; int n = read(rfd, buff, sizeof(buff) - 1); buff[n] = '\0'; if (n > 0) { Task().Execute(string(buff)); } else if (n == 0) { cerr << "读端读取到 0,写端已关闭,读端也即将关闭" << endl; break; } else { cerr << "子进程读取异常!" << endl; break; } } } // 展示可选进程 void showProc() { cout << "目前可用进程有:["; int i = 0; for (i = 0; i < _num - 1; i++) cout << i << "|"; cout << i << "]" << endl; } // 下达任务给子进程 void ctrlProc() { while (true) { cout << "==========================" << endl; int n = 0; do { showProc(); cout << "请选择子进程:> "; cin >> n; } while (n < 0 || n >= _num); Task().showTask(); string taskName; cout << "请选择任务:> "; cin >> taskName; if (taskName == "退出") break; // 将信息通过命名管道写给子进程 cout << "选择进程 ->" << _vpt[n]._name << " 执行 " << taskName << " 任务" << endl; write(_vpt[n]._wfd, taskName.c_str(), taskName.size()); sleep(1); } } // 关闭写端、删除文件、等待子进程退出 void waitProc() { for (int i = 0; i < _num; i++) { close(_vpt[i]._wfd); // 关闭写端 unlink(_vst[i].c_str()); // 关闭管道文件 waitpid(_vpt[i]._pid, nullptr, 0); // 等待子进程 } cout << "所有子进程已回收" << endl; } private: vector<ProcINfo> _vpt; // 子进程信息表 vector<string> _vst; // 命名管道信息表 int _num; // 子进程数/命名管道数 mode_t _mode; // 命名管道文件的权限 }; int main() { ProcCtrl p1; p1.ctrlProc(); return 0;
执行结果如下:
关于 父子进程间使用命名管道通信 值得注意的问题:
- 在命名管道创建后,需要先创建子进程,让子进程打开【读端或写端】,然后才让父进程打开【写端或读端】,这是因为假如先让父进程打开【写端或读端】,那么此时父进程就会进入【阻塞】状态,导致无法创建子进程,自然也就无法再打开【读端或写端】;所以正确做法是先让子进程打开,即使子进程【阻塞】了,父进程也还能运行。不要让【阻塞】阻碍子进程的创建
- 子进程继承都存在的问题:写端重复继承,因此需要关闭不必要的写端
fd
关于问题一的理解可以看看下面这两张图:
错误用法: 父进程先打开【写端或读端】,再创建子进程,最后才让子进程打开【读端或写端】
正确用法: 先创建子进程,让子进程打开【读端或写端】,再让父进程打开【写端或读端】
3.3、实现进程遥控(配合简易版 bash)
利用命名管道就可以远程遥控,原理很简单:简易版 bash
会等待命令输入,将输入源换成命名管道读端,再创建一个独立进程,作为命名管道的写端,此时就可以实现远程遥控进程,执行不同的指令
这里直接用之前写好的 简易版 bash,关于 简易版 bash 的具体实现可以看看这篇文章 《Linux模拟实现【简易版bash】》
步骤:
- 创建命名管道
- 将
bash
改装,打开命名管道文件,作为读端
- 创建独立进程,打开命名管道文件,作为
写端
- 进行
IPC
,发送命令给bash
执行
公共资源
common.h
#pragma once #include <iostream> #include <string> std::string fifo_name = "./fifo"; //管道名 uint32_t mode = 0666; //权限
简易版bash
mybash.cc
#include <iostream> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> #include <string.h> #include <assert.h> #include <sys/stat.h> #include <fcntl.h> #include "common.h" using namespace std; #define COM_SIZE 1024 #define ARGV_SIZE 64 #define DEF_CHAR " " void split(char *argv[ARGV_SIZE], char *ps) { assert(argv && ps); // 调用 C语言 中的 strtok 函数分割字符串 int pos = 0; argv[pos++] = strtok(ps, DEF_CHAR); // 有空格就分割 while (argv[pos++] = strtok(NULL, DEF_CHAR)) ; // 不断分割 argv[pos] = NULL; // 确保安全 } void showEnv() { extern char **environ; // 使用当前进行的环境变量表 int pos = 0; for (; environ[pos]; printf("%s\n", environ[pos++])) ; } // 枚举类型,用于判断不同的文件打开方式 enum redir { REDIR_INPUT = 0, REDIR_OUTPUT, REDIR_APPEND, REDIR_NONE } redir_type = REDIR_NONE; // 创建对象 redir_type,默认为 NONE // 检查是否出现重定向符 char *checkDir(char *command) { // 从右往左遍历,遇到 > >> < 就置为 '\0' size_t end = strlen(command); // 与返回值相匹配 char *ps = command + end; // 为了避免出现无符号-1,这里采取错位的方法 while (end != 0) { if (command[end - 1] == '>') { if (command[end - 2] == '>') { command[end - 2] = '\0'; redir_type = REDIR_APPEND; return ps; } command[end - 1] = '\0'; redir_type = REDIR_OUTPUT; return ps; } else if (command[end - 1] == '<') { command[end - 1] = '\0'; redir_type = REDIR_INPUT; return ps; } // 如果不是空格,就可以更新 ps指向 if (*(command + end - 1) != ' ') ps = command + end - 1; end--; } return NULL; // 如果没有重定向符,就返回空 } int main() { char myEnv[COM_SIZE][ARGV_SIZE]; // 大小与前面有关 int env_pos = 0; // 专门维护缓冲区 int exit_code = 0; // 保存退出码的全局变量 // 2023.6.7 更新 // 创建管道文件 int ret = mkfifo(fifo_name.c_str(), mode); assert(ret != -1); (void)ret; // 打开管道文件 int rfd = open(fifo_name.c_str(), O_RDONLY); assert(rfd != -1); (void)rfd; // 这是一个始终运行的程序:bash while (1) { char command[COM_SIZE]; // 存放指令的数组(缓冲区) // 打印提示符 printf("[User@myBash default]$ "); fflush(stdout); // 读取指令 //从管道中读取 int n = read(rfd, command, COM_SIZE - 1); if(n == 0) { cout << "写端已关闭,读端也即将关闭" << endl; break; } command[n] = '\0'; cout << command << endl; // 重定向 // 在获取指令后进行判断 // 如果成立,则获取目标文件名 filename char *filename = checkDir(command); // 指令分割 // 将连续的指令分割为 argv 表 char *argv[ARGV_SIZE]; split(argv, command); // 特殊处理 // 颜色高亮处理,识别是否为 ls 指令 if (strcmp(argv[0], "ls") == 0) { int pos = 0; while (argv[pos++]) ; // 找到尾 argv[pos - 1] = (char *)"--color=auto"; // 添加此字段 argv[pos] = NULL; // 结尾 } // 目录间移动处理 if (strcmp(argv[0], "cd") == 0) { // 直接调用接口,然后 continue 不再执行后续代码 if (strcmp(argv[1], "~") == 0) chdir("/home"); // 回到家目录 else if (strcmp(argv[1], "-") == 0) chdir(getenv("OLDPWD")); else if (argv[1]) chdir(argv[1]); // argv[1] 中就是路径 continue; // 终止此次循环 } // 环境变量相关 if (strcmp(argv[0], "export") == 0) { if (argv[1]) { strcpy(myEnv[env_pos], argv[1]); putenv(myEnv[env_pos++]); } continue; // 一样需要提前结束循环 } // 环境变量表 if (strcmp(argv[0], "env") == 0) { showEnv(); // 调用函数,打印父进程的环境变量表 continue; // 提前结束本次循环 } // echo 相关 // 只有 echo $ 才做特殊处理(环境变量+退出码) if (strcmp(argv[0], "echo") == 0 && argv[1][0] == '$') { if (argv[1] && argv[1][0] == '$') { if (argv[1][1] == '?') printf("%d\n", exit_code); else printf("%s\n", getenv(argv[1] + 1)); } continue; } // 子进程进行程序替换 pid_t id = fork(); if (id == 0) { // 判断是否需要进行重定向 if (redir_type == REDIR_INPUT) { int fd = open(filename, O_RDONLY); dup2(fd, 0); // 更改输入,读取文件 filename } else if (redir_type == REDIR_OUTPUT) { int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666); dup2(fd, 1); // 写入 } else if (redir_type == REDIR_APPEND) { int fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666); dup2(fd, 1); // 追加 } // 直接执行程序替换,这里使用 execvp execvp(argv[0], argv); exit(168); // 替换失败后返回 } // 父进程等待子进程终止 int status = 0; waitpid(id, &status, 0); // 在等待队列中阻塞 exit_code = WEXITSTATUS(status); if (WIFEXITED(status)) { // 假如程序替换失败 if (exit_code == 168) printf("%s: Error - %s\n", argv[0], "The directive is not yet defined"); } else printf("process run fail! [code_dump]:%d [exit_signal]:%d\n", (status >> 7) & 1, status & 0x7F); // 子进程异常终止的情况 } //关闭管道文件 close(rfd); unlink(fifo_name.c_str()); return 0; }
进程控制端
namePipeCtrl.cc
#include <iostream> #include <cassert> #include <cstring> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include "common.h" using namespace std; int main() { // 打开管道文件 --- 只写 int wfd = open(fifo_name.c_str(), O_WRONLY); assert(wfd != -1); (void)wfd; char buff[64]; while (true) { cout << "远程发送指令:> "; fgets(buff, sizeof(buff) - 1, stdin); buff[strlen(buff) - 1] = '\0'; // 去除 '\n' if (strcasecmp("exit", buff) == 0) break; // 向管道写入数据 write(wfd, buff, strlen(buff)); } close(wfd); return 0; }
实际效果如下:
注意:在进行指令处理时,需要注意 '\n'
,不能把 '\n'
带入进程替换中
3.4、实现字符实时读取
回车 '\n'
这个东西很难处理,那么有没有一种方式,能实现不输入回车也能写入数据至管道中呢?答案是有的
比如以下代码,可以实现特殊化读取,即 不需要特定条件触发缓冲区冲刷,实时写入字符
公共资源
common.h
#pragma once #include <iostream> #include <string> std::string fifo_name = "./fifo"; //管道名 uint32_t mode = 0666; //权限
服务端
server.cc
实时读取字符
#include <iostream> #include <cstring> #include <cerrno> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include "common.h" using namespace std; int main() { // 服务端 // 1、创建命名管道文件 int ret = mkfifo(fifo_name.c_str(), mode); if (ret < 0) { cerr << "mkfifo fail! errno: " << errno << " | " << strerror(errno) << endl; exit(0); } // 2、以读的方式打开文件 int rfd = open(fifo_name.c_str(), O_RDONLY); if (rfd < 0) { cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl; exit(0); } // 3、读取数据 while (true) { char buff[64]; int n = read(rfd, buff, sizeof(buff) - 1); buff[n] = '\0'; if (n > 0) { buff[n] = 0; printf("%c", buff[0]); fflush(stdout); } else if (n == 0) { cout << "写端关闭,读端读取到0,终止读端" << endl; break; } else { cout << "读取异常" << endl; break; } } close(rfd); unlink(fifo_name.c_str()); // 删除命名管道文件 return 0; }
客户端
client.cc
实时发送字符
#include <iostream> #include <cstring> #include <cerrno> #include <cassert> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include "common.h" using namespace std; int main() { // 客户端 // 1、打开文件 int wfd = open(fifo_name.c_str(), O_WRONLY); if (wfd < 0) { cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl; exit(0); } // 2、写入数据,进行通信 char buff[64] = {0}; while (true) { system("stty raw"); int c = getchar(); system("stty -raw"); ssize_t n = write(wfd, (char *)&c, sizeof(char)); assert(n >= 0); (void)n; } close(wfd); return 0; }
实时读取字符的效果如下:
本文中涉及的所有代码均在此仓库中:《命名管道博客仓库》
🌆总结
以上就是本次关于 Linux
进程间通信之命名管道的全部内容了,作为匿名管道的兄弟,命名管道具备匿名管道的大部分特性,使用方法也基本一致,不过二者在创建和打开方式上各有不同:匿名管道简单,但只能用于具有血缘关系进程间通信,命名管道虽麻烦些,但适用于所有进程间通信场景;在本文的最后,使用命名管道实现了几个简单的小程序,这些小程序的本质都是一样的:创建命名管道 -> 打开命名管道 -> 通信 -> 关闭命名管道,掌握其中一个即可融会贯通
相关文章推荐
Linux进程间通信【匿名管道】
Linux基础IO【软硬链接与动静态库】
Linux基础IO【深入理解文件系统】
Linux【模拟实现C语言文件流】
Linux基础IO【重定向及缓冲区理解】
Linux基础IO【文件理解与操作】