?個(gè)人主頁: 北 海
??所屬專欄: Linux學(xué)習(xí)之旅
??操作環(huán)境: CentOS 7.6 阿里云遠(yuǎn)程服務(wù)器
??前言
命名管道通信屬于 IPC
的其中一種方式,作為管道家族,命名管道的特點(diǎn)就是 自帶同步與互斥機(jī)制、數(shù)據(jù)單向流通,與匿名管道不同的是:命名管道有自己的名字,因此可以被沒有血緣關(guān)系的進(jìn)程看到,意味著命名管道可以實(shí)現(xiàn)毫不相干的兩個(gè)獨(dú)立進(jìn)程間通信
???正文
1、什么是命名管道
簡(jiǎn)單,給匿名管道起個(gè)名字就變成了命名管道
那么如何給 匿名管道 起名字呢?
- 結(jié)合文件系統(tǒng),給匿名管道這個(gè)純純的內(nèi)存文件分配
inode
,將文件名與之構(gòu)建聯(lián)系,關(guān)鍵點(diǎn)在于不給它分配Data block
,因?yàn)樗且粋€(gè)純純的內(nèi)存文件,是不需要將數(shù)據(jù)刷盤到磁盤中的
可以將命名管道理解為 “掛名” 后的匿名管道,把匿名管道加入文件系統(tǒng)中,但僅僅是掛個(gè)名而已,目的就是為了讓其他進(jìn)程也能看到這個(gè)文件(文件系統(tǒng)中的文件可以被所有進(jìn)程看到)
因?yàn)闆]有 Data block,所以命名管道這個(gè)特殊文件大小為 0
1.1、創(chuàng)建及簡(jiǎn)單使用
命令管道的創(chuàng)建依賴于函數(shù) mkfifo
,函數(shù)原型如下
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
關(guān)于 mkfifo
函數(shù)
組成部分 | 含義 |
---|---|
返回值 int
|
創(chuàng)建成功返回 0 ,失敗返回 -1 |
參數(shù)1 const char *pathname
|
創(chuàng)建命名管道文件時(shí)的路徑+名字 |
參數(shù)2 mode_t mode
|
創(chuàng)建命令管道文件時(shí)的權(quán)限 |
對(duì)于參數(shù)1,既可以傳遞絕對(duì)路徑 /home/xxx/namePipeCode/fifo
,也可以傳遞相對(duì)路徑 ./fifo
,當(dāng)然絕對(duì)路徑更靈活,但也更長(zhǎng)
對(duì)于參數(shù)2,mode_t
其實(shí)就是對(duì) unsigned int
的封裝,等價(jià)于 uint32_t
,而 mode
就是創(chuàng)建命名管道時(shí)的初始權(quán)限,實(shí)際權(quán)限需要經(jīng)過 umask
掩碼計(jì)算
不難發(fā)現(xiàn),mkfifo
和 mkdir
非常像,其實(shí) mkfifo
可以直接在命令行中運(yùn)行
創(chuàng)建一個(gè)名為 fifo
的命名管道文件
mkfifo fifo
成功解鎖了一種新的特殊類型文件:p
管道文件
出自:Linux 權(quán)限理解和學(xué)習(xí)
這個(gè)管道文件也非常特殊:大小為 0,從側(cè)面說明 管道文件就是一個(gè)純純的內(nèi)存級(jí)文件,有自己的上限,出現(xiàn)在文件系統(tǒng)中,只是單純掛個(gè)名而已
可以直接在命令行中使用命名管道:
echo
可以進(jìn)行數(shù)據(jù)寫入,可以重定向至fifo
cat
可以進(jìn)行數(shù)據(jù)讀取,同樣也可以重定向于fifo
- 打開兩個(gè)終端窗口(兩個(gè)進(jìn)程),即可進(jìn)行通信
當(dāng)然也可以通過程序?qū)崿F(xiàn)兩個(gè)獨(dú)立進(jìn)程 IPC
思路:創(chuàng)建 服務(wù)端 server
和 客戶端 client
兩個(gè)獨(dú)立的進(jìn)程,服務(wù)端 server
創(chuàng)建并以 讀
的方式打開管道文件,客戶端 client
以 寫
的方式打開管道文件,打開后倆進(jìn)程可以進(jìn)程通信,通信結(jié)束后,由客戶端關(guān)閉 寫端
(服務(wù)端 讀端
讀取到 0
后也關(guān)閉并刪除命令管道文件)
注意:
- 當(dāng)管道文件不存在時(shí),文件會(huì)打開失敗,因此為了確保正常通信,需要先運(yùn)行服務(wù)端
server
創(chuàng)建管道文件 - 服務(wù)端啟動(dòng)后,因?yàn)槭亲x端,所以會(huì)阻塞等待 客戶端(寫端)寫入數(shù)據(jù)
- 客戶端寫入數(shù)據(jù)時(shí),因?yàn)?
'\n'
也被讀取了,所以要去除此字符 - 通信結(jié)束后,需要服務(wù)端主動(dòng)刪除管道文件
unlink 命令管道文件名 //刪除管道文件
為了讓服務(wù)端和客戶端能享有同一個(gè)文件名,可以創(chuàng)建一個(gè)公共頭文件 common.h
,其中存儲(chǔ) 命名管道文件名及默認(rèn)權(quán)限等公有信息
公共資源
common.h
#pragma once
#include <iostream>
#include <string>
std::string fifo_name = "./fifo"; //管道名
uint32_t mode = 0666; //權(quán)限
服務(wù)端
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()
{
// 服務(wù)端
// 1、創(chuàng)建命名管道文件
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、讀取數(shù)據(jù)
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 << "寫端關(guān)閉,讀端讀取到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、寫入數(shù)據(jù),進(jìn)行通信
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
是一個(gè)字符串比較函數(shù),無論字符串大小寫,都能進(jìn)行比較
運(yùn)行效果:
所以 掛了名之后的命名管道是如何實(shí)現(xiàn)獨(dú)立進(jìn)程間 IPC
的呢?
1.2、命名管道的工作原理
把視角拉回文件系統(tǒng):當(dāng)重復(fù)多次打開同一個(gè)文件時(shí),并不會(huì)費(fèi)力的打開多次,而且在第一次打開的基礎(chǔ)上,對(duì) struct file
結(jié)構(gòu)體中的引用計(jì)數(shù) ++
,所以對(duì)于同一個(gè)文件,不同進(jìn)程打開了,看到的就是同一個(gè)
- 具體例子:顯示器文件(
stdout
)只有一個(gè)吧,是不是所有進(jìn)程都可以同時(shí)進(jìn)行寫入? - 同理,命名管道文件也是如此,先創(chuàng)建出文件,在文件系統(tǒng)中掛個(gè)名,然后讓獨(dú)立的進(jìn)程以不同的方式打開同一個(gè)命名管道文件,比如進(jìn)程
A
以只讀的方式打開,進(jìn)程B
以只寫的方式打開,那么此時(shí)進(jìn)程B
就可以向進(jìn)程A
寫文件,即IPC
因?yàn)槊艿肋m用于獨(dú)立的進(jìn)程間 IPC
,所以無論是讀端和寫端,進(jìn)程 A
、進(jìn)程 B
為其分配的 fd
是一致的,都是 3
- 如果是匿名管道,因?yàn)槭且揽坷^承才看到同一文件的,所以讀端和寫端
fd
不一樣
所以 命名管道 和 匿名管道 還是有區(qū)別的
1.3、命名管道與匿名管道的區(qū)別
不同點(diǎn):
- 匿名管道只能用于具有血緣關(guān)系的進(jìn)程間通信;而命名管道不講究,誰都可以用
- 匿名管道直接通過
pipe
函數(shù)創(chuàng)建使用;而命名管道需要先通過mkfifo
函數(shù)創(chuàng)建,然后再通過open
打開使用 - 出現(xiàn)多條匿名管道時(shí),可能會(huì)出現(xiàn)寫端
fd
重復(fù)繼承的情況;而命名管道不會(huì)出現(xiàn)這種情況
在其他方面,匿名管道與命名管道幾乎一致
- 兩個(gè)都屬于管道家族,都是最古老的進(jìn)程間通信方式,都自帶同步與互斥機(jī)制,提供的都是流式數(shù)據(jù)傳輸
2、命名管道的特點(diǎn)及特殊場(chǎng)景
命名管道的特點(diǎn)及特殊場(chǎng)景與匿名管道完全一致,這里簡(jiǎn)單回顧下,詳細(xì)內(nèi)容可跳轉(zhuǎn)至 《Linux進(jìn)程間通信【匿名管道】》
2.1、特點(diǎn)
可以簡(jiǎn)單總結(jié)為:
- 管道是半雙工通信
- 管道生命隨進(jìn)程而終止
- 命名管道任意多個(gè)進(jìn)程間通信
- 管道提供的是流式數(shù)據(jù)傳輸服務(wù)
- 管道自帶 同步與互斥 機(jī)制
2.2、四種特殊場(chǎng)景
四種場(chǎng)景分別為
- 管道為空時(shí),讀端阻塞,等待寫端寫入數(shù)據(jù)
- 管道為滿時(shí),寫端阻塞,等待讀端讀取數(shù)據(jù)
- 進(jìn)程通信時(shí),關(guān)閉讀端,
OS
發(fā)出13
號(hào)信號(hào)SIGPIPE
終止寫端進(jìn)程 - 進(jìn)程通信時(shí),關(guān)閉寫端,讀端讀取到
0
字節(jié)數(shù)據(jù),可以借此判斷終止讀端
3、命名管道實(shí)操
以下是一些使用命名管道實(shí)現(xiàn)的簡(jiǎn)單小程序,主要目的是為了熟悉命名管道的使用
3.1、實(shí)現(xiàn)文件拷貝
下載應(yīng)用的本質(zhì)是在下載文件,將服務(wù)器看作寫端,自己的電腦看作讀端,那么 下載 這個(gè)動(dòng)作本質(zhì)上就是 IPC
,不過是在網(wǎng)絡(luò)層面實(shí)現(xiàn)的
我們可以利用 命名管道實(shí)現(xiàn)不同進(jìn)程間 IPC
,即進(jìn)程從文件中讀取并寫入一批數(shù)據(jù),另一個(gè)進(jìn)程一次讀取一批數(shù)據(jù)并保存至新文件中,這樣就實(shí)現(xiàn)了文件的拷貝
目標(biāo):利用命名管道,向空文件 target.txt
中寫入數(shù)據(jù),即拷貝源文件 file.txt
公共資源
common.h
#pragma once
#include <iostream>
#include <string>
std::string fifo_name = "./fifo"; //管道名
uint32_t mode = 0666; //權(quán)限
服務(wù)端(寫端)
server.cc
提供文件拷貝服務(wù)
#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()
{
// 服務(wù)端
// 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、讀取源文件數(shù)據(jù)
char buff[1024];
int n = fread(buff, sizeof(char), sizeof(buff), fp);
//IPC區(qū)域
// 4、寫入源文件至命名管道
write(wfd, buff, strlen(buff));
cout << "服務(wù)端已向管道寫入: " << n << "字節(jié)的數(shù)據(jù)" << endl;
//IPC區(qū)域
fclose(fp);
fp = nullptr;
close(wfd);
return 0;
}
客戶端(讀端)
client.cc
從服務(wù)端中拷貝文件(下載)
#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、創(chuàng)建命名管道文件
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、打開目標(biāo)文件
FILE *fp = fopen("target.txt", "w");
if (fp == NULL)
{
cerr << "fopen fail! errno: " << errno << " | " << strerror(errno) << endl;
exit(0);
}
//IPC區(qū)域
// 4、讀取數(shù)據(jù)
char buff[1024];
int n = read(rfd, buff, sizeof(buff) - 1);
buff[n] = '\0';
if (n > 0)
cout << "客戶端已從管道讀取: " << n << "字節(jié)的數(shù)據(jù)" << endl;
else if (n == 0)
cout << "寫端關(guān)閉,讀端讀取到0,終止讀端" << endl;
else
cout << "讀取異常" << endl;
//IPC區(qū)域
//5、寫入目標(biāo)文件,完成拷貝
fwrite(buff, sizeof(char), strlen(buff), fp);
cout << "客戶端已成功從服務(wù)端下載(拷貝)了文件數(shù)據(jù)" << endl;
fclose(fp);
close(rfd);
unlink(fifo_name.c_str()); // 刪除命名管道文件
return 0;
}
拷貝結(jié)果:成功拷貝
此時(shí) 服務(wù)端是寫端,客戶端是讀端,實(shí)現(xiàn)的是 下載服務(wù);當(dāng) 服務(wù)端是讀端,客戶端是寫端時(shí),實(shí)現(xiàn)的就是 上傳服務(wù),搞兩條管道就能模擬實(shí)現(xiàn)簡(jiǎn)單的 數(shù)據(jù)雙向傳輸服務(wù)
注意:創(chuàng)建管道文件后,無論先啟動(dòng)讀端,還是先啟動(dòng)寫端,都要阻塞式的等待另一方進(jìn)行交互
3.2、實(shí)現(xiàn)進(jìn)程控制
在 Linux
匿名管道 IPC
中,我們實(shí)現(xiàn)了一個(gè)簡(jiǎn)易版的進(jìn)程控制程序,原理是通過多條匿名管道實(shí)現(xiàn)父進(jìn)程對(duì)多個(gè)子進(jìn)程執(zhí)行任務(wù)分配
匿名管道用于有血緣關(guān)系間 IPC
,命名管道也可以
所以我們可以把上一篇文章中的 匿名管道換為命名管道,一樣可以實(shí)現(xiàn)通信
任務(wù)池
Task.hpp
#include <iostream>
#include <string>
#include <functional>
#include <unordered_map>
#include <unistd.h>
using namespace std;
void PrintLOG()
{
cout << "PID: " << getpid() << " 正在執(zhí)行打印日志的任務(wù)…" << endl;
}
void InsertSQL()
{
cout << "PID: " << getpid() << " 正在執(zhí)行數(shù)據(jù)庫插入的任務(wù)…" << endl;
}
void NetRequst()
{
cout << "PID: " << getpid() << " 正在執(zhí)行網(wǎng)絡(luò)請(qǐng)求的任務(wù)…" << endl;
}
class Task
{
public:
Task()
{
// 裝載任務(wù)
_tt = {{"打印日志", PrintLOG}, {"數(shù)據(jù)庫插入", InsertSQL}, {"網(wǎng)絡(luò)請(qǐng)求", NetRequst}};
}
// 展示任務(wù)
void showTask()
{
cout << "目前可用任務(wù)有:[";
for (auto e : _tt)
cout << e.first << " ";
cout << "]" << endl;
cout << "輸入 退出 以終止程序" << endl;
}
// 執(zhí)行任務(wù)
void Execute(const string &task)
{
if (_tt.count(task) == 0)
{
cerr << "沒有這個(gè)任務(wù):" << task << endl;
}
else
{
_tt[task](); // 函數(shù)對(duì)象調(diào)用
}
}
private:
unordered_map<string, function<void(void)>> _tt;
};
控制程序
namePipeCtrl.cc
包括進(jìn)程、管道創(chuàng)建,任務(wù)執(zhí)行與進(jìn)程等待
#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
};
// 子進(jìn)程基本信息類
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;
// 進(jìn)程控制類
class ProcCtrl
{
public:
ProcCtrl(int num = 3, mode_t mode = 0666)
: _num(num), _mode(mode)
{
// 根據(jù) _num 創(chuàng)建命名管道及子進(jìn)程
CreatPipeAndProc();
}
~ProcCtrl()
{
waitProc();
}
// 創(chuàng)建管道及進(jìn)程
void CreatPipeAndProc()
{
// 因?yàn)槭抢^承的,所以也要注意寫端重復(fù)繼承問題
vector<int> fds;
for (int i = 0; i < _num; i++)
{
// 步驟:創(chuàng)建管道,存入 _vst
char pipeNameBUff[NAME_SIZE]; // 管道名緩沖區(qū)
snprintf(pipeNameBUff, NAME_SIZE, "./fifo-%d", i);
int ret = mkfifo(pipeNameBUff, _mode);
assert(ret != -1);
(void)ret;
_vst.push_back(string(pipeNameBUff));
// 創(chuàng)建子進(jìn)程,讓子進(jìn)程以只讀的方式打開管道文件
pid_t id = fork();
if (id == 0)
{
// 子進(jìn)程內(nèi)
// 先關(guān)閉不必要的寫端
for (auto e : fds)
close(e);
// 打開管道文件,并進(jìn)入任務(wù)等待默認(rèn)(讀端阻塞)
int rfd = open(_vst[i].c_str(), O_RDONLY);
assert(rfd != -1);
(void)rfd;
waitCommand(rfd);
close(rfd); // 關(guān)閉讀端
exit(0);
}
// 父進(jìn)程以寫打開管道,保存 fd 信息
int wfd = open(_vst[i].c_str(), O_WRONLY);
assert(wfd != -1);
(void)wfd;
// 注冊(cè)子進(jìn)程信息
_vpt.push_back(ProcINfo(id, wfd));
fds.push_back(wfd);
}
}
// 子進(jìn)程等待任務(wù)派發(fā)
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,寫端已關(guān)閉,讀端也即將關(guān)閉" << endl;
break;
}
else
{
cerr << "子進(jìn)程讀取異常!" << endl;
break;
}
}
}
// 展示可選進(jìn)程
void showProc()
{
cout << "目前可用進(jìn)程有:[";
int i = 0;
for (i = 0; i < _num - 1; i++)
cout << i << "|";
cout << i << "]" << endl;
}
// 下達(dá)任務(wù)給子進(jìn)程
void ctrlProc()
{
while (true)
{
cout << "==========================" << endl;
int n = 0;
do
{
showProc();
cout << "請(qǐng)選擇子進(jìn)程:> ";
cin >> n;
} while (n < 0 || n >= _num);
Task().showTask();
string taskName;
cout << "請(qǐng)選擇任務(wù):> ";
cin >> taskName;
if (taskName == "退出")
break;
// 將信息通過命名管道寫給子進(jìn)程
cout << "選擇進(jìn)程 ->" << _vpt[n]._name << " 執(zhí)行 " << taskName << " 任務(wù)" << endl;
write(_vpt[n]._wfd, taskName.c_str(), taskName.size());
sleep(1);
}
}
// 關(guān)閉寫端、刪除文件、等待子進(jìn)程退出
void waitProc()
{
for (int i = 0; i < _num; i++)
{
close(_vpt[i]._wfd); // 關(guān)閉寫端
unlink(_vst[i].c_str()); // 關(guān)閉管道文件
waitpid(_vpt[i]._pid, nullptr, 0); // 等待子進(jìn)程
}
cout << "所有子進(jìn)程已回收" << endl;
}
private:
vector<ProcINfo> _vpt; // 子進(jìn)程信息表
vector<string> _vst; // 命名管道信息表
int _num; // 子進(jìn)程數(shù)/命名管道數(shù)
mode_t _mode; // 命名管道文件的權(quán)限
};
int main()
{
ProcCtrl p1;
p1.ctrlProc();
return 0;
}
執(zhí)行結(jié)果如下:
關(guān)于 父子進(jìn)程間使用命名管道通信 值得注意的問題:
- 在命名管道創(chuàng)建后,需要先創(chuàng)建子進(jìn)程,讓子進(jìn)程打開【讀端或?qū)懚恕?,然后才讓父進(jìn)程打開【寫端或讀端】,這是因?yàn)榧偃缦茸尭高M(jìn)程打開【寫端或讀端】,那么此時(shí)父進(jìn)程就會(huì)進(jìn)入【阻塞】狀態(tài),導(dǎo)致無法創(chuàng)建子進(jìn)程,自然也就無法再打開【讀端或?qū)懚恕浚凰哉_做法是先讓子進(jìn)程打開,即使子進(jìn)程【阻塞】了,父進(jìn)程也還能運(yùn)行。不要讓【阻塞】阻礙子進(jìn)程的創(chuàng)建
- 子進(jìn)程繼承都存在的問題:寫端重復(fù)繼承,因此需要關(guān)閉不必要的寫端
fd
關(guān)于問題一的理解可以看看下面這兩張圖:
錯(cuò)誤用法: 父進(jìn)程先打開【寫端或讀端】,再創(chuàng)建子進(jìn)程,最后才讓子進(jìn)程打開【讀端或?qū)懚恕?/p>
正確用法: 先創(chuàng)建子進(jìn)程,讓子進(jìn)程打開【讀端或?qū)懚恕?,再讓父進(jìn)程打開【寫端或讀端】
3.3、實(shí)現(xiàn)進(jìn)程遙控(配合簡(jiǎn)易版 bash)
利用命名管道就可以遠(yuǎn)程遙控,原理很簡(jiǎn)單:簡(jiǎn)易版 bash
會(huì)等待命令輸入,將輸入源換成命名管道讀端,再創(chuàng)建一個(gè)獨(dú)立進(jìn)程,作為命名管道的寫端,此時(shí)就可以實(shí)現(xiàn)遠(yuǎn)程遙控進(jìn)程,執(zhí)行不同的指令
這里直接用之前寫好的 簡(jiǎn)易版 bash,關(guān)于 簡(jiǎn)易版 bash 的具體實(shí)現(xiàn)可以看看這篇文章 《Linux模擬實(shí)現(xiàn)【簡(jiǎn)易版bash】》
步驟:
- 創(chuàng)建命名管道
- 將
bash
改裝,打開命名管道文件,作為讀端
- 創(chuàng)建獨(dú)立進(jìn)程,打開命名管道文件,作為
寫端
- 進(jìn)行
IPC
,發(fā)送命令給bash
執(zhí)行
公共資源
common.h
#pragma once
#include <iostream>
#include <string>
std::string fifo_name = "./fifo"; //管道名
uint32_t mode = 0666; //權(quán)限
簡(jiǎn)易版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);
// 調(diào)用 C語言 中的 strtok 函數(shù)分割字符串
int pos = 0;
argv[pos++] = strtok(ps, DEF_CHAR); // 有空格就分割
while (argv[pos++] = strtok(NULL, DEF_CHAR))
; // 不斷分割
argv[pos] = NULL; // 確保安全
}
void showEnv()
{
extern char **environ; // 使用當(dāng)前進(jìn)行的環(huán)境變量表
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; // 創(chuàng)建對(duì)象 redir_type,默認(rèn)為 NONE
// 檢查是否出現(xiàn)重定向符
char *checkDir(char *command)
{
// 從右往左遍歷,遇到 > >> < 就置為 '\0'
size_t end = strlen(command); // 與返回值相匹配
char *ps = command + end; // 為了避免出現(xiàn)無符號(hào)-1,這里采取錯(cuò)位的方法
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]; // 大小與前面有關(guān)
int env_pos = 0; // 專門維護(hù)緩沖區(qū)
int exit_code = 0; // 保存退出碼的全局變量
// 2023.6.7 更新
// 創(chuàng)建管道文件
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;
// 這是一個(gè)始終運(yùn)行的程序:bash
while (1)
{
char command[COM_SIZE]; // 存放指令的數(shù)組(緩沖區(qū))
// 打印提示符
printf("[User@myBash default]$ ");
fflush(stdout);
// 讀取指令
//從管道中讀取
int n = read(rfd, command, COM_SIZE - 1);
if(n == 0)
{
cout << "寫端已關(guān)閉,讀端也即將關(guān)閉" << endl;
break;
}
command[n] = '\0';
cout << command << endl;
// 重定向
// 在獲取指令后進(jìn)行判斷
// 如果成立,則獲取目標(biāo)文件名 filename
char *filename = checkDir(command);
// 指令分割
// 將連續(xù)的指令分割為 argv 表
char *argv[ARGV_SIZE];
split(argv, command);
// 特殊處理
// 顏色高亮處理,識(shí)別是否為 ls 指令
if (strcmp(argv[0], "ls") == 0)
{
int pos = 0;
while (argv[pos++])
; // 找到尾
argv[pos - 1] = (char *)"--color=auto"; // 添加此字段
argv[pos] = NULL; // 結(jié)尾
}
// 目錄間移動(dòng)處理
if (strcmp(argv[0], "cd") == 0)
{
// 直接調(diào)用接口,然后 continue 不再執(zhí)行后續(xù)代碼
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; // 終止此次循環(huán)
}
// 環(huán)境變量相關(guān)
if (strcmp(argv[0], "export") == 0)
{
if (argv[1])
{
strcpy(myEnv[env_pos], argv[1]);
putenv(myEnv[env_pos++]);
}
continue; // 一樣需要提前結(jié)束循環(huán)
}
// 環(huán)境變量表
if (strcmp(argv[0], "env") == 0)
{
showEnv(); // 調(diào)用函數(shù),打印父進(jìn)程的環(huán)境變量表
continue; // 提前結(jié)束本次循環(huán)
}
// echo 相關(guān)
// 只有 echo $ 才做特殊處理(環(huán)境變量+退出碼)
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;
}
// 子進(jìn)程進(jìn)行程序替換
pid_t id = fork();
if (id == 0)
{
// 判斷是否需要進(jìn)行重定向
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); // 追加
}
// 直接執(zhí)行程序替換,這里使用 execvp
execvp(argv[0], argv);
exit(168); // 替換失敗后返回
}
// 父進(jìn)程等待子進(jìn)程終止
int status = 0;
waitpid(id, &status, 0); // 在等待隊(duì)列中阻塞
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); // 子進(jìn)程異常終止的情況
}
//關(guān)閉管道文件
close(rfd);
unlink(fifo_name.c_str());
return 0;
}
進(jìn)程控制端
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 << "遠(yuǎn)程發(fā)送指令:> ";
fgets(buff, sizeof(buff) - 1, stdin);
buff[strlen(buff) - 1] = '\0'; // 去除 '\n'
if (strcasecmp("exit", buff) == 0)
break;
// 向管道寫入數(shù)據(jù)
write(wfd, buff, strlen(buff));
}
close(wfd);
return 0;
}
實(shí)際效果如下:
注意:在進(jìn)行指令處理時(shí),需要注意 '\n'
,不能把 '\n'
帶入進(jìn)程替換中
3.4、實(shí)現(xiàn)字符實(shí)時(shí)讀取
回車 '\n'
這個(gè)東西很難處理,那么有沒有一種方式,能實(shí)現(xiàn)不輸入回車也能寫入數(shù)據(jù)至管道中呢?答案是有的
比如以下代碼,可以實(shí)現(xiàn)特殊化讀取,即 不需要特定條件觸發(fā)緩沖區(qū)沖刷,實(shí)時(shí)寫入字符
公共資源
common.h
#pragma once
#include <iostream>
#include <string>
std::string fifo_name = "./fifo"; //管道名
uint32_t mode = 0666; //權(quán)限
服務(wù)端
server.cc
實(shí)時(shí)讀取字符
#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()
{
// 服務(wù)端
// 1、創(chuàng)建命名管道文件
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、讀取數(shù)據(jù)
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 << "寫端關(guān)閉,讀端讀取到0,終止讀端" << endl;
break;
}
else
{
cout << "讀取異常" << endl;
break;
}
}
close(rfd);
unlink(fifo_name.c_str()); // 刪除命名管道文件
return 0;
}
客戶端
client.cc
實(shí)時(shí)發(fā)送字符
#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、寫入數(shù)據(jù),進(jìn)行通信
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;
}
實(shí)時(shí)讀取字符的效果如下:
本文中涉及的所有代碼均在此倉庫中:《命名管道博客倉庫》
??總結(jié)
以上就是本次關(guān)于 Linux
進(jìn)程間通信之命名管道的全部?jī)?nèi)容了,作為匿名管道的兄弟,命名管道具備匿名管道的大部分特性,使用方法也基本一致,不過二者在創(chuàng)建和打開方式上各有不同:匿名管道簡(jiǎn)單,但只能用于具有血緣關(guān)系進(jìn)程間通信,命名管道雖麻煩些,但適用于所有進(jìn)程間通信場(chǎng)景;在本文的最后,使用命名管道實(shí)現(xiàn)了幾個(gè)簡(jiǎn)單的小程序,這些小程序的本質(zhì)都是一樣的:創(chuàng)建命名管道 -> 打開命名管道 -> 通信 -> 關(guān)閉命名管道,掌握其中一個(gè)即可融會(huì)貫通
文章來源地址http://www.zghlxwxcb.cn/news/detail-475147.html
相關(guān)文章推薦 文章來源:http://www.zghlxwxcb.cn/news/detail-475147.html
Linux進(jìn)程間通信【匿名管道】
Linux基礎(chǔ)IO【軟硬鏈接與動(dòng)靜態(tài)庫】
Linux基礎(chǔ)IO【深入理解文件系統(tǒng)】
Linux【模擬實(shí)現(xiàn)C語言文件流】
Linux基礎(chǔ)IO【重定向及緩沖區(qū)理解】
Linux基礎(chǔ)IO【文件理解與操作】
到了這里,關(guān)于Linux進(jìn)程間通信【命名管道】的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!