前文中我們介紹了管道——匿名管道和命名管道來實現進程間通信,在介紹怎么進行通信時,我們有提到過不止管道的方式進行通信,還有System V IPC,今天這篇文章我們就來學習一下System V IPC中的共享內存
1. 為何引入共享內存?——管道通信的局限性
管道(匿名/命名管道)作為傳統IPC機制存在顯著缺陷:
- 數據拷貝開銷大:管道需通過內核緩沖區中轉,數據需從用戶空間→內核→用戶空間兩次拷貝
- 單向通信限制:匿名管道僅支持單向數據流,雙向通信需建立兩個管道
- 效率瓶頸:頻繁讀寫時內核緩沖區切換成為性能瓶頸
- 適用場景有限:命名管道雖突破親緣關系限制,但仍依賴文件系統路徑,且同步機制弱
??共享內存的破局:
通過多進程直接訪問同一物理內存區域,消除數據拷貝,實現零復制(Zero-Copy)通信,速度提升10-100倍
2. 共享內存核心概念:打破進程隔離的革命性設計
共享內存是 System V IPC(Inter-Process Communication)機制的一種,它允許多個不相關的進程(父子進程或完全獨立的進程)訪問同一塊物理內存區域。這是最快的進程間通信(IPC)形式,因為它完全避免了內核空間和用戶空間之間數據的復制。
本質定義
物理內存共享:多個進程通過頁表映射,直接訪問同一塊物理內存區域,實現零拷貝數據交換。
- 底層實現:操作系統內核維護共享內存區域,各進程通過修改自身的頁表項(Page Table Entry),將虛擬地址映射到相同的物理頁幀(Page Frame)上
- 典型場景:適用于大數據量進程間通信,如視頻處理管道中,解碼進程直接將幀數據寫入共享內存,渲染進程立即讀取
- 對比傳統IPC:相比管道/消息隊列需要2次數據拷貝(用戶態→內核態→用戶態),共享內存僅需1次虛擬地址映射
邏輯視圖:在進程虛擬地址空間中表現為普通內存段(如malloc分配),實則由操作系統管理共享物理頁。
- 地址空間布局:通常位于堆與棧之間的內存映射區域(mmap區域)
- API抽象:通過
shmget
創建、shmat
附加后,進程可通過指針直接讀寫,如:
int *shared_counter = (int*)shmat(shm_id, NULL, 0);
*shared_counter += 1; // 修改對其他進程立即可見
核心特性
特性 | 技術內涵 | |
---|---|---|
高效性 | 消除內核中轉與數據拷貝,吞吐量達管道通信的?5-20倍(GB/s級) | |
雙向性 | 支持多進程并發讀寫(需同步機制保障) | |
非親緣性 | 任意進程(無關父子關系)可通過唯一標識符(Key)訪問 | |
持久性 | 生命周期獨立于進程,需顯式銷毀(否則殘留內核直至重啟) | |
無內置同步 | 需開發者結合信號量/互斥鎖解決競態條件(如寫覆蓋、臟讀) |
與進程地址空間的融合
// 進程視角:共享內存如同本地變量
char *shm_ptr = shmat(shm_id, NULL, 0); // 映射共享內存到虛擬地址空間
strcpy(shm_ptr, "Hello from Process A"); // 直接寫入
關鍵理解:
- 進程通過
shmat
將物理共享頁插入自身頁表(虛擬→物理映射)。- 修改操作直接作用于物理內存,其他進程立即可見。
3. 共享內存工作原理
那操作系統是怎么管理共享內存的呢?先描述再組織
通過一個內核結構體來描述共享內存,再由操作系統統一管理這些內核結構體
共享內存數據結構:
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 */
};
一、管理機制:描述與組織的雙重架構
1.?描述層:內核數據結構定義
每個共享內存段由兩個關鍵結構體描述:
struct shmid_ds
(用戶可見元信息)
用戶提供的結構體包含基礎屬性,但內核實際使用擴展結構體:struct shmid_ds {struct ipc_perm shm_perm; // 權限控制(UID/GID/模式)size_t shm_segsz; // 段大小(字節)time_t shm_atime; // 最后一次映射時間time_t shm_dtime; // 最后一次解除映射時間time_t shm_ctime; // 最后一次修改時間pid_t shm_cpid; // 創建者PIDpid_t shm_lpid; // 最后一次操作者PIDunsigned short shm_nattch; // 當前映射進程數// ... 兼容性保留字段 };
struct shmid_kernel
(內核私有管理結構)struct shmid_kernel {struct kern_ipc_perm shm_perm; // IPC權限控制塊struct file *shm_file; // 關聯的shm文件對象unsigned long shm_nattch; // 映射計數size_t shm_segsz; // 段大小struct pid *shm_cprid; // 創建者PID(內核態)struct pid *shm_lprid; // 最后操作者PID// ... 其他內核級字段 };
關鍵擴展:
shm_file
:指向虛擬文件系統shm
中的文件對象,實現物理內存與文件系統的關聯。kern_ipc_perm
:嵌入的IPC權限控制塊,包含鍵值(key)、所有者UID等。
2.?組織層:全局管理架構
內核通過三級結構統一管理所有共享內存段:
層級 | 數據結構 | 功能 |
---|---|---|
全局入口 | struct ipc_ids shm_ids | 維護系統內所有共享內存的ID空間 |
ID索引層 | struct kern_ipc_perm*[] | 指針數組,每個元素指向一個shmid_kernel |
共享內存實例 | struct shmid_kernel | 描述單個共享內存段的完整狀態 |
動態管理演進:
- 早期內核:靜態數組管理(固定數量上限,)。
- 現代內核:?動態紅黑樹(Red-Black Tree)?,支持O(log N)復雜度的查找/插入/刪除。
二、內核操作流程剖析
1.?創建共享內存(shmget
)
int shmget(key_t key, size_t size, int shmflg) {// 1. 根據key查找或新建shmid_kernel// 2. 在shm文件系統中創建匿名文件struct file *file = shmem_file_setup("SYSV<key>", size, flags);// 3. 初始化shmid_kernel:綁定file,設置size/權限等// 4. 將shmid_kernel插入全局紅黑樹
}
關鍵動作:
- 通過
shmem_file_setup
在tmpfs中創建虛擬文件。 - 文件操作函數集指向
shmem_vm_ops
,實現物理頁幀分配。
2.?映射共享內存(shmat
)
void *shmat(int shmid, void *addr, int flag) {// 1. 根據shmid找到shmid_kernelstruct shmid_kernel *shp = find_shm(shmid);// 2. 在進程地址空間創建VMA區域vma = vm_area_alloc(current->mm);vma->vm_file = shp->shm_file; // 關聯shm文件vma->vm_ops = &shmem_vm_ops; // 設置內存操作函數// 3. 更新shm_nattch引用計數shp->shm_nattch++;
}
虛擬內存映射:
- 進程的
vm_area_struct
映射到shm_file
的物理頁。 - 頁表項(PTE)指向共享物理幀,實現零拷貝訪問。
3.?生命周期管理
操作 | 內核行為 |
---|---|
刪除(shmctl(IPC_RMID) ) | 標記為SHM_DEST ,當shm_nattch=0 時觸發物理內存回收 |
進程退出 | 自動調用shmdt 解除映射,遞減shm_nattch |
系統重啟 | 所有共享內存被銷毀(因物理內存重置) |
三、物理內存與虛擬地址的協同管理
1.?物理內存分配
- 首次訪問觸發缺頁異常:
進程讀寫映射的虛擬地址 → 缺頁中斷 → 內核調用shmem_fault
分配物理頁幀。 - 頁幀來源:內核伙伴系統(Buddy System)分配連續物理頁。
2.?多進程共享的一致性
機制 | 原理 |
---|---|
寫時復制(COW) | 若進程嘗試寫入只讀映射的共享內存,觸發COW生成私有副本 |
內存屏障 | 使用mb()/rmb() 指令保證多核CPU緩存一致性 |
原子操作 | 引用計數(如shm_nattch )通過原子指令增減 |
四、與傳統文件映射的差異
特性 | 共享內存 | 文件映射(mmap) |
---|---|---|
數據持久性 | 進程退出后數據消失 | 文件內容持久化到磁盤 |
同步機制 | 需手動同步(如msync) | 內核自動回寫臟頁 |
初始化成本 | 無磁盤I/O | 需加載文件數據到內存 |
適用場景 | 高頻臨時數據交換 | 持久化數據共享 |
五、設計哲學總結
- 抽象與隔離:
- 通過
shmid_ds
向用戶暴露可控接口,隱藏shmid_kernel
等內核細節。
- 通過
- 零拷貝思想:
- 虛擬地址直接映射物理幀,消除數據復制。
- 動態擴展性:
- 紅黑樹管理替代靜態數組,支持海量共享內存段。
- 資源自治:
- 引用計數(
shm_nattch
)實現自銷毀機制,避免資源泄漏。
- 引用計數(
共享內存的工作原理 (關鍵步驟)
創建或獲取共享內存段 (
shmget
):一個進程(通常是第一個需要該共享內存的進程)調用?
shmget(key_t key, size_t size, int shmflg)
。key
: 一個唯一標識共享內存段的鍵值。可以使用?ftok()
?基于路徑名生成,或者指定為?IPC_PRIVATE
(創建僅供親緣進程使用的新段)。size
: 請求的共享內存段的大小(字節)。如果是獲取已存在的段,此參數通常為 0。shmflg
: 標志位,指定創建選項(IPC_CREAT
,?IPC_EXCL
)和權限(如?0666
)。成功時返回共享內存標識符?
shmid
(一個非負整數),用于后續操作。內核在內存中分配一塊指定大小的物理內存區域。
將共享內存段附加到進程地址空間 (
shmat
):任何需要使用該共享內存的進程調用?
shmat(int shmid, const void *shmaddr, int shmflg)
。shmid
: 由?shmget
?返回的標識符。shmaddr
: 通常設為?NULL
,讓內核選擇附加地址。也可以指定一個地址(但需謹慎,通常不推薦)。shmflg
: 標志位(如?SHM_RDONLY
?表示只讀附加)。成功時返回一個指向共享內存段在當前進程地址空間中起始位置的?
void*
?指針。進程現在可以通過這個指針像訪問普通內存一樣讀寫共享內存區域。
使用共享內存:
多個進程通過它們各自?
shmat
?返回的指針(指向同一物理內存的不同虛擬地址)直接讀寫共享內存區域。關鍵點:共享內存本身不提供任何同步機制!?多個進程同時讀寫同一區域會導致數據競爭(Race Condition)?和數據不一致。必須結合其他同步機制使用:
System V 信號量 (
semget
,?semop
,?semctl
)POSIX 信號量 (
sem_init
,?sem_wait
,?sem_post
)互斥鎖 (
pthread_mutex_t
) 和條件變量 (pthread_cond_t
)(需要放在共享內存中并初始化為進程間共享屬性?PTHREAD_PROCESS_SHARED
)。文件鎖 (
fcntl
)
分離共享內存段 (
shmdt
):當進程不再需要訪問共享內存時,調用?
shmdt(const void *shmaddr)
。shmaddr
: 之前?shmat
?返回的指針。該調用將共享內存段從當前進程的地址空間中分離出去。進程不能再通過該指針訪問共享內存。注意:分離操作并不會銷毀共享內存段本身。
控制/銷毀共享內存段 (
shmctl
):使用?
shmctl(int shmid, int cmd, struct shmid_ds *buf)
?進行控制操作。最重要的?
cmd
?是?IPC_RMID
:標記共享內存段為待銷毀。當最后一個使用該段的進程分離 (
shmdt
) 之后,內核才會真正銷毀該段并回收資源。即使所有進程都已分離,但只要沒有調用?
IPC_RMID
,段依然存在(可能造成資源泄漏)。
其他?
cmd
?包括獲取/設置段信息 (IPC_STAT
,?IPC_SET
)。
4. 共享內存函數
4.1 shmget
函數核心解析(系統級共享內存管理)
shmget
是System V IPC中創建或獲取共享內存段的核心函數,其本質是向內核申請一塊多進程可共同訪問的物理內存區域。
1.?函數原型與基礎機制
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
- 返回值:成功返回共享內存標識符(非負整數),失敗返回-1并設置
errno
- 內核行為:
- 根據
key
查找或創建共享內存段 - 分配物理內存并初始化元數據結構
shmid_ds
- 返回邏輯標識符(非物理地址)
- 根據
2. 參數解析
參數 | 技術內涵 | 內核行為 |
---|---|---|
key | 唯一標識符: ?? IPC_PRIVATE :強制創建新段?? ftok() :基于文件路徑+項目ID生成 | 紅黑樹檢索?key ,存在則返回?shmid ;不存在且?IPC_CREAT ?置位則創建新段 |
size | 內存段大小(字節): ? 新創建時需 >0 ? 自動對齊頁大小(4KB) | 調用?shmem_file_setup() ?在 tmpfs 創建匿名文件,映射物理頁 |
shmflg | 位掩碼標志: ??權限位:低9位(如? 0666 )?? IPC_CREAT :不存在則創建?? IPC_EXCL :存在則報錯 | 初始化?shmid_ds.shm_perm ?結構,設置 UID/GID 和權限 |
高級標志:
SHM_HUGETLB
:使用2MB/1GB大頁減少TLB MissSHM_NORESERVE
:不預留Swap空間(Linux特有)
3.?內核數據結構初始化
創建新段時,內核初始化struct shmid_ds
元數據結構:
struct shmid_ds {struct ipc_perm shm_perm; // 權限控制塊size_t shm_segsz; // 段大小(=size參數)time_t shm_atime; // 最后一次attach時間time_t shm_dtime; // 最后一次detach時間time_t shm_ctime; // 最后一次修改時間pid_t shm_cpid; // 創建者PIDpid_t shm_lpid; // 最后操作者PIDunsigned short shm_nattch; // 當前附加進程數
};
初始化規則:
shm_perm.cuid/uid
?= 調用進程有效UIDshm_perm.cgid/gid
?= 調用進程有效GIDshm_perm.mode
?=?shmflg
的低9位權限shm_atime
/shm_dtime
?= 0(未映射)shm_ctime
?= 當前系統時間
💡?物理內存分配:內核調用
alloc_pages()
分配連續物理頁,內容初始化為0
4. 錯誤處理
錯誤碼 | 觸發條件 | 解決方案 |
---|---|---|
EACCES | 權限不足 | 檢查?shmflg ?權限位 |
EEXIST | IPC_CREAT+IPC_EXCL ?且段已存在 | 移除?IPC_EXCL ?或更換?key |
EINVAL | size ?無效(>?SHMMAX ?或 < 頁大小) | 調整?size ?為頁大小整數倍 |
ENOENT | key ?不存在且未設?IPC_CREAT | 增加?IPC_CREAT ?標志 |
???系統限制:
SHMMAX
:單段最大尺寸(默認32MB-128MB)SHMMNI
:系統最大段數(默認4096)
深入解析?shmget
?的?key
?參數:跨進程共享內存的標識核心
一、key
?參數的核心作用與設計哲學
key
?是?shmget
?函數中唯一標識共享內存段的整數標識符,其本質是操作系統用于區分不同共享內存段的全局鍵值。它的作用類似于文件系統中的路徑名,但以整數形式存在,核心價值在于:
- 跨進程標識:不同進程通過相同?
key
?訪問同一物理內存區域。 - 資源復用:避免重復創建相同內存段,減少資源浪費。
- 權限控制:與?
shmflg
?權限位協同管理進程訪問權限。
設計哲學:
key
?體現了操作系統對共享資源的?“命名空間抽象”?—— 用輕量級整數替代復雜路徑,實現高效資源定位。
二、key
?的生成方式與典型場景(附代碼示例)
1.?ftok()
?動態生成(推薦方案)
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
- 機制:基于文件路徑(
pathname
)和項目ID(proj_id
)生成唯一?key
。 - 原理:
取文件索引節點號(st_ino
)的低8位 + 設備號(st_dev
)的低8位 +?proj_id
?的低8位,組合成32位整數。 - 示例:
// 服務端創建共享內存 key_t config_key = ftok("/etc/app_config", 123); // 基于配置文件生成key int shmid = shmget(config_key, 4096, IPC_CREAT | 0666);// 客戶端訪問同一內存 key_t client_key = ftok("/etc/app_config", 123); // 相同參數生成相同key int client_shmid = shmget(client_key, 0, 0); // size=0表示獲取已有段
2.?硬編碼常量(簡單場景)
#define APP_SHM_KEY 0x1234 // 預定義全局常量// 進程A
int shmid_A = shmget(APP_SHM_KEY, 1024, IPC_CREAT | 0600);// 進程B
int shmid_B = shmget(APP_SHM_KEY, 0, 0); // 通過相同key訪問
風險:可能與其他應用沖突(需確保全局唯一性)。
3.?特殊值?IPC_PRIVATE
(私有段)
int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0600);
- 行為:強制創建新共享內存段,僅限親緣進程使用(如?
fork()
?的子進程)。 - 典型場景:父進程創建臨時共享區后?
fork()
,子進程通過繼承?shmid
?訪問(無需?key
)。
總結key
作用與本質
- 唯一標識:
key
是共享內存在系統中的全局唯一編號(類型為key_t
,本質是unsigned int
),用于區分不同共享內存段?。 - 進程間同步:不同進程通過相同
key
訪問同一內存段,實現通信?。
- 唯一標識:
生成方式
ftok()
函數:常用方法,基于文件路徑和項目ID生成唯一key。IPC_PRIVATE
:指定此值時,系統自動分配新key(用于父子進程間通信)。
使用場景
- 創建新內存段:當
key
不與現有段關聯,且指定IPC_CREAT
標志時,系統創建新共享內存?。 - 訪問現有段:若
key
已存在,則返回其標識符(shmid
),此時size
參數應為0?。
- 創建新內存段:當
權限與控制
- 權限位:
shmflg
的低9位定義權限(如0666
表示所有用戶可讀寫)。 - 控制標志:
IPC_CREAT
:若內存段不存在則創建?。IPC_EXCL
:與IPC_CREAT
聯用,若段已存在則返回錯誤?。
- 權限位:
錯誤處理
- 常見錯誤:
EACCES
:權限不足?。ENOENT
:key
不存在且未指定IPC_CREAT
?。ENOMEM
:內存不足或超出系統限制(如Linux默認單段最大32MB)。
- 常見錯誤:
關鍵注意點
- 唯一性沖突:若不同應用誤用相同
key
,會導致非預期通信。建議通過ftok
選擇唯一文件路徑?。 - 大小對齊:
size
會被對齊到系統頁大小(如4KB)的整數倍?。 - 特殊值
IPC_PRIVATE
:僅適用于進程組內通信(如fork()
后的父子進程)。
4.2?shmat
?函數:連接共享內存到進程地址空間
功能:將共享內存映射到進程的虛擬地址空間,使進程可訪問共享數據。
原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
參數:
shmid
:由?shmget
?返回的標識符。shmaddr
:指定連接地址:NULL
:系統自動選擇合適地址(推薦)。- 非?
NULL
:若未設置?SHM_RND
,則直接使用該地址;若設置?SHM_RND
,則地址自動向下對齊到?SHMLBA
(通常為頁大小)的整數倍。
shmflg
:模式標志:0
:讀寫模式。SHM_RDONLY
:只讀模式。
返回值:
- 成功:返回共享內存首地址指針。
- 失敗:返回?
(void*)-1
?并設置?errno
。
4.3?shmdt
?函數:斷開共享內存連接
功能:將共享內存段從當前進程的地址空間分離(解除映射),但不會刪除共享內存。
原型:
int shmdt(const void *shmaddr);
參數:
shmaddr
:由?shmat
?返回的地址指針。
返回值:
- 成功:返回?
0
。 - 失敗:返回?
-1
?并設置?errno
。
- 成功:返回?
底層機制:通過?
do_munmap()
?釋放對應的虛擬內存區間。
4.4?shmctl
?函數:控制共享內存
功能:管理共享內存段,包括刪除、狀態查詢或權限修改。
原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
參數:
shmid
:共享內存標識符。cmd
:控制命令:IPC_RMID
:標記刪除共享內存。當所有進程均斷開連接(shmdt
)后,內存才會被實際釋放。IPC_STAT
:獲取共享內存狀態(保存到?buf
?指向的?shmid_ds
?結構體)。IPC_SET
:修改共享內存權限(需權限)。SHM_LOCK
/SHM_UNLOCK
:鎖定內存禁止換頁(僅限特權進程)。
buf
:指向?shmid_ds
?結構體的指針(用于輸入/輸出數據)。
返回值:
- 成功:返回?
0
。 - 失敗:返回?
-1
?并設置?errno
.
- 成功:返回?
4.5 拓展——命令行中如何實現上面系統調用函數相同的效果
1.?ipcs
?命令:查看共享內存信息(對應函數狀態監控)
功能:
查看系統中所有共享內存段的狀態(包括shmget
創建的共享內存),相當于通過shmctl(shmid, IPC_STAT, buf)
獲取信息?。常用參數:
ipcs -m # 僅顯示共享內存段信息
輸出字段:
SHMID
:共享內存標識符(由shmget
返回的shmid
)KEY
:創建時指定的鍵值(如ftok
生成或IPC_PRIVATE
)OWNER
:創建者用戶BYTES
:內存大小(與shmget
的size
參數一致)NATTCH
:當前掛載進程數(即通過shmat
連接的進程數)
高級用法:
ipcs -m -i <SHMID> # 查看指定SHMID的詳細信息 ipcs -m -u # 匯總共享內存使用統計
2.?ipcrm
?命令:刪除共享內存(對應?shmctl(shmid, IPC_RMID, NULL)
)
功能:
刪除指定的共享內存段,效果等同于調用shmctl
的IPC_RMID
命令(標記刪除,當所有進程調用shmdt
后實際釋放內存)。語法:
ipcrm -m <SHMID> # 刪除指定SHMID的共享內存
批量刪除(根據用戶或鍵值):
# 刪除用戶alice創建的所有共享內存 ipcs -m | awk '/alice/{print $2}' | xargs -n1 ipcrm -m# 刪除鍵值為0x12345的共享內存 ipcs -m | awk '/0x12345/{system("ipcrm -m "$2)}'
?? 需注意:刪除時若仍有進程掛載(
NATTCH > 0
),內存不會立即釋放,需等待所有進程調用shmdt
?。
3.?pmap
?命令:查看進程掛載的共享內存(對應?shmat
?映射)
功能:
顯示進程虛擬地址空間中掛載的共享內存區域,相當于查看shmat
返回的映射地址?。語法:
pmap -x <PID> # 查看指定進程的內存映射
輸出示例:
Address Kbytes RSS Mode Mapping 7f2a1a000000 1024 rw-s /SYSV00000000 # 共享內存標識(KEY為0x00000000)
rw-s
中的s
表示共享內存段/SYSV
后跟16進制鍵值(如00000000
對應IPC_PRIVATE
)
4. 掛載/卸載共享內存的替代方法
- 掛載(模擬?
shmat
):
命令行無法直接掛載共享內存到進程空間,但可通過調試器臨時操作:gdb -p <PID> -ex "call shmat(<SHMID>, NULL, 0)" --batch
此操作需進程主動配合,僅用于調試?。
- 卸載(模擬?
shmdt
):
同樣需在進程內部觸發,無直接命令替代。可通過終止進程自動卸載(進程退出時會自動調用shmdt
)。
對比總結:函數與命令行操作對應關系
函數功能 | 命令行工具 | 關鍵參數/操作 | 限制說明 |
---|---|---|---|
創建共享內存 (shmget ) | 無直接替代 | – | 需編程實現 |
查看共享內存狀態 (IPC_STAT ) | ipcs -m | -i <SHMID> ?查看詳情 | 信息只讀,不可修改 |
刪除共享內存 (IPC_RMID ) | ipcrm -m <SHMID> | 需指定SHMID | 需root或所有者權限 |
查看進程映射 (shmat 地址) | pmap -x <PID> | 過濾/SYSV 字段 | 僅顯示地址,無法主動掛載 |
卸載共享內存 (shmdt ) | 終止進程 | kill <PID> | 進程退出時自動卸載 |
4.6?shmid和key的區別
1. 核心定義與功能層級
概念 | 功能描述 | 層級歸屬 | 類比關系 |
---|---|---|---|
key | 由?ftok() ?生成或用戶指定的整數值,用于在系統層面唯一標識共享內存段,內核通過?key ?區分不同共享內存 | 內核層標識符 | 類似文件的?inode 號 (唯一標識文件) |
shmid | 由?shmget() ?系統調用返回的整數值,作為用戶層操作共享內存的句柄,用于后續關聯、去關聯或控制操作 | 用戶層標識符 | 類似文件的?文件描述符 fd (用戶操作接口) |
📌 關鍵引用:
- "
key
?是內核用來區分共享內存唯一性的字段,用戶不能直接用?key
?管理共享內存;shmid
?是內核返回的標識符,用于用戶級管理"?。- "
key
?和?shmid
?的關系如同?inode
?和?fd
:inode
?標識文件唯一性,fd
?是用戶操作接口"?。
2. 生成與使用場景對比
(1)?key
?的生成與作用
- 生成方式:
- 通過?
ftok(pathname, proj_id)
?生成(如?key_t key = ftok(".", 'a');
)。 - 或直接指定整數(如?
key = 1234
),需確保系統內唯一性。
- 通過?
- 核心作用:
- 在?
shmget()
?中作為參數,供內核查找或創建共享內存段?。 - 不同進程通過相同?
key
?訪問同一共享內存(實現進程間通信)。
- 在?
(2)?shmid
?的生成與作用
- 生成方式:
- 由?
shmget(key, size, flags)
?返回(如?int shmid = shmget(key, 4096, IPC_CREAT|0666);
)。
- 由?
- 核心作用:
- 作為用戶層操作的入口參數,用于:
- 關聯內存:
shmat(shmid, NULL, 0)
?將共享內存映射到進程地址空間?。 - 去關聯:
shmdt(shmaddr)
?解除映射?。 - 控制操作:
shmctl(shmid, IPC_RMID, NULL)
?刪除共享內存?。 - 命令行工具操作:
ipcrm -m shmid
?刪除共享內存?。
- 關聯內存:
- 作為用戶層操作的入口參數,用于:
???注意:
- 用戶無法直接用?
key
?操作共享內存(如執行?shmat(key, ...)
?會報錯)。- 所有用戶層 API 均依賴?
shmid
?而非?key
?。
3. 設計目的與架構思想
維度 | key | shmid |
---|---|---|
唯一性范圍 | 全局唯一(整個操作系統內) | 進程內有效(不同進程的?shmid ?可能不同) |
生命周期 | 持久存在,直至共享內存被刪除 | 隨進程結束失效,但共享內存仍存留 |
安全隔離 | 內核維護,用戶不可直接操作 | 用戶直接使用,但無系統級權限 |
設計目標 | 解耦:內核通過?key ?管理資源唯一性 | 封裝:為用戶提供安全操作接口 |
📜 架構意義:
- "內核通過?
key
?保證共享內存的全局唯一性,用戶通過?shmid
?操作資源,實現內核與用戶層的解耦"?。- 類似設計廣泛見于系統資源管理(如信號量、消息隊列)。
key
?解決“資源是誰”的問題(系統唯一標識),shmid
?解決“如何操作”的問題(用戶接口)。
5. 代碼示例
和管道一樣,我們也來段代碼加深對共享內存通信的理解。
這里我們和命名管道一樣,實現讓兩個毫無關系的進程通信,所以我們將一個進程看作服務端,另一個進程看作客戶端,然后進行封裝。
我們先來介紹一段宏
宏定義結構
#define ERR_EXIT(m) \do \{ \perror(m); \exit(EXIT_FAILURE); \} while (0)
do { ... } while (0)
:
這是一種宏定義的慣用技巧,目的是將多條語句封裝為單條邏輯塊。- 作用1:避免宏展開后與上下文的分號沖突(例如在
if-else
語句中使用時)。 - 作用2:確保宏在任何位置(如
if
分支后)都能安全使用,不會因缺少大括號導致邏輯錯誤。
- 作用1:避免宏展開后與上下文的分號沖突(例如在
反斜杠
\
:
用于連接多行代碼,使宏定義可跨行書寫,提高可讀性。
核心功能
perror(m)
:
輸出系統錯誤信息。參數m
是自定義的錯誤提示字符串(如"open error"
),實際輸出格式為:m: 具體錯誤原因
。
例如:perror("open error")
?可能輸出?open error: No such file or directory
。- 原理:
perror
會讀取全局變量errno
的值,將其轉換為可讀的錯誤描述。
- 原理:
exit(EXIT_FAILURE)
:
立即終止程序,并返回預定義的失敗狀態碼(通常為非0值)。EXIT_FAILURE
:標準宏,表示程序異常退出(值由系統定義,通常為1
)。- 對比
EXIT_SUCCESS
:表示程序正常退出(值為0
)。
該宏是C語言中處理系統調用錯誤的通用模式,通過perror
提供清晰的錯誤診斷,并通過exit(EXIT_FAILURE)
確保程序在致命錯誤時立即終止。其設計兼顧了安全性、可讀性和可移植性。
5.1 創建并獲取共享內存
我們讓服務端創建共享內存段,客戶端則獲取共享內存段
但是創建共享內存段之前,得先將共享內存段的唯一標識符key生成,也就需要通過ftok函數來生成key,但是要基于文件路徑(pathname
)和項目ID(proj_id
)生成唯一?key
。所以我們先定義全局變量pathname和proj_id,pathname為當前路徑,proj_id則取66的十六進制,定義全局變量方便我們在服務端和客戶端構造函數時通過傳參來生成唯一key(注意:需要兩者參數相同,才能獲得同一個key,內核才能通過key找到同一個共享內存段)。服務端創建好共享內存段后,客戶端就不再需要創建了,只需要獲取即可,所以我們要實現二者隔離
代碼如下:
#pragma once#include <iostream>
#include <cstdio>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>#define ERR_EXIT(m) \do \{ \perror(m); \exit(EXIT_FAILURE); \} while (0)const int gdefaultid = -1;
const int gsize = 4096;
const std::string pathname = ".";
const int proj_id = 0x66;#define CREATER "creater"
#define USER "user"class Shm
{
private:int _shmid;int _size;key_t _key;std::string _usertype;void CreatShm(int flg){_shmid = shmget(_key, _size, flg);if(_shmid < 0){ERR_EXIT("shmget");}printf("shmid: %d\n", _shmid);}void Creat(){CreatShm(IPC_CREAT | IPC_EXCL | 0666);}void Get(){CreatShm(IPC_CREAT);}public:Shm(const std::string& pathneme, int projid, const std::string& usertype): _shmid(gdefaultid), _size(gsize), _usertype(usertype){_key = ftok(pathname.c_str(), projid);if (_key < 0){ERR_EXIT("ftok");}printf("key: 0x%x\n", _key);if(_usertype == CREATER){// 用戶是服務端則創建共享內存段Creat();}else if(_usertype == USER){// 用戶是客戶端則獲取共享內存段Get();}}~Shm() {}
};
5.2?將共享內存段附加到進程地址空間
創建好共享內存段之后,就需要將進程地址空間和共享內存段建立連接。這里我們讓操作系統來給我們映射到進程的虛擬地址空間(第二個參數為空指針,詳細請看上文shmat函數),同時如果shmat連接失敗會返回 (void*)-1 ,所以我們要把它強制轉換為long long再做判斷(注意我們是64位機器,指針是8個字節大小,而int只有4個字節,所以要強制轉換為long long),如果shmat成功就會返回共享內存首地址指針(掛接的進程虛擬地址),所以我們再增加一個獲取該指針的成員變量,方便我們將地址打印出來查看
代碼如下:
// 新增一個成員變量void* _start_mem;// 共享內存首地址指針void Attach(){_start_mem = shmat(_shmid, NULL, 0);if((long long)_start_mem < 0){ERR_EXIT("shmat");}printf("attach success\n");}public:Shm(const std::string& pathneme, int projid, const std::string& usertype): _shmid(gdefaultid), _size(gsize), _start_mem(nullptr), _usertype(usertype){_key = ftok(pathname.c_str(), projid);if (_key < 0){ERR_EXIT("ftok");}printf("key: 0x%x\n", _key);if(_usertype == CREATER){// 用戶是服務端則創建共享內存段Creat();}else if(_usertype == USER){// 用戶是客戶端則獲取共享內存段Get();}Attach();}void* VirtualAddr(){printf("VirtualAddr: %p\n", _start_mem);return _start_mem;}
當然我們也可以來一個獲取共享內存段大小的接口
代碼如下:
int Size(){return _size;}
5.3?分離共享內存段
在我們使用共享內存通信完之后,需要將內存進行回收,避免內存泄漏,但在回收共享內存之前,需要將共享內存段從當前進程的地址空間中分離出去。進程不能再通過該指針訪問共享內存。注意:分離操作并不會銷毀共享內存段本身。
void Detach(){int n = shmdt(_start_mem);if(n < 0){ERR_EXIT("shmdt");}printf("Detach success\n");}
5.4?銷毀共享內存段
銷毀前先將掛接的共享內存分離,然后再銷毀,當然誰創建的就由誰來刪除
注意:IPC_RMID
:標記刪除共享內存。當所有進程均斷開連接(shmdt
)后,內存才會被實際釋放
代碼如下:
void Destroy(){Detach();if(_shmid == gdefaultid)return;if(_usertype == CREATER){int n = shmctl(_shmid, IPC_RMID, NULL);if(n < 0){ERR_EXIT("shmctl");}printf("shmctl delete shm: %d success!\n", _shmid);}}
析構時,調用Destroy函數
5.5 測試代碼
此時我們就可以使用共享內存,怎么使用呢?多個進程通過它們各自?shmat
?返回的指針(指向同一物理內存的不同虛擬地址)直接讀寫共享內存區域。就如同我們malloc出來的一段內存對這段內存空間進行使用,我們現在就可以使用這段共享內存來讀寫。
服務端:
服務端讀內存中的數據
#include "Shm.hpp"int main()
{Shm shm(pathname, proj_id, CREATER);char* mem = (char*)shm.VirtualAddr();while(true){printf("%s\n", mem);sleep(1);}return 0;
}
客戶端:
客戶端對共享內存寫
#include "Shm.hpp"int main()
{Shm shm(pathname, proj_id, USER);char* mem = (char*)shm.VirtualAddr();for(char c = 'A'; c <= 'Z'; c++){mem[c - 'A'] = c;sleep(1);}return 0;
}
運行結果:
可以看到運行結果正常,進程掛接數也從無到2,不過由于我們的服務端是死循環在讀,所以不會自己調用析構函數,我們需要自己通過命令行ipcrm -m [shmid]來刪除共享內存,不然下次再運行就會報錯文件存在
不過共享內存也同樣存在缺點
共享內存的缺點與挑戰
缺乏內置同步:?這是最大的挑戰和風險點。?開發者必須嚴格、正確地使用額外的同步機制(信號量、互斥鎖)來協調多個進程對共享內存的并發訪問,否則極易導致數據損壞、程序崩潰等難以調試的問題。
復雜性增加:?相比管道簡單的?
read/write
?接口,共享內存的創建、附加、分離、銷毀步驟更多,并且必須手動管理同步,增加了程序的復雜性。資源管理:?共享內存段獨立于進程存在。如果進程異常終止而沒有正確分離或標記刪除 (
IPC_RMID
),共享內存段可能殘留在系統中,造成資源泄漏(ipcs
?命令查看,ipcrm
?命令刪除)。需要良好的編程習慣和可能的清理機制。安全性:?需要正確設置權限 (
shmflg
?中的權限位),防止未授權進程訪問敏感數據。內存模型:?不同進程附加的地址 (
shmat
?返回值) 可能不同,不能直接傳遞指針(傳遞指針在接收進程地址空間無效)。通常傳遞的是相對于共享內存基址的偏移量。
源碼:
Shm.hpp
#pragma once#include <iostream>
#include <cstdio>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>#define ERR_EXIT(m) \do \{ \perror(m); \exit(EXIT_FAILURE); \} while (0)const int gdefaultid = -1;
const int gsize = 4096;
const std::string pathname = ".";
const int proj_id = 0x66;#define CREATER "creater"
#define USER "user"class Shm
{
private:int _shmid;int _size;key_t _key;std::string _usertype;void* _start_mem;// 共享內存首地址指針(掛接后的進程虛擬地址)void CreatShm(int flg){_shmid = shmget(_key, _size, flg);if(_shmid < 0){ERR_EXIT("shmget");}printf("shmid: %d\n", _shmid);}void Creat(){CreatShm(IPC_CREAT | IPC_EXCL | 0666);}void Get(){CreatShm(IPC_CREAT);}void Attach(){_start_mem = shmat(_shmid, nullptr, 0);if((long long)_start_mem < 0){ERR_EXIT("shmat");}printf("attach success\n");}void Detach(){int n = shmdt(_start_mem);if(n < 0){ERR_EXIT("shmdt");}printf("Detach success\n");}void Destroy(){Detach();if(_shmid == gdefaultid)return;if(_usertype == CREATER){int n = shmctl(_shmid, IPC_RMID, NULL);if(n < 0){ERR_EXIT("shmctl");}printf("shmctl delete shm: %d success!\n", _shmid);}}public:Shm(const std::string& pathneme, int projid, const std::string& usertype): _shmid(gdefaultid), _size(gsize), _start_mem(nullptr), _usertype(usertype){_key = ftok(pathname.c_str(), projid);if (_key < 0){ERR_EXIT("ftok");}printf("key: 0x%x\n", _key);if(_usertype == CREATER){// 用戶是服務端則創建共享內存段Creat();}else if(_usertype == USER){// 用戶是客戶端則獲取共享內存段Get();}Attach();}void* VirtualAddr(){printf("VirtualAddr: %p\n", _start_mem);return _start_mem;}int Size(){return _size;}~Shm() {Destroy();}
};