文章目錄
- 💐專欄導讀
- 💐文章導讀
- 🐧共享內存原理
- 🐧共享內存相關函數
- 🐦key 與 shmid 區別
- 🐧代碼實例
💐專欄導讀
🌸作者簡介:花想云 ,在讀本科生一枚,C/C++領域新星創作者,新星計劃導師,阿里云專家博主,CSDN內容合伙人…致力于 C/C++、Linux 學習。
🌸專欄簡介:本文收錄于 Linux從入門到精通,本專欄主要內容為本專欄主要內容為Linux的系統性學習,專為小白打造的文章專欄。
🌸相關專欄推薦:C語言初階系列、C語言進階系列 、C++系列、數據結構與算法。
💐文章導讀
共享內存是一種進程間通信的機制,允許多個進程訪問同一塊物理內存
,以實現數據的共享。通過共享內存,進程可以直接讀寫共享的內存區域,而無需通過中間的數據傳輸機制(例如管道或消息隊列)進行通信,因此共享內存是最快
的IPC形式。
共享內存示意圖
🐧共享內存原理
-
創建共享內存: 在一個進程中調用系統調用(例如
shmget
),請求創建一塊共享內存。這個調用需要指定內存的大小以及一些標志,以控制共享內存的權限和行為。 -
關聯共享內存: 其他進程通過調用系統調用(例如
shmat
)將共享內存附加到它們的地址空間中。這個調用返回指向共享內存區域的指針,使得進程可以直接讀寫這塊內存。 -
讀寫共享內存: 一旦多個進程都關聯了同一塊共享內存,它們就可以直接對這塊內存進行讀寫操作,就像操作普通的內存一樣。因為它們共享同一塊物理內存,一個進程對共享內存的修改會立即反映到其他進程的視圖中。
-
分離共享內存: 當進程不再需要訪問共享內存時,它可以調用系統調用(例如
shmdt
)將共享內存從它的地址空間中分離。這并不會導致共享內存的刪除,只是使得該進程無法再訪問這塊內存。 -
刪除共享內存: 當不再需要使用共享內存時,一個進程可以調用系統調用(例如
shmctl
)請求刪除共享內存。這會導致釋放共享內存所占用的系統資源。
特點和注意事項:
- 共享內存提供了高效的進程間通信方式,因為數據直接存儲在物理內存中,無需復制或轉移。
- 進程需要謹慎地協調對共享內存的訪問,以避免數據一致性問題。例如,可以使用互斥鎖等同步機制。
- 共享內存的使用需要確保不同進程使用相同的數據結構和協議,以便正確地進行數據交換和共享。
- 在使用共享內存時,應注意防范競態條件和死鎖等并發編程的問題。
🐧共享內存相關函數
-
創建共享內存區域: 使用
shmget
函數創建一個共享內存區域。該函數的原型為:#include <sys/ipc.h> #include <sys/shm.h>int shmget(key_t key, size_t size, int shmflg);
key
是一個用于標識共享內存的鍵值。size
是要分配的共享內存的大小(字節數)。shmflg
是一組標志,通常使用IPC_CREAT
表示如果內存不存在則創建。
-
連接到共享內存區域: 使用
shmat
函數將進程連接到已經存在的共享內存區域。該函數的原型為:#include <sys/types.h> #include <sys/shm.h>void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid
是共享內存區域的標識符,由shmget
返回。shmaddr
通常設置為NULL
,讓系統自動選擇合適的地址。shmflg
可以為 0。
-
使用共享內存: 一旦連接到共享內存,進程就可以直接在這塊內存中讀寫數據。
-
分離共享內存: 使用
shmdt
函數將進程與共享內存脫離。該函數的原型為:#include <sys/types.h> #include <sys/shm.h>int shmdt(const void *shmaddr);
shmaddr
是連接到共享內存區域的地址。
-
刪除共享內存區域: 使用
shmctl
函數可以刪除或控制共享內存區域的屬性。如果不再需要共享內存,可以使用shmctl
函數的IPC_RMID
命令刪除它。函數原型為:#include <sys/ipc.h> #include <sys/shm.h>int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid
是共享內存區域的標識符。cmd
為控制命令,可以使用IPC_RMID
表示刪除共享內存。
這些函數提供了對共享內存的創建、連接、使用和刪除的基本操作。
🐦key 與 shmid 區別
在上面所示的接口中,shmget 函數需要一個 key_t 類型的參數 key。而其他的函數多數用到 shmid 而不會用到 key,那么這兩個參數分別是什么,有什么區別呢?
-
key
(鍵值):key
是一個整數,用于在共享內存創建過程中唯一標識一個共享內存段。- 它并不是由系統自動生成的,而是由應用程序提供的,通常以某種方式與程序的邏輯相關。
- 可以使用
ftok
函數將路徑名和一個整數標識符轉換為key
,以便在創建共享內存時使用。 key
通常用于在不同的進程之間共享相同的內存塊,因此它是創建共享內存的關鍵參數之一。
-
shmid
(共享內存標識符):shmid
是一個由系統生成的標識符,用于標識已經創建的共享內存段。- 在使用
shmget
函數創建共享內存時,它通過返回值返回給調用者,用于后續的操作。 shmid
是由系統內核分配的,通常是一個唯一的整數。- 通過
shmat
函數將進程連接到共享內存時,需要使用shmid
作為參數。
總的來說,key
是在共享內存創建時由應用程序指定的用戶定義的標識符,而 shmid
是由系統內核在共享內存創建時自動生成的系統級標識符。key
用于唯一標識共享內存的名字,而 shmid
用于在程序運行時標識特定的共享內存實例。
shm可以用于多個進程之間通信,在同一時刻,可能有多個共享內存被用來進行通信。所以系統中一定會有很多個共享內存同時存在,那么系統就會采取一定措施來管理這些共享內存。所以共享內存并不是單單的一塊內存空間,系統會為它構建一個結構體對象來描述它。
- 所以,共享內存 = 內核數據結構 + 真正開辟的內存空間;
共享內存數據結構
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 */
};
兩個進程使用共享內存進行通信的前提是,如何讓兩個進程使用同一塊共享內存。內存中有許許多多的共享內存,我們如何讓兩個進程使用一個共享內存呢?
這就要提到另一個函數 ftok
了。
ftok
函數是一個用于生成System V IPC(Inter-Process Communication,進程間通信)的鍵值的函數。它的主要用途是在創建System V IPC對象(如消息隊列、信號量、共享內存)時,為這些對象生成唯一的鍵值。
函數原型如下:
#include <sys/types.h>
#include <sys/ipc.h>key_t ftok(const char *pathname, int proj_id);
pathname
是一個與文件相關的路徑名,用于生成鍵值。通常是指向一個存在的文件的路徑。proj_id
是一個用戶定義的整數,用于區分不同的IPC對象。在不同的IPC對象中,如果pathname
相同,而proj_id
不同,生成的鍵值也會不同。
ftok
函數通過將 pathname
轉換為一個唯一的鍵值,以確保在不同的進程中使用相同的 pathname
和 proj_id
參數生成的鍵值是一致的。
一般來說,ftok
函數的使用場景是在創建System V IPC對象之前,通過調用 ftok
來生成一個唯一的鍵值。這個鍵值將被傳遞給諸如 msgget
、semget
、shmget
等函數,用于創建具體的消息隊列、信號量或共享內存段。
需要注意的是,ftok
存在一些限制和注意事項,比如需要確保 pathname
指向的文件是存在的,否則 ftok
會返回 -1。此外,由于 proj_id
是一個整數,因此其范圍應在0到255之間,以保證生成的鍵值在合理的范圍內。
所以,key 是在內核中使用的,類比文件的 inode 編號。而 shmid 是給用戶使用的,類比文件的文件描述符 fd。
🐧代碼實例
接下來,我們就通過一個簡單的代碼設計來熟悉共享內存的使用。該設計的內容是,通過共享內存讓兩個進程(Server 與 Client)進行通信。
communicate.hpp
該頭文件內提供shm所用到的函數方法。
/* communicate.hpp */#ifndef __COMM_HPP__
#define __COMM_HPP__#include <iostream>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <cassert>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/shm.h>
#include <unistd.h>using namespace std;#define PATHNAME "." // 文件路徑(用于key生成)
#define PROJID 0x12138 // 項目ID(用于key生成)const int gsize = 4096;key_t getKey() // 獲取鍵值
{key_t key = ftok(PATHNAME, PROJID);if (key == -1){cerr << "error: " << errno << " : " << strerror(errno) << endl;exit(1);}return key;
}static int createShmHelper(key_t key, int size, int flag)
{int shmid = shmget(key, gsize, flag);if (shmid == -1){cerr << "error: " << errno << " : " << strerror(errno) << endl;exit(2);}return shmid;
}int createShm(key_t key, int size) // 創建共享內存
{umask(0);return createShmHelper(key, size, IPC_CREAT | IPC_EXCL | 0666);
}int getShm(key_t key, int size) // 獲取共享內存
{umask(0);return createShmHelper(key, size, IPC_CREAT);
}char *attachShm(int shmid) // 關聯共享內存
{char *start = (char *)shmat(shmid, nullptr, 0); // 將共享內存段連接到進程地址空間return start;
}void detachShm(char *start) // 去關聯
{int n = shmdt(start); // 將共享內存段與當前進程脫離assert(n != -1);(void)n;
}void delShm(int shmid) // 釋放共享內存
{int n = shmctl(shmid, IPC_RMID, nullptr); // 釋放共享內存assert(n != -1);(void)n;
}#define SERVER 1
#define CLIENT 0class Init
{
public:Init(int type): type(type){key_t key = getKey();if (type == SERVER)shmid = createShm(key, gsize);elseshmid = getShm(key, gsize);start = attachShm(shmid);}char *getStart() { return start; }~Init(){detachShm(start);if (type == SERVER)delShm(shmid); // 只有共享內存創建者才負責釋放}private:char *start; // 起始地址int type; // server or clientint shmid;
};#endif
Server
/* Server.cc */
#include "communicate.hpp"
#include <unistd.h>using namespace std;int main()
{ // 建立連接Init init(SERVER);char *start = init.getStart();// 開始通信int n = 0;while (n <= 30){cout << "client -> server# " << start << endl;sleep(1);n++;}return 0;
}
Client
/* Client.cc */
#include "communicate.hpp"
using namespace std;int main()
{// 建立連接Init init(CLIENT);char *start = init.getStart();// 開始通信char c = 'A';while (c <= 'Z'){start[c - 'A'] = c;c++;start[c - 'A'] = 0;sleep(1);}return 0;
}
效果展示
注意
當我們運行完一次程序后,再次運行程序會發生錯誤:
$ ./server
error: 17 : File exists
原因是,上次程序運行時創建的共享內存仍然存在,可以使用 ipcs -m
指令來查看已經有的共享內存:
$ ipcs -m------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x3801203f 0 hxy 666 4096 0
由此可見,共享內存的生命周期是隨系統的,不隨進程。
我們可以使用 ipcrm -m
指令刪除指定的共享內存:
$ ipcrm -m shmid
本章的內容到這里就結束了!如果覺得對你有所幫助的話,歡迎三連~