公司微網(wǎng)站怎么建設(shè)廣州線上教學(xué)
目錄
前言
1.進(jìn)程間通信理論
2.使用管道進(jìn)行通信
3.管道的一些信息
4.應(yīng)用場(chǎng)景:進(jìn)程池(基于匿名管道)
5.命名管道
6.總結(jié)
前言
? 本篇我們開始學(xué)習(xí)linux進(jìn)程間通信相關(guān)的內(nèi)容,第一篇我們要介紹的是進(jìn)程間通信的理論概念以及匿名、命名管道通信的詳細(xì)操作,并且我們還會(huì)基于匿名管道實(shí)現(xiàn)一個(gè)簡(jiǎn)單的線程池,加油!!
1.進(jìn)程間通信理論
1.什么是通信
進(jìn)程間通信也叫做IPC技術(shù)
發(fā)展:從單機(jī)通信到網(wǎng)絡(luò)通信
如何理解通信的本質(zhì)問題:
1.操作系統(tǒng)需要直接或者間接給通信雙方的進(jìn)程提供 ”內(nèi)存空間“
2.要通信的進(jìn)程,必須看到一份公共的資源
通信前提:要先讓不同的進(jìn)程看到同一份資源【某一種“內(nèi)存”】(我們學(xué)通信實(shí)際上是學(xué)這個(gè))
不同的通信種類:
本質(zhì)就是:上面所說的資源,是操作系統(tǒng)的哪一個(gè)模塊提供的
2.為什么要有通信?
有時(shí)候我們是需要多進(jìn)程協(xié)同,然后去完成某種業(yè)務(wù)內(nèi)容比如: cat file | grep 'hello'
3.怎么辦?
進(jìn)程看到的同一份“資源”是由操作系統(tǒng)提供,操作系統(tǒng)需要設(shè)計(jì)統(tǒng)一的通信接口,這個(gè)接口要如何被調(diào)用就是我們的進(jìn)程間通信的方案,原則就是先統(tǒng)一標(biāo)準(zhǔn),后使用
進(jìn)程間通信的方案
采用標(biāo)準(zhǔn)做法:
POSIX —— 讓通信過程可以跨主機(jī)
System V —— 聚焦在本地通信
采用文件做法讓不同的進(jìn)程看到同一份資源:
1.管道——基于文件系統(tǒng)
a.匿名管道
b.命名管道
什么是管道
匿名管道,不需要IO,不需要刷新到磁盤上,和磁盤甚至沒關(guān)系,為內(nèi)存級(jí)文件,沒有所謂的名稱
兩進(jìn)程分別以讀和寫方式打開同一個(gè)文件,為了讓子進(jìn)程也能看到讀寫端
一般而言,我們的管道只能用來進(jìn)行單向數(shù)據(jù)通信,所以需要分別把父子進(jìn)程不需要的fd關(guān)閉
匿名管道:目前能用來進(jìn)行父子進(jìn)程之間進(jìn)程間通信
2.使用管道進(jìn)行通信
管道是被操作系統(tǒng)單獨(dú)設(shè)計(jì)的,需要配上單獨(dú)的系統(tǒng)調(diào)用
管道函數(shù):pipe
其中的pipefd為輸出型參數(shù),用來存儲(chǔ)打開文件后返回的讀寫兩個(gè)文件描述符
-
成功時(shí):返回
0
,同時(shí)會(huì)通過傳入的整數(shù)數(shù)組參數(shù)(如int pipefd[2]
)返回兩個(gè)文件描述符,pipefd[0]
用于從管道讀取數(shù)據(jù)(讀端),pipefd[1]
用于向管道寫入數(shù)據(jù)(寫端) -
失敗時(shí):返回
-1
,并設(shè)置errno
變量以標(biāo)識(shí)具體的錯(cuò)誤原因(如文件描述符用盡、參數(shù)地址不合法等)
[^] ?pipefd0是讀取(想象成嘴巴用來讀),pipefd1是寫入(想象成筆用來寫)?
我們可以看到創(chuàng)建pipe管道這個(gè)文件時(shí),pipe函數(shù)是不需要傳遞文件路徑的,因?yàn)樗莾?nèi)存級(jí)文件,壓根不需要文件路徑,也就是說也不需要文件名,所以叫做匿名管道
那沒有文件名,要怎么去保證兩個(gè)進(jìn)程打開的是同一個(gè)管道文件呢?答:子進(jìn)程繼承父進(jìn)程文件描述符表
各種 ‘printf’
前面幾種的區(qū)別是把內(nèi)容格式化到特定的文件或是字符串內(nèi),第一種printf就是格式化顯示到顯示器上
3.管道的一些信息
管道的5種特性:
-
匿名管道只能用來進(jìn)行具有”血緣關(guān)系“的進(jìn)程進(jìn)行進(jìn)程間通信(通常是父子)
-
管道文件,自帶同步機(jī)制(后面多線程詳談)
-
管道是面向字節(jié)流的
-
管道是單向通信的——屬于半雙工的一種特殊情況
a. 任何一個(gè)時(shí)刻,一個(gè)發(fā),一個(gè)收——半雙工
b. 任何一個(gè)時(shí)刻,可以同時(shí)發(fā)收——全雙工
-
(管道)文件的生命周期是隨進(jìn)程的,進(jìn)程一旦結(jié)束,所打開的文件就被操作系統(tǒng)關(guān)閉
管道的4種通信情況:
-
寫慢,讀快——讀端就要阻塞(進(jìn)程),要等寫端
-
寫快,讀慢——寫端就要阻塞(進(jìn)程),要等讀端
以上兩種情況是因?yàn)楣艿赖耐綑C(jī)制這一特性形成的
-
寫關(guān),繼續(xù)讀——read就會(huì)讀到返回值(\0也就是0),表示文件結(jié)尾,如果讓n=read(),那么n的值此時(shí)就為0
-
讀關(guān),繼續(xù)寫——寫端在寫入,沒有任何意義(os不會(huì)做沒有意義的事情),所以這種情況發(fā)生之后,操作系統(tǒng)會(huì)殺掉寫端進(jìn)程,然后發(fā)送異常信號(hào):13->SIGPIPE
管道的容量
[^] ?也就是65536/1024=64kb?
管道的寫入原子性
管道寫入的原子性是指:當(dāng)進(jìn)程向管道寫入數(shù)據(jù)時(shí),若數(shù)據(jù)量不超過 PIPE_BUF
(如 Linux 中為 4096 字節(jié),POSIX 規(guī)定至少 512 字節(jié)),該寫入操作要么完全成功(數(shù)據(jù)全部寫入),要么完全失敗(數(shù)據(jù)未寫入),不會(huì)出現(xiàn)部分寫入的情況
4.應(yīng)用場(chǎng)景:進(jìn)程池(基于匿名管道)
池化技術(shù)
可以減少我們創(chuàng)建對(duì)應(yīng)某種資源的成本,提高訪問時(shí)的效率
如果父進(jìn)程把所有任務(wù)都分配給一個(gè)子進(jìn)程——負(fù)載不均衡
我們要雨露均沾地分配任務(wù)——負(fù)載均衡
幾種方式實(shí)現(xiàn)均衡
-
輪詢
-
隨機(jī)
-
channel添加負(fù)載指標(biāo)
進(jìn)程池信道的建立
#pragma once
#include <iostream>
#include <cstdlib> //cstdlib->stdlib.h
#include <vector>
#include <unistd.h>
using namespace std;
?
//.hpp后綴可以使得方法實(shí)現(xiàn)和聲明在一個(gè)文件,這樣Main.cc在調(diào)用時(shí)只需要包含.hpp頭文件
?
// 進(jìn)程池
?
// 對(duì)(信道)管道進(jìn)行管理
// 先描述:描述管道信息
class channel
{
public:channel(int fd, pid_t id): _wfd(fd),_subid(id){// 使用to_string將fd和id都轉(zhuǎn)成字符加到后面作為名字一部分_name = "channel-" + to_string(_wfd) + "-" + to_string(_subid);}
?~channel() {}
?// 外部需要拿到channel內(nèi)部的信息,我們需要一些get方法int Fd() { return _wfd; }pid_t SubId() { return _subid; }string Name() { return _name; }
?
private:// 每個(gè)管道都需要有對(duì)應(yīng)的文件描述符int _wfd;// 還需要知道對(duì)應(yīng)的子進(jìn)程的idpid_t _subid;// 以及為了之后方便打印提示消息對(duì)應(yīng)管道的名字string _name;
};?
// 要?jiǎng)?chuàng)建多少個(gè)進(jìn)程池
const int gdefaultnum = 5;
?
// 再組織;管理信道(管道)的接口類,我們這里通過vector數(shù)據(jù)結(jié)構(gòu)來組織
class ChannelManager
{
public:ChannelManager() {}~ChannelManager() {}
?void Insert(int wfd, pid_t subid){// 構(gòu)建一個(gè)channel然后push到組織的vector數(shù)組中(先描述,再組織)// channel c(wfd, subid);//_channels.push_back(move(c)); //c為臨時(shí)對(duì)象,應(yīng)該使用右值引用減少拷貝// 或者也可以使用vector容器中提供的emplace_back方法// 直接調(diào)用內(nèi)部構(gòu)造函數(shù)幫我們創(chuàng)建一個(gè)對(duì)象在插入vector中_channels.emplace_back(wfd, subid);}
?// 打印_channels中各channel的信息的方法void PrintChannels(){for (auto &channel : _channels){cout << channel.Name() << endl;}}
?
private:// 通過vector來組織vector<channel> _channels;
};?
// 進(jìn)程池類
class ProcessPool
{
public:ProcessPool(int num): _process_num(num){}
?~ProcessPool() {}
?// 子進(jìn)程要做的工作void Work(int rfd){// 偽工作while (true){cout << "我的rfd是: " << rfd << endl;sleep(5);}}
?// 創(chuàng)建進(jìn)程池方法bool Create(){// 要?jiǎng)?chuàng)建process_num個(gè)進(jìn)程for (int i = 0; i < _process_num; i++){// 1. 創(chuàng)建管道int pipefd[2] = {0};int n = pipe(pipefd);if (n < 0){return false;}
?// 2. 創(chuàng)建子進(jìn)程// 子進(jìn)程讀,父進(jìn)程寫int subid = fork();if (subid < 0)return false;else if (subid == 0){// 子進(jìn)程// 3. 關(guān)閉寫端close(pipefd[1]);Work(pipefd[0]); // 子進(jìn)程要做的讀端相關(guān)工作close(pipefd[0]);
?// 執(zhí)行完直接退出// 不會(huì)干擾for循環(huán),只有父進(jìn)程會(huì)一直循環(huán)創(chuàng)建執(zhí)行循環(huán)內(nèi)代碼exit(0);}else{// 父進(jìn)程// 3. 關(guān)閉讀端close(pipefd[0]);// 需要在ChannelManager內(nèi)部把我們的fd和subid信息傳入創(chuàng)建的channel中// 再由ChannelManager把創(chuàng)建好的channel插入到數(shù)組中// 所以在ChannelManager類中需要Insert方法來完成上述操作// 其中我們父進(jìn)程對(duì)應(yīng)的fd文件描述符是寫端:pipefd[1]_cn.Insert(pipefd[1], subid);}}// 創(chuàng)建成功return true;}
?// Debug方法中查看每個(gè)channel信道(管道)的信息void Debug(){// 調(diào)用ChannelManager中的PrintChannels方法_cn.PrintChannels();}
?
private:// 進(jìn)程池類內(nèi)部需要包含我們的通信信道// 所以我們通過管理通信信道的類創(chuàng)建一個(gè)對(duì)象在進(jìn)程池內(nèi)部ChannelManager _cn;// 要?jiǎng)?chuàng)建的進(jìn)程池對(duì)應(yīng)的進(jìn)程的個(gè)數(shù),這樣就能確定管道的個(gè)數(shù)int _process_num;
};
采用輪詢的方式使得負(fù)載平衡
進(jìn)程池相關(guān)完整源代碼可以看:ProcessPool?
5.命名管道
匿名管道只能用來進(jìn)行具有血緣關(guān)系的進(jìn)程進(jìn)行進(jìn)程間通信(常用于父子進(jìn)程)
如果兩個(gè)進(jìn)程不相關(guān),該如何進(jìn)行通信呢?
通過上圖其實(shí)我們就已經(jīng)通過打開同一路徑下的同一個(gè)文件來做到讓不同的進(jìn)程看到了同一份資源(進(jìn)程間通信的前提),而因?yàn)槲募新窂?#xff08;使得這個(gè)路徑下文件具有唯一性,通過路徑來區(qū)分)、有名字——所以這種通信的方式叫做——命名管道!
我們一般文件是要刷新到磁盤的,我們的命名管道這種作為通信的文件是不能被刷新的,所以命名管道需要一種特殊的文件——管道文件(只會(huì)被打開,不需要刷新)
我們通過mkfifo命令來創(chuàng)建管道文件,也就是命名管道
echo和cat在執(zhí)行時(shí)就是進(jìn)程(shell運(yùn)行原理,不管是不是內(nèi)建命令,它都是進(jìn)程)
所以上圖中我們就利用命名管道完成了一次進(jìn)程間通信,echo將"hello fifo"重定向輸入,然后cat重定向輸出從fifo中顯示內(nèi)容到終端上
我們可以通過unlink命令來刪除管道文件
我們?cè)诖a上可以通過mkfifo接口來創(chuàng)建管道文件
記住這里的第一個(gè)參數(shù)是c語言格式的字符,如果用string對(duì)象,得調(diào)用c_str()
路徑加名字就構(gòu)成我們要構(gòu)建的命名管道了
string fifoname = _path + "/" + _name;
int n = mkfifo(fifoname.c_str(), 0666);
[^] ?上圖內(nèi)容不理解沒關(guān)系,我們?cè)诰W(wǎng)絡(luò)部分才會(huì)進(jìn)一步去理解?
同樣的,我們?cè)诖a上可以用unlink函數(shù)來關(guān)閉管道文件
[^] ?關(guān)閉成功返回0,失敗返回-1,和mkfifo創(chuàng)建返回邏輯是一樣的?
簡(jiǎn)單用命名管道通信代碼:
comm.hpp
#pragma once
?
#define FIFO_FILE "fifo"
server.cc
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
#include <string>
#include <fcntl.h>
#include "comm.hpp"
using namespace std;
?
int main()
{umask(0);// 新建管道int n = mkfifo(FIFO_FILE, 0666);if (n != 0){cerr << "mkdir fifo error" << endl;return 1;}cout << "mkfifo success" << endl;
?// 以讀打開管道文件// write方?jīng)]有執(zhí)行open時(shí),read方就要在open內(nèi)部阻塞// 直到有人把管道文件打開了,open才會(huì)返回int fd = open(FIFO_FILE, O_RDONLY);if (fd < 0){cerr << "open fifo error" << endl;return 2;}cout << "open fifo success" << endl;
?// 正常讀取char buffer[1024];while (true){int number = read(fd, buffer, sizeof(buffer) - 1);if (number > 0){// 我們把讀到內(nèi)容當(dāng)成字符串,所以得在n位置加上\0buffer[number] = '\0';cout << "client say: " << buffer << endl;}else if (number == 0){cout << "client quit! me too " << number << endl;break;}else{cerr << "read error" << endl;break;}}
?// 關(guān)閉管道文件close(fd);
?// 刪除管道文件n = unlink(FIFO_FILE);if (n == 0){cout << "remove fifo success" << endl;}else{cout << "remove fifo failed" << endl;}
?return 0;
}
client.cc
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
#include <string>
#include <fcntl.h>
#include "comm.hpp"
using namespace std;
?
int main()
{// client端不需要?jiǎng)?chuàng)建管道文件了,因?yàn)槲覀冊(cè)趕erver端已經(jīng)創(chuàng)建好了int fd = open(FIFO_FILE, O_WRONLY);// 剩下的都是文件操作了if (fd < 0){cerr << "open fifo error" << endl;return 2;}
?// 寫入操作string message;int cnt = 1;pid_t id = getpid();while (true){cout << "Please Enter# ";getline(cin, message);message += (",message number: " + to_string(cnt++) + ",[" + to_string(id) + "]");
?// c_str()用于將string對(duì)象轉(zhuǎn)換為 C 風(fēng)格的字符串(即以空字符\0結(jié)尾的字符數(shù)組)int n = write(fd, message.c_str(), message.size());if (n > 0){}}
?close(fd);
?return 0;
}
我們需要先執(zhí)行server編譯后的可執(zhí)行程序,來創(chuàng)建命名管道,而后它會(huì)阻塞在open那塊,因?yàn)槊艿佬枰獌蓚€(gè)進(jìn)程來同時(shí)打開(我們這里write方?jīng)]有執(zhí)行open時(shí),read方就要在open內(nèi)部阻塞,直到有人把管道文件打開了,open才會(huì)返回),所以接下來我們需要在另一個(gè)終端上執(zhí)行client編譯后的可執(zhí)行程序,就達(dá)到了用命名管道實(shí)現(xiàn)進(jìn)程間通信(從客戶端進(jìn)程向服務(wù)端進(jìn)程發(fā)送消息)的簡(jiǎn)單效果
當(dāng)我們關(guān)閉寫端client時(shí),讀端的number就為0了,然后也跟著退出后刪除管道文件
6.總結(jié)
命名管道和匿名管道的區(qū)別
其他的特性是完全和匿名管道一樣的,只有一點(diǎn)不同,那就是命名管道可以用來進(jìn)行讓不相關(guān)進(jìn)程進(jìn)行進(jìn)程間通信,命名管道也可以用來實(shí)現(xiàn)文件的拷貝