【Linux】-- 进程间通讯
目錄
進程間通訊概念的引入
意義(手段)
思維構建
進程間通信方式
管道
站在用戶角度-淺度理解管道
匿名管道 pipe函數
站在文件描述符角度-深度理解管道
管道的特點總結
管道的拓展
單機版的負載均衡
匿名管道讀寫規則
命名管道
前言
原理
創建一個命名管道
用命名管道實現myServer&myClient通信
匿名管道與命名管道的區別
命名管道的打開規則
system V共享內存
共享內存數據結構
共享內存的創建
key概念引入
key概念解析
基于共享內存理解信號量
總結
進程間通訊概念的引入
意義(手段)
? ? ? ? 在沒有進程間通訊之前,理論上都是單進程的,那么也就無法使用并發能力,更無法實現多進程協同(將一個事,分幾個進程做)。而進程間通訊,就是對于實現多進程協同的手段。
- 數據傳輸:一個進程需要將它的數據發送給另一個進程
- 資源共享:多個進程之間共享同樣的資源。
- 通知事件:一個進程需要向另一個或一組進程發送消息,通知它(它們)發生了某種事件(如進程終止時要通知父進程)。
- 進程控制:有些進程希望完全控制另一個進程的執行(如Debug進程),此時控制進程希望能夠攔截另一個進程的所有陷入和異常,并能夠及時知道它的狀態改變。
思維構建
? ? ? ? 進程間通訊重點,就在與如何讓不同的進程資源的傳遞。而進程是具有獨立性的,也就是說進程相通訊會難度較大? --? 因為進程間通訊的本質是:先讓不同的進程看見同一份資源。
融匯貫通的理解:
? ? ? ? 進程的設計天然就是為了保證獨立性的(即,進程之間無瓜葛),所以深入的說:所謂的同一份資源不能所屬于任何一個進程,更強調共享,不屬于任何一個進程。
進程間通信方式
管道
- 匿名管道pipe
- 命名管道
System V IPC
- System V 消息隊列
- System V 共享內存
- System V 信號量
POSIX IPC
- 消息隊列
- 共享內存
- 信號量
- 互斥量
- 條件變量
- 讀寫鎖
管道
????????我們把從一個進程連接到另一個進程的數據流稱為一個“管道”。
????????當在兩個命令之間設置管道 "|" 時,管道符 "|" 左邊命令的輸出就變成了右邊命令的輸入。只要第一個命令向標準輸出寫入,而第二個令是從標準輸入讀取,那么這兩個命令就可以形成一個管道。大部分的 Linux 命令都可以用來形成管道。
命令:who | wc -l
用于查看當前服務器下登陸的用戶人數。
補充:
????????Linux who命令:用于顯示系統中有哪些使用者正在上面,顯示的資料包含了使用者 ID、使用的終端機、從哪邊連上來的、上線時間、呆滯時間、CPU 使用量、動作等等。使用權限:所有使用者都可使用。
????????Linux wc命令:用于計算字數。在此處由于who中一個用戶為一行,所以此處用 -l?顯示行數,即登錄用戶個數。
? ? ? ? 其中,運行起來后who命令與wc命令就是兩個不同的進程。who進程作為數據提供方,通過標準輸入將數據寫入管道,wc進程再通過標準輸入將數據從管道中讀取出,進而再將數據進行處理?"-l" ,后以標準輸出的方式將結果給用戶。
站在用戶角度-淺度理解管道
匿名管道 pipe函數
#include <unistd.h>
功能: ????????創建一無名管道。 原型: ????????int pipe(int?pipefd[2]); 參數: ? ? ? ? 輸出型參數,通過調用該參數,得到被打開的文件fd。| pipefd[0] | 管道讀端文件描述符 |
| pipefd[1] | 管道寫端文件描述符 |
返回值:
????????成功時,返回0。出現錯誤時,返回-1。
1. 父進程創建管道?
2. 父進程fork出子進程
3. 父進程關閉讀 / 寫,子進程關閉寫 / 讀。(fork之后各自關掉不用的描述符)
Note:對于pipe函數創建的管道,只能夠進行單向通信。(反之,會導致讀寫導致管道中數據污染、混亂)。我們需要對于父或子進程中的fd參數中的,文件符號進行關閉。
????????pipe函數的使用需要結合fork函數的父子進程。
站在文件描述符角度-深度理解管道
#問:如何做到讓不同的進程,看到同一份資源?
? ? ? ? 以fork讓子進程繼承,能夠讓具有“血緣關系”的進程進行進程間通訊。(管道:常用于父進程進程)
融匯貫通的理解:
? ? ? ? fork創建子進程,等于系統中多了一個子進程。而進程 = 內核數據結構 + 進程代碼和數據。進程相關內核數據結構來源于操作系統,進程代碼和數據一般來源于磁盤。
????????而由于為了進程具有獨立性,所以創建子進程的同時,需要分配對應的進程相關內核結構。對于數據,被寫入更改時操作系統采用寫時拷貝技術,進行對父子進程數據的分離。
? ? ? ? 父進程與子進程擁有自身的fd_array[]存儲文件描述符fd,但是其中存儲的fd時相同的,而文件相關內核數據,并不屬于進程數據結構,所以并不會單獨為子進程創建。于是:父進程與子進程指向的是一個文件? ->? 這就讓不同的進程看到了同一份資源。
? ? ? ? 管道本質上就是一個文件。一個具有讀寫功能,并且無需放入磁盤的文件(通道是進程進行通訊的臨時內存空間,無需將內容放入磁盤中保留)。
(tty:標準輸入、標準輸出、標準錯誤)?
1. 父進程創建管道?
?2. 父進程fork出子進程
?3. 父進程關閉讀 / 寫,子進程關閉寫 / 讀。(fork之后各自關掉不用的描述符)
代碼實現的關鍵:
管道的特點總結
1. 管道是用來進程具有血緣關系的進程進行進程間通訊。
2. 管道具有通過讓進程間通訊,提供訪問控制。
? ? ? ? a、寫端快,讀端慢,寫滿了不能再寫了。
? ? ? ? b、寫端慢,讀端快,管道沒有數據的時候,讀需要等待。
補充:
? ? ? ? c、寫端關閉,讀端為0,標識讀到了文件結尾。
? ? ? ? d、讀端關閉,寫端繼續寫,操作系統終止寫端進程。
3. 管道提供的是面向流式的通信服務 -- 面向字節流。
4. 管道是基于文件的,文件的生命周期是隨進程的,所以管道的生命周期是隨進程的。
5. 管道是單向通行的,就是半雙工通信的一種特殊情況。
數據的傳送方式可以分為三種:
? ? ? ? 單工通信(Half Duplex):是通訊傳輸的一個術語。一方固定為發送端,另一方固定為接收端。即:一方只能寫一方只能讀。
????????半雙工通信(Half Duplex):是通訊傳輸的一個術語。指數據傳輸指數據可以在一個信號載體的兩個方向上傳輸,但是不能同時傳輸。即:一段時間內,只能一方寫一方讀。
????????全雙工通信(Full Duplex):是通訊傳輸的一個術語。指通信允許數據在兩個方向上同時傳輸,它在能力上相當于兩個單工通信方式的結合。即:一段時間內,每方能寫且讀。
管道的拓展
單機版的負載均衡
? ? ? ? 以循環fork函數開辟多個子進程,并利用pipe函數。針對于每一個子進程開辟一個管道,父進程通過管道安排其中一個子進程做某任務。
#pragma once#include <iostream> #include <unordered_map> #include <string> #include <functional>typedef std::function<void()> func;std::vector<func> callbacks; std::unordered_map<int, std::string> desc;void readMySQL() {std::cout << "sub process[" << getpid() << "]執行訪問數據庫的任務" << std::endl; }void executeUlt() {std::cout << "sub process[" << getpid() << "]執行url解析\n" << std::endl; }void cal() {std::cout << "sub process[" << getpid() << "] 執行加密任務\n" << std::endl; }void save() {std::cout << "sub process[" << getpid() << "] 執行數據持久化任務\n" << std::endl; }void load() {callbacks.push_back(readMySQL);desc.insert({callbacks.size(), "readMySQL: 執行訪問數據庫的任務"});callbacks.push_back(executeUlt);desc.insert({callbacks.size(), "executeUlt: 進行url解析"});callbacks.push_back(cal);desc.insert({callbacks.size(), "cal: 進行加密計算"});callbacks.push_back(save);desc.insert({callbacks.size(), "save: 執行數據持久化任務"}); }// 功能展示 void showHandler() {for(const auto &iter : desc)std::cout << iter.first << " -> " << iter.second << std::endl; }// 具有的功能數 int handlerSize() {return callbacks.size(); } #include <iostream> #include <vector> #include <unistd.h> #include <cassert> #include <sys/wait.h> #include <sys/types.h> #include "Task.hpp"using namespace std;#define PROCESS_NUM 4int waitCommand(int waitfd, bool& quit) {//此處由于是父進程寫入一個整數 -- 用以子進程執行相關內容//規定:子進程讀取的數據必須是4字節uint32_t command = 0;ssize_t s = read(waitfd, &command, sizeof(command));if(s == 0){quit = 1;return -1;}assert(s == sizeof(uint32_t));return command; }void wakeUp(pid_t who, int fd, uint32_t command) {write(fd, &command, sizeof(command));cout << "main process call: " << who << "process," << " execute: " << desc[command] << ", through write fd: " << fd << endl; }int main() {load();// 存儲:<子進程id,父進程對應寫端符fd>vector<pair<pid_t, int>> slots;//1. 創建多個進程for(int i = 0; i < PROCESS_NUM; ++i){//1.1 創建管道int pipefd[2] = {0};int n = pipe(pipefd);assert(n == 0);(void)n;//1.2 fork創建子進程pid_t id = fork();assert(id != -1);(void)id;if(id == 0){// 子進程 -- 關閉寫端close(pipefd[1]);while(true){// 用于判斷是否bool quit = 0;int command = waitCommand(pipefd[0], quit);if(quit){break;}if(command >= 1 && command <= handlerSize())callbacks[command - 1]();elsecout << "error, 非法操作" << endl;}exit(1);}//將父進程讀端關閉close(pipefd[0]);slots.push_back(make_pair(id, pipefd[1]));}while(true){int select;int command;cout << "############################################" << endl;cout << "## 1. show funcitons 2.command ##" << endl;cout << "############################################" << endl;cout << "Please Select> ";cin >> select;if(select == 1)showHandler();else if(select == 2){cout << "Enter command" << endl;cin >> command;// 隨機挑選進程int choice = rand() % PROCESS_NUM;//將任務指派給指定的進程wakeUp(slots[choice].first, slots[choice].second, command);}elsecout << "輸入錯誤,請重新輸入" << endl;}// 關閉父進程寫端fd,所有的子進程都會退出for(const auto &slot : slots)close(slot.second);// 回收所有的子進程信息for(const auto &slot : slots)waitpid(slot.first, nullptr, 0);return 0; }匿名管道讀寫規則
- 當沒有數據可讀時
- O_NONBLOCK disable:read調用阻塞,即進程暫停執行,一直等到有數據來到為止
- O_NONBLOCK enable:read調用返回-1,errno值為EAGAIN。
- 當管道滿的時候
- O_NONBLOCK disable: write調用阻塞,直到有進程讀走數據
- O_NONBLOCK enable:調用返回-1,errno值為EAGAIN
- 如果所有管道寫端對應的文件描述符被關閉,則read返回0
- 如果所有管道讀端對應的文件描述符被關閉,則write操作會產生信號SIGPIPE,進而可能導致write進程退出
- 當要寫入的數據量不大于PIPE_BUF時,linux將保證寫入的原子性。
- 當要寫入的數據量大于PIPE_BUF時,linux將不再保證寫入的原子性。
原子性:要么做,要么不做,沒有所謂的中間狀態。
POSIX.1-2001要求PIPE_BUF至少為512字節。(在Linux上,PIPE_BUF為4096字節。)
拓展:
? ? ? ? 討論原子性,需要在多執行流下,數據出現并發訪問的時候,討論原子性才有意義。(此處不深入)
融會貫通的理解:
? ? ? ? 匿名管道就是一個文件,一個內存級別的文件,并不會在磁盤上存儲,并不會有自身的文件名。作為基礎間通訊的方式是:看見同一個文件 -- 通過父子進程父子繼承的方式看見。
? ? ? ? 是一個,只有通過具有 “血緣關系” 的進程進行使用,可以稱做:父子進程通訊。
命名管道
前言
? ? ? ? 匿名管道只能使用于具有“親緣關系”的進程之間通信,而對于毫無關系的兩個進程無法使用匿名管道通訊,如果我們想在不相關的進程之間交換數據,可以使用FIFO文件來做這項工作,它經常被稱為命名管道。命名管道是一種特殊類型的文件。
原理
? ? ? ? 當兩個進程需要同時帶開一個文件的時候,由于為了保證進程的獨立性,所以兩個進程會有各自的files_struct,而對于文件數據,并不會為每一個進程都備一份(是內存的浪費),此時A進程的files_struct與B進程的files_struct是不同的,但是其中的文件符fd指向的是由磁盤文件加載到內存中的同一份數據空間。
? ? ? ? 命名管道就是如此,其原理與匿名管道很相識。命名管道在磁盤中,所以其有自己的文件名、屬性信息、路徑位置……,但是其沒有文件內容。即,命名管道是內存文件,其在磁盤中的本質是命名管道在磁盤中的映像,且映像的大小永遠為0。意義就是為了讓毫無關系的基進程,皆能夠調用到命名管道。而管道中的數據是進程通訊時的臨時數據,無存儲的意義,所以命名管道在磁盤中為空。
創建一個命名管道
- 命名管道可以從命令行上創建:
命令:mkfifo fifo
創建一個名為fifo命名管道
此時文件類型不是常用 - 與 d ,而是 p ,此文件的類型為管道:
?? ? ? ? 此時會發現處于等待狀態。因為由于我們寫了,但是對方還沒有打開,于是處于阻塞狀態。
? ? ? ? 此時 echo "hello name_pipe"(進程A)就是寫入的進程, cat(進程B)就是讀取的進程。這就是所謂的一個進程向另一個進程寫入消息的過程(通過管道寫入的方式)。
? ? ? ? 我們可以在命令行上使用循環的方式,往管道內每隔1s寫入數據。即,進程A原來應向顯示器文件寫入的數據,通過輸入重定向的方式,將數據寫入管道中,再將管道中數據通過輸出重定向,通過進程B將數據寫入到顯示器文件中。如此,以毫無相關的進程A與進程B通過命名管道進行數據傳輸 -?進程間通信。
? ? ? ? 此時我們通過終止讀取進程方,導致寫入端向管道寫入的數據無意義了(無讀取端),此時作為寫入端的進程就應該被操作系統殺掉。此時需要注意,echo是內置命令,所以是bush本身自行執行的命令,所以此時殺掉寫入端的進程無疑就是殺掉bush。于是bush被操作系統殺死,云服務器即退出。
內置命令:讓父進程(myshell)自己執行的命令,叫做內置命令,內建命令。
- 命名管道可以從程序里創建:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
參數:
? ? pathname:創建的命名管道文件。
- 路徑的方式給出。(會在對應路徑下創建)
- 文件名的方式給出。(默認當前路徑下創建)
????mode:創建命名管道文件的默認權限。
- 我們創建的文件權限會被umask(文件默認掩碼)進行影響,umask的默認值:0002,而實際創建出來文件的權限為:mode&(~umask)。于是導致我們創建的權限未隨我們的想法,如:0666 -> 0664。需要我們利用umask函數更改默認。
- umask(0); //將默認值設為 0000
返回值:????????
????????命名管道創建成功,返回0。
????????命名管道創建失敗,返回-1。
用命名管道實現myServer&myClient通信
? ? ? ? 利用命名管道,實現服務端myServer與客戶端myClient之間進行通訊。將服務端myServer運行起來并用mkfifo函數開辟一個命名管道。而客戶端myClient中利用open打開命名管道(命名管道本質為文件),以write向管道中輸入數據。以此服務端myServer利用open打開命名管道,以read從管道中讀取數據。
comm.hpp
? ? ? ? 所展開的頭文件集合。
#ifndef _COMM_H_ #define _COMM_H_#include <iostream> #include <string> #include <cstring> #include <cstdlib> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/wait.h>std::string ipcPath = "./fifo.ipc";#endifLog.hpp
? ? ? ? 編程的日志:就是當前程序運行的狀態。
#ifndef _LOG_H_ #define _LOG_H_#include <iostream> #include <ctime>#define Debug 0 #define Notice 1 #define Warning 2 #define Error 3std::string msg[] = {"Debug","Notice","Warning","Error" }std::ostream &Log(std::string message, int level) {std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message; }#endifmyServer.cc?
細節:
? ? ? ? mkfifo的第二個參數傳入權限0666之前需要以umask(0),對于服務端因為只需要在命名管道中讀取數據,所以以只讀的方式(O_RDONLY)open管道文件,后序以fork開辟子進程,讓子進程read讀取即可,同時也需要注意,C語言的字符串結尾必須是 '\0'(讀取大小:sizeof(buffer) - 1)。
? ? ? ? 由于我們讓子進程執行讀取工作,所以需要以waitpid等在子進程(此處我們讓nums個子進程進行,所以waitpid的第一個參數為 -1 ,等待任意一個子進程)。
? ? ? ? 由于open打開了管道類型的文件,所以需要以close(fd)關閉文件,由于mkfifo開辟了管道,所以需要以unlink刪除管道文件。
#include "comm.hpp"// 管道文件創建權限(umask == 0) #define MODE 0x0666// 讀取數據大小 #define READ_SIZE 64// 從管道文件讀取數據 static void getMessage(int fd) {char buffer[READ_SIZE];while(true){memset(buffer, '\0', sizeof(buffer));ssize_t s = read(fd, buffer, sizeof(buffer) - 1); // C語言字符串需要保證結尾為'\0'if(s > 0){std::cout <<"[" << getpid() << "] "<< "myClient say> " << buffer << std::endl;}else if(s == 0){// 寫端關閉 - 讀到文件結尾std::cerr <<"[" << getpid() << "] " << "read end of file, clien quit, server quit too!" << std::endl;}else{// 讀取錯誤perror("read");exit(3);}} }int main() {//1. 創建管道文件umask(0);if(mkfifo(ipcPath.c_str(), MODE) < 0){perror("mkfifo");exit(1);}#ifdef DEBUGLog("創建管道文件成功", Debug) << " step 1 " << std::endl;#endif//2. 正常的文件操作int fd = open(ipcPath.c_str(), O_RDONLY);if(fd < 0){perror("open");exit(2);}#ifdef DEBUGLog("打開管道文件成功", Debug) << " step 2 " << std::endl;#endifint nums = 3;// 創建3個子進程for(int i = 0; i < nums; ++i){pid_t id = fork();if(fd == 0){// 子進程 - 讀取管道數據getMessage(fd);exit(1);}}// 父進程 - 等待子進程for(int i = 0; i < nums; i++){waitpid(-1, nullptr, 0);}// 4. 關閉管道文件close(fd);#ifdef DEBUGLog("關閉管道文件成功", Debug) << " step 3 " << std::endl;#endifunlink(ipcPath.c_str()); // 通信完畢,就刪除管道文件#ifdef DEBUGLog("刪除管道文件成功", Debug) << " step 4 " << std::endl;#endifreturn 0; }myClient.cc
細節:
? ? ? ? 對于客戶端因為只需要在命名管道中寫入數據,所以以只寫的方式(O_WRONLY)open管道文件,后序write即可。
#include "comm.hpp"int main() {//1. 獲取管道文件 - 以寫的方式打開命名管道文件int fd = open(ipcPath.c_str(), O_WRONLY);if(fd < 0){perror("open");exit(1);}//2. ipc過程std::string buffer; //用戶級緩沖區while(true){std::cout << "Please Enter Message Line :> ";std::getline(std::cin, buffer);write(fd, buffer.c_str(), buffer.size());}//3. 通信完畢,關閉命名管道文件close(fd);return 0; }????????由于命名管道的創建是在服務端myServer中,所以需要先運行myServer。
? ? ? ? 服務端myServer進程運行起來,我們就能看到創建的命名管道文件。此時服務端myServer處于阻塞狀態也是管道文件的特性(寫入端未開辟,讀取端需要等待寫入端開辟)。
? ? ? ? 可以通過 ps 命令查看進程是否相關:
? ? ? ? 從此可以看出myServer與myClient是毫無相關的進程,即myServer的三個子進程與myClient也是毫無相關的進程。
匿名管道與命名管道的區別
- 匿名管道由pipe函數創建并打開。
- 命名管道由mkfififo函數創建,打開用open
- FIFO(命名管道)與pipe(匿名管道)之間唯一的區別在它們創建與打開的方式不同,一但這些工作完成之后,它們具有相同的語義。
命名管道的打開規則
- 如果當前打開操作是為讀而打開FIFO時
- O_NONBLOCK disable:阻塞直到有相應進程為寫而打開該FIFO
- O_NONBLOCK enable:立刻返回成功
- 如果當前打開操作是為寫而打開FIFO時
- O_NONBLOCK disable:阻塞直到有相應進程為讀而打開該FIFO
- O_NONBLOCK enable:立刻返回失敗,錯誤碼為ENXIO
system V共享內存
????????system V共享內存是與管道不同的,管道基于操作系統已有的文件操作。文件部分,無論有沒有通訊的需求,這個文件都需要維護,有沒有通訊都需要和指定進程建立關聯,通不通訊都會有。
????????而共享內存是,不用來通訊,操作系統就不用進行管理,只有需要使用時,操作系統才提供 - 有通訊才會有,共享內存。共享內存是操作系統單獨設立的內核模塊,專門為進程間通訊設計?--? 這個內核模塊就是system V。
? ? ? ? 即:前面的匿名管道、命名管道通訊是恰好使用文件方案可以實現。而共享內存是操作系統專門為了通訊設計。
共享內存的建立:
- 共享區:共享內存、內存映射和共享庫保存位置。
共享內存數據結構
? ? ? ? 共享內存的提供者,是操作系統。
? ? ? ? 大量的進程進行通訊 -> 共享內存是大量的。所以,操作系統對于共享內存需要進行管理,需要管理 -> 先描述,再組織 -> 重新理解:共享內存 = 共享內存塊 + 對應的共享內存的內核數據結構。
共享內存的數據結構?shmid_ds 在?/usr/include/linux/shm.h 中定義:
(cat命令即可)
struct shmid_ds
{
????????struct ipc_perm?? ??? ?shm_perm;?? ?/* operation perms */
????????int?? ??? ??? ?shm_segsz;?? ?/* size of segment (bytes) */
????????__kernel_time_t?? ??? ?shm_atime;?? ?/* last attach time */
????????__kernel_time_t?? ??? ?shm_dtime;?? ?/* last detach time */
????????__kernel_time_t?? ??? ?shm_ctime;?? ?/* last change time */
????????__kernel_ipc_pid_t?? ?shm_cpid;?? ?/* pid of creator */
????????__kernel_ipc_pid_t?? ?shm_lpid;?? ?/* pid of last operator */
????????unsigned short?? ??? ?shm_nattch;?? ?/* no. of current attaches */
????????unsigned short ?? ??? ?shm_unused;?? ?/* compatibility */
????????void ?? ??? ??? ?*shm_unused2;?? ?/* ditto - used by DIPC */
????????void?? ??? ??? ?*shm_unused3;?? ?/* unused */
};
????????此處首先提一下key值(后面共享內存的建立引入),其是在上面的共享內存的第一個參數struct ipc_perm類型的shm_perm變量中的一個變量。
在?/usr/include/linux/ipc.h?中定義:
struct ipc_perm
{
????????__kernel_key_t?? ?key;
????????__kernel_uid_t?? ?uid;
????????__kernel_gid_t?? ?gid;
????????__kernel_uid_t?? ?cuid;
????????__kernel_gid_t?? ?cgid;
????????__kernel_mode_t?? ?mode;?
????????unsigned short?? ?seq;
};
共享內存的創建
#include <sys/ipc.h> #include <sys/shm.h> // 用來創建共享內存 int shmget(key_t key, size_t size, int shmflg);參數:
????????key:這個共享內存段名字。
????????size:共享內存大小。
- 大小建議為4096的整數倍。(原因使用時講解)
????????shmflg:由九個權限標志構成,它們的用法和創建文件時使用的mode模式標志是一樣的。
| IPC_CREAT | 創建共享內存,如果底層已經存在,獲取之,并且返回。如果底層不存在,創建之,并且返回。 |
| IPC_EXCL | 沒有意義 |
| IPC_CREAT | IPC_EXCL | 創建共享內存,如果底層不存在,創建之,并且返回。如果底層存在,出錯返回。 |
IPC_CREAT | IPC_EXCL意義:可以保證,放回成功一定是一個全新的共享內存(shm)。
此外創建需要權限的初始化:
????????如:IPC_CREAT | IPC_EXCL | 0666
返回值:
????????成功返回一個非負整數,即該共享內存段的標識碼(用戶層標識符);失敗返回-1。
key概念引入
????????進程間通訊,首先需要保證的看見同一份資源。
融會貫通的理解:
- 匿名管道:通過pipe函數開辟內存級管道 -- 本質是文件 -- 通過pipe函數的參數(文件符fd)--?看見同一份資源。
- 命名管道:通過mkfifo函數根據路徑開辟管道文件(可以從權限p看出)-- 本質是開辟一個文件(可以從第二個參數需要初始化權限看出)-- 利用open、write、read、close文件級操作?--?看見同一份資源。
????????管道 -- 內存級文件 -- 恰巧利用文件操作。前面已有所提system V共享內存,是操作系統為進程間通訊專門設計?,并無法利用類似于管道利用文件實現。于是便有了key。
key概念解析
? ? ? ? key其實就是一個整數,是一個利用算法實現的整數。我們可以將key想象為一把鑰匙,而共享內存為一把鎖。
? ? ? ? 更像是同心鎖和一對對情侶,情侶拿著同樣的鑰匙只可解一堆鎖中的一把鎖。
? ? ? ? 如同一把鑰匙會按照固定的形狀制造。其會使用同樣的算法規則形成一個唯一值key,同時再創建共享內存時,會將key值設置進其中,此時兩個毫無關系的進程,就可以通過key值用共享內存進行通訊(一方創建共享內存,一方獲取共享內存)。
制造唯一值key的算法:
#include <sys/types.h> #include <sys/ipc.h>key_t ftok(const char *pathname, int proj_id);????????其不進行任何系統調用,其內部是一套算法,該算法就是將兩個參數合起來,形成一個唯一值就可以,數值是幾不重要。(對于第一個參數,ftok是拿帶文件的inode標號,所以路徑可以隨意寫,但必須保證具體訪問權限),proj_id(項目id),隨意寫即可,一般是0~255之間,可以隨便寫,因為超了其也會直接截斷。
返回值:
? ? ? ? 成功后,返回生成的key_t值。失敗時返回-1。
note:
- 終究就是個簡易的算法,所以key值可能會產生沖突,于是可以對傳入ftok函數的參數進行修改。
- 需要保證需要通訊的進程使用的?pathname?與?proj_id 相同,如此才能保證生成的是同一個key值。
簡易的使用shmget函數結合ftok函數:
????????其不進行任何系統調用,其內部是一套算法,該算法就是將兩個參數合起來,形成一個唯一值就可以,數值是幾不重要。(對于第一個參數,ftok是拿帶文件的inode標號,路徑可以隨意寫,但必須保證具體訪問權限)
? ? ? ? 兩個進程要通訊,就要保證兩個看見統一個共享內存,本質上:保證兩個看到同一個key。
? ? ? ? 與文件不同,文件是打開了,最后進程退出,文件沒有進程與其關聯,文件就會自動釋放。
? ? ? ? 操作系統為了維護共享內存,就需要先描述,再組織。所以,共享內存在內核里,處理共享內存的存儲內存空間,也需要存儲對其描述信息的數據結構。所以,為了設置或獲取其的屬性,就通過第三個參數。(當只需要刪除的時候,第三個參數設為nullptr即可)
? ? ? ? 操作系統管理物理內存的時候,頁得大小是以4KB為單位。也就是4096byte,如果我們用4097byte,就多這1byte,操作系統就會在底層,直接創建4096 * 2byte的空間,此時多余的4095byte并不會使用,就浪費了。
? ? ? ? 此處,我們以4097byte申請,操作系統開辟了4096 * 2byte,但是查詢下是4097byte,因為,操作系統分配了空間,但是并不代表對所有都有權利訪問,我們要的是4097byte,那操作系統只會給對應的權限。所以建議配4096byte的整數倍。
????????prems:權限。此處為0 ,代表任何一個人,包括我們,都沒有權力讀寫共享內存,此時創建共性內存也就沒了意義。于是我們需要再加一個選項,設置權限。
? ? ? ? nattch:n標識個數,attch表示關聯。表示有多少個進程與該共享內存關聯。
? ? ? ? 需要將指定的共享內存,掛接到自己的進程的地址空間。
參數:
·? ? ? ? 范圍值,共享內存的起始地址。
文件描述符,文件有其對應的文件指針,可用戶從來不會用文件指針,用的全是文件描述符,它們都可以用來標定一個文件。同樣的道理shmid與key,它們都可以用來標定共享內存的唯一性。(key:標定共享內存在系統級別上的唯一性。shmid:標定共享內存的用戶級別上的唯一性。)所以我們在用的時候全部都是shmid。只要是指令編寫的時候,就是在用戶層次的,所以ipcs等用的是shmid。
????????system V IPC資源,生命周期隨內核,與之相對的是生命周期隨進程。即,操作系統會一直保存這個資源,除非用戶用手動命令刪除,否則用代碼刪除。
????????共享內存由操作系統提供,并對其進行管理(先描述,再組織) -> 共享內存 = 共享內存塊 + 對應的共享內存的內核數據結構。
融會貫通的理解:
????????一個內存為4G的地址空間,0~3G屬于用戶,3~4G屬于內核。所謂的操作系統在進行調度的時候,執行系統調用接口、庫函數。本質上都是要將代碼映射到地址空間當中,所以我們的代碼無論是執行動態庫,還是執行操作系統的代碼。都是在其地址空間中完成的。所以對于任何進程,3~4G都是操作系統的代碼和數據,所以無論進程如何千變萬化,操作系統永遠都能被找到。
? ? ? ? 堆棧之間的共享區:是用戶空間,該空間拿到了,無需經過系統調用便可直接訪問。 -- 共享內存,是不用經過系統調用,直接可以進行訪問!雙方進程如果要通訊,直接進行內存級的讀和寫即。
融會貫通的理解:
????????前面所說的匿名管道(pipe)、命名管道(fifo)。都需要通過read、write(IO系統調用)來進行通訊。因為這兩個屬于文件,而文件是在內核當中的特定數據結構,所以其是操作系統維護的 -- 其是在3~4G的操作系統空間范圍中。(無權訪問,必須使用系統接口)
?共享內存在被創建號之后,默認被清成全0,所以打印字符是空串。
????????共享內存就是天然的為了讓我們可以快速訪問的機制,所以其內部沒有提供任何的控制策略。(共享內存中有數據讀端讀,沒數據讀端也讀。甚至客戶端(寫入端)不在了,其也讀。)更直接的說:寫入端和讀取端根本不知道對方的存在。
? ? ? ? 缺乏控制策略 -- 會帶來并發的問題。
拓展:
并發的問題,如:
????????客戶端想讓一個進程處理一個完整的數據內容,然而客戶端在未完全寫入共享內存時,讀取方就將不完整的數據讀取并處理,此時處理結果為未定義。 --? 數據不一致問題
基于共享內存理解信號量
根據前面的學習:
- 匿名管道通過派生子進程的方式看見同一份資源。
- 命名管道通過路徑的方式看見同一份資源。
- 共享內存通過key值得方式看見同一份資源。
????????所以,為了讓進程間通訊?-> 讓不同的進程之間,看見同一份資源 -> 本質:讓不同的進程看見同一份資源。
? ? ? ? 通過前面得到學習我們會發現,如共享進程,其并沒有訪問控制,即:獨斷讀取的時機是不確定的,這也就帶來了一些時序問題 ——?照成數據的不一致問題。
引入兩個概念:
? ? ? ? 所以,多個進程(執行流),互相運行的時候互相干擾,主要是我們不加以保護的訪問了相同的資源(臨界資源),在非臨界區多個進程(執行流)互相是不干擾的。
? ? ? ? 而為了更好的進行臨界資源的保護,可以讓多個進程(執行流)在任何時刻,都只能有一個進程進入臨界區 ——? 互斥?。
互斥的理解:
? ? ? ? 我們可以將,一個執行流:人,臨界區:電影院(一個位置的電影院)。
? ? ? ? 看電影一定要有位置(電影院中的唯一位子)。當前一個人在其中看電影,那么其他人必須等待他看完才可進入觀看。并且電影院中,此唯一的位置是并不屬于觀影人的,而是買票,只要買了票,即在你進去看完電影之前,就擁有了這個位置。買票:就是對座位的?預定?機制。
? ? ? ? 同樣的道理,進程想進入臨界資源,訪問臨界資源,不能讓進程直接去使用臨界資源(不能讓用戶直接去電影院內部占資源),需要先申請票 ——? 信號量 。
? ? ? ? 信號量 的存在是等于一張票。"票"的意義是互斥,而互斥的本質是串形化,互斥就是一個在跑另一個就不能跑,需要等待跑完才能跑。其必須串形的去執行。但是一旦串形的去執行,多并發的效率就差了。所以:
? ? ? ? 當有一份公共資源,只要有多個執行流訪問的是這個公共資源的不同區域,這個時候可以允許多個執行流同時進入臨界區。這個時候可以根據區域的數量(如同電影院座位的個數 -> 允許觀影的人數),可以讓對應的進程個數并發的執行自己臨界區的代碼(看電影的自行觀影)。
? ? ? ? 信號量本質上:就是一個計數器,類似于int count = n(n張票)。
申請信號量:
????????只要申請信號量成功 ……只要申請成功,一定在臨界區中有一個資源對應提供的。
????????換句話說:首先,我們要進行訪問信號量計數器,要每一個線程訪問計數器,必須保證信號量本身的 --操作 以及 ++操作 是原子的。否者很難保護臨界資源。其次,信號量需要是公共的,能被所有進程能看到的資源,叫做臨界資源 —— 而信號量計數器存在的意義就是保護臨界資源,但是其有又成了臨界資源,所以其必須保證自己是安全的,才能保證臨界資源的安全。
#:如果用一個整數,表示信號量。假設讓多個進程(整數n在共享內存里),看見同一個全局變量,都可以進行申請信號量?—— 不可以的。
CPU執行指令的時候:
復習:
? ? ? ? 執行流在執行的時候,在任何時刻都可能被切換。
切換的本質:CPU內的寄存器是只有一份的,但是寄存器需要存儲的臨時數據(上下文)是多份的,分別對應不同的進程!
? ? ? ? 我們知道,每一個進程的上下文是不一樣的,寄存器只有一份,那么根據并發,為下一個進程讓出位置。并且由于,上下文數據絕而對不可以被拋棄!
? ? ? ? 當進程A暫時被切下來的時候,需要進程A順便帶走直接的上下文數據!帶走暫時保存數據的是為了下一次回來的時候,能夠恢復上去,以此繼續按照之前的邏輯繼續向后運行,就如同沒有中斷過一樣。
? ? ? ? 由于寄存器只有一套,被所有的執行流共享,但是寄存器里面的數據,屬于一個執行流(屬于該執行流的上下文數據)。所以對應的執行流需要將上下文數據進行保護,方便與上下文數據恢復(重新回到CPU,更具上下文數據繼續執行)。
????????當myClient執行的時候,重點在于n--,到n++,因為時序的問題,會導致n有中間狀態。切換為myServer執行的時候,中間狀態會導致數據不一致。
? ? ? ? 即,CPU執行myClient中的寫入數據到共享內存時,就被替換了:
(CUP執行到n的中間狀態)
(myClient被切換為myServer)
(myServer信號量執行完了,并將n寫回)
(myCilent帶著自己的上下文數據,并將n寫回)
????????此時1 -> 2,凸顯了信號量操作必須是原子性的,只有原子性才不會怕因時序,導致的數據不一致問題。
總結:
- 申請信號量 -> 計數器-- -> P操作 -> 必須是原子的
- 申請信號量 -> 計數器++?-> V操作 -> 必須是原子的
總結
????????所以,由于信號量的思想,也是讓我們看見同一份資源,所以其本質與上面的管道、共享內存沒有太大的區別。所以,信號量被納入進程間通訊的范疇。
? ? ? ? 信號量是為了保證特定的臨界資源不被受侵害,保證臨界資源數據一致性。前面所講:信號量也是一個臨界資源,所以首先其需要保證自己的安全性 ——?提出信號量操作需是原子性的。?
? ? ? ? 而信號量理論的提出是由于:臨界區、臨界資源的?互斥 ,當多個執行流(進程)才會真正的凸顯出來,所以此處由于是進程間通訊 —— 需要提出信號量,但作用凸顯在多線程 —— 多線程再深入講解信號量。
總結
以上是生活随笔為你收集整理的【Linux】-- 进程间通讯的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 经典技术书籍大全
- 下一篇: 使用 CSS 创建下拉导航栏