[Linux]共享記憶體

羡鱼OvO發表於2024-12-07

共享記憶體

共享記憶體允許兩個或多個程序訪問同一塊實體記憶體空間,就好像它們對這塊記憶體擁有共同的讀寫許可權一樣。這塊共享的記憶體區域由作業系統核心負責管理和維護,程序透過特定的系統呼叫將其對映到自己的虛擬地址空間中,之後便可以像訪問普通記憶體一樣對其進行讀寫操作,從而實現程序間的資料共享。

相關介面

建立共享記憶體

  1. 函式原型:int shmget(key_t key, size_t size, int shmflg);

    引數:

    • key:是一個整數值,用於標識共享記憶體段。通常可以使用ftok()函式來生成一個唯一的key值。不同的程序可以透過相同的key值來獲取或建立同一個共享記憶體段。
    • size:指定了要建立或獲取的共享記憶體段的大小,以位元組為單位。
    • shmflg:是一組標誌位,用於控制shmget()函式的行為。常用的取值如下:
      • IPC_CREAT:如果共享記憶體段不存在,則建立一個新的共享記憶體段;如果已經存在,則返回已存在共享記憶體段的識別符號(shmid)。
      • IPC_EXEL:與IPC_CREAT一起使用時,如果共享記憶體段已經存在,則shmget()函式會返回錯誤。這個標誌主要用於確保建立的是一個全新的、獨一無二的共享記憶體段,避免多個程序同時建立同名的共享記憶體段而導致衝突。
      • 許可權值:用於設定共享記憶體段的訪問許可權,與檔案許可權設定相同。

    返回值:成功返回共享記憶體識別符號,失敗返回-1。

  2. 函式原型:key_t ftok(const char *pathname, int proj_id);

    引數:

    • pathname:是一個有效的檔案路徑名。ftok()函式會根據這個檔案的索引節點號(inode)來生成鍵值的一部分。通常,可以選擇一個系統中存在的且不會被輕易刪除或移動的檔案,一般使用當前目錄下的某個檔案或一個已知的配置檔案等。
    • proj_id:是一個字元型的專案識別符號,它與檔案路徑名結合起來生成最終的鍵值。其取值範圍是 0 到 255 之間的整數。

    返回值:成功返回一個有效的鍵值(key_t 型別),失敗返回-1。

刪除共享記憶體

在命令列中透過ipcs -m來檢視共享記憶體相關資訊。

若是在命令列中可以用ipcrm -m [shmid]來進行刪除。

函式原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);

引數:

  • shmid:共享記憶體段的識別符號
  • cmd:決定了shmctl()函式具體要對共享記憶體段執行何種操作。常用的命令如下:
    • IPC_RMID:用於刪除共享記憶體段。當所有的對映都解除時,才會正真進行刪除。
    • IPC_STAT:用於獲取共享記憶體段的當前狀態資訊,並將這些資訊儲存到buf所指向的struct shmid_ds結構體中。
    • IPC_SET:用於設定共享記憶體段的某些屬性,這些屬性通常儲存在buf所指向的struct shmid_ds結構體中。
  • buf:是一個指向struct shmid_ds結構體的指標,通常設定為NULL

返回值:成功返回0,失敗返回-1。

對映共享記憶體到程序地址空間(關聯)

函式原型:void *shmat(int shmid, const void *shmaddr, int shmflg);

引數:

  • shmaddr:指定共享記憶體段連線到程序地址空間的起始地址。通常設定為NULL,表示由系統自動選擇一個合適的地址來連線共享記憶體段。
  • shmflg:是一組標誌位,用於控制共享記憶體段的連線方式和許可權等。通常設定為0,表示以讀寫方式連線共享記憶體段,且不進行地址舍入操作,這是預設的連線方式。

返回值:成功返回一個指向共享記憶體段連線到程序地址空間的起始地址的指標(與malloc的返回值差不多)。失敗返回-1。

撤銷共享記憶體對映(去關聯)

函式原型:int shmdt(const void *shmaddr);

引數:

  • shmaddr:是之前透過shmat()函式連線共享記憶體段時返回的地址指標。

返回值:成功返回0,失敗返回-1。

shmid 和 key

建立共享記憶體的時候,我們透過key值來保證共享記憶體在系統中的唯一性。它只要確保它是唯一的就行了,具體的值其實並不重要。那麼key在哪裡呢?共享記憶體=實體記憶體塊+共享記憶體的相關屬性,既然key是用來標識共享記憶體的唯一性的,那麼在共享記憶體相關屬性的結構體中一定會有key這個屬性。如下圖:

[Linux]共享記憶體

既然我們使用key來標識共享記憶體的唯一性,那為什麼還要有shmid(共享記憶體識別符號)呢?這就有點像檔案描述符fd和檔案的索引節點號inode之間的關係。fd是我們來使用的屬於使用者級,key同樣如此;而inode通常由作業系統使用屬於核心級,shmid同樣如此。保證了底層和上層的解耦。

示例程式碼

以下程式碼完整的演示了共享記憶體的通訊過程。客戶端向共享記憶體中發訊息,伺服器端從共享記憶體中讀取訊息。

comm.hpp

#ifndef _COMM_HPP_
#define _COMM_HPP_
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define PATHNAE "."
#define PROJ_ID 0x77
#define MAX_SIZE 4096

//獲取key值
key_t getKey()
{
    key_t k = ftok(PATHNAE, PROJ_ID);
    if (k < 0)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(1);
    }
}

//透過不同的標誌位來執行不同的操作
int getShmHelper(key_t k, int flags)
{
    int shmid = shmget(k, MAX_SIZE, flags);
    if (shmid < 0)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(2);
    }
}

//建立共享記憶體
int createShm(key_t k)
{
    return getShmHelper(k, IPC_CREAT | IPC_EXCL | 0600);
}

//獲取共享記憶體
int getShm(key_t k)
{
    return getShmHelper(k, IPC_CREAT);
}

//刪除共享記憶體
void delShm(int shmid)
{
    if (shmctl(shmid, IPC_RMID, nullptr) == -1)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
    }
}

//關聯
void* attachShm(int shmid)
{
    void *mem = shmat(shmid, nullptr, 0);
    if ((long long)mem == -1L)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(3);
    }
    return mem;
}

//去關聯
void detachShm(void *shmaddr)
{
    if (shmdt(shmaddr) == -1) 
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(4);
    }
}

#endif

shm_server.cc

#include "comm.hpp"

int main()
{
    //建立共享記憶體
    key_t k = getKey();
    printf("key: 0x%x\n", k);
    int shmid = createShm(k);
    printf("shmid: %d\n", shmid);
    //sleep(3);

    //關聯
    char *start = (char*)attachShm(shmid);
    printf("attach success, address start: %p\n", start);

    //使用
    while (true)
    {
        if (strcmp(start, "break") == 0) break;
        printf("client say: %s\n", start);
        sleep(1);
    }

    //去關聯
    detachShm(start);
    printf("server: detachShm success!!!\n");
    //sleep(3);

    //刪除共享記憶體
    delShm(shmid);
    printf("server: delShm success!!!\n");

    return 0;
}

shm_client.cc

#include "comm.hpp"

int main()
{
    //獲取共享記憶體
    key_t k = getKey();
    int shmid = getShm(k);
	//關聯
    char* start = (char*)attachShm(shmid);
    printf("client: attach success, address start: %p\n", start);

    const char *msg = "我是客戶端,正在和你發訊息!";
    int cnt = 0;
    pid_t pid = getpid();
    while (true)
    {
        sleep(1);
        //傳送5條訊息
        if (cnt == 5)
        {
            snprintf(start, MAX_SIZE, "break");
            break;
        }
        snprintf(start, MAX_SIZE, "%s[pid:%d][訊息編號:%d]", msg, pid, ++cnt);

    }
	//去關聯
    detachShm(start);
    printf("client: detachShm success!!!\n");

    return 0;
}

優缺點

優點:由於多個程序直接訪問同一塊記憶體,不需要像其他程序間通訊方式(如管道)那樣進行資料的複製和傳遞,因此共享記憶體的通訊效率非常高,資料傳輸速度快,特別適合需要頻繁共享大量資料的場景。

缺點:自身沒有同步互斥機制,沒有對資料做保護。

相關文章