【Linux系統】進程間通信:System V IPC——共享內存

前文中我們介紹了管道——匿名管道和命名管道來實現進程間通信,在介紹怎么進行通信時,我們有提到過不止管道的方式進行通信,還有System V IPC,今天這篇文章我們就來學習一下System V IPC中的共享內存

1. 為何引入共享內存?——管道通信的局限性

管道(匿名/命名管道)作為傳統IPC機制存在顯著缺陷:

  1. 數據拷貝開銷大:管道需通過內核緩沖區中轉,數據需從用戶空間→內核→用戶空間兩次拷貝
  2. 單向通信限制:匿名管道僅支持單向數據流,雙向通信需建立兩個管道
  3. 效率瓶頸:頻繁讀寫時內核緩沖區切換成為性能瓶頸
  4. 適用場景有限:命名管道雖突破親緣關系限制,但仍依賴文件系統路徑,且同步機制弱

??共享內存的破局
通過多進程直接訪問同一物理內存區域,消除數據拷貝,實現零復制(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_setuptmpfs中創建虛擬文件。
  • 文件操作函數集指向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需加載文件數據到內存
適用場景高頻臨時數據交換持久化數據共享

五、設計哲學總結

  1. 抽象與隔離
    • 通過shmid_ds向用戶暴露可控接口,隱藏shmid_kernel等內核細節。
  2. 零拷貝思想
    • 虛擬地址直接映射物理幀,消除數據復制。
  3. 動態擴展性
    • 紅黑樹管理替代靜態數組,支持海量共享內存段。
  4. 資源自治
    • 引用計數(shm_nattch)實現自銷毀機制,避免資源泄漏。

共享內存的工作原理 (關鍵步驟)

  1. 創建或獲取共享內存段 (shmget):

    • 一個進程(通常是第一個需要該共享內存的進程)調用?shmget(key_t key, size_t size, int shmflg)

    • key: 一個唯一標識共享內存段的鍵值。可以使用?ftok()?基于路徑名生成,或者指定為?IPC_PRIVATE(創建僅供親緣進程使用的新段)。

    • size: 請求的共享內存段的大小(字節)。如果是獲取已存在的段,此參數通常為 0。

    • shmflg: 標志位,指定創建選項(IPC_CREAT,?IPC_EXCL)和權限(如?0666)。

    • 成功時返回共享內存標識符?shmid(一個非負整數),用于后續操作。內核在內存中分配一塊指定大小的物理內存區域。

  2. 將共享內存段附加到進程地址空間 (shmat):

    • 任何需要使用該共享內存的進程調用?shmat(int shmid, const void *shmaddr, int shmflg)

    • shmid: 由?shmget?返回的標識符。

    • shmaddr: 通常設為?NULL,讓內核選擇附加地址。也可以指定一個地址(但需謹慎,通常不推薦)。

    • shmflg: 標志位(如?SHM_RDONLY?表示只讀附加)。

    • 成功時返回一個指向共享內存段在當前進程地址空間中起始位置的?void*?指針。進程現在可以通過這個指針像訪問普通內存一樣讀寫共享內存區域。

  3. 使用共享內存:

    • 多個進程通過它們各自?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)

  4. 分離共享內存段 (shmdt):

    • 當進程不再需要訪問共享內存時,調用?shmdt(const void *shmaddr)

    • shmaddr: 之前?shmat?返回的指針。

    • 該調用將共享內存段從當前進程的地址空間中分離出去。進程不能再通過該指針訪問共享內存。注意:分離操作并不會銷毀共享內存段本身。

  5. 控制/銷毀共享內存段 (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 Miss
  • SHM_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;  // 當前附加進程數
};

初始化規則

  1. shm_perm.cuid/uid?= 調用進程有效UID
  2. shm_perm.cgid/gid?= 調用進程有效GID
  3. shm_perm.mode?=?shmflg的低9位權限
  4. shm_atime/shm_dtime?= 0(未映射)
  5. shm_ctime?= 當前系統時間

💡?物理內存分配:內核調用alloc_pages()分配連續物理頁,內容初始化為0


4. 錯誤處理

錯誤碼觸發條件解決方案
EACCES權限不足檢查?shmflg?權限位
EEXISTIPC_CREAT+IPC_EXCL?且段已存在移除?IPC_EXCL?或更換?key
EINVALsize?無效(>?SHMMAX?或 < 頁大小)調整?size?為頁大小整數倍
ENOENTkey?不存在且未設?IPC_CREAT增加?IPC_CREAT?標志

???系統限制

  • SHMMAX:單段最大尺寸(默認32MB-128MB)
  • SHMMNI:系統最大段數(默認4096)

深入解析?shmget?的?key?參數:跨進程共享內存的標識核心

一、key?參數的核心作用與設計哲學

key?是?shmget?函數中唯一標識共享內存段的整數標識符,其本質是操作系統用于區分不同共享內存段的全局鍵值。它的作用類似于文件系統中的路徑名,但以整數形式存在,核心價值在于:

  1. 跨進程標識:不同進程通過相同?key?訪問同一物理內存區域。
  2. 資源復用:避免重復創建相同內存段,減少資源浪費。
  3. 權限控制:與?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

  1. 作用與本質

    • 唯一標識key是共享內存在系統中的全局唯一編號(類型為key_t,本質是unsigned int),用于區分不同共享內存段?。
    • 進程間同步:不同進程通過相同key訪問同一內存段,實現通信?。
  2. 生成方式

    • ftok()函數:常用方法,基于文件路徑和項目ID生成唯一key。
    • IPC_PRIVATE:指定此值時,系統自動分配新key(用于父子進程間通信)。
  3. 使用場景

    • 創建新內存段:當key不與現有段關聯,且指定IPC_CREAT標志時,系統創建新共享內存?。
    • 訪問現有段:若key已存在,則返回其標識符(shmid),此時size參數應為0?。
  4. 權限與控制

    • 權限位shmflg的低9位定義權限(如0666表示所有用戶可讀寫)。
    • 控制標志
      • IPC_CREAT:若內存段不存在則創建?。
      • IPC_EXCL:與IPC_CREAT聯用,若段已存在則返回錯誤?。
  5. 錯誤處理

    • 常見錯誤
      • EACCES:權限不足?。
      • ENOENTkey不存在且未指定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:內存大小(與shmgetsize參數一致)
    • NATTCH:當前掛載進程數(即通過shmat連接的進程數)
  • 高級用法

    ipcs -m -i <SHMID>  # 查看指定SHMID的詳細信息
    ipcs -m -u          # 匯總共享內存使用統計
    

2.?ipcrm?命令:刪除共享內存(對應?shmctl(shmid, IPC_RMID, NULL)

  • 功能
    刪除指定的共享內存段,效果等同于調用shmctlIPC_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?和?fdinode?標識文件唯一性,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. 設計目的與架構思想

維度keyshmid
唯一性范圍全局唯一(整個操作系統內)進程內有效(不同進程的?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分支后)都能安全使用,不會因缺少大括號導致邏輯錯誤。
  • 反斜杠\
    用于連接多行代碼,使宏定義可跨行書寫,提高可讀性。

核心功能

  • 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]來刪除共享內存,不然下次再運行就會報錯文件存在


不過共享內存也同樣存在缺點

共享內存的缺點與挑戰

  1. 缺乏內置同步:?這是最大的挑戰和風險點。?開發者必須嚴格、正確地使用額外的同步機制(信號量、互斥鎖)來協調多個進程對共享內存的并發訪問,否則極易導致數據損壞、程序崩潰等難以調試的問題。

  2. 復雜性增加:?相比管道簡單的?read/write?接口,共享內存的創建、附加、分離、銷毀步驟更多,并且必須手動管理同步,增加了程序的復雜性。

  3. 資源管理:?共享內存段獨立于進程存在。如果進程異常終止而沒有正確分離或標記刪除 (IPC_RMID),共享內存段可能殘留在系統中,造成資源泄漏(ipcs?命令查看,ipcrm?命令刪除)。需要良好的編程習慣和可能的清理機制。

  4. 安全性:?需要正確設置權限 (shmflg?中的權限位),防止未授權進程訪問敏感數據。

  5. 內存模型:?不同進程附加的地址 (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();}
};

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/95770.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/95770.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/95770.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

[優選算法專題二滑動窗口——最大連續1的個數 III]

題目鏈接 最大連續1的個數 III 題目描述 題目解析 問題本質 輸入&#xff1a;二進制數組nums&#xff08;只包含 0 和 1&#xff09;和整數k操作&#xff1a;最多可以將k個 0 翻轉成 1目標&#xff1a;找到翻轉后能得到的最長連續 1 的子數組長度 這個問題的核心是要找到一…

C#單元測試(xUnit + Moq + coverlet.collector)

C#單元測試 xUnit Moq coverlet.collector 1.添加庫 MlyMathLib 2.編寫庫函數內容 using System;namespace MlyMathLib {public interface IUserRepo{string GetName(int id);}public class UserService{private readonly IUserRepo _repo;public UserService(IUserRepo repo…

【數據庫】Oracle學習筆記整理之五:ORACLE體系結構 - 參數文件與控制文件(Parameter Files Control Files)

Oracle體系結構 - 參數文件與控制文件&#xff08;Parameter Files & Control Files&#xff09; 參數文件與控制文件是Oracle數據庫的“雙核基石”&#xff1a;參數文件是實例的“啟動配置中心”&#xff0c;定義運行環境與規則&#xff1b;控制文件是數據庫的“物理元數據…

GDB典型開發場景深度解析

GDB典型開發場景深度解析 以下是開發過程中最常見的GDB使用場景&#xff0c;結合具體實例和調試技巧&#xff0c;幫助開發者高效解決實際問題&#xff1a;一、崩潰分析&#xff08;Core Dump調試&#xff09; 場景&#xff1a;程序突然崩潰&#xff0c;生成了core文件 # 啟動調…

存儲、硬盤、文件系統、 IO相關常識總結

目錄 &#xff08;一&#xff09;存儲 &#xff08;1&#xff09;定義 &#xff08;2&#xff09;分類 &#xff08;二&#xff09;硬盤 &#xff08;1&#xff09;容量&#xff08;最主要的參數&#xff09; &#xff08;2&#xff09;轉速 &#xff08;3&#xff09;訪…

docker安裝mongodb及java連接實戰

1.docker部署mongodb docker run --name mongodb -d -p 27017:27017 -v /data/mongodbdata:/data/db -e MONGO_INITDB_ROOT_USERNAMEtestmongo -e MONGO_INITDB_ROOT_PASSWORDtest123456 mongodb:4.0.112.項目實戰 <dependencies><dependency><groupId>org.m…

Java設計模式之《工廠模式》

目錄 1、介紹 1.1、定義 1.2、優缺點 1.3、使用場景 2、實現 2.1、簡單工廠模式 2.2、工廠方法模式 2.3、抽象工廠模式 3、小結 前言 在面向對象編程中&#xff0c;創建對象實例最常用的方式就是通過 new 操作符構造一個對象實例&#xff0c;但在某些情況下&#xff0…

【異步】js中異步的實現方式 async await /Promise / Generator

JS的異步相關知識 js里面一共有以下異步的解決方案 傳統的回調 省略 。。。。 生成器 Generator 函數是 ES6 提供的一種異步編程解決方案, 語法上&#xff0c;首先可以把它理解成&#xff0c;Generator 函數是一個狀態機&#xff0c;封裝了多個內部狀態。執行 Generator 函數…

JVM字節碼文件結構

Class文件結構class文件是二進制文件&#xff0c;這里要介紹的是這個二級制文件的結構。思考&#xff1a;一個java文件編譯成class文件&#xff0c;如果要描述一個java文件&#xff0c;需要哪些信息呢&#xff1f;基本信息&#xff1a;類名、父類、實現哪些接口、方法個數、每個…

11.web api 2

5. 操作元素屬性 5.1操作元素常用屬性 &#xff1a;通過 JS 設置/修改標簽元素屬性&#xff0c;比如通過 src更換 圖片最常見的屬性比如&#xff1a; href、title、src 等5.2 操作元素樣式屬性 &#xff1a;通過 JS 設置/修改標簽元素的樣式屬性。使用 className 有什么好處&a…

java中數組和list的區別是什么?

在Java中&#xff0c;數組&#xff08;Array&#xff09;和List&#xff08;通常指java.util.List接口的實現類&#xff0c;如ArrayList、LinkedList&#xff09;是兩種常用的容器&#xff0c;但它們在設計、功能和使用場景上有顯著區別。以下從核心特性、使用方式等方面詳細對…

Python爬取推特(X)的各種數據

&#x1f31f; Hello&#xff0c;我是蔣星熠Jaxonic&#xff01; &#x1f308; 在浩瀚無垠的技術宇宙中&#xff0c;我是一名執著的星際旅人&#xff0c;用代碼繪制探索的軌跡。 &#x1f680; 每一個算法都是我點燃的推進器&#xff0c;每一行代碼都是我航行的星圖。 &#x…

Oracle數據庫文件管理與空間問題解決指南

在Oracle數據庫運維中&#xff0c;表空間、數據文件及相關日志文件的管理是保障數據庫穩定運行的核心環節。本文將系統梳理表空間與數據文件的調整、關鍵文件的移動、自動擴展配置&#xff0c;以及常見空間不足錯誤的排查解決方法&#xff0c;為數據庫管理員提供全面參考。 一、…

華為實驗綜合小練習

描述&#xff1a; 1 內網有A、B、C 三個部門。所在網段如圖所示。 2 內網服務器配置靜態IP,網關192.168.100.1。 3 sw1和R1之間使用vlan200 192.168.200.0/30 互聯。 4 R1向運營商申請企業寬帶并申請了5個公網IP&#xff1a;200.1.1.1-.5子網掩碼 255.255.255.248&#xff0c;網…

Flink面試題及詳細答案100道(1-20)- 基礎概念與架構

《前后端面試題》專欄集合了前后端各個知識模塊的面試題&#xff0c;包括html&#xff0c;javascript&#xff0c;css&#xff0c;vue&#xff0c;react&#xff0c;java&#xff0c;Openlayers&#xff0c;leaflet&#xff0c;cesium&#xff0c;mapboxGL&#xff0c;threejs&…

爬蟲逆向之滑塊驗證碼加密分析(軌跡和坐標)

本文章中所有內容僅供學習交流使用&#xff0c;不用于其他任何目的。否則由此產生的一切后果均與作者無關&#xff01;在爬蟲開發過程中&#xff0c;滑塊驗證碼常常成為我們獲取數據的一大阻礙。而滑塊驗證碼的加密方式多種多樣&#xff0c;其中軌跡加密和坐標加密是比較常見的…

微信小程序實現導航至目的地

本人做的導航頁面相關功能和效果的代碼 javascript相關 Page({data: {markers: [],latitude: , // 中心點坐標longitude: ,FIXED_LAT: 31.2304, // 1. 寫死的目標點坐標, 示例&#xff1a;上海人民廣場FIXED_LNG: 121.4737},onLoad: function () {// 如果要顯示地圖可以看onLo…

中國科學社簡史

中國科學社簡史中國科學社&#xff0c;作為中國近代史上第一個民間綜合性科學團體&#xff0c;為中國現代科學文化事業的發展做出了卓越貢獻。其歷程不僅見證了中國科學從萌芽到蓬勃發展的轉變&#xff0c;還反映了中國科學體制化的艱難探索與輝煌成就。中國科學社的起源可追溯…

若爾當型,Jordon Form

文章目錄一、相似二、若爾當型1.1 認識若爾當型1.2 凱萊-哈密頓定理 (Cayley-Hamilton Theorem)一、相似 Every matrix CB?1ABC B^{-1}ABCB?1AB has the same eigenvalues as A. These C’s are similar to A. 任意一個矩陣C&#xff0c;滿足 CB?1ABC B^{-1}ABCB?1AB 都和…

解決uni-app微信小程序編譯報錯:unexpected character `1`

問題原因在uni-app微信小程序開發中&#xff0c;當template模板中包含英文符號<或>時&#xff0c;微信小程序的編譯器會將這些符號誤認為是HTML標簽的開閉符號&#xff0c;從而導致類似unexpected character 1的編譯錯誤。錯誤示例<view class"plan-bmi">…