解鎖Linux共享內存:進程間通信的超高速通道
在Linux系統的進程間通信 “江湖” 中,眾多通信方式各顯神通。管道,如同隱秘的地下通道,讓有親緣關系的進程能夠悄然傳遞信息;消息隊列則似郵局,進程可投遞和接收格式化的消息包裹。然而,有一種通信方式卻以其獨特的 “高速” 特性脫穎而出,它就是共享內存。想象一下,進程們原本各自生活在獨立的 “小天地” 里,有著自己專屬的虛擬地址空間。但共享內存卻如同神奇的 “任意門”,打破了進程間的隔閡,讓多個進程能夠直接訪問同一塊內存區域。這種獨特的機制,使得數據在進程間的傳遞無需繁瑣的復制過程,極大地提升了通信效率,堪稱進程間通信的超高速通道。
在使用共享內存時,需要注意對于并發訪問的控制,如使用鎖或其他同步機制來保證數據的一致性和安全性。此外,還需要謹慎處理資源管理問題,確保正確地釋放共享內存以避免內存泄漏。接下來,就讓我們一同深入探索 Linux 共享內存的奧秘,揭開它神秘的面紗,看看它是如何在 Linux 系統中發揮這一獨特且強大的作用 。
一、Linux內存管理初窺
1.1 虛擬內存與物理內存
Linux 采用虛擬內存管理機制,為每個進程分配獨立的虛擬地址空間。這意味著每個進程都可以認為自己擁有 4GB(32 位系統)或更大(64 位系統)的連續內存空間,而不必擔心物理內存的實際大小和其他進程的干擾。虛擬內存與物理內存通過內存映射機制建立聯系,進程訪問的虛擬地址會被轉換為實際的物理地址。
舉個例子,當你在 Linux 系統上同時運行多個程序時,每個程序都覺得自己獨占了大量內存,但實際上物理內存是有限的。通過虛擬內存管理,操作系統可以巧妙地在物理內存和磁盤之間交換數據,使得系統能夠運行比物理內存更大的程序集。就好比一個小型圖書館,雖然書架空間有限(物理內存),但通過一個龐大的倉庫(磁盤)來存放暫時不用的書籍(數據),當讀者需要某本書時,管理員(操作系統)會從倉庫中取出并放到書架上供讀者使用。
1.2 內存分頁
為了更高效地管理內存,Linux 采用內存分頁機制。將虛擬內存和物理內存按照固定大小的頁(通常為 4KB)進行劃分,頁是內存管理的最小單位。操作系統通過維護頁表來記錄虛擬頁和物理頁之間的映射關系,當進程訪問某個虛擬地址時,CPU 會根據頁表將其轉換為對應的物理地址。
想象一下,內存就像一本巨大的書籍,每一頁都有固定的頁碼(虛擬頁號和物理頁號)。當你想要查找書中的某個內容(訪問內存數據)時,通過目錄(頁表)可以快速定位到具體的頁碼,從而找到所需內容。
1.3 內存分配與回收
內存管理包括內存的分配和回收。當進程需要內存時,它會向操作系統請求分配內存,操作系統根據一定的算法從空閑內存中分配相應大小的內存塊給進程;當進程不再需要某些內存時,它會將這些內存釋放回操作系統,以便操作系統重新分配給其他需要的進程。
例如,當你在 Linux 系統上運行一個新的程序時,程序會向操作系統申請內存來存放代碼和數據。操作系統會從空閑內存池中找到合適大小的內存塊分配給該程序。當程序運行結束后,它占用的內存會被操作系統回收,重新加入空閑內存池,等待下一個程序的請求。
二、共享內存詳解
2.1 共享內存是什么
共享內存是一種高效的進程間通信(IPC,Inter - Process Communication)機制,它允許兩個或多個進程直接訪問同一塊物理內存區域。簡單來說,就好比多個房間(進程)都有一扇門可以直接通向同一個儲物間(共享內存),大家可以直接在這個儲物間里存放和取用物品(數據) 。
在 Linux 系統中,共享內存的實現依賴于操作系統的支持。當一個進程創建共享內存時,操作系統會在物理內存中分配一塊區域,并為這塊區域生成一個唯一的標識符。其他進程可以通過這個標識符將該共享內存映射到自己的虛擬地址空間中,從而實現對共享內存的訪問。
2.2 為什么要用共享內存
在進程間通信的眾多方式中,共享內存之所以備受青睞,是因為它具有其他方式難以比擬的優勢。
首先,與管道和消息隊列等通信方式相比,共享內存的速度極快。管道和消息隊列在數據傳輸時,需要進行多次數據拷貝,數據要在內核空間和用戶空間之間來回傳遞,這會消耗大量的時間和系統資源。而共享內存則不同,多個進程直接訪問同一塊內存區域,數據不需要在不同進程的地址空間之間拷貝,大大減少了數據傳輸的開銷,提高了通信效率。例如,在一個實時數據處理系統中,多個進程需要頻繁地交換大量數據,如果使用管道或消息隊列,可能會因為數據傳輸的延遲而影響系統的實時性;而使用共享內存,就可以快速地傳遞數據,滿足系統對實時性的要求。
其次,共享內存的使用非常靈活。它可以用于任何類型的進程間通信,無論是有親緣關系的進程(如父子進程)還是毫無關系的進程,都可以通過共享內存進行數據共享和交互。而且,共享內存區域可以存儲各種類型的數據結構,開發者可以根據實際需求自定義數據格式,這為復雜應用場景的實現提供了便利。比如,在一個多進程協作的圖形處理程序中,不同進程可以通過共享內存共享圖像數據和處理參數,各自完成不同的處理任務,如一個進程負責圖像的濾波處理,另一個進程負責圖像的邊緣檢測,共享內存使得它們能夠高效地協同工作。
此外,共享內存還能有效地節省內存資源。多個進程共享同一塊內存區域,而不是每個進程都單獨開辟一塊內存來存儲相同的數據,這在內存資源有限的情況下顯得尤為重要。例如,在一個服務器系統中,可能同時有多個進程需要訪問一些公共的配置信息或緩存數據,使用共享內存可以避免這些數據在每個進程中重復存儲,從而提高內存的利用率。
2.3 共享內存原理
共享內存是System V版本的最后一個進程間通信方式。共享內存,顧名思義就是允許兩個不相關的進程訪問同一個邏輯內存,共享內存是兩個正在運行的進程之間共享和傳遞數據的一種非常有效的方式。不同進程之間共享的內存通常為同一段物理內存。進程可以將同一段物理內存連接到他們自己的地址空間中,所有的進程都可以訪問共享內存中的地址。如果某個進程向共享內存寫入數據,所做的改動將立即影響到可以訪問同一段共享內存的任何其他進程。
特別提醒:共享內存并未提供同步機制,也就是說,在第一個進程結束對共享內存的寫操作之前,并無自動機制可以阻止第二個進程開始對它進行讀取,所以我們通常需要用其他的機制來同步對共享內存的訪問,例如信號量。
在Linux中,每個進程都有屬于自己的進程控制塊(PCB)和地址空間(Addr Space),并且都有一個與之對應的頁表,負責將進程的虛擬地址與物理地址進行映射,通過內存管理單元(MMU)進行管理。兩個不同的虛擬地址通過頁表映射到物理空間的同一區域,它們所指向的這塊區域即共享內存。
共享內存的通信原理示意圖:
圖片
對于上圖我的理解是:當兩個進程通過頁表將虛擬地址映射到物理地址時,在物理地址中有一塊共同的內存區,即共享內存,這塊內存可以被兩個進程同時看到。這樣當一個進程進行寫操作,另一個進程讀操作就可以實現進程間通信。但是,我們要確保一個進程在寫的時候不能被讀,因此我們使用信號量來實現同步與互斥。
對于一個共享內存,實現采用的是引用計數的原理,當進程脫離共享存儲區后,計數器減一,掛架成功時,計數器加一,只有當計數器變為零時,才能被刪除。當進程終止時,它所附加的共享存儲區都會自動脫離。
為什么共享內存速度最快?
借助上圖說明:Proc A 進程給內存中寫數據, Proc B 進程從內存中讀取數據,在此期間一共發生了兩次復制
(1)Proc A 到共享內存
(2)共享內存到 Proc B
因為直接在內存上操作,所以共享內存的速度也就提高了。
三、共享內存使用指南
3.1 關鍵函數全解析
在 Linux 中使用共享內存,離不開一些關鍵的系統調用函數,它們是我們操作共享內存的有力工具。
(1)shmget 函數:用于創建共享內存段或獲取已存在的共享內存段的標識符。其函數原型為:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
key:是一個用于標識共享內存段的鍵值,它就像是共享內存的 “門牌號”。通常可以使用ftok函數根據文件路徑和項目 ID 生成一個唯一的key值。例如:
key_t key = ftok("/tmp/somefile", 1);
這里/tmp/somefile是一個已存在的文件路徑,1 是項目 ID。如果key取值為IPC_PRIVATE,則會創建一個新的私有共享內存段,通常用于父子進程間的通信。
size:指定共享內存段的大小,單位是字節。例如,若要創建一個 1024 字節大小的共享內存段,可以這樣設置:
int shmid = shmget(key, 1024, IPC_CREAT | 0666);
- shmflg:是一組標志位,常用的標志包括IPC_CREAT(如果共享內存不存在則創建)和IPC_EXCL(與IPC_CREAT一起使用,確保創建新的共享內存段,若已存在則報錯)。權限設置與文件權限類似,如0666表示所有者、組和其他用戶都有讀寫權限 。如果shmget函數執行成功,會返回一個非負整數,即共享內存段的標識符shmid;若失敗,則返回 -1。
(2)shmat 函數:將共享內存段連接到調用進程的地址空間,使得進程可以訪問共享內存中的數據。
其函數原型為:
void *shmat(int shmid, const void *shmaddr, int shmflg);
- shmid:是由shmget函數返回的共享內存標識符。
- shmaddr:指定共享內存連接到當前進程中的地址位置,通常設置為NULL,表示讓系統自動選擇合適的地址。例如:
void *shared_mem = shmat(shmid, NULL, 0);
shmflg:通常為 0,表示默認的連接方式。如果設置了SHM_RDONLY,則以只讀方式連接共享內存。如果shmat函數調用成功,會返回一個指向共享內存起始地址的指針;若失敗,返回(void *)-1。
(3)shmdt 函數:用于將共享內存段從當前進程的地址空間中分離。函數原型為:
int shmdt(const void *shmaddr);
shmaddr:是shmat函數返回的共享內存起始地址。調用該函數后,進程不再能夠訪問該共享內存,但共享內存本身并不會被刪除。例如:
int result = shmdt(shared_mem);
if (result == -1) {
perror("shmdt failed");
}
如果分離成功,shmdt返回 0;若失敗,返回 -1。
(4)shmctl 函數:用于對共享內存進行控制操作,如獲取共享內存信息、設置權限、刪除共享內存等。函數原型為:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- shmid:共享內存標識符。
- cmd:指定要執行的控制命令,常用的命令有IPC_STAT(獲取共享內存的狀態信息,存入buf指向的結構體)、IPC_SET(設置共享內存的狀態信息,如權限等,從buf指向的結構體中獲取設置值)和IPC_RMID(刪除共享內存段)。
- buf:是一個指向shmid_ds結構體的指針,用于傳遞或獲取共享內存的相關信息。當cmd為IPC_RMID時,buf通常設置為NULL。例如,刪除共享內存段的操作如下:
int result = shmctl(shmid, IPC_RMID, NULL);
if (result == -1) {
perror("shmctl IPC_RMID failed");
}
如果操作成功,shmctl返回 0;若失敗,返回 -1。
3.2 代碼實戰:共享內存的讀寫操作
下面通過一個完整的代碼示例,展示如何在兩個進程間使用共享內存進行數據讀寫。假設我們要在一個進程中寫入數據,另一個進程讀取這些數據。
首先,定義一個數據結構,用于在共享內存中存儲數據。這里我們定義一個簡單的結構體,包含一個整數和一個字符數組:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define SHM_SIZE 1024
// 定義共享內存中使用的數據結構
typedef struct {
int num;
char text[100];
} SharedData;
int main() {
int shmid;
key_t key;
SharedData *shared_data;
// 生成唯一的key值
key = ftok(".", 'a');
if (key == -1) {
perror("ftok");
exit(EXIT_FAILURE);
}
// 創建共享內存段
shmid = shmget(key, sizeof(SharedData), IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget");
exit(EXIT_FAILURE);
}
// 將共享內存連接到當前進程的地址空間
shared_data = (SharedData *)shmat(shmid, NULL, 0);
if (shared_data == (SharedData *)-1) {
perror("shmat");
exit(EXIT_FAILURE);
}
// 寫入數據到共享內存
shared_data->num = 42;
strcpy(shared_data->text, "Hello, shared memory!");
printf("Data written to shared memory: num = %d, text = %s\n", shared_data->num, shared_data->text);
// 分離共享內存
if (shmdt(shared_data) == -1) {
perror("shmdt");
exit(EXIT_FAILURE);
}
return 0;
}
上述代碼中,首先使用ftok函數生成一個key值,然后通過shmget創建一個共享內存段,其大小為SharedData結構體的大小。接著使用shmat將共享內存連接到當前進程地址空間,向共享內存中寫入數據,最后使用shmdt分離共享內存。
下面是讀取共享內存數據的代碼:
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define SHM_SIZE 1024
// 定義共享內存中使用的數據結構
typedef struct {
int num;
char text[100];
} SharedData;
int main() {
int shmid;
key_t key;
SharedData *shared_data;
// 生成唯一的key值,必須與寫入進程一致
key = ftok(".", 'a');
if (key == -1) {
perror("ftok");
exit(EXIT_FAILURE);
}
// 獲取已存在的共享內存段
shmid = shmget(key, sizeof(SharedData), 0666);
if (shmid == -1) {
perror("shmget");
exit(EXIT_FAILURE);
}
// 將共享內存連接到當前進程的地址空間
shared_data = (SharedData *)shmat(shmid, NULL, 0);
if (shared_data == (SharedData *)-1) {
perror("shmat");
exit(EXIT_FAILURE);
}
// 從共享內存讀取數據
printf("Data read from shared memory: num = %d, text = %s\n", shared_data->num, shared_data->text);
// 分離共享內存
if (shmdt(shared_data) == -1) {
perror("shmdt");
exit(EXIT_FAILURE);
}
// 刪除共享內存段(這里僅演示,實際應用中需謹慎操作)
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl IPC_RMID");
exit(EXIT_FAILURE);
}
return 0;
}
在讀取代碼中,同樣先使用ftok生成與寫入進程相同的key值,然后通過shmget獲取共享內存段(注意這里沒有使用IPC_CREAT標志,因為共享內存已經由寫入進程創建),接著連接共享內存并讀取數據,最后分離共享內存并刪除共享內存段(在實際應用中,刪除共享內存段的操作需要謹慎考慮,確保沒有其他進程再使用該共享內存)。
3.3 模擬共享內存
我們用server來創建共享存儲段,用client獲取共享存儲段的標識符,二者關聯起來之后server將數據寫入共享存儲段,client從共享區讀取數據。通信結束之后server與client斷開與共享區的關聯,并由server釋放共享存儲段。
comm.h
//comm.h
#ifndef _COMM_H__
#define _COMM_H__
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#define PATHNAME "."
#define PROJ_ID 0x6666
int CreateShm(int size);
int DestroyShm(int shmid);
int GetShm(int size);
#endif
comm.c
//comm.c
#include"comm.h"
static int CommShm(int size,int flags)
{
key_t key = ftok(PATHNAME,PROJ_ID);
if(key < 0)
{
perror("ftok");
return -1;
}
int shmid = 0;
if((shmid = shmget(key,size,flags)) < 0)
{
perror("shmget");
return -2;
}
return shmid;
}
int DestroyShm(int shmid)
{
if(shmctl(shmid,IPC_RMID,NULL) < 0)
{
perror("shmctl");
return -1;
}
return 0;
}
int CreateShm(int size)
{
return CommShm(size,IPC_CREAT | IPC_EXCL | 0666);
}
int GetShm(int size)
{
return CommShm(size,IPC_CREAT);
}
client.c
//client.c
#include"comm.h"
int main()
{
int shmid = GetShm(4096);
sleep(1);
char *addr = shmat(shmid,NULL,0);
sleep(2);
int i = 0;
while(i < 26)
{
addr[i] = 'A' + i;
i++;
addr[i] = 0;
sleep(1);
}
shmdt(addr);
sleep(2);
return 0;
}
server.c
//server.c
#include"comm.h"
int main()
{
int shmid = CreateShm(4096);
char *addr = shmat(shmid,NULL,0);
sleep(2);
int i = 0;
while(i++ < 26)
{
printf("client# %s\n",addr);
sleep(1);
}
shmdt(addr);
sleep(2);
DestroyShm(shmid);
return 0;
}
Makefile
//Makefile
.PHONY:all
all:server client
client:client.c comm.c
gcc -o $@ $^
server:server.c comm.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f client server
運行結果:
圖片
- 優點:我們可以看到使用共享內存進行進程之間的通信是非常方便的,而且函數的接口也比較簡單,數據的共享還使進程間的數據不用傳送,而是直接訪問內存,加快了程序的效率。
- 缺點:共享內存沒有提供同步機制,這使得我們在使用共享內存進行進程之間的通信時,往往需要借助其他手段來保證進程之間的同步工作。
3.4 權限與生命周期管理
權限設置:在創建共享內存時,可以通過shmget函數的shmflg參數設置共享內存的訪問權限。權限設置與文件權限類似,使用三位八進制數表示,分別對應所有者、組和其他用戶的讀、寫、執行權限。例如,0666表示所有者、組和其他用戶都有讀寫權限;0644表示所有者有讀寫權限,組和其他用戶只有讀權限。合理的權限設置可以保證共享內存的安全性,防止未經授權的進程訪問或修改共享內存中的數據。比如,在一個多用戶的服務器環境中,如果有一些共享內存用于存儲敏感數據,就需要嚴格設置權限,只允許特定的用戶或用戶組訪問。
生命周期管理:共享內存的生命周期獨立于使用它的進程。當最后一個使用共享內存的進程將其分離(調用shmdt)后,共享內存仍然存在于系統中,直到被顯式刪除(調用shmctl并傳入IPC_RMID命令)或系統重啟。這就需要開發者在使用共享內存時,謹慎管理其生命周期。在程序結束時,應該確保及時刪除不再使用的共享內存,以避免內存泄漏和資源浪費。
比如,在一個長期運行的服務器程序中,如果不斷創建共享內存而不刪除,隨著時間的推移,系統中會殘留大量無用的共享內存,占用系統資源,影響系統性能。同時,在刪除共享內存之前,要確保所有使用該共享內存的進程都已經將其分離,否則可能會導致其他進程訪問非法內存地址,引發程序崩潰等問題。
四、深入共享內存的實現原理
4.1 內核視角:共享內存的數據結構
在 Linux 內核中,有幾個關鍵的數據結構用于管理共享內存,其中struct shmid_kernel和struct shmid_ds起著重要作用。
struct shmid_kernel是內核中用于表示共享內存對象的內部數據結構,它包含了共享內存的各種屬性和狀態信息。雖然這個結構體對于普通開發者來說并不直接可見,但了解它有助于深入理解共享內存的工作機制。它記錄了共享內存段的大小、所屬的進程組、創建時間、最后訪問時間等重要信息。例如,通過這個結構體,內核可以跟蹤共享內存的使用情況,判斷哪些進程正在使用它,以及何時需要回收共享內存資源。
而struct shmid_ds則是一個更常用的數據結構,開發者可以通過shmctl函數來訪問和修改這個結構體中的信息。它的定義如下:
struct shmid_ds {
struct ipc_perm shm_perm; /* 所有權和權限相關信息 */
size_t shm_segsz; /* 共享內存段的大小(字節) */
time_t shm_atime; /* 最后一次連接到共享內存的時間 */
time_t shm_dtime; /* 最后一次從共享內存分離的時間 */
time_t shm_ctime; /* 共享內存狀態最后一次改變的時間 */
pid_t shm_cpid; /* 創建共享內存的進程ID */
pid_t shm_lpid; /* 最后一次執行shmat或shmdt操作的進程ID */
shmatt_t shm_nattch; /* 當前連接到共享內存的進程數 */
...
};
- shm_perm:包含了共享內存的所有權和權限信息,如所有者 ID、組 ID、訪問權限等,類似于文件的權限管理。例如,通過設置shm_perm中的權限位,可以控制哪些進程可以訪問共享內存,以及以何種方式(讀、寫等)訪問。
- shm_segsz:明確了共享內存段的大小,以字節為單位。在創建共享內存時,開發者需要根據實際需求指定合適的大小。比如,在一個簡單的進程間通信場景中,如果只是傳遞少量的狀態信息,可能只需要分配幾十或幾百字節的共享內存;而在一個需要共享大量數據的場景中,如共享視頻幀數據,可能需要分配幾兆甚至更大的共享內存空間。
- shm_atime、shm_dtime和shm_ctime:分別記錄了共享內存的連接時間、分離時間和狀態改變時間。這些時間戳對于調試和性能分析非常有幫助,例如,通過查看shm_atime和shm_dtime,可以了解進程對共享內存的使用時間間隔,判斷是否存在長時間占用共享內存而不釋放的情況;shm_ctime則可以幫助開發者追蹤共享內存的狀態變化歷史。
- shm_cpid和shm_lpid:記錄了創建共享內存的進程 ID 和最后一次執行shmat或shmdt操作的進程 ID。這對于調試和管理共享內存的使用非常有用,當出現共享內存相關的問題時,可以通過這些 ID 來追溯問題的源頭,查看是哪個進程創建了共享內存,以及最近哪些進程對共享內存進行了連接或分離操作。
- shm_nattch:表示當前連接到共享內存的進程數。內核通過這個字段來管理共享內存的生命周期,當shm_nattch變為 0 時,并且沒有其他進程持有對該共享內存的引用,內核可以考慮回收該共享內存資源。例如,在一個多進程協作的服務器程序中,當所有使用共享內存的進程都完成任務并與共享內存分離后,shm_nattch變為 0,此時內核可以及時釋放共享內存,避免內存資源的浪費。
4.2 映射機制:虛擬內存與物理內存的橋梁
共享內存能夠實現高效的進程間通信,關鍵在于其巧妙的內存映射機制,通過頁表將虛擬內存映射到物理內存。
在 Linux 系統中,每個進程都有自己獨立的虛擬地址空間。當進程創建或連接到共享內存時,操作系統會在進程的虛擬地址空間中分配一段虛擬地址范圍,并將這段虛擬地址與共享內存所在的物理內存區域建立映射關系。這個映射關系是通過頁表來維護的。
頁表是一種數據結構,它記錄了虛擬頁號(VPN,Virtual Page Number)與物理頁號(PPN,Physical Page Number)之間的對應關系。當進程訪問共享內存中的數據時,CPU 首先會根據當前進程的頁表,將虛擬地址中的虛擬頁號轉換為物理頁號,然后再加上頁內偏移量,得到實際的物理內存地址,從而訪問到共享內存中的數據。
例如,假設進程 A 和進程 B 共享一塊大小為 4KB 的共享內存。當進程 A 創建共享內存時,操作系統會在物理內存中分配一塊 4KB 大小的內存區域,并為這塊區域分配一個物理頁號。然后,操作系統在進程 A 的頁表中創建一個頁表項,將虛擬頁號與該物理頁號關聯起來,使得進程 A 可以通過虛擬地址訪問這塊共享內存。當進程 B 連接到該共享內存時,操作系統同樣在進程 B 的頁表中創建一個頁表項,將其虛擬地址空間中的一段虛擬頁號也映射到相同的物理頁號上。這樣,進程 A 和進程 B 就可以通過各自的虛擬地址訪問同一塊物理內存區域,實現數據共享。
在這個過程中,如果所需的共享內存數據不在物理內存中(例如,由于內存不足,共享內存的部分數據被交換到磁盤上),會發生頁面錯誤(page fault)。此時,操作系統會負責將所需的數據從磁盤讀入物理內存,并更新頁表,確保進程能夠正確訪問共享內存。這種動態的內存管理機制使得共享內存能夠在有限的物理內存條件下高效運行,同時也保證了進程間通信的穩定性和可靠性。
Linux提供了內存映射函數mmap, 它把文件內容映射到一段內存上(準確說是虛擬內存上,運行著進程), 通過對這段內存的讀取和修改, 實現對文件的讀取和修改。mmap()系統調用使得進程之間可以通過映射一個普通的文件實現共享內存。普通文件映射到進程地址空間后,進程可以像訪問內存的方式對文件進行訪問,不需要其他內核態的系統調用(read,write)去操作。
這里是講設備或者硬盤存儲的一塊空間映射到物理內存,然后操作這塊物理內存就是在操作實際的硬盤空間,不需要經過內核態傳遞。比如你的硬盤上有一個文件,你可以使用linux系統提供的mmap接口,將這個文件映射到進程一塊虛擬地址空間,這塊空間會對應一塊物理內存,當你讀寫這塊物理空間的時候,就是在讀取實際的磁盤文件,就是這么直接高效。通常諸如共享庫的加載都是通過內存映射的方式加載到物理內存的。
mmap系統調用并不完全是為了共享內存來設計的,它本身提供了不同于一般對普通文件的訪問的方式,進程可以像讀寫內存一樣對普通文件進行操作,IPC的共享內存是純粹為了共享。
內存映射指的是將 :進程中的1個虛擬內存區域 & 1個磁盤上的對象,使得二者存在映射關系。當然,也可以多個進程同時映射到一個對象上面。
實現過程
- 內存映射的實現過程主要是通過Linux系統下的系統調用函數:mmap()
- 該函數的作用 = 創建虛擬內存區域 + 與共享對象建立映射關系
其函數原型、具體使用 & 內部流程 如下:
/** * 函數原型 */
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
/**
* 具體使用(用戶進程調用mmap())
* 下述代碼即常見了一片大小 = MAP_SIZE的接收緩存區 & 關聯到共享對象中(即建立映射)
*/
mmap(NULL, MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);
/**
* 內部原理
* 步驟1:創建虛擬內存區域
* 步驟2:實現地址映射關系,即:進程的虛擬地址空間 ->> 共享對象
* 注:
* a. 此時,該虛擬地址并沒有任何數據關聯到文件中,僅僅只是建立映射關系
* b. 當其中1個進程對虛擬內存寫入數據時,則真正實現了數據的可見
*/
優點
進程在讀寫磁盤的時候,大概的流程是:
以write 為例:進程(用戶空間) -> 系統調用,進入內核 -> 將要寫入的數據從用戶空間拷貝到內核空間的緩存區 -> 調用磁盤驅動 -> 寫在磁盤上面。
使用mmap之后進程(用戶空間)--> 讀寫映射的內存 --> 寫在磁盤上面。(這樣的優點是 避免了頻繁的進入內核空間,進行系統調用,提高了效率)
(1)mmap系統調用
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
這就是mmap系統調用的接口,mmap函數成功返回指向內存區域的指針,失敗返回MAP_FAILED。
- addr,某個特定的地址作為起始地址,當被設置為NULL,標識系統自動分配地址。實實在在的物理區域。
- length說的是內存段的長度。
- prot是用來設定內存段的訪問權限。
PROT_READ 內存段可讀
PROT_WRITE 內存段可寫
PROT_EXEC 內存段可執行
PROT_NONE 內存段不能被訪問
flags參數控制內存段內容被修改以后程序的行為。
MAP_SHARED 進程間共享內存,對該內存段修改反映到映射文件中。提供了POSIX共享內存
MAP_PRIVATE 內存段為調用進程所私有。對該內存段的修改不會反映到映射文件
MAP_ANNOYMOUS 這段內存不是從文件映射而來的。內容被初始化為全0
MAP_FIXED 內存段必須位于start參數指定的地址處,start必須是頁大小的整數倍(4K整數倍)
MAP_HUGETLB 按照大內存頁面來分配內存空間
fd參數是用來被映射文件對應的文件描述符,通過open系統調用得到,offset設定從何處進行映射。
(2)mmap用于共享內存的方式
- 我們可以使用普通文件進行提供內存映射,例如,open系統調用打開一個文件,然后進行mmap操作,得到共享內存,這種方式適用于任何進程之間。
- 可以使用特殊文件進行匿名內存映射,這個相對的是具有血緣關系的進程之間,當父進程調用mmap,然后進行fork,這樣父進程創建的子進程會繼承父進程匿名映射后的地址空間,這樣,父子進程之間就可以進行通信了。相當于是mmap的返回地址此時是父子進程同時來維護。
- 另外POSIX版本的共享內存底層也是使用了mmap。所以,共享內存在在posix上一定程度上就是指的內存映射了。
五、Mmap和System V共享內存的比較
共享內存:
圖片
這是System V版本的共享內存(以下我們統稱為shm),下面看下mmap的:
圖片
mmap是在磁盤上建立一個文件,每個進程地址空間中開辟出一塊空間進行映射。而shm共享內存,每個進程最終會映射到同一塊物理內存。shm保存在物理內存,這樣讀寫的速度肯定要比磁盤要快,但是存儲量不是特別大,相對于shm來說,mmap更加簡單,調用更加方便,所以這也是大家都喜歡用的原因。
另外mmap有一個好處是當機器重啟,因為mmap把文件保存在磁盤上,這個文件還保存了操作系統同步的映像,所以mmap不會丟失,但是shmget在內存里面就會丟失,總之,共享內存是在內存中創建空間,每個進程映射到此處。內存映射是創建一個文件,并且映射到每個進程開辟的空間中,但在posix中的共享內存就是指這種使用文件的方式“內存映射”。
六、POSIX共享內存
6.1 IPC機制
共享內存是最快的可用IPC形式。它允許多個不相關(無親緣關系)的進程去訪問同一部分邏輯內存。
如果需要在兩個進程之間傳輸數據,共享內存將是一種效率極高的解決方案。一旦這樣的內存區映射到共享它的進程的地址空間,這些進程間數據的傳輸就不再涉及內核。這樣就可以減少系統調用時間,提高程序效率。
共享內存是由IPC為一個進程創建的一個特殊的地址范圍,它將出現在進程的地址空間中。其他進程可以把同一段共享內存段“連接到”它們自己的地址空間里去。所有進程都可以訪問共享內存中的地址。如果一個進程向這段共享內存寫了數據,所做的改動會立刻被有訪問同一段共享內存的其他進程看到。
要注意的是共享內存本身沒有提供任何同步功能。也就是說,在第一個進程結束對共享內存的寫操作之前,并沒有什么自動功能能夠預防第二個進程開始對它進行讀操作。共享內存的訪問同步問題必須由程序員負責。可選的同步方式有互斥鎖、條件變量、讀寫鎖、紀錄鎖、信號燈。
實際上,進程之間在共享內存時,并不總是讀寫少量數據后就解除映射,有新的通信時,再重新建立共享內存區域。而是保持共享區域,直到通信完畢為止。
6.2 POSIX共享內存API
使用POSIX共享內存需要用到下面這些API:
#include <sys/types.h>
#include <sys/stat.h> /* For mode constants */
#include <sys/mman.h>
#include <fcntl.h> /* For O_* constants */
#include <unistd.h>
int shm_open(const char *name, int oflag, mode_t mode);
int shm_unlink(const char *name);
int ftruncate(int fildes, off_t length);
void *mmap(void *addr, size_t len, int prot, int flags, int fildes, off_t off);
int munmap(void *addr, size_t len);
int close(int fildes);
int fstat(int fildes, struct stat *buf);
int fchown(int fildes, uid_t owner, gid_t group);
int fchmod(int fildes, mode_t mode);
- shm_open:穿件并打開一個新的共享內存對象或者打開一個既存的共享內存對象, 與函數open的用法是類似的函數返回值是一個文件描述符,會被下面的API使用。
- ftruncate:設置共享內存對象的大小,新創建的共享內存對象大小為0。
- mmap:將共享內存對象映射到調用進程的虛擬地址空間。
- munmap:取消共享內存對象到調用進程的虛擬地址空間的映射。
- shm_unlink:刪除一個共享內存對象名字。
- close:當shm_open函數返回的文件描述符不再使用時,使用close函數關閉它。
- fstat:獲得共享內存對象屬性的stat結構體. 結構體中會包含共享內存對象的大小(st_size), 權限(st_mode), 所有者(st_uid), 歸屬組 (st_gid)。
- fchown:改變一個共享內存對象的所有權。
- fchmod:改變一個共享內存對象的權限。
七、共享內存的同步問題
雖然共享內存為進程間通信提供了高效的數據共享方式,但由于多個進程可以同時訪問同一塊內存區域,這就帶來了同步和互斥的問題。如果沒有合適的同步機制,可能會出現以下情況:
- 競態條件(Race Condition):當多個進程同時訪問和修改共享內存中的數據時,由于進程執行的先后順序不確定,可能導致最終的數據結果不可預測。例如,有兩個進程 P1 和 P2 同時讀取共享內存中的一個整數變量 count,然后各自對其加 1,最后再寫回共享內存。如果沒有同步機制,可能會出現 P1 和 P2 讀取到相同的 count 值,然后各自加 1 后寫回,這樣 count 只增加了 1,而不是預期的 2 。
- 數據不一致性:一個進程正在對共享內存中的數據進行修改時,另一個進程可能同時讀取這些未完全修改的數據,從而導致數據不一致。比如,一個進程正在更新共享內存中的一個復雜數據結構,在更新過程中,另一個進程讀取該數據結構,可能會讀到部分更新的數據,使數據處于不一致的狀態,進而導致程序出現錯誤。
解決方案:信號量與互斥鎖的應用
為了解決共享內存帶來的同步和互斥問題,通常會使用信號量(Semaphore)和互斥鎖(Mutex)等同步機制。
(1)信號量:信號量是一種計數器,用于控制對共享資源的訪問。它可以用來實現進程間的同步和互斥。在共享內存的場景中,信號量可以用來控制對共享內存的訪問權限。例如,我們可以創建一個信號量,初始值設為 1,表示共享內存資源可用。當一個進程要訪問共享內存時,它首先嘗試獲取信號量(通過對信號量執行 P 操作,即減 1 操作)。如果信號量的值大于等于 0,說明資源可用,進程可以繼續執行對共享內存的訪問操作;如果信號量的值小于 0,說明資源已被其他進程占用,該進程會被阻塞,直到信號量的值大于等于 0。當進程完成對共享內存的訪問后,它會釋放信號量(通過對信號量執行 V 操作,即加 1 操作),通知其他進程可以訪問共享內存。在 Linux 中,有 POSIX 有名信號量、POSIX 無名信號量和 System V 信號量等不同類型,開發者可以根據具體需求選擇使用。例如,使用 POSIX 有名信號量實現共享內存同步的代碼示例如下:
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#define SHM_SIZE 1024
#define SEM_NAME "/my_semaphore"
int main() {
int shm_fd;
void *shared_memory;
sem_t *sem;
// 創建共享內存對象
shm_fd = shm_open("/my_shared_memory", O_CREAT | O_RDWR, 0666);
if (shm_fd == -1) {
perror("shm_open");
exit(1);
}
// 配置共享內存大小
if (ftruncate(shm_fd, SHM_SIZE) == -1) {
perror("ftruncate");
exit(1);
}
// 將共享內存映射到進程地址空間
shared_memory = mmap(0, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
if (shared_memory == MAP_FAILED) {
perror("mmap");
exit(1);
}
// 創建信號量
sem = sem_open(SEM_NAME, O_CREAT, 0666, 1);
if (sem == SEM_FAILED) {
perror("sem_open");
exit(1);
}
// 等待信號量,獲取共享內存訪問權限
if (sem_wait(sem) == -1) {
perror("sem_wait");
exit(1);
}
// 訪問共享內存
printf("Accessed shared memory: %s\n", (char *)shared_memory);
// 釋放信號量,允許其他進程訪問共享內存
if (sem_post(sem) == -1) {
perror("sem_post");
exit(1);
}
// 取消映射并關閉共享內存
if (munmap(shared_memory, SHM_SIZE) == -1) {
perror("munmap");
exit(1);
}
if (close(shm_fd) == -1) {
perror("close");
exit(1);
}
// 刪除共享內存對象
if (shm_unlink("/my_shared_memory") == -1) {
perror("shm_unlink");
exit(1);
}
// 關閉并刪除信號量
if (sem_close(sem) == -1) {
perror("sem_close");
exit(1);
}
if (sem_unlink(SEM_NAME) == -1) {
perror("sem_unlink");
exit(1);
}
return 0;
}
(2)互斥鎖:互斥鎖是一種二元信號量,用于保證在同一時刻只有一個進程能夠訪問共享資源,即實現對共享內存的互斥訪問。當一個進程獲取到互斥鎖后,其他進程如果試圖獲取該互斥鎖,會被阻塞,直到持有互斥鎖的進程釋放它。在 Linux 中,使用 pthread 庫中的互斥鎖相關函數來實現互斥鎖的操作。例如,初始化互斥鎖可以使用pthread_mutex_init函數,獲取互斥鎖使用pthread_mutex_lock函數,釋放互斥鎖使用pthread_mutex_unlock函數,銷毀互斥鎖使用pthread_mutex_destroy函數。以下是使用互斥鎖實現共享內存同步的簡單代碼示例:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#define SHM_SIZE 1024
typedef struct {
pthread_mutex_t mutex;
char data[SHM_SIZE];
} SharedData;
int main() {
int shm_fd;
SharedData *shared_data;
// 創建共享內存對象
shm_fd = shm_open("/my_shared_memory", O_CREAT | O_RDWR, 0666);
if (shm_fd == -1) {
perror("shm_open");
exit(1);
}
// 配置共享內存大小
if (ftruncate(shm_fd, sizeof(SharedData)) == -1) {
perror("ftruncate");
exit(1);
}
// 將共享內存映射到進程地址空間
shared_data = (SharedData *)mmap(0, sizeof(SharedData), PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
if (shared_data == MAP_FAILED) {
perror("mmap");
exit(1);
}
// 初始化互斥鎖
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
if (pthread_mutex_init(&shared_data->mutex, &attr) != 0) {
perror("pthread_mutex_init");
exit(1);
}
// 獲取互斥鎖,訪問共享內存
if (pthread_mutex_lock(&shared_data->mutex) != 0) {
perror("pthread_mutex_lock");
exit(1);
}
printf("Accessed shared memory: %s\n", shared_data->data);
// 釋放互斥鎖
if (pthread_mutex_unlock(&shared_data->mutex) != 0) {
perror("pthread_mutex_unlock");
exit(1);
}
// 取消映射并關閉共享內存
if (munmap(shared_data, sizeof(SharedData)) == -1) {
perror("munmap");
exit(1);
}
if (close(shm_fd) == -1) {
perror("close");
exit(1);
}
// 刪除共享內存對象
if (shm_unlink("/my_shared_memory") == -1) {
perror("shm_unlink");
exit(1);
}
return 0;
}
通過合理使用信號量和互斥鎖等同步機制,可以有效地解決共享內存帶來的同步和互斥問題,確保多個進程能夠安全、高效地共享內存數據。
八、實際應用場景及常見問題解答
8.1 實際應用場景
(1)數據庫緩存優化
在數據庫系統中,共享內存發揮著至關重要的作用,尤其是在緩存優化方面。以 Oracle 數據庫為例,它使用共享全局區(SGA,Shared Global Area)來實現共享內存。SGA 是一個共享的內存結構,用于存儲數據塊、SQL 語句和其他共享信息 。
當數據庫接收到查詢請求時,首先會在共享內存的緩存中查找相關數據。如果數據存在于緩存中,即命中緩存,數據庫可以直接從共享內存中讀取數據并返回給用戶,這大大減少了磁盤 I/O 操作。因為從磁盤讀取數據的速度遠遠低于從內存讀取數據的速度,通過共享內存緩存數據,可以顯著提高查詢性能。例如,在一個高并發的在線交易系統中,大量用戶頻繁查詢訂單信息。如果沒有共享內存緩存,每次查詢都需要從磁盤讀取數據,磁盤 I/O 很快就會成為系統的瓶頸,導致查詢響應時間變長。而使用共享內存緩存訂單數據后,大部分查詢可以直接從內存中獲取數據,大大提高了系統的響應速度和吞吐量。
同時,共享內存還可以減少內存的重復使用,提高內存利用率。多個數據庫進程可以共享同一塊內存區域,避免了每個進程都單獨開辟內存來存儲相同的數據,從而節省了內存資源。比如,在一個包含多個數據庫實例的系統中,這些實例可以共享 SGA 中的數據緩存,減少了內存的浪費,使得系統能夠在有限的內存資源下高效運行。
(2)高性能計算中的數據共享
在高性能計算領域,共享內存同樣有著廣泛的應用。在大規模的科學計算和工程模擬中,往往需要處理海量的數據和復雜的計算任務,這些任務通常需要多個處理器核心或多個計算節點協同工作。
以分子動力學模擬為例,這是一種用于研究分子系統微觀行為的計算方法,需要對大量分子的運動軌跡進行模擬計算。在計算過程中,不同的處理器核心需要共享分子的初始位置、速度等數據,以及模擬過程中的中間結果。通過共享內存,這些數據可以被多個處理器核心直接訪問,避免了數據在不同處理器之間通過網絡或其他方式傳輸的開銷,提高了計算效率。
再比如,在氣象預報模型中,需要對全球范圍內的氣象數據進行分析和預測。這些數據量巨大,計算任務復雜,通常會在分布式計算集群上進行。共享內存可以用于在不同計算節點之間共享氣象數據和計算參數,使得各個節點能夠協同工作,共同完成氣象預報的計算任務。在這種場景下,共享內存不僅提高了數據共享的效率,還減少了節點之間的通信開銷,對于提高整個高性能計算系統的性能起著關鍵作用。
8.2 避坑指南與常見問題解答
在使用 Linux 共享內存的過程中,開發者常常會遇到一些棘手的問題,下面我們就來總結一下這些常見問題,并給出相應的解決方案。
(1)共享內存創建失敗
①問題描述:調用shmget函數創建共享內存時,返回值為 -1,導致創建失敗。
②可能原因
- 系統資源限制:系統對共享內存的數量和大小有限制,如SHMMAX(單個共享內存段的最大大小)和SHMMNI(系統中共享內存段的最大數量)等參數。如果要創建的共享內存超過了這些限制,就會導致創建失敗。例如,當系統的SHMMAX設置為 32MB,而你嘗試創建一個 64MB 的共享內存段時,就會失敗。
- 權限不足:創建共享內存需要適當的權限。如果當前用戶沒有足夠的權限(如在一些安全限制較嚴格的系統中),shmget調用也會失敗。比如,普通用戶在沒有特殊權限配置的情況下,無法創建共享內存。
③解決方案
檢查系統參數:通過cat /proc/sys/kernel/shmmax和cat /proc/sys/kernel/shmmni等命令查看系統的共享內存參數設置。如果需要,可以通過修改/etc/sysctl.conf文件來調整這些參數,例如:
echo "kernel.shmmax = 2147483648" >> /etc/sysctl.conf
sysctl -p
上述命令將SHMMAX設置為 2GB,并使其立即生效。
④確認權限:確保當前用戶具有創建共享內存的權限,必要時可以切換到具有足夠權限的用戶(如 root 用戶)來創建共享內存,或者通過修改文件權限和用戶組等方式來賦予相應權限。
(2)共享內存訪問異常
①問題描述:在進程訪問共享內存時,出現段錯誤(Segmentation Fault)或其他訪問異常。
②可能原因
- 未正確映射共享內存:調用shmat函數時,可能由于參數設置錯誤,導致共享內存沒有正確映射到進程的地址空間。例如,shmat返回的指針為(void *)-1,表示映射失敗,但程序沒有正確處理這種情況,仍然嘗試使用該指針訪問共享內存,就會導致訪問異常。
- 內存越界訪問:在對共享內存進行讀寫操作時,沒有正確檢查邊界條件,導致訪問超出了共享內存的范圍。比如,共享內存大小為 1024 字節,而程序嘗試寫入 2048 字節的數據,就會造成內存越界。
- 同步問題:多個進程同時訪問共享內存時,如果沒有正確的同步機制(如信號量、互斥鎖等),可能會導致數據競爭和訪問沖突,進而引發訪問異常。例如,一個進程正在修改共享內存中的數據,另一個進程同時讀取這些未完全修改的數據,就可能導致數據不一致和訪問錯誤。
③解決方案
檢查映射結果:在調用shmat后,仔細檢查返回值。如果返回(void *)-1,則根據errno變量的值進行錯誤處理,例如:
void *shared_mem = shmat(shmid, NULL, 0);
if (shared_mem == (void *)-1) {
perror("shmat failed");
exit(EXIT_FAILURE);
}
- 邊界檢查:在對共享內存進行讀寫操作時,務必進行嚴格的邊界檢查,確保不會越界訪問。例如,在寫入數據時,要檢查數據大小是否超過共享內存的剩余空間;在讀取數據時,要確保讀取的長度不超過共享內存的有效范圍。
- 完善同步機制:引入合適的同步機制,如使用信號量或互斥鎖來確保對共享內存的訪問是安全的。在訪問共享內存之前,先獲取同步鎖(如信號量的 P 操作或互斥鎖的加鎖操作),訪問完成后再釋放同步鎖(如信號量的 V 操作或互斥鎖的解鎖操作)。
(3)共享內存未及時釋放
①問題描述:共享內存不再被使用,但沒有被及時刪除,導致系統資源浪費。
②可能原因
- 程序邏輯錯誤:在程序中沒有正確處理共享內存的生命周期,例如沒有在合適的時機調用shmctl函數并傳入IPC_RMID命令來刪除共享內存。
- 進程異常退出:使用共享內存的進程由于某種原因(如程序崩潰、收到異常信號等)異常退出,而沒有來得及執行共享內存的刪除操作。
③解決方案
優化程序邏輯:在程序設計時,明確共享內存的生命周期,確保在不再需要共享內存時,及時調用shmctl函數刪除共享內存。例如,在程序結束時,添加如下代碼:
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl IPC_RMID failed");
exit(EXIT_FAILURE);
}
捕獲異常信號:在進程中捕獲常見的異常信號(如SIGSEGV、SIGABRT等),在信號處理函數中添加釋放共享內存的操作。例如,使用signal函數注冊信號處理函數:
#include <signal.h>
void cleanup_shared_memory(int signum) {
// 釋放共享內存相關資源
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl IPC_RMID in signal handler failed");
}
exit(EXIT_FAILURE);
}
int main() {
// 注冊信號處理函數
signal(SIGSEGV, cleanup_shared_memory);
signal(SIGABRT, cleanup_shared_memory);
// 其他程序邏輯
}
通過上述方法,可以有效避免共享內存未及時釋放的問題,提高系統資源的利用率。