日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > linux >内容正文

linux

【Linux】-- 进程间通讯

發布時間:2024/3/24 linux 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【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之后各自關掉不用的描述符)

代碼實現的關鍵:

  • 創建管道 -- 分別以讀寫方式打開同一個問題
  • 創建子進程 -- 以fork函數創建子進程
  • 構造單向通訊的通道 -- 雙方進程各自關閉自己不需要的文件描述符
  • #include <iostream> #include <unistd.h> #include <assert.h> #include <string> #include <string.h> #include <sys/wait.h> #include <sys/types.h>using namespace std;int main() {//1.創建管道int pipefd[2] = {0};int n = pipe(pipefd);assert(n != -1);(void)n; // 只被定義沒有被使用,Release下就會出現代碼大量告警 ?-- ?證明使用過// 用于調試驗證fd申請 #ifdef DEBUGcout << "pipefd[0]: " << pipefd[0] << endl;cout << "pipefd[1]: " << pipefd[1] << endl; #endif//2.創建子進程pid_t id = fork();assert(id != -1);if(id > 1){// 子進程 -- 只讀// 3.構造單向通訊的通道, 父進程寫入,子進程讀取// 3.1 關閉子進程不需要的fdclose(pipefd[1]);char child_buffer[1024*4];while(true){ssize_t s = read(pipefd[0], child_buffer, sizeof(child_buffer) - 1);//3.2 訪問控制:// a、寫入的一方,fd沒有關閉,如果有數據,就讀,沒有數據就等// b、寫入的一方,fd關閉, 讀取的一方,read會返回0,表示讀到了文件的結尾!if(s > 0){child_buffer[s] = 0;cout << "child get a message[" << getpid() << "] Father# " << child_buffer << endl;}else if(s == 0){cout << "-----------writer quit(father), me quit!-----------" << endl;break;}}exit(0);}// 父進程 -- 只寫// 3.構造單向通訊的通道, 父進程寫入,子進程讀取// 3.1 關閉父進程不需要的fdclose(pipefd[0]);string message = "我是父進程,發送有效信息。";int count = 0; // 傳遞的次數char father_buffer[1024*4];while(true){//3.2 構建一個變化的字符串snprintf(father_buffer, sizeof(father_buffer), "%s[%d] : %d",message.c_str(), getpid(), count++);//3.3 寫入write(pipefd[1], father_buffer, strlen(father_buffer));//3.4 故意sleep凸顯訪問控制sleep(1);if(count == 3){cout << "----------------father wirte quit!----------------" <<endl;break;} }close(pipefd[1]);pid_t ret = waitpid(id, nullptr, 0);assert(ret > 0);(void)ret;return 0; }

    管道的特點總結

    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";#endif

    Log.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; }#endif

    myServer.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表示關聯。表示有多少個進程與該共享內存關聯。

    ? ? ? ? 需要將指定的共享內存,掛接到自己的進程的地址空間。

    參數:

  • 要掛接的共享內存的用戶管理的對應的id。(獲取共享內存時的id)
  • 我們需要指定的虛擬地址。共享內存掛接時,可將其掛接到指定的虛擬地址。(一般不推薦,因為虛擬地址的使用情況我們并不是十分的清楚。即使,我們能獲取到),設置為nullptr讓操作系統自行掛接即可。
  • 掛接方式。設置為0即可,默認會以讀寫的方式掛好。
  • ·? ? ? ? 范圍值,共享內存的起始地址。

    文件描述符,文件有其對應的文件指針,可用戶從來不會用文件指針,用的全是文件描述符,它們都可以用來標定一個文件。同樣的道理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內的寄存器中(讀指令)。
  • n--(分析 && 執行指令)。
  • 將CPU修改完的數據n寫回到內存(寫回結果)。
  • 復習:

    ? ? ? ? 執行流在執行的時候,在任何時刻都可能被切換。

    切換的本質:CPU內的寄存器是只有一份的,但是寄存器需要存儲的臨時數據(上下文)是多份的,分別對應不同的進程!

    ? ? ? ? 我們知道,每一個進程的上下文是不一樣的,寄存器只有一份,那么根據并發,為下一個進程讓出位置。并且由于,上下文數據絕而對不可以被拋棄!

    ? ? ? ? 當進程A暫時被切下來的時候,需要進程A順便帶走直接的上下文數據!帶走暫時保存數據的是為了下一次回來的時候,能夠恢復上去,以此繼續按照之前的邏輯繼續向后運行,就如同沒有中斷過一樣。

    ? ? ? ? 由于寄存器只有一套,被所有的執行流共享,但是寄存器里面的數據,屬于一個執行流(屬于該執行流的上下文數據)。所以對應的執行流需要將上下文數據進行保護,方便與上下文數據恢復(重新回到CPU,更具上下文數據繼續執行)。

    ????????當myClient執行的時候,重點在于n--,到n++,因為時序的問題,會導致n有中間狀態。切換為myServer執行的時候,中間狀態會導致數據不一致。

    ? ? ? ? 即,CPU執行myClient中的寫入數據到共享內存時,就被替換了:

    (CUP執行到n的中間狀態)

    (myClient被切換為myServer)

    (myServer信號量執行完了,并將n寫回

    (myCilent帶著自己的上下文數據,并將n寫回)

    ????????此時1 -> 2,凸顯了信號量操作必須是原子性的,只有原子性才不會怕因時序,導致的數據不一致問題。

    總結:

    • 申請信號量 -> 計數器-- -> P操作 -> 必須是原子的
    • 申請信號量 -> 計數器++?-> V操作 -> 必須是原子的

    總結

    ????????所以,由于信號量的思想,也是讓我們看見同一份資源,所以其本質與上面的管道、共享內存沒有太大的區別。所以,信號量被納入進程間通訊的范疇。

    ? ? ? ? 信號量是為了保證特定的臨界資源不被受侵害,保證臨界資源數據一致性。前面所講:信號量也是一個臨界資源,所以首先其需要保證自己的安全性 ——?提出信號量操作需是原子性的。?

    ? ? ? ? 而信號量理論的提出是由于:臨界區、臨界資源的?互斥 ,當多個執行流(進程)才會真正的凸顯出來,所以此處由于是進程間通訊 —— 需要提出信號量,但作用凸顯在多線程 —— 多線程再深入講解信號量。

    總結

    以上是生活随笔為你收集整理的【Linux】-- 进程间通讯的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。