? ? ? ? 本篇博客整理了進程間通信的方式管道、 system V IPC的原理,結合大量的系統調用接口,和代碼示例,旨在讓讀者透過進程間通信去體會操作系統的設計思想和管理手段。
目錄
一、進程間通信
二、管道
1.匿名管道
1.1-通信原理
1.2-系統調用 pipe()
1.3-管道的容量
1.4-管道通信時的特殊情況
1.5-管道的特征總結
?補- 匿名管道模擬簡易的進程池
2.命名管道
2.1-指令 mkfifo
2.2-系統調用 mkfifo()
補- 命名管道實現簡單的本地聊天程序?
三、共享內存
1.基本原理
2.相關系統調用?
2.1-創建 shmget() 和 ftok()
2.2-掛接?shmat()
2.3-取消關聯 shmdt()
2.4-釋放 shmctl()
3.相比管道,通信效率更高
補.共享內存實現簡單的本地聊天程序
四、消息隊列
1.基本原理
2.相關系統調用
2.1-創建?msgget() 和 ftok()
2.2-釋放?msgctl()
2.3-發送數據?msgsnd()
2.4-獲取數據?msgrcv()
五、信號量
1.基本原理
?2.相關系統調用
六、內核對 IPC 資源的管理
一、進程間通信
????????進程間通信(Interprocess communication,簡稱IPC),是兩個或多個進程實現數據層面的交互,傳播或交換信息。
【Tips】目的:數據傳輸、資源共享、通知事件、進程控制。
- 數據傳輸:一個進程需要將它的數據發送給另外一個進程。
- 資源共享:多個進程之間共享同樣的資源。
- 通知事件:一個進程需要向另一個或者一組進程發送消息,通知它(它們)發生了某種事件(如進程終止時需要通知父進程)。
- 進程控制:有些進程希望完全控制另一個進程的執行(如 Debug 進程),此時控制進程希望及時知道它的狀態改變。
【Tips】本質:讓不同的進程看到同一份資源。
????????進程用于通信的資源是一種特定形式的內存空間,為了不破壞進程的獨立性,這份資源由操作系統提供,所以,進程使用通信資源進行進程間的通信,本質就是在訪問操作系統。
????????進程所代表的是用戶,但操作系統是不相信用戶的,也就是說,進程不能直接去使用操作系統提供給進程的資源,必須在進程內部通過系統調用去使用。
????????在操作系統內部,可能會存在多組進程需要通信,因此資源可能有多份。操作系統會將這多份資源管理起來,例如,一般操作系統會有一個獨立的通信模塊(IPC通信模塊),隸屬于文件系統。 通信模塊有 system V 和 posix 兩個標準,前者主要是針對本機內部通信,后者是針對網絡通信。在這兩個標準發布之前(也就是操作系統還沒有通信模塊的時候),進程之間是通過基于文件級別的通信方式——管道來進行通信的。
【Tips】分類:管道、 system V 、?posix。
- 管道:匿名地址 pipe、命名管道。
- System V IPC:System V 消息隊列、System V 共享內存、System V 信號量。
- POSIX IPC:消息隊列、共享內存、信號量、互斥量、條件變量、讀寫鎖。
二、管道
????????管道是 Unix 中最古老的進程間通信的形式,從一個進程連接到另一個進程的一個數據流就被稱為一個“管道”,本質是一種不進行IO的內存級文件。
????????例如,使用指令來統計當前登錄的用戶個數:
????????who?指令用于當前登錄的用戶名, wc -l 指令用于顯示文件的行數,它們都是兩個可執行程序,在加載后成為兩個進程。
????????who 進程通過 stdout 將數據輸出至“管道”當中,wc 進程再通過 stdin 從“管道”當中讀取數據,至此便完成了數據的傳輸,進而可以完成數據的進一步加工處理。
1.匿名管道
????????匿名管道,顧名思義,就是沒有名字的文件,僅用于本地的父子進程間、或由同一個父進程創建的兄弟進程之間的通信。
【Tips】匿名管道的特點
- 具有血緣關系的進程之間的通信。
- 只能單向通信。
- 父子進程是會協同的,進行同步與互斥,以保證管道文件中數據的安全。
- 管道是面向字節流的。
- 管道是基于文件的,而文件的生命周期是隨進程的,進程如果退出了,管道也會被自動關閉掉。
1.1-通信原理
????????匿名管道實現父子進程間通信的原理就是,讓兩個父子進程能夠看到同一份文件資源,然后父子進程就可以對同一個文件進行寫入或讀取操作,進而實現父子進程間通信。
? ? ? ? 當父進程打開一個文件,操作系統就會在內存上創建一個 struct file 結構體,里面包含文件的各種屬性、對磁盤文件的操作方法、inode 結構等。被父進程創建的子進程,會和父進程一起訪問這個由父進程打開的文件,具體的方式是,它們的 struct file 中封裝了同一個 inode 結構,而這個 inode 結構指向了從磁盤加載到文件頁緩沖區里的一個由父進程打開的文件。
? ? ? ? 由于父子進程看到的同一份文件資源是由操作系統來維護的,因此在父子進程分別對這個文件進行寫入時,文件頁緩沖區中的數據并不會發生寫時拷貝。父進程可以向文件中寫內容,寫完后可以繼續干自己的事,且不破壞父進程的獨立性。子進程可以向文件中讀內容,讀完后可以繼續干自己的事,也不破壞子進程的獨立性。這樣一讀一寫,父子進程就完成了一次進程間通信,而這種通信模式是單向的。
? ? ? ? 對文件進行IO操作時,需要訪問硬盤,從這個外設上讀取數據,因此IO的速度非常慢。但父子進程進行通信,顯然,磁盤中文件的內容并不重要,重要的是父進程寫了什么,子進程又讀到了什么。于是,操作系統為了提高效率,關閉了內存中 struct file 與硬盤的 IO 通道, 而讓父子進程在內存的文件頁緩沖區中,一個無名的文件中分別進行讀寫。父進程會把數據寫到文件頁緩沖區的這個無名文件中,子進程會從文件頁緩沖區的這個無名文件中讀取數據。此時,父子間通信不僅正常進行,效率還非常高,且對進程之間的獨立性沒有影響。
????????這種不進行IO的文件就叫做內存級文件。也就是說,其實磁盤文件和內存文件未必一一對應,有的文件只在內存中存在,而不在磁盤中存在。
? ? ? ? 而這種操作系統為了支持進程間通信而為進程提供的匿名文件資源,就叫匿名管道。
1.2-系統調用 pipe()
#include<unistd.h>
int pipe(int pipefd[2]);
參數:一個至少有兩個元素的數組,實際上傳參傳的是數組名。這里的pipefd[0]是管道讀端的文件描述符,pipefd[1]是管道寫端的文件描述符。
返回值:管道創建成功則返回0;創建失敗則返回-1,并設置合適的錯誤碼。
【Tips】創建匿名管道的一般步驟
????????在創建匿名管道實現父子進程間通信的過程中,需要 pipe() 和 fork() 搭配使用。
- step1:父進程調用 pipe() 創建管道
- step2:父進程調用 fork() 創建子進程
- step3:父/子進程調用 close() 關閉 pipe() 的寫端 fd[0],子/父進程關閉 pipe() 的讀端 fd[1]
?????????為了演示 pipe() 的用法,此處引入以下代碼:
//子進程向匿名管道當中寫入10行數據,父進程從匿名管道當中將數據讀出。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{//創建匿名管道int fd[2] = { 0 };if (pipe(fd) < 0){ perror("pipe");return 1;}//創建子進程pid_t id = fork(); if (id == 0){//子進程//關閉讀端close(fd[0]); //向管道寫入數據const char* msg = "hello father, I am child...";int count = 10;while (count--){write(fd[1], msg, strlen(msg));sleep(1);}//寫入完畢,關閉文件(寫端)close(fd[1]); exit(0);}//父進程//關閉寫端close(fd[1]); //從管道讀取數據char buff[64];while (1){ssize_t s = read(fd[0], buff, sizeof(buff)-1);if (s > 0){buff[s] = '\0';printf("child send to father:%s\n", buff);}else if (s == 0){printf("read file end\n");break;}else{printf("read error\n");break;}}//讀取完畢,關閉文件(讀端)close(fd[0]);waitpid(id, NULL, 0);return 0;
}
【Tips】?命令行中的匿名管道
????????命令行中的 | ,其底層就是通過 pipe() 來創建管道的。
? ? ? ? 輸入管道相關的指令后,?bash 會對輸入的指令做分析,統計出指令中 | 的個數,創建出對應數量的管道,然后通過 fork() 創建出一批子進程進行重定向工作,將管道左邊進程的輸出,重定向到管道文件中,將管道右邊進程的輸入,重定向到管道文件中,最終通過程序替換去執行指令(程序替換不會影響預先設置好的重定向)。
//【補】pipe() 的升級版:系統調用 pipe2()
#include<unistd.h>
int pipe2(int pipefd[2], int flags);
參數:1.pipefd[2]:一個至少有兩個元素的數組,實際上傳參傳的是數組名。這里的pipefd[0]是管道讀端的文件描述符,pipefd[1]是管道寫端的文件描述符。2.flags:用于設置選項1)當管道為空,沒有數據可讀時:· O_NONBLOCK disable:read調用阻塞,即進程暫停執行,一直等到有數據來為止。· O_NONBLOCK enable:read調用返回-1,errno值為EAGAIN。2)當管道被寫滿時:· O_NONBLOCK disable:write調用阻塞,直到有進程讀走數據。· O_NONBLOCK enable:write調用返回-1,errno值為EAGAIN。3)若管道寫端被關閉,則read返回0。4)若讀端被關閉,則write操作會產生信號SIGPIPE,進而可能導致write進程退出。5)若寫入的數據量不大于PIPE_BUF(內核管道緩沖區的容量)時,Linux將保證寫入的原子性,數據會被連續地寫入管道。6)若寫入的數據量大于PIPE_BUF(內核管道緩沖區的容量)時,Linux將不再保證寫入的原子性。
返回值:管道創建成功則返回0;創建失敗則返回-1,并設置合適的錯誤碼。
1.3-管道的容量
????????管道的容量是有限的,具體與內核管道緩沖區的容量、緩沖條目的數量有關,如果管道已滿,那么寫端將阻塞或失敗。要查看管道的容量,有以下方法:
- 方法一:指令 man 7 pipe
????????——“在2.6.11之前的Linux版本中,管道的最大容量與系統頁面大小相同,從Linux 2.6.11往后,管道的最大容量是65536字節。”
? ? ? ? 小編使用的 Linux 是2.6.11之后的版本,因此小編使用的Linux下,管道的容量是65536字節。
【ps】65536字節 =?4096(內核管道緩沖區的容量) x 16(緩沖條目的數量)
- 方法二:指令 ulimit -a
? ? ? ? 指令 ulimit -a 可以查看內核管道緩沖區的容量。
????????只要得知內核管道緩沖區的容量和緩沖條目的數量,就能推導出管道的容量。
????????(內核管道緩沖區的容量 x 緩沖條目的數量 =?管道的容量)
? ? ? ? 小編的Linux下,管道的容量是 512 × 8 = 4096 字節 = 4KB。
????????如果寫入數據的大小,小于內核管道緩沖區的容量(這里為4kb),那么寫入操作就是原子性的,數據會被連續地寫入管道。
- ?方法三:通過代碼粗暴測試
????????當管道被寫滿,寫端的進程就會被掛起。可以利用這一點,讓讀端的進程一直不讀取管道的數據,而寫端的進程一直向管道寫入數據,寫端的進程被掛起時,就能得知管道的最大容量。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{//創建匿名管道int fd[2] = { 0 };if (pipe(fd) < 0){ perror("pipe");return 1;}//創建子進程pid_t id = fork(); if (id == 0){//子進程//關閉讀端close(fd[0]); char c = 'a';int count = 0;//一直進行寫入,每次寫入一個字節while (1){write(fd[1], &c, 1);count++;//每寫入一次,就打印已寫入的字節數printf("%d\n", count); }close(fd[1]);exit(0);}//父進程//關閉寫端close(fd[1]); //但不進行讀取//...waitpid(id, NULL, 0);close(fd[0]);return 0;
}
? ? ? ? 由演示圖,以上代碼運行后在顯示出 65536 后掛起了,這說明管道的容量就是65536字節。
1.4-管道通信時的特殊情況
????????在使用匿名管道通信時,可能會出現四種特殊情況:
- 讀寫端正常,若管道為空,則讀端堵塞;
- 讀寫端正常,若管道被寫滿,則寫端阻塞;
- 讀端正常,寫端關閉,讀端就會讀到0,表示讀到了管道文件的結尾,不會被阻塞;
- 寫端正常,讀端關閉,操作系統會通過 13 號信號(SIGPIPE)把正在寫入的進程 kill 掉。
1.5-管道的特征總結
(1)管道的生命周期取決于進程的創建和終止
????????管道本質上也是文件,依賴于文件系統,由于所有打開文件的進程都退出后,文件資源也就被釋放掉了,因此,管道的生命周期與進程的生命周期有關。
(2)管道內部自帶同步與互斥機制
????????一次只允許一個進程使用的資源,被稱為臨界資源,而管道在同一時刻只允許一個進程進行寫入或讀取操作,因此,管道其實就是一種臨界資源。
????????臨界資源是需要被保護的,如果不對臨界資源進行任何保護,就可能出現同一時刻有多個進程對同一臨界資源進行操作,導致同時讀寫、交叉讀寫、讀取數據不一致等問題。
? ? ? ? 保護臨界資源的手段一般是同步與互斥機制,于是就有內核會對管道的操作進行同步與互斥:
- 同步: 兩個或兩個以上的進程在運行過程中協同步調,按預定的先后次序運行,例如,A任務的讀取操作依賴于B任務因寫入操作而產生的數據。
- 互斥: 一個公共資源同一時刻只能被一個進程使用,多個進程不能同時使用公共資源。
? ? ? ? 其實,同步是一種復雜的互斥,互斥則是一種特殊的同步。互斥具有唯一性和排它性,且不限制任務的運行順序,而同步的任務之間則有明確的順序關系。
????????對于管道來說,互斥就是兩個進程不能同時對管道進行操作,它們必須等其中一個進程操作完畢,另外一個才能操作。同步也是指兩個進程不能同時對管道進行操作,而必須要按照某種次序來對管道進行操作。
(3)管道提供流式服務
? ? ? ? 數據的讀取分為流式服務和數據報服務:
- 流式服務: 數據沒有明確的分割,并不分固定的報文段。
- 數據報服務: 數據有明確的分割,讀取數據必須按固定的報文段來讀取。
????????而管道提供的是流式服務,具體來說就是,進程A寫入管道中的數據,進程B每次想讀多少都可以。
(4)管道中的數據傳輸方式屬于半雙工通信
????????數據在線路上的傳輸方式可以分為單工通信、半雙工通信、全雙工通信:
- 單工通信:數據傳輸是單向的,在通信雙方中,一方固定為發送端,另一方固定為接收端。
- 半雙工通信:數據可以在一個信號載體的兩個方向上傳輸,但是不能同時傳輸。
- 全雙工通信:數據在兩個方向上同時傳輸,相當于兩個單工通信的結合,全雙工可以同時/瞬時進行信號的雙向傳輸。
? ? ? ? 顯然,管道中數據的傳輸方式屬于半雙工通信。
【補】由管道引申出的一些概念:
- 臨界資源:多個進程/執行流看到的公共的一份資源。
- 臨界區:進程訪問臨界資源的代碼。
- 同步:每個進程按預定的先后次序進入臨界區。
- 互斥:在任何時刻,都只能有一個進程進入臨界區。
- 原子性:要么就不做,要么就做完,沒有中間狀態。
?補- 匿名管道模擬簡易的進程池
- Makefile:
ProcessPool:ProcessPool.cppg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f ProcessPool
- Task.hpp:
#pragma once#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <unistd.h>
#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() << " ] 執行訪問數據庫的任務\n" << std::endl;
}void execuleUrl()
{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() // 操作表,先插入描述再插入方法,下標就對齊了
{desc.insert({ callbacks.size(), "readMySQL: 讀取數據庫" });callbacks.push_back(readMySQL);desc.insert({ callbacks.size(), "execuleUrl: 進行url解析" });callbacks.push_back(execuleUrl);desc.insert({ callbacks.size(), "cal: 進行加密計算" });callbacks.push_back(cal);desc.insert({ callbacks.size(), "save: 進行數據的文件保存" });callbacks.push_back(save);
}void showHandler() // 查看有多少方法
{for (const auto& iter : desc){std::cout << iter.first << "\t" << iter.second << std::endl; // \t制表符}
}int handlerSize() // 直接返回有多少個任務的方法
{return callbacks.size();
}
- ProcessPool.cpp:
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
#include <cassert>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include "Task.hpp" #define PROCESS_NUM 5 // 創建的子進程數目using namespace std;int waitCommand(int waitFd, bool& quit) //如果對方不發,我們就阻塞
{uint32_t command = 0; // uint32_t四個字節ssize_t s = read(waitFd, &command, sizeof(command)); // 期望讀取四個字節if (s == 0) // 讀到0讓子進程退出{quit = true;return -1;}assert(s == sizeof(uint32_t)); // 不是四個字節就報錯return command;
}void sendAndWakeup(pid_t who, int fd, uint32_t command) // 通過文件描述符,向哪一個文件發什么命令
{ // who給哪個進程,這個進程的idwrite(fd, &command, sizeof(command));cout << "main process: call process " << who << " execute " << desc[command] << " through " << fd << endl;
}int main()
{// 代碼中關于fd的處理,有一個小問題,不影響我們使用,但是你能找到嗎??load();vector<pair<pid_t, int>> slots; // 存放子進程pid和子進程寫端id(pipefd)vector<int> deleteFd; // 存放要刪除的子進程寫端fd(不刪除也不會出問題)for (int i = 0; i < PROCESS_NUM; i++) // 先創建多個進程{int pipefd[2] = { 0 };int ret = pipe(pipefd); // 創建管道assert(ret == 0); // 等于0才創建成功(void)ret;pid_t id = fork();assert(id != -1);if (id == 0) // 子進程,進行讀取{close(pipefd[1]); // 關閉寫端for (int i = 0; i < deleteFd.size(); i++) // 關閉所以繼承下來的寫端fd{close(deleteFd[i]);}while (true){// 等命令bool quit = false; // 默認不退出int command = waitCommand(pipefd[0], quit); // 如果對方不發,我們就阻塞if (quit) // 讀到0就退出關閉所有進程{break;}if (command >= 0 && command < handlerSize()) // 執行對應的命令{ // handlerSize任務方法的個數callbacks[command]();}else{cout << "非法command: " << command << endl;}}exit(1);}close(pipefd[0]); // 父進程,進行寫入,關閉讀端slots.push_back(pair<pid_t, int>(id, pipefd[1])); // 把此次循環得到的子進程id和子進程寫端的id保存deleteFd.push_back(pipefd[1]); // 把要被繼承下去的子進程寫端fd保存起來}// 父進程均衡地派發任務(單機版的負載均衡)srand((unsigned long)time(nullptr) ^ getpid() ^ 2335643123L); // 僅僅讓數據源更隨機while (true){// 選擇一個任務int command = rand() % handlerSize();// 選擇一個進程 ,采用隨機數的方式,選擇進程來完成任務,隨機數方式的負載均衡int choice = rand() % slots.size();// 把任務給指定的進程sendAndWakeup(slots[choice].first, slots[choice].second, command);sleep(1);}for (const auto& slot : slots) // 關閉fd, 所有的子進程都會退出{close(slot.second);}for (const auto& slot : slots) // 回收所有的子進程信息{waitpid(slot.first, nullptr, 0);}
}
2.命名管道
????????命名管道,顧名思義,就是有名字的管道,也是系統中的一個內存級文件。和匿名管道一樣,命名管道的不會向磁盤中刷新數據,且它的通信原理也和匿名管道大致相同。
? ? ? ? 要找到一個文件一般有兩種方法,一種是通過文件的 inode 號,另一種則是通過路徑和文件名。要找到一個命名管道,顯然是通過后者,“路徑 + 文件名”唯一地標識了一個命名管道。
2.1-指令 mkfifo
? ? ? ? 使用指令 mkfifo 可以創建一個命名管道:
mkfifo 命名管道名
2.2-系統調用 mkfifo()
#include<sys/type.h>
#include<sys/stat.h>
int mkfifo(const char* pathname,mode_t mode);
參數:1. pathname:命名管道所在路徑或命名管道名若pathname以路徑的方式給出,則將命名管道文件創建在pathname路徑下;若pathname以文件名的方式給出,則將命名管道文件默認創建在當前路徑下。2. mode:權限
返回值:管道創建成功則返回0;失敗則返回-1,并設置合適的錯誤碼。【補】命名管道的打開規則1)以讀而打開命名管道時:· O_NONBLOCK disable:阻塞直到有相應進程以寫而打開命名管道。· O_NONBLOCK enable:立刻返回成功。2)以寫而打開命名管道時:· O_NONBLOCK disable:阻塞直到有相應進程以讀而打開命名管道。· O_NONBLOCK enable:立刻返回失敗,錯誤碼為ENXIO。
? ? ? ? 為演示?mkfifo() 的用法,此處引入以下代碼:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>#define FILE_NAME "myfifo"int main()
{//將文件默認掩碼設置為0umask(0); //創建命名管道文件 if (mkfifo(FILE_NAME, 0666) < 0){ perror("mkfifo");return 1;}//create success...return 0;
}
補- 命名管道實現簡單的本地聊天程序?
- makefile:
.PHONY:all
all:client serverclient:client.ccg++ -o $@ $^ -std=c++11
server:server.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f client server
- comm.hpp:
//comm.hpp
#pragma once
#include <sys/types.h>
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <cstdio>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>#define FIFO_FILE "./myfifo"
#define MODE 0664enum
{FIFO_CREAT_ERR = 1,FIFO_DELET_ERR,FIFO_OPEN_ERR,FIFO_CLOSE_ERR,FIFO_WRITE_ERR,FIFO_READ_ERR
};class Init
{
public:Init(){int n = mkfifo(FIFO_FILE, MODE); // 創建成功返回0,創建失敗返回-1if (n == -1){// 創建失敗perror("mkfifo");exit(FIFO_CREAT_ERR);}}~Init(){int m = unlink(FIFO_FILE); // unlink 可以刪除任意文件if (m == -1){perror("unlink");exit(FIFO_DELET_ERR);}}
};
- 服務端:?
//server.cc
#include "comm.hpp"using namespace std;int main()
{// 創建管道Init init;// 打開管道int fd = open(FIFO_FILE, O_RDONLY); // 等待寫入方打開之后,自己才會打開文件,向后執行, open 會阻塞if(fd < 0){// 文件打開失敗perror("open fifo");exit(FIFO_OPEN_ERR);}cout << "server open file done" << endl;// 開始通信while(true){char buffer[1024];int x = read(fd, buffer, sizeof(buffer));if(x > 0){buffer[x] = 0;cout << "client say@ " << buffer << endl;}else if(x == 0) {cout << "client quit, me too!" << endl;break;}else{perror("read");exit(FIFO_READ_ERR);}}// 關閉管道int p = close(fd);if(p == -1){perror("close");exit(FIFO_CLOSE_ERR);}return 0;
}
- 客戶端:
// client.cc
#include "comm.hpp"using namespace std;int main()
{// 打開管道int fd = open(FIFO_FILE, O_WRONLY);if(fd < 0){perror("open");exit(FIFO_OPEN_ERR);}cout << "client open file done" << endl;// 開始通信string message;int num = 0;while (true){cout << "Please Enter@ ";getline(cin, message);int ret = write(fd, message.c_str(), message.size());if(ret == -1){perror("write");exit(FIFO_WRITE_ERR);}}// 關閉管道int m = close(fd);if (m == -1){perror("close");exit(FIFO_CLOSE_ERR);}return 0;
}
三、共享內存
【補】system V IPC
????????system V IPC是操作系統中的一種通信模塊,與實現管道通信的目的類似,都是要讓不同的進程看到同一份資源。
????????system V IPC 所提供的通信方式有三種:
- system V共享內存
- system V消息隊列
- system V信號量
????????其中,共享內存和消息隊列用于傳送數據,信號量用于保證進程間的同步與互斥。
1.基本原理
????????共享內存的原理與動態庫加載的原理基本一致。
? ? ? ? 操作系統會在物理內存中取一塊內存空間,然后將其分別與各個進程之間建立頁表映射,使共享內存與進程地址空間的共享區存在對應關系,從而讓不同的進程看到了同一份內存資源。
????????在操作系統中,可能存在大量的進程正在通信,于是就可能同時存在大量的共享內存,那么操作系統就需要對這些內存資源做管理,因此,操作系統除了要從物理內存中取一塊內存空間,還得為其維護相關的內核數據結構。?
//【補】共享內存的相關數據結構
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;
};
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 */
};
//...
//ps:shmid_ds和ipc_perm結構體在/usr/include/linux/shm.h和/usr/include/linux/ipc.h下可以找到
【Tips】申請共享內存的大致過程:
- 操作系統在物理內存上申請一塊空間;
- 將申請到的空間,通過頁表掛接到進程地址空間的共享區;
- 返回起始虛擬地址,供程序中使用。
【Tips】釋放共享內存的大致過程:
- 取消共享內存與地址空間之間關聯;
- 釋放空間,將內存資源歸還。
【ps】申請、掛接、去關聯、釋放這些動作都是由操作系統來完成的。
2.相關系統調用?
2.1-創建 shmget() 和 ftok()
/* 接口1 */
#include<sys/ipc.h>
#include<sys/shem.h>
int shmget((key_t key, size_t size, int shmflg);
功能:申請共享內存
參數:1. key:共享內存的內核標識符。2. size:共享內存的開辟的字節數。3. shmflg:共享內存的創建方式,其中包括:1)IPC_CREAT:沒有創建,有則返回。2)IPC_EXIT: 有則出錯返回。若要使用也得跟上方式1(IPC_CREAT | IPC_EXCL)
返回值:成功,返回一個有效的共享內存標識符(用戶層標識符);失敗,返回-1,并設置合適的錯誤碼。/* 接口2 */
#include<sys/ipc.h>
#include<sys/shm.h>
key_t ftok(const char *pathname, int proj_id);
功能:將一個已存在的路徑名pathname和一個整數標識符proj_id轉換成一個key值(IPC鍵值),在使用shmget()獲取共享內存時,這個key值會被填充進維護共享內存的數據結構當中。
參數:1. pathname:任意的文件路徑,但pathname所指定的文件必須存在且可存取,一般都寫成當前路徑"."。2. proj_id:整數標識符/項目ID,可自定義,但不能是0。說明:這兩個參數都是為了生成key_t類型的內核的標識符。
返回值:成功,返回獨一無二的key值;失敗,返回-1
[ps]1.ftok()生成的key值可能存在沖突,此時修改ftok()的參數即可。2.需要通信的各個進程,在使用ftok()獲取key值時,都需要采用同樣的路徑名和和整數標識符,進而生成同一種key值,然后才能找到同一個共享內存。
[ps]key值為什么是由用戶傳參來指定生成的,而非操作系統直接生成的?這是因為,具體哪兩個進程需要通信,取決于用戶,而非操作系統。其實ftok()就相當于是兩個通信進程之間的一種約定,只要它們約定好同一個pathname和proj_id,那么這兩個進程就能得到同一個key,從而找到同一個共享內存。
【Tips】shmget() 與?ftok() 之間的關系:
? ? ? ? ?為演示?shmget() 和 ftok() 的用法,此處引入以下代碼:
#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<string>
using namespace std;const string pathname = "/home/CVEer";
const int proj_id = 0xFFFF; int main()
{key_t key = ftok(pathname.c_str(),proj_id);if(key == -1) return 1;int ud = shmget(key,4096,IPC_CREAT);//int ud = shmget(key,4096,IPC_CREAT | 0666);//設置共享內存的權限為0666if(ud == -1) return 1;cout << "創建成功!" << endl;return 0;
}
【ps】由于共享內存是由操作系統進行管理的,因此在沒有調用相應的系統調用時,就算進程退出了,操作系統也不會釋放共享內存。
?【補】指令?ipcs?
- 參數 -m:列出共享內存的相關信息。
- 參數 -q:列出消息隊列的相關信息。
- 參數 -s:列出信號量的相關信息。
????????ipcs -m 所列出的信息含義:
【補】刪除一個共享內存的指令:ipcrm -m? +? 共享內存的用戶層id(shmid)
2.2-掛接?shmat()
#include<sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:將共享內存掛接到進程地址空間的共享區中
參數:1.shmid:共享內存的用戶層id/標識符。2.shmaddr:指向掛接在進程地址空間中的共享區地址,一般設為空指針即可,系統會自動分配地址,3.shmflg:設置當前進程對掛接的共享內存的權限,一般設置成0,表示采用共享內存自身的權限。1)SHM_RDONLY:關聯共享內存后只進行讀取操作2)SHM_RND:若shmaddr不為NULL,則關聯地址自動向下調整為SHMLBA的整數倍。(公式:shmaddr - (shmaddr % SHMLBA))3)0:默認為讀寫權限
返回值:成功,返回共享內存掛接在進程空間中的共享區地址;失敗,返回(void*)-1,并設置合適的錯誤碼。
? ? ? ? ?為演示?shmmat() 的用法,此處引入以下代碼:
#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<string>
#include <unistd.h>
using namespace std;const string pathname = "/home/CVEer";
const int proj_id = 0xFFFF; int main()
{//創建key_t key = ftok(pathname.c_str(),proj_id);if(key == -1) return 1;int shmid = shmget(key,4096,IPC_CREAT | IPC_EXCL | 0666);if(shmid == -1) return 1;cout << "創建成功!" << endl;sleep(2);//掛接int* shmptr = (int*)shmat(shmid ,NULL,0);if(*shmptr == -1) return 1;cout << "掛接成功"<< endl; sleep(5);return 0;
}
2.3-取消關聯 shmdt()
#include<sys/shm.h>
int shmdt(const void *shmaddr);
功能:取消共享內存與進程地址空間中共享區的映射關系
參數:shmaddr:共享內存掛接在進程地址空間中的共享區地址
返回值:成功則返回0;失敗則返回-1,并設置合適的錯誤碼。
?? ? ? ? ?為演示?shmmdt() 的用法,此處引入以下代碼:
#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<string>
#include <unistd.h>
using namespace std;const string pathname = "/home/CVEer";
const int proj_id = 0xFFFF; int main()
{//創建key_t key = ftok(pathname.c_str(),proj_id);if(key == -1) return 1;int shmid = shmget(key,4096,IPC_CREAT | IPC_EXCL | 0666);if(shmid == -1) return 1;cout << "創建成功" << endl;sleep(2);//掛接int* shmptr = (int*)shmat(shmid ,NULL,0);if(*shmptr == -1) return 1;cout << "掛接成功"<< endl; sleep(5);//取消關聯int ret = shmdt((const void *)shmptr);if(ret < 0) return 1;cout << "關聯已取消" << endl;sleep(5);return 0;
}
2.4-釋放 shmctl()
#include<sys/ipc.h>
#include<sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:釋放一個共享內存
參數:1.shmid:共享內存的用戶層id/標識符。2.cmd:操作選項,常見有:1)IPC_STAT,獲取共享內存的狀態信息,放在buf指向的變量中。2)IPC_SET,設置共享內存的狀態,需要將buf變量傳進去,方便修改3)IPC_RMID,刪除共享內存。4)SHM_LOCK,鎖定共享內存。5)SHM_UNLOCK,解鎖共享內存。3.buf:語言層面用來描述一個共享內存的結構體,里面保存了共享內存的部分屬性
返回值:如果操作是 IPC_RMID ,那么刪除成功,則返回0;失敗則返回-1.并設置合適的錯誤碼。
?? ? ? ? ?為演示?shmctl() 的用法,此處引入以下代碼:?
#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<string>
#include <unistd.h>
using namespace std;const string pathname = "/home/CVEer";
const int proj_id = 0xFFFF; int main()
{//創建key_t key = ftok(pathname.c_str(),proj_id);if(key == -1) return 1;int shmid = shmget(key,4096,IPC_CREAT | 0666);if(shmid == -1) return 1;cout << "創建成功" << endl;//掛接char* shmptr = (char*)shmat(shmid,NULL,0);if(shmptr == (void*)(-1)) return 1;cout << "掛接成功"<< endl; sleep(5);//去關聯int ret = shmdt(shmptr);if(ret < 0) return 1;cout << "關聯已取消" << endl;sleep(5);//釋放ret = shmctl(shmid,IPC_RMID,NULL);if(ret == -1){cout << "釋放失敗" << endl;return 1;}cout << "釋放成功"<< endl;sleep(5);return 0;
}
3.相比管道,通信效率更高
????????管道創建好后,通信仍需要調用 read()、write() 等系統接口,而共享內存創建好后,通信無需再調用系統接口。
????????通信雙方,一端寫入,一端讀取,會發生數據的拷貝。
? ? ? ? 對于管道來說,一次通信會發生四次數據拷貝。
? ? ? ? 而對于共享內存來說,一次通信僅發生兩次數據拷貝。?
? ? ? ? ?所以相較于管道,共享內存通信效率更高。
? ? ? ??但這并不意味著共享內存就全面優于管道。管道是自帶同步與互斥,對共享的內存資源有保護機制,而共享內存并沒有為共享的內存資源提供任何保護機制,包括同步與互斥。所以,共享內存的安全性和穩定性要劣于管道。
補.共享內存實現簡單的本地聊天程序
- makefile:
.PHONY:all
all:client serverclient:client.ccg++ -o $@ $^ -std=c++11
server:server.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f client server
- comm.hpp:?
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <string>
#include <string.h>
#include <errno.h>
using namespace std;const int size = 4096;
const string path_name = "/home/CVEer";
const int proj_id = 0x6666;key_t GetKey() // 獲取 key
{key_t k = ftok(path_name.c_str(), proj_id);if(k < 0){perror("ftok fail");exit(1);}return k;
}int GetShareMem() // 創建共享內存
{key_t key = GetKey();int shmid = shmget(key, size, IPC_CREAT|IPC_EXCL|0666);if(shmid < 0){perror("shmid fail");exit(2);}return shmid;
}
- 服務端:?
//server.cc
#include "comm.hpp"
#include <unistd.h>int main()
{// 創建共享內存int shmid = CreatMem();// 掛接共享內存char *shamem = (char*)shmat(shmid, NULL, 0);// ipc-cod 通信代碼while(true){cout << "client asy@ " << shamem << endl; // 直接訪問共享內存sleep(1);}// 去關聯shmdt(shamem);// 釋放共享內存int ret = shmctl(shmid, IPC_RMID, NULL);return 0;
}
- 客戶端:
//client.cc?
#include "comm.hpp"
#include <unistd.h>int main()
{// 獲取共享內存int shmid = GetMem();// 掛接char *shmaddr = (char*)shmat(shmid, NULL, 0);// ipc-code 通信代碼while(true){cout << "Please enter:";fgets(shmaddr, size, stdin);}// 去關聯shmdt(shmaddr);return 0;
}
?
四、消息隊列
? ? ? ? 消息隊列是 system V IPC 所提供的一種通信方式,用于數據的傳輸。
1.基本原理
????????消息隊列是能讓不同進程看到同一份資源的、一個在內核中維護的隊列,隊列中的每個成員都是一個數據塊,這些數據塊本質上是一個個結構體,都由類型和信息兩部分構成,其中,類型字段用來標識一個數據塊是由哪個進程發送的,信息就是進程通信的內容。
????????兩個通信的進程,通過某種方式找到同一個消息隊列,要發送數據時,都在消息隊列的隊尾添加數據塊,要獲取數據時,都在消息隊列的隊頭取數據塊。
????????系統中也可能會存在大量的消息隊列,于是,內核也需要為消息隊列維護相關的數據結構。
//消息隊列的相關數據結構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;
};struct msqid_ds {struct ipc_perm msg_perm;struct msg *msg_first; /* first message on queue,unused */struct msg *msg_last; /* last message in queue,unused */__kernel_time_t msg_stime; /* last msgsnd time */__kernel_time_t msg_rtime; /* last msgrcv time */__kernel_time_t msg_ctime; /* last change time */unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */unsigned long msg_lqbytes; /* ditto */unsigned short msg_cbytes; /* current number of bytes on queue */unsigned short msg_qnum; /* number of messages in queue */unsigned short msg_qbytes; /* max number of bytes on queue */__kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */__kernel_ipc_pid_t msg_lrpid; /* last receive pid */
};
//...
?
2.相關系統調用
2.1-創建?msgget() 和 ftok()
/* 接口1 */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
參數:1.key:消息隊列的內核標識符。2. msgflg:消息隊列的創建方式,其中包括:1)IPC_CREAT:沒有創建,有則返回。2)IPC_EXIT: 有則出錯返回。若要使用也得跟上方式1(IPC_CREAT | IPC_EXCL)
返回值:創建成功,返回的一個有效的用戶層標識符;失敗,返回-1,并設置合適的錯誤碼。/* 接口2 */
#include<sys/ipc.h>
#include<sys/shm.h>
key_t ftok(const char *pathname, int proj_id);
功能:將一個已存在的路徑名pathname和一個整數標識符proj_id轉換成一個key值(IPC鍵值),在使用msgget()獲取消息隊列時,這個key值會被填充進維護消息隊列的數據結構當中。
參數:1. pathname:任意的文件路徑,但pathname所指定的文件必須存在且可存取,一般都寫成當前路徑"."。2. proj_id:整數標識符/項目ID,可自定義,但不能是0。說明:這兩個參數都是為了生成key_t類型的內核的標識符。
返回值:成功,返回獨一無二的key值;失敗,返回-1
【補】消息隊列的相關指令操作
- ipcs -q:查看當前操作系統中所有的消息隊列。
- ipcrm -q + 用戶層標識符(msqid):釋放一個消息隊列。
?
2.2-釋放?msgctl()
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
參數:1.mspid:共享內存的用戶層id/標識符。2.cmd:操作選項,常見有:1)IPC_STAT,獲取消息隊列的狀態信息,放在buf指向的變量中。2)IPC_SET,設置消息隊列的狀態,需要將buf變量傳進去,方便修改3)IPC_RMID,刪除消息隊列。4)SHM_LOCK,鎖定消息隊列。5)SHM_UNLOCK,解鎖消息隊列。3.buf:語言層面用來描述一個消息隊列的結構體,里面保存了消息隊列的部分屬性
返回值:如果操作是 IPC_RMID ,那么刪除成功,則返回0;失敗則返回-1.并設置合適的錯誤碼。
?
2.3-發送數據?msgsnd()
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
參數:1.msqid:表示消息隊列的用戶級標識符。2.msgp:表示待發送的數據塊。必須為以下結構:struct msgbuf{long mtype; /* message type, must be > 0 */char mtext[自定義大小]; /* message data */};3.msgsz:表示所發送數據塊的大小4.msgflg:表示發送數據塊的方式,一般默認為0即可。
返回值:發生成功,返回0;發生失敗,返回-1,并設置合適的錯誤碼。
2.4-獲取數據?msgrcv()
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
參數:1.msqid:表示消息隊列的用戶級標識符。2.msgp:表示獲取到的數據塊,是一個輸出型參數,必須為以下結構:struct msgbuf{long mtype; /* message type, must be > 0 */char mtext[自定義大小]; /* message data */};3.msgsz:表示要獲取數據塊的大小4.msgtyp:表示要接收數據塊的類型。
返回值:獲取成功,返回實際獲取到 mtext 數組中的字節數;獲取失敗,返回-1,并設置合適的錯誤碼。
五、信號量
? ? ? ? 信號量是?system V IPC 所提供的一種通信方式,用于保證進程間的同步與互斥。
1.基本原理
????????由于進程有共享資源的需要,而有的資源要求進程互斥使用,因此,使用這些資源的各進程就處于競爭關系,而這種競爭關系就叫做進程互斥。系統中,一次只允許一個進程使用的資源,被稱為臨界資源或互斥資源,在進程中涉及到臨界資源的程序段就叫臨界區。
? ? ? ? 例如,管道就是一種臨界資源,自帶同步與互斥機制,以保護進程共享的內存資源。而共享內存則不是臨界資源,它沒有同步與互斥機制,可能會出現 A 進程正在向共享內存中寫入,還沒有寫完,B 進程就來讀取,導致發方和收方的數據不完整,引起數據不一致問題。
? ? ? ? 為了方便管理和使用,系統中一大塊的臨界資源會被劃分成多個小塊的臨界資源,當有進程申請使用時按需分配給其小塊的臨界資源。
????????信號量就是一種用于控制多個進程或線程訪問臨界資源的同步機制,它本質是一個計數器,用于記錄臨界資源的可用數量。
????????信號量保證的是,假設只有 n 個臨界資源,不會出現 n+1 個執行流來訪問臨界資源,以防數據不一致問題的發生。如果在臨界資源充足的情況下,出現多個進程/執行流訪問同一個臨界資源,這樣的情況就屬于編碼 Bug ,而非數據不一致問題。
? ? ? ? 信號量(計數器)可以有效的保證訪問臨界資源的執行流的數量。一個進程/執行流成功申請到了信號量,就表示這個進程/執行流具有訪問臨界資源的權限了。申請到了信號量,但沒有去訪問臨界資源,是對臨界資源的一種預定機制,也就是說,每個進程/執行流想要訪問臨界資源的時候,并不是直接訪問,而是先向系統申請信號量資源,再按需訪問臨界資源。那么,信號量其實也是一種共享資源。
? ? ? ? 既然信號量也是一種共享資源,那么就可能出現多個進程/執行流同時在申請同一個信號量。信號量本就是是用來保護臨界資源的,如此,信號量得首先保證自身的安全。
????????申請信號量,本質是對計數器 --,被稱為 P 操作(申請一個資源,如果資源不夠就阻塞等待);釋放共享資源,本質是對計數器 ++,被稱為 V 操作(釋放一個資源,如果有進程在等待該資源,則喚醒一個進程)。-- 和 ++ 操作轉成匯編,一般會對應三條匯編指令——從內存中讀取數據到 CPU 中、CPU 內進行操作、CPU 將結果寫回內存——進程在運行的時候,隨時可能被替換,于是,在多進程同時在申請同一個信號量、共享信號量的前提下, -- 和 ++ 操作可能會導致信號量的值發生錯亂,引發數據不一致問題。而PV操作,經過互斥機制的保護,具有原子性,只對應一條匯編指令,確保了信號量的安全性。
????????在系統中也可能存在大量的信號量,內核也為信號量維護了相關的數據結構。
//信號量的相關數據結構
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;
};struct semid_ds {struct ipc_perm sem_perm; /* permissions .. see ipc.h */__kernel_time_t sem_otime; /* last semop time */__kernel_time_t sem_ctime; /* last change time */struct sem *sem_base; /* ptr to first semaphore in array */struct sem_queue *sem_pending; /* pending operations to be processed */struct sem_queue **sem_pending_last; /* last pending operation */struct sem_undo *undo; /* undo requests on this array */unsigned short sem_nsems; /* no. of semaphores in array */
};
//...
????????如果臨界資源只有一份,那么相應信號量(計數器)的值只能是 1 或者 0,且在任何時候只允許一個進程/執行流訪問共享資源,這種只能為 1、0 兩態的計數器就叫做二元信號量。二元信號量主要用于實現進程/執行流對臨界資源的互斥訪問,本質是一把鎖,計數器的值最大為 1,意味著臨界資源只有一份,換句話說,臨界資源不會分成很多塊,而是當做一個整體,整體申請,整體釋放,以實現互斥。
【Tips】信號量
- 信號量本質是一個計數器,申請和釋放涉及PV操作,具有原子性。
- 一個執行流要申請臨界資源,必須先申請信號量資源,只有申請到信號量資源,才能訪問臨界資源。
- 申請信號量,本質是臨界資源的預定機制。
- 二元信號量是值只有0、1兩態的特殊信號量,本質是一把互斥鎖。
?
?2.相關系統調用
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
//創建信號量/信號量集:
int semget(key_t key, int nsems, int semflg);
//控制信號量:
int semctl(int semid, int semnum, int cmd, ...);
//申請或釋放信號量(PV操作):
int semop(int semid, struct sembuf *sops, unsigned nsops);
六、內核對 IPC 資源的管理
????????共享內存、消息隊列、信號量,統稱為操作系統中的 IPC 資源。為了管理這些資源,操作系統分別為它們維護了三個結構體:struct shmid_kernel、struct msg_queue、struct sem_array,然后通過一個 struct kern_ipc_perm* 類型的柔性數組,將所有的 IPC 資源管理起來。
? ? ? ? 在這三個結構體中,第一個成員變量都是 struct kern_ipc_perm 類型的。可以理解為,struct kern_ipc_perm 是一個基類,struct shmid_kernel、struct msg_queue、struct sem_array 是繼承了?struct kern_ipc_perm 的三個子類。
? ? ? ? 如何在維護 IPC 資源的柔性數組中找到一個 IPC 對象呢?
????????在 struct kern_ipc_perm 中,字段 “key_t??key;” 鍵值用于標識一個 kern_ipc_perm 對象屬于哪種 IPC資源。只要有了一個 kern_ipc_perm 的地址,再將該地址通過強制類型轉換,轉換成這個 kern_ipc_perm 對象所屬的 IPC 對象的類型,就可以由轉換后的結果,訪問到這個 kern_ipc_perm 對象所屬的 IPC 對象了。也就是說,用戶層上使用的 shmid(共享內存標識符)、msqid(消息隊列標識符)、semid(信號量標識符)本質上就是維護 IPC 資源的柔性數組的下標。這其中包含了多態的思想。
????????維護 IPC 資源柔性數組被封裝在一個名為 ipc_id_ary 結構體對象中。ipc_id_ary 是一張順序表,隸屬于操作系統,不屬于任何進程。它使得柔性數組的下標是一直線性遞增的,這個遞增屬性不會因為 IPC 資源的釋放而改變,例如,如果此時操作系統中最后一個 IPC 資源的下標是 12,釋放掉這個 IPC 資源,下一次再創建 IPC 資源,新創建的 IPC 資源的下標是會是 13,直到遞增到一定值的時候,才會回歸0。