自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

解鎖Linux共享內(nèi)存:進程間通信的超高速通道

系統(tǒng) Linux
Linux 采用虛擬內(nèi)存管理機制,為每個進程分配獨立的虛擬地址空間。這意味著每個進程都可以認為自己擁有 4GB(32 位系統(tǒng))或更大(64 位系統(tǒng))的連續(xù)內(nèi)存空間,而不必擔(dān)心物理內(nèi)存的實際大小和其他進程的干擾。

在Linux系統(tǒng)的進程間通信 “江湖” 中,眾多通信方式各顯神通。管道,如同隱秘的地下通道,讓有親緣關(guān)系的進程能夠悄然傳遞信息;消息隊列則似郵局,進程可投遞和接收格式化的消息包裹。然而,有一種通信方式卻以其獨特的 “高速” 特性脫穎而出,它就是共享內(nèi)存。想象一下,進程們原本各自生活在獨立的 “小天地” 里,有著自己專屬的虛擬地址空間。但共享內(nèi)存卻如同神奇的 “任意門”,打破了進程間的隔閡,讓多個進程能夠直接訪問同一塊內(nèi)存區(qū)域。這種獨特的機制,使得數(shù)據(jù)在進程間的傳遞無需繁瑣的復(fù)制過程,極大地提升了通信效率,堪稱進程間通信的超高速通道。

在使用共享內(nèi)存時,需要注意對于并發(fā)訪問的控制,如使用鎖或其他同步機制來保證數(shù)據(jù)的一致性和安全性。此外,還需要謹慎處理資源管理問題,確保正確地釋放共享內(nèi)存以避免內(nèi)存泄漏。接下來,就讓我們一同深入探索 Linux 共享內(nèi)存的奧秘,揭開它神秘的面紗,看看它是如何在 Linux 系統(tǒng)中發(fā)揮這一獨特且強大的作用 。

一、Linux內(nèi)存管理初窺

1.1 虛擬內(nèi)存與物理內(nèi)存

Linux 采用虛擬內(nèi)存管理機制,為每個進程分配獨立的虛擬地址空間。這意味著每個進程都可以認為自己擁有 4GB(32 位系統(tǒng))或更大(64 位系統(tǒng))的連續(xù)內(nèi)存空間,而不必擔(dān)心物理內(nèi)存的實際大小和其他進程的干擾。虛擬內(nèi)存與物理內(nèi)存通過內(nèi)存映射機制建立聯(lián)系,進程訪問的虛擬地址會被轉(zhuǎn)換為實際的物理地址。

舉個例子,當(dāng)你在 Linux 系統(tǒng)上同時運行多個程序時,每個程序都覺得自己獨占了大量內(nèi)存,但實際上物理內(nèi)存是有限的。通過虛擬內(nèi)存管理,操作系統(tǒng)可以巧妙地在物理內(nèi)存和磁盤之間交換數(shù)據(jù),使得系統(tǒng)能夠運行比物理內(nèi)存更大的程序集。就好比一個小型圖書館,雖然書架空間有限(物理內(nèi)存),但通過一個龐大的倉庫(磁盤)來存放暫時不用的書籍(數(shù)據(jù)),當(dāng)讀者需要某本書時,管理員(操作系統(tǒng))會從倉庫中取出并放到書架上供讀者使用。

1.2 內(nèi)存分頁

為了更高效地管理內(nèi)存,Linux 采用內(nèi)存分頁機制。將虛擬內(nèi)存和物理內(nèi)存按照固定大小的頁(通常為 4KB)進行劃分,頁是內(nèi)存管理的最小單位。操作系統(tǒng)通過維護頁表來記錄虛擬頁和物理頁之間的映射關(guān)系,當(dāng)進程訪問某個虛擬地址時,CPU 會根據(jù)頁表將其轉(zhuǎn)換為對應(yīng)的物理地址。

想象一下,內(nèi)存就像一本巨大的書籍,每一頁都有固定的頁碼(虛擬頁號和物理頁號)。當(dāng)你想要查找書中的某個內(nèi)容(訪問內(nèi)存數(shù)據(jù))時,通過目錄(頁表)可以快速定位到具體的頁碼,從而找到所需內(nèi)容。

1.3 內(nèi)存分配與回收

內(nèi)存管理包括內(nèi)存的分配和回收。當(dāng)進程需要內(nèi)存時,它會向操作系統(tǒng)請求分配內(nèi)存,操作系統(tǒng)根據(jù)一定的算法從空閑內(nèi)存中分配相應(yīng)大小的內(nèi)存塊給進程;當(dāng)進程不再需要某些內(nèi)存時,它會將這些內(nèi)存釋放回操作系統(tǒng),以便操作系統(tǒng)重新分配給其他需要的進程。

例如,當(dāng)你在 Linux 系統(tǒng)上運行一個新的程序時,程序會向操作系統(tǒng)申請內(nèi)存來存放代碼和數(shù)據(jù)。操作系統(tǒng)會從空閑內(nèi)存池中找到合適大小的內(nèi)存塊分配給該程序。當(dāng)程序運行結(jié)束后,它占用的內(nèi)存會被操作系統(tǒng)回收,重新加入空閑內(nèi)存池,等待下一個程序的請求。

二、共享內(nèi)存詳解

2.1 共享內(nèi)存是什么

共享內(nèi)存是一種高效的進程間通信(IPC,Inter - Process Communication)機制,它允許兩個或多個進程直接訪問同一塊物理內(nèi)存區(qū)域。簡單來說,就好比多個房間(進程)都有一扇門可以直接通向同一個儲物間(共享內(nèi)存),大家可以直接在這個儲物間里存放和取用物品(數(shù)據(jù)) 。

在 Linux 系統(tǒng)中,共享內(nèi)存的實現(xiàn)依賴于操作系統(tǒng)的支持。當(dāng)一個進程創(chuàng)建共享內(nèi)存時,操作系統(tǒng)會在物理內(nèi)存中分配一塊區(qū)域,并為這塊區(qū)域生成一個唯一的標(biāo)識符。其他進程可以通過這個標(biāo)識符將該共享內(nèi)存映射到自己的虛擬地址空間中,從而實現(xiàn)對共享內(nèi)存的訪問。

2.2 為什么要用共享內(nèi)存

在進程間通信的眾多方式中,共享內(nèi)存之所以備受青睞,是因為它具有其他方式難以比擬的優(yōu)勢。

首先,與管道和消息隊列等通信方式相比,共享內(nèi)存的速度極快。管道和消息隊列在數(shù)據(jù)傳輸時,需要進行多次數(shù)據(jù)拷貝,數(shù)據(jù)要在內(nèi)核空間和用戶空間之間來回傳遞,這會消耗大量的時間和系統(tǒng)資源。而共享內(nèi)存則不同,多個進程直接訪問同一塊內(nèi)存區(qū)域,數(shù)據(jù)不需要在不同進程的地址空間之間拷貝,大大減少了數(shù)據(jù)傳輸?shù)拈_銷,提高了通信效率。例如,在一個實時數(shù)據(jù)處理系統(tǒng)中,多個進程需要頻繁地交換大量數(shù)據(jù),如果使用管道或消息隊列,可能會因為數(shù)據(jù)傳輸?shù)难舆t而影響系統(tǒng)的實時性;而使用共享內(nèi)存,就可以快速地傳遞數(shù)據(jù),滿足系統(tǒng)對實時性的要求。

其次,共享內(nèi)存的使用非常靈活。它可以用于任何類型的進程間通信,無論是有親緣關(guān)系的進程(如父子進程)還是毫無關(guān)系的進程,都可以通過共享內(nèi)存進行數(shù)據(jù)共享和交互。而且,共享內(nèi)存區(qū)域可以存儲各種類型的數(shù)據(jù)結(jié)構(gòu),開發(fā)者可以根據(jù)實際需求自定義數(shù)據(jù)格式,這為復(fù)雜應(yīng)用場景的實現(xiàn)提供了便利。比如,在一個多進程協(xié)作的圖形處理程序中,不同進程可以通過共享內(nèi)存共享圖像數(shù)據(jù)和處理參數(shù),各自完成不同的處理任務(wù),如一個進程負責(zé)圖像的濾波處理,另一個進程負責(zé)圖像的邊緣檢測,共享內(nèi)存使得它們能夠高效地協(xié)同工作。

此外,共享內(nèi)存還能有效地節(jié)省內(nèi)存資源。多個進程共享同一塊內(nèi)存區(qū)域,而不是每個進程都單獨開辟一塊內(nèi)存來存儲相同的數(shù)據(jù),這在內(nèi)存資源有限的情況下顯得尤為重要。例如,在一個服務(wù)器系統(tǒng)中,可能同時有多個進程需要訪問一些公共的配置信息或緩存數(shù)據(jù),使用共享內(nèi)存可以避免這些數(shù)據(jù)在每個進程中重復(fù)存儲,從而提高內(nèi)存的利用率。

2.3 共享內(nèi)存原理

共享內(nèi)存是System V版本的最后一個進程間通信方式。共享內(nèi)存,顧名思義就是允許兩個不相關(guān)的進程訪問同一個邏輯內(nèi)存,共享內(nèi)存是兩個正在運行的進程之間共享和傳遞數(shù)據(jù)的一種非常有效的方式。不同進程之間共享的內(nèi)存通常為同一段物理內(nèi)存。進程可以將同一段物理內(nèi)存連接到他們自己的地址空間中,所有的進程都可以訪問共享內(nèi)存中的地址。如果某個進程向共享內(nèi)存寫入數(shù)據(jù),所做的改動將立即影響到可以訪問同一段共享內(nèi)存的任何其他進程。

特別提醒:共享內(nèi)存并未提供同步機制,也就是說,在第一個進程結(jié)束對共享內(nèi)存的寫操作之前,并無自動機制可以阻止第二個進程開始對它進行讀取,所以我們通常需要用其他的機制來同步對共享內(nèi)存的訪問,例如信號量。

在Linux中,每個進程都有屬于自己的進程控制塊(PCB)和地址空間(Addr Space),并且都有一個與之對應(yīng)的頁表,負責(zé)將進程的虛擬地址與物理地址進行映射,通過內(nèi)存管理單元(MMU)進行管理。兩個不同的虛擬地址通過頁表映射到物理空間的同一區(qū)域,它們所指向的這塊區(qū)域即共享內(nèi)存。

共享內(nèi)存的通信原理示意圖:

圖片圖片

對于上圖我的理解是:當(dāng)兩個進程通過頁表將虛擬地址映射到物理地址時,在物理地址中有一塊共同的內(nèi)存區(qū),即共享內(nèi)存,這塊內(nèi)存可以被兩個進程同時看到。這樣當(dāng)一個進程進行寫操作,另一個進程讀操作就可以實現(xiàn)進程間通信。但是,我們要確保一個進程在寫的時候不能被讀,因此我們使用信號量來實現(xiàn)同步與互斥。

對于一個共享內(nèi)存,實現(xiàn)采用的是引用計數(shù)的原理,當(dāng)進程脫離共享存儲區(qū)后,計數(shù)器減一,掛架成功時,計數(shù)器加一,只有當(dāng)計數(shù)器變?yōu)榱銜r,才能被刪除。當(dāng)進程終止時,它所附加的共享存儲區(qū)都會自動脫離。

為什么共享內(nèi)存速度最快?

借助上圖說明:Proc A 進程給內(nèi)存中寫數(shù)據(jù), Proc B 進程從內(nèi)存中讀取數(shù)據(jù),在此期間一共發(fā)生了兩次復(fù)制

(1)Proc A 到共享內(nèi)存       
(2)共享內(nèi)存到 Proc B

因為直接在內(nèi)存上操作,所以共享內(nèi)存的速度也就提高了。

三、共享內(nèi)存使用指南

3.1 關(guān)鍵函數(shù)全解析

在 Linux 中使用共享內(nèi)存,離不開一些關(guān)鍵的系統(tǒng)調(diào)用函數(shù),它們是我們操作共享內(nèi)存的有力工具。

(1)shmget 函數(shù):用于創(chuàng)建共享內(nèi)存段或獲取已存在的共享內(nèi)存段的標(biāo)識符。其函數(shù)原型為:

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

key:是一個用于標(biāo)識共享內(nèi)存段的鍵值,它就像是共享內(nèi)存的 “門牌號”。通??梢允褂胒tok函數(shù)根據(jù)文件路徑和項目 ID 生成一個唯一的key值。例如:

key_t key = ftok("/tmp/somefile", 1);

這里/tmp/somefile是一個已存在的文件路徑,1 是項目 ID。如果key取值為IPC_PRIVATE,則會創(chuàng)建一個新的私有共享內(nèi)存段,通常用于父子進程間的通信。

size:指定共享內(nèi)存段的大小,單位是字節(jié)。例如,若要創(chuàng)建一個 1024 字節(jié)大小的共享內(nèi)存段,可以這樣設(shè)置:

int shmid = shmget(key, 1024, IPC_CREAT | 0666);
  • shmflg:是一組標(biāo)志位,常用的標(biāo)志包括IPC_CREAT(如果共享內(nèi)存不存在則創(chuàng)建)和IPC_EXCL(與IPC_CREAT一起使用,確保創(chuàng)建新的共享內(nèi)存段,若已存在則報錯)。權(quán)限設(shè)置與文件權(quán)限類似,如0666表示所有者、組和其他用戶都有讀寫權(quán)限 。如果shmget函數(shù)執(zhí)行成功,會返回一個非負整數(shù),即共享內(nèi)存段的標(biāo)識符shmid;若失敗,則返回 -1。

(2)shmat 函數(shù):將共享內(nèi)存段連接到調(diào)用進程的地址空間,使得進程可以訪問共享內(nèi)存中的數(shù)據(jù)。

其函數(shù)原型為:

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • shmid:是由shmget函數(shù)返回的共享內(nèi)存標(biāo)識符。
  • shmaddr:指定共享內(nèi)存連接到當(dāng)前進程中的地址位置,通常設(shè)置為NULL,表示讓系統(tǒng)自動選擇合適的地址。例如:
void *shared_mem = shmat(shmid, NULL, 0);

shmflg:通常為 0,表示默認的連接方式。如果設(shè)置了SHM_RDONLY,則以只讀方式連接共享內(nèi)存。如果shmat函數(shù)調(diào)用成功,會返回一個指向共享內(nèi)存起始地址的指針;若失敗,返回(void *)-1。

(3)shmdt 函數(shù):用于將共享內(nèi)存段從當(dāng)前進程的地址空間中分離。函數(shù)原型為:

int shmdt(const void *shmaddr);

shmaddr:是shmat函數(shù)返回的共享內(nèi)存起始地址。調(diào)用該函數(shù)后,進程不再能夠訪問該共享內(nèi)存,但共享內(nèi)存本身并不會被刪除。例如:

int result = shmdt(shared_mem);
if (result == -1) {
    perror("shmdt failed");
}

如果分離成功,shmdt返回 0;若失敗,返回 -1。

(4)shmctl 函數(shù):用于對共享內(nèi)存進行控制操作,如獲取共享內(nèi)存信息、設(shè)置權(quán)限、刪除共享內(nèi)存等。函數(shù)原型為:

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • shmid:共享內(nèi)存標(biāo)識符。
  • cmd:指定要執(zhí)行的控制命令,常用的命令有IPC_STAT(獲取共享內(nèi)存的狀態(tài)信息,存入buf指向的結(jié)構(gòu)體)、IPC_SET(設(shè)置共享內(nèi)存的狀態(tài)信息,如權(quán)限等,從buf指向的結(jié)構(gòu)體中獲取設(shè)置值)和IPC_RMID(刪除共享內(nèi)存段)。
  • buf:是一個指向shmid_ds結(jié)構(gòu)體的指針,用于傳遞或獲取共享內(nèi)存的相關(guān)信息。當(dāng)cmd為IPC_RMID時,buf通常設(shè)置為NULL。例如,刪除共享內(nèi)存段的操作如下:
int result = shmctl(shmid, IPC_RMID, NULL);
if (result == -1) {
    perror("shmctl IPC_RMID failed");
}

如果操作成功,shmctl返回 0;若失敗,返回 -1。

3.2 代碼實戰(zhàn):共享內(nèi)存的讀寫操作

下面通過一個完整的代碼示例,展示如何在兩個進程間使用共享內(nèi)存進行數(shù)據(jù)讀寫。假設(shè)我們要在一個進程中寫入數(shù)據(jù),另一個進程讀取這些數(shù)據(jù)。

首先,定義一個數(shù)據(jù)結(jié)構(gòu),用于在共享內(nèi)存中存儲數(shù)據(jù)。這里我們定義一個簡單的結(jié)構(gòu)體,包含一個整數(shù)和一個字符數(shù)組:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

#define SHM_SIZE 1024

// 定義共享內(nèi)存中使用的數(shù)據(jù)結(jié)構(gòu)
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);
    }

    // 創(chuàng)建共享內(nèi)存段
    shmid = shmget(key, sizeof(SharedData), IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(EXIT_FAILURE);
    }

    // 將共享內(nèi)存連接到當(dāng)前進程的地址空間
    shared_data = (SharedData *)shmat(shmid, NULL, 0);
    if (shared_data == (SharedData *)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }

    // 寫入數(shù)據(jù)到共享內(nèi)存
    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);

    // 分離共享內(nèi)存
    if (shmdt(shared_data) == -1) {
        perror("shmdt");
        exit(EXIT_FAILURE);
    }

    return 0;
}

上述代碼中,首先使用ftok函數(shù)生成一個key值,然后通過shmget創(chuàng)建一個共享內(nèi)存段,其大小為SharedData結(jié)構(gòu)體的大小。接著使用shmat將共享內(nèi)存連接到當(dāng)前進程地址空間,向共享內(nèi)存中寫入數(shù)據(jù),最后使用shmdt分離共享內(nèi)存。

下面是讀取共享內(nèi)存數(shù)據(jù)的代碼:

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

#define SHM_SIZE 1024

// 定義共享內(nèi)存中使用的數(shù)據(jù)結(jié)構(gòu)
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);
    }

    // 獲取已存在的共享內(nèi)存段
    shmid = shmget(key, sizeof(SharedData), 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(EXIT_FAILURE);
    }

    // 將共享內(nèi)存連接到當(dāng)前進程的地址空間
    shared_data = (SharedData *)shmat(shmid, NULL, 0);
    if (shared_data == (SharedData *)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }

    // 從共享內(nèi)存讀取數(shù)據(jù)
    printf("Data read from shared memory: num = %d, text = %s\n", shared_data->num, shared_data->text);

    // 分離共享內(nèi)存
    if (shmdt(shared_data) == -1) {
        perror("shmdt");
        exit(EXIT_FAILURE);
    }

    // 刪除共享內(nèi)存段(這里僅演示,實際應(yīng)用中需謹慎操作)
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl IPC_RMID");
        exit(EXIT_FAILURE);
    }

    return 0;
}

在讀取代碼中,同樣先使用ftok生成與寫入進程相同的key值,然后通過shmget獲取共享內(nèi)存段(注意這里沒有使用IPC_CREAT標(biāo)志,因為共享內(nèi)存已經(jīng)由寫入進程創(chuàng)建),接著連接共享內(nèi)存并讀取數(shù)據(jù),最后分離共享內(nèi)存并刪除共享內(nèi)存段(在實際應(yīng)用中,刪除共享內(nèi)存段的操作需要謹慎考慮,確保沒有其他進程再使用該共享內(nèi)存)。

3.3 模擬共享內(nèi)存

我們用server來創(chuàng)建共享存儲段,用client獲取共享存儲段的標(biāo)識符,二者關(guān)聯(lián)起來之后server將數(shù)據(jù)寫入共享存儲段,client從共享區(qū)讀取數(shù)據(jù)。通信結(jié)束之后server與client斷開與共享區(qū)的關(guān)聯(lián),并由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

運行結(jié)果:

圖片圖片

  • 優(yōu)點:我們可以看到使用共享內(nèi)存進行進程之間的通信是非常方便的,而且函數(shù)的接口也比較簡單,數(shù)據(jù)的共享還使進程間的數(shù)據(jù)不用傳送,而是直接訪問內(nèi)存,加快了程序的效率。
  • 缺點:共享內(nèi)存沒有提供同步機制,這使得我們在使用共享內(nèi)存進行進程之間的通信時,往往需要借助其他手段來保證進程之間的同步工作。

3.4 權(quán)限與生命周期管理

權(quán)限設(shè)置:在創(chuàng)建共享內(nèi)存時,可以通過shmget函數(shù)的shmflg參數(shù)設(shè)置共享內(nèi)存的訪問權(quán)限。權(quán)限設(shè)置與文件權(quán)限類似,使用三位八進制數(shù)表示,分別對應(yīng)所有者、組和其他用戶的讀、寫、執(zhí)行權(quán)限。例如,0666表示所有者、組和其他用戶都有讀寫權(quán)限;0644表示所有者有讀寫權(quán)限,組和其他用戶只有讀權(quán)限。合理的權(quán)限設(shè)置可以保證共享內(nèi)存的安全性,防止未經(jīng)授權(quán)的進程訪問或修改共享內(nèi)存中的數(shù)據(jù)。比如,在一個多用戶的服務(wù)器環(huán)境中,如果有一些共享內(nèi)存用于存儲敏感數(shù)據(jù),就需要嚴格設(shè)置權(quán)限,只允許特定的用戶或用戶組訪問。

生命周期管理:共享內(nèi)存的生命周期獨立于使用它的進程。當(dāng)最后一個使用共享內(nèi)存的進程將其分離(調(diào)用shmdt)后,共享內(nèi)存仍然存在于系統(tǒng)中,直到被顯式刪除(調(diào)用shmctl并傳入IPC_RMID命令)或系統(tǒng)重啟。這就需要開發(fā)者在使用共享內(nèi)存時,謹慎管理其生命周期。在程序結(jié)束時,應(yīng)該確保及時刪除不再使用的共享內(nèi)存,以避免內(nèi)存泄漏和資源浪費。

比如,在一個長期運行的服務(wù)器程序中,如果不斷創(chuàng)建共享內(nèi)存而不刪除,隨著時間的推移,系統(tǒng)中會殘留大量無用的共享內(nèi)存,占用系統(tǒng)資源,影響系統(tǒng)性能。同時,在刪除共享內(nèi)存之前,要確保所有使用該共享內(nèi)存的進程都已經(jīng)將其分離,否則可能會導(dǎo)致其他進程訪問非法內(nèi)存地址,引發(fā)程序崩潰等問題。

四、深入共享內(nèi)存的實現(xiàn)原理

4.1 內(nèi)核視角:共享內(nèi)存的數(shù)據(jù)結(jié)構(gòu)

在 Linux 內(nèi)核中,有幾個關(guān)鍵的數(shù)據(jù)結(jié)構(gòu)用于管理共享內(nèi)存,其中struct shmid_kernel和struct shmid_ds起著重要作用。

struct shmid_kernel是內(nèi)核中用于表示共享內(nèi)存對象的內(nèi)部數(shù)據(jù)結(jié)構(gòu),它包含了共享內(nèi)存的各種屬性和狀態(tài)信息。雖然這個結(jié)構(gòu)體對于普通開發(fā)者來說并不直接可見,但了解它有助于深入理解共享內(nèi)存的工作機制。它記錄了共享內(nèi)存段的大小、所屬的進程組、創(chuàng)建時間、最后訪問時間等重要信息。例如,通過這個結(jié)構(gòu)體,內(nèi)核可以跟蹤共享內(nèi)存的使用情況,判斷哪些進程正在使用它,以及何時需要回收共享內(nèi)存資源。

而struct shmid_ds則是一個更常用的數(shù)據(jù)結(jié)構(gòu),開發(fā)者可以通過shmctl函數(shù)來訪問和修改這個結(jié)構(gòu)體中的信息。它的定義如下:

struct shmid_ds {
    struct ipc_perm shm_perm;    /* 所有權(quán)和權(quán)限相關(guān)信息 */
    size_t          shm_segsz;   /* 共享內(nèi)存段的大小(字節(jié)) */
    time_t          shm_atime;   /* 最后一次連接到共享內(nèi)存的時間 */
    time_t          shm_dtime;   /* 最后一次從共享內(nèi)存分離的時間 */
    time_t          shm_ctime;   /* 共享內(nèi)存狀態(tài)最后一次改變的時間 */
    pid_t           shm_cpid;    /* 創(chuàng)建共享內(nèi)存的進程ID */
    pid_t           shm_lpid;    /* 最后一次執(zhí)行shmat或shmdt操作的進程ID */
    shmatt_t        shm_nattch;  /* 當(dāng)前連接到共享內(nèi)存的進程數(shù) */
    ...
};
  • shm_perm:包含了共享內(nèi)存的所有權(quán)和權(quán)限信息,如所有者 ID、組 ID、訪問權(quán)限等,類似于文件的權(quán)限管理。例如,通過設(shè)置shm_perm中的權(quán)限位,可以控制哪些進程可以訪問共享內(nèi)存,以及以何種方式(讀、寫等)訪問。
  • shm_segsz:明確了共享內(nèi)存段的大小,以字節(jié)為單位。在創(chuàng)建共享內(nèi)存時,開發(fā)者需要根據(jù)實際需求指定合適的大小。比如,在一個簡單的進程間通信場景中,如果只是傳遞少量的狀態(tài)信息,可能只需要分配幾十或幾百字節(jié)的共享內(nèi)存;而在一個需要共享大量數(shù)據(jù)的場景中,如共享視頻幀數(shù)據(jù),可能需要分配幾兆甚至更大的共享內(nèi)存空間。
  • shm_atime、shm_dtime和shm_ctime:分別記錄了共享內(nèi)存的連接時間、分離時間和狀態(tài)改變時間。這些時間戳對于調(diào)試和性能分析非常有幫助,例如,通過查看shm_atime和shm_dtime,可以了解進程對共享內(nèi)存的使用時間間隔,判斷是否存在長時間占用共享內(nèi)存而不釋放的情況;shm_ctime則可以幫助開發(fā)者追蹤共享內(nèi)存的狀態(tài)變化歷史。
  • shm_cpid和shm_lpid:記錄了創(chuàng)建共享內(nèi)存的進程 ID 和最后一次執(zhí)行shmat或shmdt操作的進程 ID。這對于調(diào)試和管理共享內(nèi)存的使用非常有用,當(dāng)出現(xiàn)共享內(nèi)存相關(guān)的問題時,可以通過這些 ID 來追溯問題的源頭,查看是哪個進程創(chuàng)建了共享內(nèi)存,以及最近哪些進程對共享內(nèi)存進行了連接或分離操作。
  • shm_nattch:表示當(dāng)前連接到共享內(nèi)存的進程數(shù)。內(nèi)核通過這個字段來管理共享內(nèi)存的生命周期,當(dāng)shm_nattch變?yōu)?0 時,并且沒有其他進程持有對該共享內(nèi)存的引用,內(nèi)核可以考慮回收該共享內(nèi)存資源。例如,在一個多進程協(xié)作的服務(wù)器程序中,當(dāng)所有使用共享內(nèi)存的進程都完成任務(wù)并與共享內(nèi)存分離后,shm_nattch變?yōu)?0,此時內(nèi)核可以及時釋放共享內(nèi)存,避免內(nèi)存資源的浪費。

4.2 映射機制:虛擬內(nèi)存與物理內(nèi)存的橋梁

共享內(nèi)存能夠?qū)崿F(xiàn)高效的進程間通信,關(guān)鍵在于其巧妙的內(nèi)存映射機制,通過頁表將虛擬內(nèi)存映射到物理內(nèi)存。

在 Linux 系統(tǒng)中,每個進程都有自己獨立的虛擬地址空間。當(dāng)進程創(chuàng)建或連接到共享內(nèi)存時,操作系統(tǒng)會在進程的虛擬地址空間中分配一段虛擬地址范圍,并將這段虛擬地址與共享內(nèi)存所在的物理內(nèi)存區(qū)域建立映射關(guān)系。這個映射關(guān)系是通過頁表來維護的。

頁表是一種數(shù)據(jù)結(jié)構(gòu),它記錄了虛擬頁號(VPN,Virtual Page Number)與物理頁號(PPN,Physical Page Number)之間的對應(yīng)關(guān)系。當(dāng)進程訪問共享內(nèi)存中的數(shù)據(jù)時,CPU 首先會根據(jù)當(dāng)前進程的頁表,將虛擬地址中的虛擬頁號轉(zhuǎn)換為物理頁號,然后再加上頁內(nèi)偏移量,得到實際的物理內(nèi)存地址,從而訪問到共享內(nèi)存中的數(shù)據(jù)。

例如,假設(shè)進程 A 和進程 B 共享一塊大小為 4KB 的共享內(nèi)存。當(dāng)進程 A 創(chuàng)建共享內(nèi)存時,操作系統(tǒng)會在物理內(nèi)存中分配一塊 4KB 大小的內(nèi)存區(qū)域,并為這塊區(qū)域分配一個物理頁號。然后,操作系統(tǒng)在進程 A 的頁表中創(chuàng)建一個頁表項,將虛擬頁號與該物理頁號關(guān)聯(lián)起來,使得進程 A 可以通過虛擬地址訪問這塊共享內(nèi)存。當(dāng)進程 B 連接到該共享內(nèi)存時,操作系統(tǒng)同樣在進程 B 的頁表中創(chuàng)建一個頁表項,將其虛擬地址空間中的一段虛擬頁號也映射到相同的物理頁號上。這樣,進程 A 和進程 B 就可以通過各自的虛擬地址訪問同一塊物理內(nèi)存區(qū)域,實現(xiàn)數(shù)據(jù)共享。

在這個過程中,如果所需的共享內(nèi)存數(shù)據(jù)不在物理內(nèi)存中(例如,由于內(nèi)存不足,共享內(nèi)存的部分數(shù)據(jù)被交換到磁盤上),會發(fā)生頁面錯誤(page fault)。此時,操作系統(tǒng)會負責(zé)將所需的數(shù)據(jù)從磁盤讀入物理內(nèi)存,并更新頁表,確保進程能夠正確訪問共享內(nèi)存。這種動態(tài)的內(nèi)存管理機制使得共享內(nèi)存能夠在有限的物理內(nèi)存條件下高效運行,同時也保證了進程間通信的穩(wěn)定性和可靠性。

Linux提供了內(nèi)存映射函數(shù)mmap, 它把文件內(nèi)容映射到一段內(nèi)存上(準確說是虛擬內(nèi)存上,運行著進程), 通過對這段內(nèi)存的讀取和修改, 實現(xiàn)對文件的讀取和修改。mmap()系統(tǒng)調(diào)用使得進程之間可以通過映射一個普通的文件實現(xiàn)共享內(nèi)存。普通文件映射到進程地址空間后,進程可以像訪問內(nèi)存的方式對文件進行訪問,不需要其他內(nèi)核態(tài)的系統(tǒng)調(diào)用(read,write)去操作。

這里是講設(shè)備或者硬盤存儲的一塊空間映射到物理內(nèi)存,然后操作這塊物理內(nèi)存就是在操作實際的硬盤空間,不需要經(jīng)過內(nèi)核態(tài)傳遞。比如你的硬盤上有一個文件,你可以使用linux系統(tǒng)提供的mmap接口,將這個文件映射到進程一塊虛擬地址空間,這塊空間會對應(yīng)一塊物理內(nèi)存,當(dāng)你讀寫這塊物理空間的時候,就是在讀取實際的磁盤文件,就是這么直接高效。通常諸如共享庫的加載都是通過內(nèi)存映射的方式加載到物理內(nèi)存的。

mmap系統(tǒng)調(diào)用并不完全是為了共享內(nèi)存來設(shè)計的,它本身提供了不同于一般對普通文件的訪問的方式,進程可以像讀寫內(nèi)存一樣對普通文件進行操作,IPC的共享內(nèi)存是純粹為了共享。

內(nèi)存映射指的是將 :進程中的1個虛擬內(nèi)存區(qū)域 & 1個磁盤上的對象,使得二者存在映射關(guān)系。當(dāng)然,也可以多個進程同時映射到一個對象上面。

實現(xiàn)過程

  • 內(nèi)存映射的實現(xiàn)過程主要是通過Linux系統(tǒng)下的系統(tǒng)調(diào)用函數(shù):mmap()
  • 該函數(shù)的作用 = 創(chuàng)建虛擬內(nèi)存區(qū)域 + 與共享對象建立映射關(guān)系

其函數(shù)原型、具體使用 & 內(nèi)部流程 如下:

/** * 函數(shù)原型 */ 
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
 /** 
* 具體使用(用戶進程調(diào)用mmap()) 
* 下述代碼即常見了一片大小 = MAP_SIZE的接收緩存區(qū) & 關(guān)聯(lián)到共享對象中(即建立映射) 
*/ 

mmap(NULL, MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0); 
/** 
* 內(nèi)部原理 
* 步驟1:創(chuàng)建虛擬內(nèi)存區(qū)域 
* 步驟2:實現(xiàn)地址映射關(guān)系,即:進程的虛擬地址空間 ->> 共享對象 
* 注:
* a. 此時,該虛擬地址并沒有任何數(shù)據(jù)關(guān)聯(lián)到文件中,僅僅只是建立映射關(guān)系 
* b. 當(dāng)其中1個進程對虛擬內(nèi)存寫入數(shù)據(jù)時,則真正實現(xiàn)了數(shù)據(jù)的可見 
*/

優(yōu)點

進程在讀寫磁盤的時候,大概的流程是:

以write 為例:進程(用戶空間) -> 系統(tǒng)調(diào)用,進入內(nèi)核 -> 將要寫入的數(shù)據(jù)從用戶空間拷貝到內(nèi)核空間的緩存區(qū) -> 調(diào)用磁盤驅(qū)動 -> 寫在磁盤上面。

使用mmap之后進程(用戶空間)--> 讀寫映射的內(nèi)存 --> 寫在磁盤上面。(這樣的優(yōu)點是 避免了頻繁的進入內(nèi)核空間,進行系統(tǒng)調(diào)用,提高了效率)

(1)mmap系統(tǒng)調(diào)用

void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);

這就是mmap系統(tǒng)調(diào)用的接口,mmap函數(shù)成功返回指向內(nèi)存區(qū)域的指針,失敗返回MAP_FAILED。

  • addr,某個特定的地址作為起始地址,當(dāng)被設(shè)置為NULL,標(biāo)識系統(tǒng)自動分配地址。實實在在的物理區(qū)域。
  • length說的是內(nèi)存段的長度。
  • prot是用來設(shè)定內(nèi)存段的訪問權(quán)限。
PROT_READ	內(nèi)存段可讀
PROT_WRITE	內(nèi)存段可寫
PROT_EXEC	內(nèi)存段可執(zhí)行
PROT_NONE	內(nèi)存段不能被訪問

flags參數(shù)控制內(nèi)存段內(nèi)容被修改以后程序的行為。

MAP_SHARED	進程間共享內(nèi)存,對該內(nèi)存段修改反映到映射文件中。提供了POSIX共享內(nèi)存
MAP_PRIVATE	內(nèi)存段為調(diào)用進程所私有。對該內(nèi)存段的修改不會反映到映射文件
MAP_ANNOYMOUS	這段內(nèi)存不是從文件映射而來的。內(nèi)容被初始化為全0
MAP_FIXED	內(nèi)存段必須位于start參數(shù)指定的地址處,start必須是頁大小的整數(shù)倍(4K整數(shù)倍)
MAP_HUGETLB	按照大內(nèi)存頁面來分配內(nèi)存空間

fd參數(shù)是用來被映射文件對應(yīng)的文件描述符,通過open系統(tǒng)調(diào)用得到,offset設(shè)定從何處進行映射。

(2)mmap用于共享內(nèi)存的方式

  1. 我們可以使用普通文件進行提供內(nèi)存映射,例如,open系統(tǒng)調(diào)用打開一個文件,然后進行mmap操作,得到共享內(nèi)存,這種方式適用于任何進程之間。
  2. 可以使用特殊文件進行匿名內(nèi)存映射,這個相對的是具有血緣關(guān)系的進程之間,當(dāng)父進程調(diào)用mmap,然后進行fork,這樣父進程創(chuàng)建的子進程會繼承父進程匿名映射后的地址空間,這樣,父子進程之間就可以進行通信了。相當(dāng)于是mmap的返回地址此時是父子進程同時來維護。
  3. 另外POSIX版本的共享內(nèi)存底層也是使用了mmap。所以,共享內(nèi)存在在posix上一定程度上就是指的內(nèi)存映射了。

五、Mmap和System V共享內(nèi)存的比較

共享內(nèi)存:

圖片圖片

這是System V版本的共享內(nèi)存(以下我們統(tǒng)稱為shm),下面看下mmap的:

圖片圖片

mmap是在磁盤上建立一個文件,每個進程地址空間中開辟出一塊空間進行映射。而shm共享內(nèi)存,每個進程最終會映射到同一塊物理內(nèi)存。shm保存在物理內(nèi)存,這樣讀寫的速度肯定要比磁盤要快,但是存儲量不是特別大,相對于shm來說,mmap更加簡單,調(diào)用更加方便,所以這也是大家都喜歡用的原因。

另外mmap有一個好處是當(dāng)機器重啟,因為mmap把文件保存在磁盤上,這個文件還保存了操作系統(tǒng)同步的映像,所以mmap不會丟失,但是shmget在內(nèi)存里面就會丟失,總之,共享內(nèi)存是在內(nèi)存中創(chuàng)建空間,每個進程映射到此處。內(nèi)存映射是創(chuàng)建一個文件,并且映射到每個進程開辟的空間中,但在posix中的共享內(nèi)存就是指這種使用文件的方式“內(nèi)存映射”。

六、POSIX共享內(nèi)存

6.1 IPC機制

共享內(nèi)存是最快的可用IPC形式。它允許多個不相關(guān)(無親緣關(guān)系)的進程去訪問同一部分邏輯內(nèi)存。

如果需要在兩個進程之間傳輸數(shù)據(jù),共享內(nèi)存將是一種效率極高的解決方案。一旦這樣的內(nèi)存區(qū)映射到共享它的進程的地址空間,這些進程間數(shù)據(jù)的傳輸就不再涉及內(nèi)核。這樣就可以減少系統(tǒng)調(diào)用時間,提高程序效率。

共享內(nèi)存是由IPC為一個進程創(chuàng)建的一個特殊的地址范圍,它將出現(xiàn)在進程的地址空間中。其他進程可以把同一段共享內(nèi)存段“連接到”它們自己的地址空間里去。所有進程都可以訪問共享內(nèi)存中的地址。如果一個進程向這段共享內(nèi)存寫了數(shù)據(jù),所做的改動會立刻被有訪問同一段共享內(nèi)存的其他進程看到。

要注意的是共享內(nèi)存本身沒有提供任何同步功能。也就是說,在第一個進程結(jié)束對共享內(nèi)存的寫操作之前,并沒有什么自動功能能夠預(yù)防第二個進程開始對它進行讀操作。共享內(nèi)存的訪問同步問題必須由程序員負責(zé)??蛇x的同步方式有互斥鎖、條件變量、讀寫鎖、紀錄鎖、信號燈。

實際上,進程之間在共享內(nèi)存時,并不總是讀寫少量數(shù)據(jù)后就解除映射,有新的通信時,再重新建立共享內(nèi)存區(qū)域。而是保持共享區(qū)域,直到通信完畢為止。

6.2 POSIX共享內(nèi)存API

使用POSIX共享內(nèi)存需要用到下面這些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:穿件并打開一個新的共享內(nèi)存對象或者打開一個既存的共享內(nèi)存對象, 與函數(shù)open的用法是類似的函數(shù)返回值是一個文件描述符,會被下面的API使用。
  • ftruncate:設(shè)置共享內(nèi)存對象的大小,新創(chuàng)建的共享內(nèi)存對象大小為0。
  • mmap:將共享內(nèi)存對象映射到調(diào)用進程的虛擬地址空間。
  • munmap:取消共享內(nèi)存對象到調(diào)用進程的虛擬地址空間的映射。
  • shm_unlink:刪除一個共享內(nèi)存對象名字。
  • close:當(dāng)shm_open函數(shù)返回的文件描述符不再使用時,使用close函數(shù)關(guān)閉它。
  • fstat:獲得共享內(nèi)存對象屬性的stat結(jié)構(gòu)體. 結(jié)構(gòu)體中會包含共享內(nèi)存對象的大小(st_size), 權(quán)限(st_mode), 所有者(st_uid), 歸屬組 (st_gid)。
  • fchown:改變一個共享內(nèi)存對象的所有權(quán)。
  • fchmod:改變一個共享內(nèi)存對象的權(quán)限。

七、共享內(nèi)存的同步問題

雖然共享內(nèi)存為進程間通信提供了高效的數(shù)據(jù)共享方式,但由于多個進程可以同時訪問同一塊內(nèi)存區(qū)域,這就帶來了同步和互斥的問題。如果沒有合適的同步機制,可能會出現(xiàn)以下情況:

  • 競態(tài)條件(Race Condition):當(dāng)多個進程同時訪問和修改共享內(nèi)存中的數(shù)據(jù)時,由于進程執(zhí)行的先后順序不確定,可能導(dǎo)致最終的數(shù)據(jù)結(jié)果不可預(yù)測。例如,有兩個進程 P1 和 P2 同時讀取共享內(nèi)存中的一個整數(shù)變量 count,然后各自對其加 1,最后再寫回共享內(nèi)存。如果沒有同步機制,可能會出現(xiàn) P1 和 P2 讀取到相同的 count 值,然后各自加 1 后寫回,這樣 count 只增加了 1,而不是預(yù)期的 2 。
  • 數(shù)據(jù)不一致性:一個進程正在對共享內(nèi)存中的數(shù)據(jù)進行修改時,另一個進程可能同時讀取這些未完全修改的數(shù)據(jù),從而導(dǎo)致數(shù)據(jù)不一致。比如,一個進程正在更新共享內(nèi)存中的一個復(fù)雜數(shù)據(jù)結(jié)構(gòu),在更新過程中,另一個進程讀取該數(shù)據(jù)結(jié)構(gòu),可能會讀到部分更新的數(shù)據(jù),使數(shù)據(jù)處于不一致的狀態(tài),進而導(dǎo)致程序出現(xiàn)錯誤。

解決方案:信號量與互斥鎖的應(yīng)用

為了解決共享內(nèi)存帶來的同步和互斥問題,通常會使用信號量(Semaphore)和互斥鎖(Mutex)等同步機制。

(1)信號量:信號量是一種計數(shù)器,用于控制對共享資源的訪問。它可以用來實現(xiàn)進程間的同步和互斥。在共享內(nèi)存的場景中,信號量可以用來控制對共享內(nèi)存的訪問權(quán)限。例如,我們可以創(chuàng)建一個信號量,初始值設(shè)為 1,表示共享內(nèi)存資源可用。當(dāng)一個進程要訪問共享內(nèi)存時,它首先嘗試獲取信號量(通過對信號量執(zhí)行 P 操作,即減 1 操作)。如果信號量的值大于等于 0,說明資源可用,進程可以繼續(xù)執(zhí)行對共享內(nèi)存的訪問操作;如果信號量的值小于 0,說明資源已被其他進程占用,該進程會被阻塞,直到信號量的值大于等于 0。當(dāng)進程完成對共享內(nèi)存的訪問后,它會釋放信號量(通過對信號量執(zhí)行 V 操作,即加 1 操作),通知其他進程可以訪問共享內(nèi)存。在 Linux 中,有 POSIX 有名信號量、POSIX 無名信號量和 System V 信號量等不同類型,開發(fā)者可以根據(jù)具體需求選擇使用。例如,使用 POSIX 有名信號量實現(xiàn)共享內(nèi)存同步的代碼示例如下:

#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;

    // 創(chuàng)建共享內(nèi)存對象
    shm_fd = shm_open("/my_shared_memory", O_CREAT | O_RDWR, 0666);
    if (shm_fd == -1) {
        perror("shm_open");
        exit(1);
    }

    // 配置共享內(nèi)存大小
    if (ftruncate(shm_fd, SHM_SIZE) == -1) {
        perror("ftruncate");
        exit(1);
    }

    // 將共享內(nèi)存映射到進程地址空間
    shared_memory = mmap(0, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
    if (shared_memory == MAP_FAILED) {
        perror("mmap");
        exit(1);
    }

    // 創(chuàng)建信號量
    sem = sem_open(SEM_NAME, O_CREAT, 0666, 1);
    if (sem == SEM_FAILED) {
        perror("sem_open");
        exit(1);
    }

    // 等待信號量,獲取共享內(nèi)存訪問權(quán)限
    if (sem_wait(sem) == -1) {
        perror("sem_wait");
        exit(1);
    }

    // 訪問共享內(nèi)存
    printf("Accessed shared memory: %s\n", (char *)shared_memory);

    // 釋放信號量,允許其他進程訪問共享內(nèi)存
    if (sem_post(sem) == -1) {
        perror("sem_post");
        exit(1);
    }

    // 取消映射并關(guān)閉共享內(nèi)存
    if (munmap(shared_memory, SHM_SIZE) == -1) {
        perror("munmap");
        exit(1);
    }
    if (close(shm_fd) == -1) {
        perror("close");
        exit(1);
    }

    // 刪除共享內(nèi)存對象
    if (shm_unlink("/my_shared_memory") == -1) {
        perror("shm_unlink");
        exit(1);
    }

    // 關(guān)閉并刪除信號量
    if (sem_close(sem) == -1) {
        perror("sem_close");
        exit(1);
    }
    if (sem_unlink(SEM_NAME) == -1) {
        perror("sem_unlink");
        exit(1);
    }

    return 0;
}

(2)互斥鎖:互斥鎖是一種二元信號量,用于保證在同一時刻只有一個進程能夠訪問共享資源,即實現(xiàn)對共享內(nèi)存的互斥訪問。當(dāng)一個進程獲取到互斥鎖后,其他進程如果試圖獲取該互斥鎖,會被阻塞,直到持有互斥鎖的進程釋放它。在 Linux 中,使用 pthread 庫中的互斥鎖相關(guān)函數(shù)來實現(xiàn)互斥鎖的操作。例如,初始化互斥鎖可以使用pthread_mutex_init函數(shù),獲取互斥鎖使用pthread_mutex_lock函數(shù),釋放互斥鎖使用pthread_mutex_unlock函數(shù),銷毀互斥鎖使用pthread_mutex_destroy函數(shù)。以下是使用互斥鎖實現(xiàn)共享內(nèi)存同步的簡單代碼示例:

#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;

    // 創(chuàng)建共享內(nèi)存對象
    shm_fd = shm_open("/my_shared_memory", O_CREAT | O_RDWR, 0666);
    if (shm_fd == -1) {
        perror("shm_open");
        exit(1);
    }

    // 配置共享內(nèi)存大小
    if (ftruncate(shm_fd, sizeof(SharedData)) == -1) {
        perror("ftruncate");
        exit(1);
    }

    // 將共享內(nèi)存映射到進程地址空間
    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);
    }

    // 獲取互斥鎖,訪問共享內(nèi)存
    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);
    }

    // 取消映射并關(guān)閉共享內(nèi)存
    if (munmap(shared_data, sizeof(SharedData)) == -1) {
        perror("munmap");
        exit(1);
    }
    if (close(shm_fd) == -1) {
        perror("close");
        exit(1);
    }

    // 刪除共享內(nèi)存對象
    if (shm_unlink("/my_shared_memory") == -1) {
        perror("shm_unlink");
        exit(1);
    }

    return 0;
}

通過合理使用信號量和互斥鎖等同步機制,可以有效地解決共享內(nèi)存帶來的同步和互斥問題,確保多個進程能夠安全、高效地共享內(nèi)存數(shù)據(jù)。

八、實際應(yīng)用場景及常見問題解答

8.1 實際應(yīng)用場景

(1)數(shù)據(jù)庫緩存優(yōu)化

在數(shù)據(jù)庫系統(tǒng)中,共享內(nèi)存發(fā)揮著至關(guān)重要的作用,尤其是在緩存優(yōu)化方面。以 Oracle 數(shù)據(jù)庫為例,它使用共享全局區(qū)(SGA,Shared Global Area)來實現(xiàn)共享內(nèi)存。SGA 是一個共享的內(nèi)存結(jié)構(gòu),用于存儲數(shù)據(jù)塊、SQL 語句和其他共享信息 。

當(dāng)數(shù)據(jù)庫接收到查詢請求時,首先會在共享內(nèi)存的緩存中查找相關(guān)數(shù)據(jù)。如果數(shù)據(jù)存在于緩存中,即命中緩存,數(shù)據(jù)庫可以直接從共享內(nèi)存中讀取數(shù)據(jù)并返回給用戶,這大大減少了磁盤 I/O 操作。因為從磁盤讀取數(shù)據(jù)的速度遠遠低于從內(nèi)存讀取數(shù)據(jù)的速度,通過共享內(nèi)存緩存數(shù)據(jù),可以顯著提高查詢性能。例如,在一個高并發(fā)的在線交易系統(tǒng)中,大量用戶頻繁查詢訂單信息。如果沒有共享內(nèi)存緩存,每次查詢都需要從磁盤讀取數(shù)據(jù),磁盤 I/O 很快就會成為系統(tǒng)的瓶頸,導(dǎo)致查詢響應(yīng)時間變長。而使用共享內(nèi)存緩存訂單數(shù)據(jù)后,大部分查詢可以直接從內(nèi)存中獲取數(shù)據(jù),大大提高了系統(tǒng)的響應(yīng)速度和吞吐量。

同時,共享內(nèi)存還可以減少內(nèi)存的重復(fù)使用,提高內(nèi)存利用率。多個數(shù)據(jù)庫進程可以共享同一塊內(nèi)存區(qū)域,避免了每個進程都單獨開辟內(nèi)存來存儲相同的數(shù)據(jù),從而節(jié)省了內(nèi)存資源。比如,在一個包含多個數(shù)據(jù)庫實例的系統(tǒng)中,這些實例可以共享 SGA 中的數(shù)據(jù)緩存,減少了內(nèi)存的浪費,使得系統(tǒng)能夠在有限的內(nèi)存資源下高效運行。

(2)高性能計算中的數(shù)據(jù)共享

在高性能計算領(lǐng)域,共享內(nèi)存同樣有著廣泛的應(yīng)用。在大規(guī)模的科學(xué)計算和工程模擬中,往往需要處理海量的數(shù)據(jù)和復(fù)雜的計算任務(wù),這些任務(wù)通常需要多個處理器核心或多個計算節(jié)點協(xié)同工作。

以分子動力學(xué)模擬為例,這是一種用于研究分子系統(tǒng)微觀行為的計算方法,需要對大量分子的運動軌跡進行模擬計算。在計算過程中,不同的處理器核心需要共享分子的初始位置、速度等數(shù)據(jù),以及模擬過程中的中間結(jié)果。通過共享內(nèi)存,這些數(shù)據(jù)可以被多個處理器核心直接訪問,避免了數(shù)據(jù)在不同處理器之間通過網(wǎng)絡(luò)或其他方式傳輸?shù)拈_銷,提高了計算效率。

再比如,在氣象預(yù)報模型中,需要對全球范圍內(nèi)的氣象數(shù)據(jù)進行分析和預(yù)測。這些數(shù)據(jù)量巨大,計算任務(wù)復(fù)雜,通常會在分布式計算集群上進行。共享內(nèi)存可以用于在不同計算節(jié)點之間共享氣象數(shù)據(jù)和計算參數(shù),使得各個節(jié)點能夠協(xié)同工作,共同完成氣象預(yù)報的計算任務(wù)。在這種場景下,共享內(nèi)存不僅提高了數(shù)據(jù)共享的效率,還減少了節(jié)點之間的通信開銷,對于提高整個高性能計算系統(tǒng)的性能起著關(guān)鍵作用。

8.2 避坑指南與常見問題解答

在使用 Linux 共享內(nèi)存的過程中,開發(fā)者常常會遇到一些棘手的問題,下面我們就來總結(jié)一下這些常見問題,并給出相應(yīng)的解決方案。

(1)共享內(nèi)存創(chuàng)建失敗

①問題描述:調(diào)用shmget函數(shù)創(chuàng)建共享內(nèi)存時,返回值為 -1,導(dǎo)致創(chuàng)建失敗。

②可能原因

  • 系統(tǒng)資源限制:系統(tǒng)對共享內(nèi)存的數(shù)量和大小有限制,如SHMMAX(單個共享內(nèi)存段的最大大小)和SHMMNI(系統(tǒng)中共享內(nèi)存段的最大數(shù)量)等參數(shù)。如果要創(chuàng)建的共享內(nèi)存超過了這些限制,就會導(dǎo)致創(chuàng)建失敗。例如,當(dāng)系統(tǒng)的SHMMAX設(shè)置為 32MB,而你嘗試創(chuàng)建一個 64MB 的共享內(nèi)存段時,就會失敗。
  • 權(quán)限不足:創(chuàng)建共享內(nèi)存需要適當(dāng)?shù)臋?quán)限。如果當(dāng)前用戶沒有足夠的權(quán)限(如在一些安全限制較嚴格的系統(tǒng)中),shmget調(diào)用也會失敗。比如,普通用戶在沒有特殊權(quán)限配置的情況下,無法創(chuàng)建共享內(nèi)存。

③解決方案

檢查系統(tǒng)參數(shù):通過cat /proc/sys/kernel/shmmax和cat /proc/sys/kernel/shmmni等命令查看系統(tǒng)的共享內(nèi)存參數(shù)設(shè)置。如果需要,可以通過修改/etc/sysctl.conf文件來調(diào)整這些參數(shù),例如:

echo "kernel.shmmax = 2147483648" >> /etc/sysctl.conf
sysctl -p

上述命令將SHMMAX設(shè)置為 2GB,并使其立即生效。

④確認權(quán)限:確保當(dāng)前用戶具有創(chuàng)建共享內(nèi)存的權(quán)限,必要時可以切換到具有足夠權(quán)限的用戶(如 root 用戶)來創(chuàng)建共享內(nèi)存,或者通過修改文件權(quán)限和用戶組等方式來賦予相應(yīng)權(quán)限。

(2)共享內(nèi)存訪問異常

①問題描述:在進程訪問共享內(nèi)存時,出現(xiàn)段錯誤(Segmentation Fault)或其他訪問異常。

②可能原因

  • 未正確映射共享內(nèi)存:調(diào)用shmat函數(shù)時,可能由于參數(shù)設(shè)置錯誤,導(dǎo)致共享內(nèi)存沒有正確映射到進程的地址空間。例如,shmat返回的指針為(void *)-1,表示映射失敗,但程序沒有正確處理這種情況,仍然嘗試使用該指針訪問共享內(nèi)存,就會導(dǎo)致訪問異常。
  • 內(nèi)存越界訪問:在對共享內(nèi)存進行讀寫操作時,沒有正確檢查邊界條件,導(dǎo)致訪問超出了共享內(nèi)存的范圍。比如,共享內(nèi)存大小為 1024 字節(jié),而程序嘗試寫入 2048 字節(jié)的數(shù)據(jù),就會造成內(nèi)存越界。
  • 同步問題:多個進程同時訪問共享內(nèi)存時,如果沒有正確的同步機制(如信號量、互斥鎖等),可能會導(dǎo)致數(shù)據(jù)競爭和訪問沖突,進而引發(fā)訪問異常。例如,一個進程正在修改共享內(nèi)存中的數(shù)據(jù),另一個進程同時讀取這些未完全修改的數(shù)據(jù),就可能導(dǎo)致數(shù)據(jù)不一致和訪問錯誤。

③解決方案

檢查映射結(jié)果:在調(diào)用shmat后,仔細檢查返回值。如果返回(void *)-1,則根據(jù)errno變量的值進行錯誤處理,例如:

void *shared_mem = shmat(shmid, NULL, 0);
if (shared_mem == (void *)-1) {
    perror("shmat failed");
    exit(EXIT_FAILURE);
}
  • 邊界檢查:在對共享內(nèi)存進行讀寫操作時,務(wù)必進行嚴格的邊界檢查,確保不會越界訪問。例如,在寫入數(shù)據(jù)時,要檢查數(shù)據(jù)大小是否超過共享內(nèi)存的剩余空間;在讀取數(shù)據(jù)時,要確保讀取的長度不超過共享內(nèi)存的有效范圍。
  • 完善同步機制:引入合適的同步機制,如使用信號量或互斥鎖來確保對共享內(nèi)存的訪問是安全的。在訪問共享內(nèi)存之前,先獲取同步鎖(如信號量的 P 操作或互斥鎖的加鎖操作),訪問完成后再釋放同步鎖(如信號量的 V 操作或互斥鎖的解鎖操作)。

(3)共享內(nèi)存未及時釋放

①問題描述:共享內(nèi)存不再被使用,但沒有被及時刪除,導(dǎo)致系統(tǒng)資源浪費。

②可能原因

  • 程序邏輯錯誤:在程序中沒有正確處理共享內(nèi)存的生命周期,例如沒有在合適的時機調(diào)用shmctl函數(shù)并傳入IPC_RMID命令來刪除共享內(nèi)存。
  • 進程異常退出:使用共享內(nèi)存的進程由于某種原因(如程序崩潰、收到異常信號等)異常退出,而沒有來得及執(zhí)行共享內(nèi)存的刪除操作。

③解決方案

優(yōu)化程序邏輯:在程序設(shè)計時,明確共享內(nèi)存的生命周期,確保在不再需要共享內(nèi)存時,及時調(diào)用shmctl函數(shù)刪除共享內(nèi)存。例如,在程序結(jié)束時,添加如下代碼:

if (shmctl(shmid, IPC_RMID, NULL) == -1) {
    perror("shmctl IPC_RMID failed");
    exit(EXIT_FAILURE);
}

捕獲異常信號:在進程中捕獲常見的異常信號(如SIGSEGV、SIGABRT等),在信號處理函數(shù)中添加釋放共享內(nèi)存的操作。例如,使用signal函數(shù)注冊信號處理函數(shù):

#include <signal.h>

void cleanup_shared_memory(int signum) {
    // 釋放共享內(nèi)存相關(guān)資源
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl IPC_RMID in signal handler failed");
    }
    exit(EXIT_FAILURE);
}

int main() {
    // 注冊信號處理函數(shù)
    signal(SIGSEGV, cleanup_shared_memory);
    signal(SIGABRT, cleanup_shared_memory);
    // 其他程序邏輯
}

通過上述方法,可以有效避免共享內(nèi)存未及時釋放的問題,提高系統(tǒng)資源的利用率。

責(zé)任編輯:武曉燕 來源: 深度Linux
相關(guān)推薦

2018-01-12 14:35:00

Linux進程共享內(nèi)存

2019-05-08 11:10:05

Linux進程語言

2013-07-29 09:36:05

100G傳輸100G

2019-03-25 15:00:38

工具代碼測試

2012-04-17 13:12:48

2010-03-04 10:20:59

超高速寬帶谷歌市

2010-01-05 10:00:48

Linux進程間通信

2014-09-04 16:40:17

FTTx

2021-03-08 17:09:14

5G網(wǎng)絡(luò)俄羅斯

2023-08-31 14:24:06

5G技術(shù)物聯(lián)網(wǎng)

2014-02-18 09:19:04

LTE100G400G

2016-12-28 17:04:51

1Gbps寬帶‘網(wǎng)絡(luò)

2017-08-06 00:05:18

進程通信開發(fā)

2009-08-18 17:14:47

100G超高速以太網(wǎng)

2010-03-08 10:52:29

思科超高速互聯(lián)網(wǎng)接入系統(tǒng)

2009-07-09 11:19:01

2018-05-30 13:58:02

Linux進程通信

2017-06-19 13:36:12

Linux進程消息隊列

2010-03-10 09:29:54

寬帶超高速互聯(lián)網(wǎng)思科

2015-01-05 15:11:23

日本光纖400Gbit
點贊
收藏

51CTO技術(shù)棧公眾號