目錄
system V共享內存
共享內存示意圖
共享內存函數
shmget函數
shmat函數
shmdt函數
shmctl函數
代碼示例
shm頭文件
構造函數
獲取key值
創建者的構造方式
GetShmHelper?函數
GetShmUseCreate?函數
使用者的構造方式
GetShmForUse?函數
分離附加操作
DetachShm?函數
AttachShm?函數
RoleToString
析構函數
客戶端
服務端
命名管道與共享內存的區別
一、定義與工作原理
二、性能與效率
三、使用場景與限制
四、實現與操作
system V共享內存
共享內存區是最快的IPC形式。一旦這樣的內存映射到共享它的進程的地址空間,這些進程間數據傳遞不再涉及到內核,換句話說是進程不再通過執行進入內核的系統調用來傳遞彼此的數據。
共享內存示意圖
共享內存在我們的虛擬地址空間的共享區當中,這意味著它一旦通信建立,一有數據就能一下被拿到。
共享內存函數
shmget函數
功能:用來創建共享內存 原型int shmget(key_t key, size_t size, int shmflg); 參數key:這個共享內存段名字size:共享內存大小shmflg:由九個權限標志構成,它們的用法和創建文件時使用的mode模式標志是一樣的 返回值:成功返回一個非負整數,即該共享內存段的標識碼;失敗返回-1
shmat函數
功能:將共享內存段連接到進程地址空間 原型void *shmat(int shmid, const void *shmaddr, int shmflg); 參數shmid: 共享內存標識shmaddr:指定連接的地址shmflg:它的兩個可能取值是SHM_RND和SHM_RDONLY 返回值:成功返回一個指針,指向共享內存第一個節;失敗返回-1
說明:
shmaddr為NULL,核心自動選擇一個地址 shmaddr不為NULL且shmflg無SHM_RND標記,則以shmaddr為連接地址。 shmaddr不為NULL且shmflg設置了SHM_RND標記,則連接的地址會自動向下調整為SHMLBA的整數倍。公式:shmaddr - (shmaddr % SHMLBA) shmflg=SHM_RDONLY,表示連接操作用來只讀共享內存
shmdt函數
功能:將共享內存段與當前進程脫離 原型int shmdt(const void *shmaddr); 參數shmaddr: 由shmat所返回的指針 返回值:成功返回0;失敗返回-1 注意:將共享內存段與當前進程脫離不等于刪除共享內存段
shmctl函數
功能:用于控制共享內存 原型int shmctl(int shmid, int cmd, struct shmid_ds *buf); 參數shmid:由shmget返回的共享內存標識碼cmd:將要采取的動作(有三個可取值)buf:指向一個保存著共享內存的模式狀態和訪問權限的數據結構 返回值:成功返回0;失敗返回-1
代碼示例
shm頭文件
#ifndef _SHM_HPP_ #define _SHM_HPP_#include <iostream> #include <cstdio> #include <cerrno> #include <string> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <fcntl.h> #include <sys/ipc.h> #include <sys/shm.h> #include <string.h>#define Creater 1 #define User 2 const int gShmSize = 4096; const std::string gpathname = "/home/lsf/lesson24/shm"; const int gproj_id = 0x66;class Shm { private:key_t GetCommKey(){key_t k = ftok(_pathname.c_str(), _proj_id);if (k < 0){perror("ftok");}return k;}int GetShmHelper(key_t key, int size, int flag){int shmid = shmget(key, size, flag);if (shmid < 0){perror("shmget");}return shmid;}std::string RoleToString(int who){if (who == Creater)return "Creater";else if (who == User)return "User";elsereturn "None";}void *AttachShm(){if (_addrshm != nullptr)DetachShm(_addrshm);void *shmaddr = shmat(_shmid, nullptr, 0);if (shmaddr == nullptr){perror("shmat");}std::cout << "who: " << RoleToString(_who) << " attach shm..." << std::endl;return shmaddr;}void DetachShm(void *shmaddr){if (shmaddr == nullptr)return;shmdt(shmaddr);std::cout << "who: " << RoleToString(_who) << " detach shm..." << std::endl;}public:Shm(const std::string &pathname, int proj_id, int who): _pathname(pathname), _proj_id(proj_id), _who(who), _addrshm(nullptr){_key = GetCommKey();if (_who == Creater)GetShmUseCreate();else if (_who == User)GetShmForUse();_addrshm = AttachShm();std::cout << "shmid: " << _shmid << std::endl;std::cout << "_key: " << ToHex(_key) << std::endl;}std::string ToHex(key_t key){char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", key);return buffer;}bool GetShmForUse(){if (_who == User){_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT);// sleep(10);if (_shmid >= 0){std::cout << "shm get done..." << std::endl;return true;}}return false;}bool GetShmUseCreate(){if (_who == Creater){_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | IPC_EXCL | 0666);// sleep(10);if (_shmid >= 0){std::cout << "shm create done..." << std::endl;return true;}}return false;}void *Addr(){return _addrshm;}void Zero(){if (_addrshm){memset(_addrshm, 0, gShmSize);}}~Shm(){if (_who == Creater){int res = shmctl(_shmid, IPC_RMID, nullptr);}std::cout << "shm remove done..." << std::endl;}private:key_t _key;int _shmid;std::string _pathname;int _proj_id;int _who;void *_addrshm; };#endif
成員變量中除了_pathname和_who一個表示路徑名一個表示身份外,其余的都是我們上文提到的函數的參數,我們遇到時一一講解。
構造函數
Shm(const std::string &pathname, int proj_id, int who): _pathname(pathname), _proj_id(proj_id), _who(who), _addrshm(nullptr){_key = GetCommKey();if (_who == Creater)GetShmUseCreate();else if (_who == User)GetShmForUse();_addrshm = AttachShm();std::cout << "shmid: " << _shmid << std::endl;std::cout << "_key: " << ToHex(_key) << std::endl;}
我們分別將我們的路徑,_proj_id(后面說),身份,_addeshm這些能設的初始值先設置好。然后我們就可以先獲得一個我們共享內存的key標識值,為什么不直接設置,而是要寫個函數呢?這是為了達到我們唯一性的目的,我們程序員自己設置的話難免會設置到重復的,為了避免它我們就可以用一個ftok函數,它會根據我們的路徑和_proj_id用一種算法設計出一個key值,我們采用它能達到我們心目中的要求。
它的返回值就是我們要的key值。
項目標識符(proj_id):除了文件路徑外,
ftok()
還需要一個項目標識符(proj_id
)。這個標識符的最低有效8位將被用于生成鍵值。proj_id
必須是非零的,以確保生成的鍵值不是全零(全零的鍵值在System V IPC中有特殊含義,通常表示無效的鍵值)。獲取key值
key_t GetCommKey(){key_t k = ftok(_pathname.c_str(), _proj_id);if (k < 0){perror("ftok");}return k;}
這就是我們的獲取key值的函數,我們只是對ftok簡單的封裝了一下。
我們回到構造函數,在我們完成了key值的設置之后我們就要對創建者和使用則進行不同的構造方式了。
創建者的構造方式
int GetShmHelper(key_t key, int size, int flag){int shmid = shmget(key, size, flag);if (shmid < 0){perror("shmget");}return shmid;}bool GetShmUseCreate(){if (_who == Creater){_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | IPC_EXCL | 0666);// sleep(10);if (_shmid >= 0){std::cout << "shm create done..." << std::endl;return true;}}return false;}
GetShmHelper函數就是對我們的shmget函數做個簡單封裝,
GetShmHelper
?函數這個函數根據給定的鍵值(key)、大小(size)和標志(flag)來獲取或創建一個共享內存段。
- 參數:
key_t key
:共享內存段的鍵值,由ftok()
函數生成。int size
:共享內存段的大小(以字節為單位)。int flag
:控制shmget()
行為的標志。可以是IPC_CREAT
(如果鍵不存在,則創建新的共享內存段)、IPC_EXCL
(與IPC_CREAT
一起使用時,如果鍵已存在,則調用失敗)等標志的組合。- 返回值:
- 成功時返回共享內存段的標識符(
shmid
)。- 失敗時返回-1,并通過
perror()
函數打印錯誤信息。
GetShmUseCreate
?函數這個函數檢查某個條件(這里是通過
_who
變量與Creater
比較),如果條件滿足,則嘗試創建一個新的共享內存段。
- 變量:
_who
:用于指示當前進程的角色。_key
:共享內存段的鍵值。gShmSize
:共享內存段的大小。_shmid
:用于存儲共享內存段標識符的變量。- 邏輯:
- 如果
_who
等于Creater
,則調用GetShmHelper
函數嘗試創建新的共享內存段。GetShmHelper
的調用使用了IPC_CREAT | IPC_EXCL | 0666
作為標志,這意味著如果鍵值已存在,則調用將失敗(因為IPC_EXCL
標志的存在)。0666
是權限設置,但由于IPC_CREAT
和IPC_EXCL
的存在,它實際上只在創建新段時有效。- 如果
GetShmHelper
返回非負值(即成功創建了共享內存段或獲取了已存在的共享內存段),則函數返回true
。- 如果
GetShmHelper
返回-1(即失敗),則函數返回false
,并且不會打印"shm create done..."消息。使用者的構造方式
bool GetShmForUse(){if (_who == User){_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT);// sleep(10);if (_shmid >= 0){std::cout << "shm get done..." << std::endl;return true;}}return false;}
GetShmForUse
?函數
目的:嘗試獲取一個共享內存段以供使用。當前角色是用戶(
_who == User
),嘗試獲取或創建共享內存段。參數:
- 隱式參數(全局變量):
_who
:當前進程的角色。_key
:共享內存段的鍵值。gShmSize
:共享內存段的大小。_shmid
:用于存儲共享內存段標識符的變量。邏輯:
- 如果
_who
等于User
,則調用GetShmHelper
函數嘗試獲取或創建共享內存段。GetShmHelper
的調用使用了IPC_CREAT
作為標志。這意味著:
- 如果鍵值對應的共享內存段已存在,則
shmget
將返回該段的標識符。- 如果鍵值對應的共享內存段不存在,則
shmget
將創建一個新的共享內存段,并返回其標識符。- 如果
GetShmHelper
返回非負值(即成功獲取了已存在的段或創建了新段),則函數返回true
,打印"shm get done..."。- 如果
GetShmHelper
返回-1(即失敗,這通常不應該發生,因為IPC_CREAT
允許創建新段)。- 如果
_who
不是User
,則函數直接返回false
。分離附加操作
void DetachShm(void *shmaddr){if (shmaddr == nullptr)return;shmdt(shmaddr);std::cout << "who: " << RoleToString(_who) << " detach shm..." << std::endl;}void *AttachShm(){if (_addrshm != nullptr)DetachShm(_addrshm);void *shmaddr = shmat(_shmid, nullptr, 0);if (shmaddr == nullptr){perror("shmat");}std::cout << "who: " << RoleToString(_who) << " attach shm..." << std::endl;return shmaddr;}
DetachShm
?函數
目的:分離(detach)之前附加(attach)的共享內存段。
參數:
shmaddr
:指向之前附加的共享內存段的指針。邏輯:
- 如果傳入的
shmaddr
是nullptr
,則函數直接返回,不執行任何操作。這是一種防御性編程實踐,用于防止對空指針進行解引用。- 使用
shmdt
函數分離共享內存段。shmdt
接受一個指向共享內存段的指針,并分離該進程與該內存段之間的關聯。- 打印一條消息,指示當前角色(通過
RoleToString(_who)
獲取)正在分離共享內存段。RoleToString
是一個將角色轉換為字符串的函數。
AttachShm
?函數
目的:附加(attach)到一個已存在的共享內存段。
參數:無。
邏輯:
- 如果全局變量(或類成員變量)
_addrshm
不是nullptr
,則首先調用DetachShm
函數分離當前附加的共享內存段(如果有的話)。這是一種清理操作,確保在附加新的共享內存段之前不會泄漏舊的內存段。- 使用
shmat
函數附加到共享內存段。shmat
接受共享內存段的標識符、一個指向期望附加地址的指針(這里傳入nullptr
,讓系統選擇地址)、以及一組標志(這里傳入0
,表示使用默認行為)。- 如果
shmat
返回nullptr
,則使用perror
函數打印一條錯誤消息。這意味著附加操作失敗,可能是由于資源不足、權限問題或其他原因。- 打印一條消息,指示當前角色(通過
RoleToString(_who)
獲取)正在附加共享內存段。- 返回
shmat
返回的指針,即指向附加的共享內存段的指針。如果shmat
失敗,則返回nullptr
。RoleToString
std::string RoleToString(int who){if (who == Creater)return "Creater";else if (who == User)return "User";elsereturn "None";}
將_who轉換成字符串。
接下來我們可以打印一下共享內存標識符和我們的key值,我們將key值轉換成十六進制,方便觀察。
std::string ToHex(key_t key){char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", key);return buffer;}
void *Addr(){return _addrshm;}
我們可以使用這個函數讓上層獲取共享內存的地址。
void Zero(){if (_addrshm){memset(_addrshm, 0, gShmSize);}}
我們可以用memset將這段共享內存清空。
析構函數
~Shm(){if (_who == Creater){int res = shmctl(_shmid, IPC_RMID, nullptr);}std::cout << "shm remove done..." << std::endl;}
這里我們也是讓創建者把共享內存移除。
int res = shmctl(_shmid, IPC_RMID, nullptr);
:這行代碼調用?shmctl
?函數嘗試刪除共享內存。_shmid
?是共享內存的標識符,IPC_RMID
?是刪除共享內存的命令,nullptr
?是指向?shmid_ds
?結構的指針(在這個命令中不需要,因此傳遞?nullptr
)。函數返回的結果存儲在?res
?變量中。客戶端
#include "Shm.hpp" #include "namedPipe.hpp"int main() {//1.先創建共享內存Shm shm(gpathname,gproj_id,User);shm.Zero();char *shmaddr = (char*)shm.Addr();sleep(3);//2.打開管道NamePiped fifo(comm_path,User);fifo.OpenForWrite();char ch = 'A';while(ch <= 'Z'){shmaddr[ch-'A'] = ch;ch++;std::string temp = "weakup";std::cout<< "add" <<ch<<" into shm, "<<"weakup reader"<<std::endl;fifo.WriteNamedPipe(temp);sleep(2);}return 0; }
我們創建共享內存并清空原先的數據,獲取一下共享內存地址,然后我們使用之前的管道,管道在這里的作用就是同步,因為我們的共享內存是直接內存訪問,這意味著我們的客戶端一發送服務端就能看到。所以我們需要借助管道的特性達到同步的效果,管道在這里發送什么信息并不重要。
服務端
#include "Shm.hpp" #include "namedPipe.hpp"int main() {//1.先創建共享內存Shm shm(gpathname,gproj_id,Creater);char *shmaddr = (char*)shm.Addr();//2.創建管道NamePiped fifo(comm_path,Creater);fifo.OpenForRead();while(true){std::string temp;fifo.ReadNamedPipe(&temp);std::cout<<"shm memory content: " << shmaddr << std::endl;sleep(1);}sleep(5);return 0; }
服務端的邏輯也是一樣的,不同的是服務端是只讀。
代碼效果:
命名管道與共享內存的區別
命名管道與共享內存是兩種常見的進程間通信(IPC)機制,它們各自具有獨特的特點和適用場景。以下是兩者的主要區別:
一、定義與工作原理
命名管道(Named Pipe):
- 也稱為FIFO(First In First Out)管道。
- 它允許無親緣關系的進程間進行通信,通過一個在文件系統中存在的名字來標識,進程可以通過這個名字來訪問和通信。
- 數據流動是單向的,即數據只能從一個進程流向另一個進程。如果需要雙向通信,通常需要創建兩個管道。
- 命名管道常用于跨進程通信,并且允許數據以流的方式從一個進程傳輸到另一個進程。
共享內存(SharedMemory):
- 它是映射一段能被其他進程所訪問的內存,這段共享內存由一個進程創建,但多個進程都可以訪問。
- 它是針對其他進程間通信方式運行效率低而專門設計的,允許兩個或多個進程共享一個給定的存儲區。
- 一個進程寫入共享內存的信息,可以被其他使用這個共享內存的進程通過一個簡單的內存讀取讀出,從而實現了進程間的通信。
二、性能與效率
命名管道:
- 數據傳輸通過內核緩沖區進行,存在一定的開銷。
- 相比其他IPC機制(如信號、消息隊列等),命名管道的性能處于中等水平。
- 它支持阻塞和非阻塞操作,提供了靈活的通信方式。
共享內存:
- 是最快的IPC方式之一,因為它允許進程直接訪問共享的內存區域,減少了數據的拷貝次數。
- 相比命名管道等機制,共享內存具有更高的數據傳輸效率和更低的延遲。
- 但是,共享內存需要額外的同步機制(如信號量)來避免數據競爭和不一致性問題。
三、使用場景與限制
命名管道:
- 適用于需要在不同進程間傳遞流式數據的場景。
- 由于其半雙工特性和需要創建兩個管道以實現雙向通信的限制,命名管道在某些復雜通信場景中可能不夠靈活。
共享內存:
- 適用于需要高效數據傳輸和大量數據共享的場景。
- 由于允許多個進程直接訪問同一塊內存區域,因此需要注意同步和互斥問題,以避免數據競爭和訪問沖突。
四、實現與操作
命名管道:
- 在類Unix系統中,可以通過mkfifo函數創建命名管道,并使用open、read、write等系統調用進行讀寫操作。
- 在Windows系統中,命名管道通常通過特定的API進行創建和管理。
共享內存:
- 在類Unix系統中,可以使用shmget、shmat、shmdt、shmctl等系統調用進行共享內存的創建、掛接、去關聯和控制操作。
- 在Windows系統中,也有相應的API用于共享內存的創建和管理。
綜上所述,命名管道和共享內存是兩種各具特色的進程間通信機制。命名管道適用于需要在不同進程間傳遞流式數據的場景,而共享內存則適用于需要高效數據傳輸和大量數據共享的場景。在選擇使用哪種IPC機制時,需要根據具體的應用需求和場景進行權衡和選擇。