上次結束了基礎IO:Linux:基礎IO(三.軟硬鏈接、動態庫和靜態庫、動精態庫的制作和加載)
文章目錄
- 1.認識進程間通信
- 2.管道
- 2.1匿名管道
- 2.2pipe()函數 —創建匿名管道
- 2.3匿名管道的四種情況
- 2.4管道的特征
- 3.基于管道的進程池設計
- 4.命名管道
- 4.1引入與性質
- 4.2命令行創建
- 4.3程序中創建命名管道
- 寫個小項目
- 項目規劃
- Cnmm.hpp
- PipeClient.cpp
- PipeServe.cpp
- 5.System V共享內存
- 5.1相關函數介紹
1.認識進程間通信
我們通過之前的知識知道,進程具有獨立性。兩個進程之間時不能進行數據的直接傳遞的
但我們之前學校的fork()函數不是能傳遞子進程的pid給父進程嗎?——這個嚴格來說不算通信
-
為什么我們需要進程間通信?
- 數據傳輸:一個進程需要將自己的數據發送給另一個進程。這種通信方式可以實現進程之間的數據交換和共享,從而實現協作和協同工作。
- 資源共享:多個進程之間共享同樣的資源,如共享內存、共享文件等。通過進程間通信,可以實現多個進程對同一資源的訪問和操作,提高資源的利用率和效率。
- 通知事件:一個進程需要向另一個或一組進程發送消息,通知它們發生了某種事件,如進程終止、資源可用等。通過通知事件,進程可以及時響應和處理其他進程的狀態變化,實現進程之間的協作和同步。
- 進程控制:有些進程希望完全控制另一個進程的執行,如調試進程需要攔截另一個進程的陷入和異常,并能夠及時知道其狀態改變。通過進程控制,可以實現對其他進程的監控、調試和管理,確保系統的穩定和安全運行。
我們往往需要多個進程協同來完成一些任務
-
進程間通信是什么?
一個進程能把自己的數據給另外一個進程(一直)
本質:讓不同的進程看到同一份資源(一般都是要由OS提供)
-
如何進行進程間通信
- 我們要有一個來進行數據交換的空間(一般是內存)。不是直接去另外一個進程里拿,這樣會破壞進程的獨立性
- 這段空間不能由這雙方來提供。由OS(話事人)來提供
- OS提供的”空間“有不同的樣式,就決定了有不同的通信方式
那么OS提供的樣式有:
- 管道(匿名、命名)
- 共享內存
- 消息隊列
- 信號量
2.管道
基于文件的,讓不同進程看到同一份資源的通信方式叫做管道
管道只能被設計成為單向通信
在Linux中,管道確實可以被視為一種機制,同時也是一種特殊的文件類型。這種雙重性來自于Linux操作系統的設計和其對所有資源采取的抽象化處理方式。
作為一種機制,管道用于進程間通信(IPC)。它允許一個進程的輸出直接成為另一個進程的輸入,從而實現了數據的快速傳遞。這種機制大大簡化了進程間的通信過程,提高了通信效率。
從文件的角度來看,管道在Linux中被實現為一種特殊的文件類型。這意味著管道具有文件的某些屬性和操作方式,比如可以通過文件描述符進行打開、讀取、寫入和關閉等操作。然而,與普通文件不同的是,管道并不在磁盤上占用實際的物理空間,它的內容存儲在內核的緩沖區中,只在內存中存在。
這種雙重性使得管道既具有機制的靈活性,又具有文件的可操作性。它可以在不同的進程之間建立連接,實現數據的傳遞和共享,同時也可以通過標準的文件操作接口進行訪問和控制。
為了支持管道通信,OS提供了一個接口:pipe()
2.1匿名管道
匿名管道(Anonymous Pipe)Linux中提供的一種進程間通信(IPC)機制。匿名管道沒有名字,它們僅存在于創建它們的進程及其子進程之間,并且一旦這些進程終止,管道也將隨之消失。
匿名管道的主要特點如下:
- 單向通信:匿名管道是半雙工的,這意味著數據只能在一個方向上流動。通常,一個進程向管道寫入數據,而另一個進程從管道讀取數據。如果需要雙向通信,則需要創建兩個管道,一個用于每個方向。
- 親緣關系:匿名管道只能用于具有親緣關系的進程之間,即一個進程和它的子進程之間。這是因為管道的文件描述符是通過fork()系統調用在父子進程之間復制的。
- 自動管理:當所有使用管道的文件描述符都被關閉時,管道將自動被刪除。這意味著不需要像命名管道那樣顯式地打開和關閉它。
- 內存中的緩沖區:管道實際上是一個在內核中維護的緩沖區,用于存儲從寫入端發送但尚未被讀取端讀取的數據。這個緩沖區的大小是有限的,如果寫入的數據超過了緩沖區的大小,寫操作可能會被阻塞,直到有空間可用。
管道文件的數據是存儲在內存中的(是內存級的文件),而不是磁盤上。這使得對管道的訪問速度非常快,類似于對內存的直接訪問
匿名管道是通過創建子進程,而子進程會繼承父進程的相關屬性信息,來實現不同的進程看到同一份資源
通過管道,一個進程(寫端)可以將數據發送給另一個進程(讀端),實現數據的共享和傳遞。當讀端從管道中讀取數據時,這些數據會被從內核的緩沖區中移除(或稱為消費),從而為寫端提供了更多的空間來寫入新的數據
在C語言中,可以使用pipe()
函數來創建一個匿名管道。這個函數接受一個包含兩個文件描述符的數組作為參數,并返回兩個文件描述符:一個用于讀操作,另一個用于寫操作。然后,可以使用fork()
創建一個子進程,并在父進程和子進程之間使用這些文件描述符進行通信。
2.2pipe()函數 —創建匿名管道
pipe
函數用于創建管道,這是一種特殊的文件,用于連接一個程序的標準輸出和另一個程序的標準輸入,從而實現這兩個程序之間的通信。在C語言中函數原型為:int pipe(int pipefd[2]);
參數:
pipe
函數接受一個整型數組作為參數(這是個輸出型參數),即int pipefd[2]
。這個數組用于存儲管道的兩個文件描述符:pipefd[0]
表示管道的讀端,而pipefd[1]
表示管道的寫端。
作用:
調用pipe
函數后,系統會創建一個匿名管道,并將這個管道的兩個端點(一個用于讀,一個用于寫)的文件描述符分別賦值給pipefd[0]
和pipefd[1]
。這樣,一個進程就可以通過pipefd[1]
向管道寫入數據,而另一個進程則可以通過pipefd[0]
從管道中讀取數據。這種機制使得兩個進程之間可以通過管道進行通信。
返回值:
如果pipe
函數成功創建了管道,則返回0。如果創建失敗,則返回-1,并將錯誤原因存儲在全局變量errno
中。可能的錯誤原因包括:
EMFILE
:進程已達到其文件描述符的最大數量。ENFILE
:系統已達到其文件描述符的最大數量。EFAULT
:傳遞給pipe
函數的數組地址不合法。
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>void writer(int wfd)//寫端的操作
{const char *str = "hi father, I am child";char buffer[128];int cnt = 0;pid_t pid = getpid();while(1){//先調用snprintf向buffer數組里寫,然后在把buffer數組寫到fd為wfd的文件里(這里就是管道的寫端)snprintf(buffer, sizeof(buffer), "message: %s, pid: %d, count: %d\n", str, pid, cnt);write(wfd, buffer, strlen(buffer));cnt++;sleep(1);}
}void reader(int rfd)//讀端的操作
{char buffer[1024];while(1){ssize_t n = read(rfd, buffer, sizeof(buffer)-1);(void)n;//沒有使用這個 n 變量。如果編譯器被配置為警告未使用的變量,那么它就會為 n 發出一個警告printf("father gets a message: %s", buffer);}
}int main()
{//創建管道int pipefd[2];int n = pipe(pipefd);// pipefd[0]-->read pipefd[1]-->write 0是寫端,1是讀端// 0-->嘴巴 讀書 1-->鋼筆 寫字if(n < 0) return 1;//創建子進程pid_t id = fork();if(id == 0){//child: w 我們讓子進程來寫close(pipefd[0]);//那么就要關閉讀端writer(pipefd[1]);exit(0);}// father: r我們讓父進程來讀close(pipefd[1]);//那么就要關閉寫端reader(pipefd[0]);wait(NULL);return 0;
}
2.3匿名管道的四種情況
-
管道內部沒有數據而且子進程不關閉自己的寫端文件fd, 讀端(父)就要阻塞等待,直到pipe有數據
管道中沒有數據時,讀端繼續讀取的默認行為是阻塞當前正在讀取的進程。在這種情況下,進程會進入等待狀態,其進程控制塊(PCB)會被放置在管道文件的等待隊列中。只要管道中沒有新的數據到來,讀端進程就會一直阻塞等待
-
管道內部被寫滿而且讀端(父進程)不關閉自己的fd,寫端(子進程)寫滿之后,就要阻塞等待
管道具有固定的緩沖區大小,當緩沖區中的數據量達到上限時,寫端進程就會被阻塞,直到有讀端進程從管道中讀取數據并釋放緩沖區空間
#include<stdio.h> #include<string.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h> #include<sys/wait.h>void writer(int wfd)//寫端的操作 {const char *str = "hi father, I am child";char buffer[128];int cnt = 0;pid_t pid = getpid();while(1){char ch='A';write(wfd, &ch, 1);cnt++;printf("cnt=%d\n",cnt);} }void reader(int rfd)//讀端的操作 {char buffer[1024];while(1){sleep(10);ssize_t n = read(rfd, buffer, sizeof(buffer)-1);(void)n;//沒有使用這個 n 變量。如果編譯器被配置為警告未使用的變量,那么它就會為 n 發出一個警告printf("father gets a message: %s", buffer);} }int main() {//創建管道int pipefd[2];int n = pipe(pipefd);// pipefd[0]-->read pipefd[1]-->write 0是寫端,1是讀端// 0-->嘴巴 讀書 1-->鋼筆 寫字if(n < 0) return 1;//創建子進程pid_t id = fork();if(id == 0){//child: w 我們讓子進程來寫close(pipefd[0]);//那么就要關閉讀端writer(pipefd[1]);exit(0);}// father: r我們讓父進程來讀close(pipefd[1]);//那么就要關閉寫端reader(pipefd[0]);wait(NULL);return 0; }
-
不再向管道寫入數據并且關閉了寫端(子進程)文件描述符時,讀端(父進程)可以繼續從管道中讀取剩余的數據,直到管道中的數據全部被讀取完畢。最后就會讀到返回值為0,表示讀結束,類似讀到了文件的結尾
-
讀端關閉其文件描述符并且不再讀取數據時,如果寫端繼續向管道寫入數據,操作系統會發送一個
SIGPIPE
信號給寫端進程。默認情況下,這個信號會終止寫端進程。SIGPIPE
信號是一個用于處理管道寫端在寫操作時無讀端接收的情況的信號。SIGPIPE
信號(信號編號為13)的發送是為了通知寫端進程,其寫操作因為管道的另一端沒有讀端而不再有意義。這是一種保護機制,防止寫端進程在沒有讀端的情況下無限期地等待或繼續寫入數據到一個不再被讀取的管道中。#include<stdio.h> #include<string.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h> #include<sys/wait.h>void writer(int wfd)//寫端的操作 {int cnt = 0;while(1){sleep(1);char ch='A';write(wfd, &ch, 1);cnt++;printf("cnt=%d\n",cnt);}//子進程一直寫 }void reader(int rfd)//讀端的操作 {int cnt=8;char buffer[1024];while(1){sleep(1);ssize_t n = read(rfd, buffer, sizeof(buffer)-1);if(n>0){printf("father get a message: %s, n : %ld\n", buffer, n);}else if(n==0){printf("reading has done: %s %ld\n", buffer,n);break;}else {break;}cnt--;if(cnt==0){break;}}close(rfd);//8秒后,父進程不再讀,直接關閉printf("end"); }int main() {//創建管道int pipefd[2];int n = pipe(pipefd);// pipefd[0]-->read pipefd[1]-->write 0是寫端,1是讀端// 0-->嘴巴 讀書 1-->鋼筆 寫字if(n < 0) return 1;//創建子進程pid_t id = fork();if(id == 0){//child: w 我們讓子進程來寫close(pipefd[0]);//那么就要關閉讀端writer(pipefd[1]);exit(0);}// father: r我們讓父進程來讀close(pipefd[1]);//那么就要關閉寫端reader(pipefd[0]);int status=0;pid_t rid=waitpid(id,&status,0);printf("exit code: %d, exit signal: %d\n",WEXITSTATUS(status),status&0x7f);return 0; }
2.4管道的特征
-
匿名管道自帶同步機制:在匿名管道中,寫端在寫數據且沒有寫完時,讀端是不可能訪問管道這塊公共資源的。這種機制確保了數據的完整性和一致性,避免了數據沖突和錯誤
-
管道(Pipe)是一種常用于具有血緣關系進程間通信的機制,特別是在父子進程之間。這里的“血緣關系”指的是進程之間的創建關系,即一個進程創建了另一個進程,它們之間存在直接的父子關系
-
管道(pipe)是面向字節流的:這意味著管道在傳輸數據時,是以字節為單位進行處理的。無論是字符、整數還是其他類型的數據,都會被轉換成字節序列進行傳輸。因此,管道不關心數據的具體格式或類型,只負責將數據以字節流的形式從一個進程傳遞到另一個進程
-
管道(pipe)是半雙工的:它只能在一個方向上傳輸數據,屬于單向通信的特殊概念。具體來說,一個管道有一個輸入端和一個輸出端,數據可以從輸入端流入管道,并從輸出端流出。但管道不允許數據在相反的方向上流動,即不能從輸出端流回輸入端
半雙工(Half Duplex)數據傳輸指的是數據可以在一個信號載體的兩個方向上傳輸,但是不能同時傳輸。也就是說,在一個時間點,數據只能在一個方向上流動
-
父子進程退出后,管道會自動釋放。這是由操作系統的內存管理機制決定的。當進程結束時,操作系統會回收其占用的所有資源,包括打開的文件、管道、網絡連接等
-
我們之前在命令行里使用的
|
其實就是匿名管道:在命令行中,當我們使用|
來連接兩個命令時,實際上是在這兩個命令之間創建了一個匿名管道。這使得前一個命令的輸出能夠直接傳輸給后一個命令,實現了兩個命令之間的數據共享和傳輸
3.基于管道的進程池設計
4.命名管道
4.1引入與性質
我們設想一個這樣的情況:
- 當一個進程打開一個文件(比如
log.txt
),內核會為該進程創建一個struct file
結構體,其中包含指向inode
結構體、函數指針數組和緩沖區的指針。這個struct file
結構體會指向已加載的inode
結構體和緩沖區,用于表示文件在內核中的信息和緩存文件數據。 - 當另一個進程也打開同一個文件時,內核會為該進程創建另一個
struct file
結構體,其中也包含指向相同的inode
結構體和緩沖區的指針。這意味著多個進程可以共享相同的inode
結構體和緩沖區,而不會為每個進程創建一份完全一樣的inode
結構體和緩沖區。 - 由于
inode
結構體和緩沖區是在內核中維護的,因此多個進程可以共享相同的inode
結構體和緩沖區,而不需要為每個進程復制一份。這種共享機制可以節省內存空間,并確保多個進程對同一文件的操作是一致的。
此時這兩個進程就看到了同一塊資源(log.txt 文件)
當兩個進程共享同一個文件(例如
log.txt
)時,它們實際上是在操作同一塊資源。這是因為文件系統中的路徑和文件名是唯一的,所以無論哪個進程打開同一個路徑下的文件,都會訪問到同一個文件。在多個進程共享文件時,它們可以通過共享同一個緩沖區來進行數據交換。這個緩沖區可以被看作是一個管道,用于在進程之間傳遞數據。通過這種方式,進程可以實現數據共享和通信。
在上面這種情況下,這個管道(緩沖區)可以被稱為命名管道(named pipe)。
命名管道是一種特殊的文件類型,它允許進程之間通過文件系統進行通信。通過路徑+文件名來確定(唯一的路徑+文件名來找到并訪問這個管道),多個進程可以通過打開同一個命名管道來實現數據交換。
- 在這種情況下,這個管道不需要與磁盤進行交互,因為數據是在內存中進行傳遞的。進程通過讀取和寫入管道來實現數據共享,而不需要直接與磁盤進行交互。
4.2命令行創建
命名管道(Named Pipe)是一種特殊的文件,用于進程間通信。它是一種半雙工通信方式,允許一個或多個進程之間通過讀寫同一個文件來進行通信。
-
創建命名管道:
命名管道是通過調用mkfifo
系統調用來創建的。命名管道在文件系統中以文件的形式存在,但實際上它是一個FIFO(First In First Out)的通信通道。創建命名管道的語法為:mkfifo <管道名稱>
-
打開和關閉命名管道:
命名管道可以像普通文件一樣被打開和關閉。進程可以通過open
系統調用打開一個命名管道文件,并通過close
系統調用關閉它。在打開命名管道時,進程需要指定相應的讀寫權限。 -
讀寫數據:
進程可以通過打開的文件描述符對命名管道進行讀寫操作。一個進程往管道中寫入數據,另一個進程從管道中讀取數據。命名管道是阻塞的,如果寫入進程寫入數據時,沒有進程讀取數據,寫入進程會被阻塞直到有進程讀取數據。 -
進程間通信:
命名管道通常用于實現進程間通信,特別是在父子進程或者**不相關進程之間**。一個進程可以向命名管道寫入數據,另一個進程可以從命名管道讀取數據,實現了進程間的數據交換。
4.3程序中創建命名管道
mkfifo
函數是一個UNIX系統中用于創建命名管道(named pipe)的函數。它的作用是在文件系統中創建一個特殊類型的文件,這個文件可以被多個進程用來進行進程間通信。
在C語言中,可以使用mkfifo
函數來創建一個命名管道,其原型如下:
int mkfifo(const char *pathname, mode_t mode);
pathname
參數是指定要創建的命名管道的路徑和文件名。mode
參數是指定創建的管道的權限模式,通常以八進制表示(例如0666
)。
使用mkfifo
函數創建命名管道后,其他進程可以通過打開這個路徑+文件名來訪問這個管道,從而實現進程間的通信。一旦創建了命名管道,它就可以在文件系統中像普通文件一樣被打開、讀取和寫入。
寫個小項目
項目規劃
- Cnmm.hpp:管道的封裝,頭文件的包含、宏定義等任務
- PipeClient.cpp:客戶端,進行管道的寫入
- PipeServe.cpp:服務端(服務器),進行管道的創建、讀取
Cnmm.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>using namespace std;#define Mode 0666
#define Path "./fifo"class Fifo
{
public:Fifo(const string &path) : _path(path){umask(0);int n = mkfifo(_path.c_str(), Mode);if (n == 0){cout << "mkfifo success" << endl;}else{cerr << "mkfifo failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;}}~Fifo(){int n = unlink(_path.c_str());if (n == 0){cout << "remove fifo file " << _path << " success" << endl;}else{cerr << "remove failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;}}private:string _path; // 文件路徑+文件名
};#endif//條件編譯結束
整體上使用一個條件編譯:
在C++頭文件中,通常會使用條件編譯指令來防止頭文件被多次包含,以避免重復定義的問題。條件編譯指令的一般結構如下:
#ifndef __HEADER_NAME__ #define __HEADER_NAME__// 頭文件內容#endif
#ifndef __HEADER_NAME__
:這是條件編譯指令的開始標記,用于檢查是否已經定義了名為__HEADER_NAME__
的宏。如果之前沒有定義這個宏,那么下面的代碼將被執行。#define __HEADER_NAME__
:在條件編譯指令的開始處,定義名為__HEADER_NAME__
的宏,表示這個頭文件已經被包含過了。// 頭文件內容
:在這個部分可以放置頭文件的內容,包括類的定義、函數的聲明等。#endif
:這是條件編譯指令的結束標記,表示條件編譯的范圍結束。
#ifndef __COMM_HPP__
是條件編譯指令的開始標記,而#endif
是條件編譯指令的結束標記。
-
cerr
:cerr
是C++標準庫中的標準錯誤流,它用于輸出錯誤信息到標準錯誤設備(通常是顯示器)。- 與
cout
(標準輸出流)類似,cerr
也是一個對象,可以使用插入運算符<<
來將數據插入到cerr
中進行輸出。 - 與
cout
不同的是,cerr
通常用于輸出錯誤消息,而不是普通的程序輸出。它是線程安全的,可以在多線程環境中使用。
-
errno
:errno
是一個全局變量,通常定義在<cerrno>
頭文件中,用于存儲函數調用發生錯誤時的錯誤碼。- 當某個函數發生錯誤時,它會設置適當的錯誤碼到
errno
中,以便程序能夠檢測和處理錯誤。 - 錯誤碼是整數類型,每個錯誤碼對應于一種特定類型的錯誤。可以通過查看系統的錯誤碼表來了解每個錯誤碼的含義。
-
strerror
:strerror
是一個C標準庫函數,通常定義在<cstring>
或<string.h>
頭文件中,用于將錯誤碼轉換為對應的錯誤消息字符串。strerror
接受一個錯誤碼作為參數,并返回一個指向描述該錯誤的字符串的指針。- 通過調用
strerror(errno)
,可以獲取與當前errno
值對應的錯誤消息字符串,以便程序輸出或記錄錯誤信息。
PipeClient.cpp
#include "Comm.hpp"int main()
{// 打開管道,進行寫入,最后關閉int wfd = open(Path, O_WRONLY | O_CREAT); // 以只寫方式打開if (wfd < 0){cerr << "open failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;return 1;}string buffer; // 開始寫入while (true){cout << "please write your message:" << endl;getline(cin, buffer);ssize_t n = write(wfd, buffer.c_str(), buffer.size());if (n < 0){cerr << "write failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;break;}}close(wfd);return 0;
}
PipeServe.cpp
#include "Comm.hpp"
#include <unistd.h>int main()
{Fifo fifo(Path);// 打開管道,進行讀取,最后關閉int rfd = open(Path, O_RDONLY); // 以只讀方式打開if (rfd < 0){cerr << "open failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;return 1;}char buffer[1024];//開始讀取while (true){ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = '\0';cout << "client say : " << buffer << endl;}else if (n == 0){cout << "client quit, me too!!" << endl;break;}else{cerr << "read failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;break;}}close(rfd);return 0;
}
這里我自己有個疑問:本來讀端是一直堵塞在read函數的,我們一輸入abcde,第一次read就能讀取完,然后輸出。下一次循環就應該接著讀,讀到末尾,返回0了吧? 但為什么這里是接著阻塞呢?
- 在非阻塞模式下,如果讀取到文件末尾(沒有更多的數據可讀取),
read
函數會立即返回 0。- 在阻塞模式下,
read
函數會阻塞等待直到有數據可讀取或者發生錯誤,它不會因為讀取到文件末尾而返回 0。相反,只有當管道被關閉或者讀取操作被中斷時,read
函數才會返回 0。- 默認都是阻塞模式
文件描述符的阻塞模式和非阻塞模式指的是在進行I/O操作時的行為方式。
-
阻塞模式:
- 在阻塞模式下,當進行I/O操作時,如果數據尚未準備好或者操作無法立即完成,程序會被阻塞,也就是暫停執行,直到操作完成或者出現錯誤為止。
- 例如,在阻塞模式下,如果調用read函數讀取一個文件描述符,但是文件中沒有數據可讀,程序將會被阻塞,直到有數據到達為止。類似地,如果調用write函數寫入數據到一個已滿的管道中,程序也會被阻塞,直到有足夠的空間寫入數據。
-
非阻塞模式:
- 在非阻塞模式下,進行I/O操作時,如果操作無法立即完成,程序不會被阻塞,而是立即返回一個錯誤或者一個特定的狀態碼,提示當前操作無法立即完成。
- 例如,在非阻塞模式下,如果調用read函數讀取一個文件描述符,但是文件中沒有數據可讀,read函數將立即返回一個錯誤碼,而不會等待數據到達。類似地,如果調用write函數寫入數據到一個已滿的管道中,write函數也會立即返回一個錯誤碼,而不會等待空間可用。
5.System V共享內存
實現進程間通信的前提就是如何讓不同的進程看到同一份資源
- 匿名管道我們是通過子進程繼承父進程打開的資源
- 命名管道是通過兩個進程都打開具有唯一性標識的命名管道文件(路徑+文件名)
- 共享內存其實是通過OS創建一塊shm
System V共享內存(Shared Memory)是一種Linux中用于進程間通信(IPC)的機制。它允許多個進程訪問同一塊物理內存區域,從而實現數據的快速共享和交換。
- 原理:
- 在物理內存中申請一塊內存空間作為共享內存。
- 將這塊內存空間與各個進程的頁表建立映射關系,使得這些進程在虛擬地址空間中可以看到并訪問這塊共享內存。
- 通過這種方式,多個進程可以像訪問自己的內存一樣訪問共享內存,從而實現數據的快速共享和交換。
- 使用方式:
- 創建:使用
shmget()
系統調用來創建共享內存。這個函數會分配一塊指定大小的內存區域,并返回一個標識符,用于后續對這塊共享內存的操作。 - 關聯:使用
shmat()
系統調用來將共享內存關聯到進程的地址空間。這個函數會將共享內存的地址告訴進程,使得進程可以通過這個地址來訪問共享內存。 - 取消關聯:當進程不再需要訪問共享內存時,可以使用
shmdt()
系統調用來取消關聯。這個函數會斷開進程與共享內存之間的映射關系。 - 釋放:當所有進程都不再需要這塊共享內存時,可以使用
shmctl()
系統調用來釋放它。這個函數會回收這塊內存區域,并釋放相關的資源。
- 創建:使用
5.1相關函數介紹
ftok()
函數 Linux中用于生成一個唯一的鍵值(key)的系統調用,這個鍵值通常用于在進程間通信(IPC)中標識共享內存段、消息隊列或信號量集。ftok()
函數基于一個已經存在的文件路徑和一個非零的標識符(通常是一個小的正整數)來生成這個鍵值。
#include <sys/ipc.h>
#include <sys/types.h> key_t ftok(const char *pathname, int proj_id);
參數:
pathname
:指向一個已經存在的文件路徑的指針。這個文件通常被用作生成鍵值的“種子”或“基礎”。proj_id
:一個非零的標識符,通常是一個小的正整數。這個值將與文件路徑一起被用于生成鍵值。返回值:
如果成功,ftok()
函數返回一個唯一的鍵值(key_t
類型),該鍵值可以在后續的 IPC 調用(如 shmget()
, msgget()
, semget()
等)中用作參數。如果失敗,則返回 (key_t) -1
并設置 errno
以指示錯誤。
shmget()
:創建或獲取共享內存
shmget()
系統調用用于創建一個新的共享內存對象,或者如果它已存在,則返回該對象的標識符。
函數原型:
int shmget(key_t key, size_t size, int shmflg);
參數:
-
key
:一個鍵,用于唯一標識共享內存對象。通常使用ftok()
函數生成。- 共享內存在內核中同時可以存在很多個,OS必須要管理所有的共享內存
- 如何管理呢?先描述,在組織
- 系統中會存在很多共享內存,怎么保證,多個不同的進程看到的是同共享內存呢? 要給共享內存提供唯一性的標識
key
便是那個唯一性標識符。那么為什么這個key要由我們用戶來傳入呢?
- 如果然系統生成,將值返回讓我們得到。那我們如何給另外一個進程呢?要做到就要有進程間通信,這不倒反天罡了?
-
size
:共享內存的大小(以字節為單位)。 -
shmflg
:權限標志和選項。通常設置為IPC_CREAT
(如果對象不存在則創建,存在的話直接獲取)和權限(如0666
)。若設置為
IPC_CREAT|IPC_EXCL
(如果對象不存在則創建,存在的話出錯返回)
返回值:成功時返回共享內存對象的標識符;失敗時返回-1并設置errno
。
shmat()
:將共享內存關聯到進程的地址空間
shmat()
系統調用用于將共享內存對象關聯到調用進程的地址空間。
函數原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
參數:
shmid
:shmget()
返回的共享內存對象標識符。shmaddr
:希望將共享內存附加到的進程的地址。如果設置為NULL,則系統選擇地址。shmflg
:通常設置為0或SHM_RND
(使附加地址向下舍入到最接近的SHMLBA邊界)。
返回值:成功時返回共享內存附加到進程的地址;失敗時返回(void *)-1并設置errno
。
shmdt()
:取消共享內存的關聯
shmdt()
系統調用用于取消之前通過shmat()
附加到進程的共享內存的關聯。
函數原型:
int shmdt(const void *shmaddr);
參數:
shmaddr
:shmat()
返回的共享內存附加到進程的地址。
返回值:成功時返回0;失敗時返回-1并設置errno
。
shmctl()
:控制共享內存
shmctl()
系統調用用于獲取或設置共享內存的屬性,或者刪除共享內存對象。
函數原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
參數:
shmid
:共享內存對象標識符。cmd
:要執行的操作。例如,IPC_RMID
用于刪除共享內存對象,IPC_STAT
用于獲取其狀態。buf
:指向shmid_ds
結構的指針,用于傳遞或接收共享內存的狀態信息。
返回值:成功時返回0;失敗時返回-1并設置errno
。
今天就到這里了,也是結束了期末周,現在就開始正常更新啦