1. 程式人生 > >linux中幾種共享記憶體

linux中幾種共享記憶體

共享記憶體用處

使用檔案或者管道進行程序間通訊會有很多侷限性。管道只能在父程序和子程序間使用;通過檔案共享,在處理效率上又差一些,而且訪問檔案描述符不如訪問記憶體地址方便。

Linux系統在程式設計上提供的共享記憶體方案有三種:

  • mmap記憶體共享對映
  • XSI共享記憶體
  • POSIX共享記憶體

mmap記憶體共享對映

mmap本來是儲存對映功能。它可以將一個檔案對映到記憶體中,在程式裡就可以直接使用記憶體地址對檔案內容進行訪問。

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int port, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);

Linux通過系統呼叫fork派生出的子程序和父程序共用記憶體地址空間,Linux的mmap實現了一種可以在父子程序之間共享記憶體地址的方式。

  1. 父程序將flags引數設定MAP_SHARED方式通過mmap申請一段記憶體。記憶體可以對映某個具體檔案(fd),也可以不對映具體檔案(fd置為-1,flag設定為MAP_ANONYMOUS).
  2. 父程序呼叫fork產生子程序,之後在父子程序內都可以訪問到mmap所返回的地址,就可以共享記憶體了。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/file.h>
#include <sys/wait.h>
#include <sys/mman.h>

#define COUNT 100

int do_child(int *count)
{
        int interval;

        // critical section
        interval = *count;
        interval++;
        usleep(1);
        *count = interval;
        // critical section

        exit(0);
}

int main()
{
    pid_t pid;
    int count;
    int *shm_p;

    shm_p = (int *)mmap(NULL, sizeof(int), PROT_WRITE|PROT_READ, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
    if(MAP_FAILED == shm_p) {
        perror("mmap()");
        exit(1);
    }

    *shm_p = 0;

    for(count = 0; count < COUNT; count++) {
        pid = fork();
        if(pid < 0) {
            perror("fork()");
            exit(1);
        }

        if(pid == 0) {
            do_child(shm_p);
        }
    }

    for(count = 0; count < COUNT; count++) {
        wait(NULL);
    }

    printf("shm_p: %d\n", *shm_p);
    munmap(shm_p, sizeof(int));
    exit(0);
}

這段共享記憶體的使用是有競爭條件的。程序間通訊不僅僅是通訊這麼簡單,還要處理類似的這樣的臨界區程式碼。在這裡,可以採用檔案鎖進行處理。但是共享記憶體使用檔案鎖顯得不太協調。除了不方便和效率低下以外,檔案鎖還不能進行更高階的程序控制。這裡可以使用訊號量這種更高階的程序同步控制原語來實現相關功能。

下面這段程式用來幫助理解mmap的記憶體佔用情況。

#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<errno.h>
#include<fcntl.h>
#include<string.h>
#include<sys/file.h>
#include<sys/wait.h>
#include<sys/mman.h>

#define COUNT 100
#define MEMSIZE 1024*1024*1023*2

int main()
{
    pid_t pid;
    int count;
    void *shm_p;

    shm_p = mmap(NULL, MEMSIZE, PROT_WRITE|PROT_READ, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
    if(MAP_FAILED == shm_p) {
        perror("mmap()");
        exit(1);
    }

    bzero(shm_p, MEMSIZE);

    sleep(3000);

    munmap(shm_p, MEMSIZE);
    exit(0);
}

申請了一段近2G的記憶體,並置0.觀察記憶體變化

[[email protected] sharemem]$ free -g
              total        used        free      shared  buff/cache   available
Mem:             15           2           2           0          10          11
Swap:            31           0          31
[[email protected] sharemem]$ ./mmap_mem &
[1] 32036
[[email protected] sharemem]$ free -g
              total        used        free      shared  buff/cache   available
Mem:             15           2           0           2          12           9
Swap:            31           0          31

可以看出,這段記憶體被記錄到shared和buff/cache中了。

mmap有一個缺點,那就是共享的記憶體只能在父程序和fork產生的子程序間使用,除此之外的其它程序無法得到共享記憶體段的地址。

XSI共享記憶體

XSI是X/Open組織對UNIX定義的一套介面標準(X/Open System Interface)。XSI共享記憶體在Linux底層的實現實際上跟mmap沒有什麼本質不同,只是在使用方法上有所區別。

#include<sys/ipc.h>
#include<sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

#include<sys/types.h>
#include<sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);

shmget的第三個引數,指定建立標誌。支援的標誌為:IPC_CREAT、IPC_EXCL。從Linux 2.6之後,還引入了支援大頁的共享記憶體,標誌為:SHM_HUGETLB、SHM_HUGE_2MB等。shemget除了可以建立一個新的共享記憶體外,還可以訪問一個已經存在的記憶體,此時可以將shmflg置為0,不加任何標誌開啟。

shmget返回的int型別的shmid類似於檔案描述符,注意只是類似,而並非同樣的實現,所以,不能用select、poll、epoll這樣的方法去控制一個XSI共享記憶體。對於一個XSI共享記憶體,其key是系統全域性唯一的,這就方便其它程序使用同樣的key,開啟同樣一段共享記憶體,以便進行程序間通訊。而是用fork產生的子程序,可以直接通過shmid訪問到相關共享記憶體段。這就是key的本質:系統中對XSI共享記憶體的全域性唯一表示符。

#include<sys/types.h>
#include<sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);

key是通過ftok函式,使用一個約定好的檔名和proj_id生成的。ftok不會建立檔案,所以必須指定一個存在並且程序可以訪問的pathname路徑。另外,ftok並不是根據檔案的路徑和檔名生成key的,在具體實現上,它使用的是指定檔案的inode編號和檔案所在裝置的裝置編號。所以,不同的檔名也可能得到同一個key(不同的檔名指向同一個inode,硬連結)。同樣的檔名也不一定就能得到相同的key,一個檔名有可能被刪除重建,這種行為會導致inode變化。

#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<errno.h>
#include<fcntl.h>
#include<string.h>
#include<sys/file.h>
#include<sys/wait.h>
#include<sys/mman.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>

#define COUNT 100
#define PATHNAME "/etc/passwd"

int do_child(int proj_id)
{
    int interval;
    int *shm_p, shm_id;
    key_t shm_key;

    if((shm_key = ftok(PATHNAME, proj_id)) == -1) {
        perror("ftok()");
        exit(1);
    }

    shm_id = shmget(shm_key, sizeof(int), 0);
    if(shm_id < 0)
    {
        perror("shmget()");
        exit(1);
    }

    //使用shmat將相關共享記憶體對映到本程序的記憶體地址
    shm_p = (int *)shmat(shm_id, NULL, 0);
    if((void *)shm_p == (void *)-1)
    {
        perror("shmat()");
        exit(1);
    }

    // critical section
    interval = *shm_p;
    interval++;
    usleep(1);
    *shm_p = interval;
    // critical section

    //使用shmdt解除本程序記憶體對共享記憶體的地址對映,本操作不會刪除共享記憶體
    if(shmdt(shm_p) < 0){
        perror("shmdt()");
        exit(1);
    }

    exit(0);
}

int main()
{
    pid_t pid;
    int count;
    int *shm_p;
    int shm_id, proj_id;
    key_t shm_key;

    proj_id = 1234;

    if((shm_key = ftok(PATHNAME, proj_id)) == -1)
    {
        perror("ftok()");
        exit(1);
    }

    //使用shm_key建立一個共享記憶體,如果系統中已經存在此共享記憶體,則報錯退出。創建出來的共享記憶體許可權為0600
    shm_id = shmget(shm_key, sizeof(int), IPC_CREAT|IPC_EXCL|0600);
    if(shm_id < 0) {
        perror("shmget()");
        exit(1);
    }

    shm_p = (int *)shmat(shm_id, NULL, 0);
    if((void *)shm_p == (void *) -1)
    {
        perror("shmat()");
        exit(1);
    }

    *shm_p = 0;

    for(count = 0; count < COUNT; count++) {
        pid = fork();
        if(pid < 0) {
            perror("fork()");
            exit(1);
        }

        if(pid == 0) {
            do_child(proj_id);
        }
    }

    for(count = 0; count < COUNT; count ++) {
        wait(NULL);
    }

    printf("shm_p: %d\n", *shm_p);

    if(shmdt(shm_p) < 0) {
        perror("shmdt()");
        exit(1);
    }

    if(shmctl(shm_id, IPC_RMID, NULL) < 0) {
        perror("shmctl");
        exit(1);
    }

    exit(0);
}

在某些情況下,也可以不通過一個key來建立共享記憶體。此時可以在key的引數所在位置填IPC_PRIVATE,這樣核心會在保證不衝突的共享記憶體段id的情況下新建一段共享記憶體。因為只能是建立,所以flag位一定是IPC_CREAT。可以將shmid傳給子程序。

當獲取到shmid之後,就可以使用shmat來進行地址對映。shmat之後,通過訪問返回的當前程序的虛擬地址就可以訪問到共享記憶體段了。注意使用之後要呼叫shmdt解除對映,否則對於長期執行的程式,可能會造成虛擬記憶體地址洩露。shmdt並不能刪除共享記憶體段,只是解除共享記憶體段和程序虛擬地址的對映關係。只要shmid對應的共享記憶體段還存在,就可以使用shmat繼續對映使用。想要刪除一個共享記憶體段,需要使用shmctl的IPC_RMID指令處理,或者在命令列中使用ipcrm刪除指定的共享記憶體id或key。

shmctl還可以檢視、修改共享記憶體的相關屬性,可以在man 2 shmctl中檢視。在系統中還可以使用ipcs -m 命令檢視系統中所有共享記憶體的資訊。

ipcs - provide information on ipc facilities
ipcs [-asmq] [-tclup]
ipcs [-smq] -i id

-m 共享記憶體
-q 訊息佇列
-s 訊號量陣列
-a all(預設)

輸出選項:

-t time
-p pid
-c creator
-l limits
-u summary

在Linux系統中,使用XSI共享記憶體呼叫shmget時,可以通過設定shmflg引數來申請大頁記憶體(huge pages)。

SHM_HUGETLB(since Linux 2.6)
SHM_HUGE_2MB, SHM_HUGE_1GB(since Linux 3.8)

使用大頁記憶體的好處是提高核心對記憶體管理的處理效率。因為在相同記憶體大小的情況下,使用大頁記憶體(2M一頁)將比使用一般記憶體頁(4K一頁)的記憶體頁管理的數量大大減少,從而減少記憶體頁表項的快取壓力和CPU cache快取記憶體地址的對映壓力。但是需要注意一些地方:

  • 大頁記憶體不能交換(SWAP)
  • 使用不當時可能造成更大的記憶體洩露
  • 大頁記憶體需要使用root許可權
  • 需要修改系統配置
shm_id = shmget(IPC_PRIVATE, MEMSIZE, SHM_HUGETLB|0600)

如果要申請2G以下的大頁記憶體,需要系統預留2G以上的大頁記憶體。

echo 2048 > /proc/sys/vm/nr_hugepages
cat /proc/meminfo | grep -i huge
    AnonHugePages:      841728 KB
    HugePages_Total:    2020
    HugePages_Free:     2020
    HugePages_Rsvd:     0
    HugePages_Surp:     0
    Hugepagesize:       2048 kB

2048是頁數,每頁2M。

還需要注意共享記憶體的限制:

echo 2147483648 > /proc/sys/kernel/shmmax
echo 33554432 > /proc/sys/kernel/shmall

/proc/sys/kernel/shmall:限制系統用在共享記憶體上的記憶體頁總數。一頁一般是4k(可以通過getconf PAGE_SIZE檢視)

/proc/sys/kernel/shmmax:限制一個共享記憶體段的最大長度,單位是位元組

/proc/sys/kernel/shmmni:限制整個系統可以建立的最大的共享記憶體段的個數

POSIX共享記憶體

POSIX共享記憶體實際上毫無新意,它本質上是mmap對檔案的共享方式對映,只不過對映的是tmpfs檔案系統上的檔案。

tmpfs是將一部分記憶體空間用作檔案系統,一般掛在/dev/shm目錄。

Linux提供的POSIX共享記憶體,實際上就是在/dev/shm下建立一個檔案,並將其mmap之後對映其記憶體地址即可。可以通過man shm_overview檢視使用方法。

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/file.h>
#include <sys/wait.h>
#include <sys/mman.h>

#define COUNT 100
#define SHMPATH "shm"

int do_child(char * shmpath)
{
    int interval, shmfd, ret;
    int *shm_p;
    // 使用shm_open訪問一個已經建立的POSIX共享記憶體
    shmfd = shm_open(shmpath, O_RDWR, 0600);
    if (shmfd < 0) {
        perror("shm_open()");
        exit(1);
    }

    // 用mmap將對應的tmpfs檔案對映到本程序記憶體 */
    shm_p = (int *)mmap(NULL, sizeof(int), PROT_WRITE|PROT_READ, MAP_SHARED, shmfd, 0);
    if (MAP_FAILED == shm_p) {
        perror("mmap()");
        exit(1);
    }
    /* critical section */
    interval = *shm_p;
    interval++;
    usleep(1);
    *shm_p = interval;
    /* critical section */

    munmap(shm_p, sizeof(int));
    close(shmfd);

    exit(0);
}

int main()
{
    pid_t pid;
    int count, shmfd, ret;
    int *shm_p;

    /* 建立一個POSIX共享記憶體 */
    shmfd = shm_open(SHMPATH, O_RDWR|O_CREAT|O_TRUNC, 0600);
    if (shmfd < 0) {
        perror("shm_open()");
        exit(1);
    }
    /* 使用ftruncate設定共享記憶體段大小 */
    ret = ftruncate(shmfd, sizeof(int));
    if (ret < 0) {
        perror("ftruncate()");
        exit(1);
    }
    /* 使用mmap將對應的tmpfs檔案對映到本程序記憶體 */
    shm_p = (int *)mmap(NULL, sizeof(int), PROT_WRITE|PROT_READ, MAP_SHARED, shmfd, 0);
    if (MAP_FAILED == shm_p) {
        perror("mmap()");
        exit(1);
    }
    *shm_p = 0;

    for (count=0;count<COUNT;count++) {
        pid = fork();
        if (pid < 0) {
            perror("fork()");
            exit(1);
        }

        if (pid == 0) {
            do_child(SHMPATH);
        }
    }

    for (count=0;count<COUNT;count++) {
        wait(NULL);
    }

    printf("shm_p: %d\n", *shm_p);
    munmap(shm_p, sizeof(int));
    close(shmfd);
    shm_unlink(SHMPATH);
    exit(0);
}

編譯該段程式碼的時候需要指定一個庫,-lrt,這是linux的real time庫。

  • shm_open的SHMPATH引數是一個路徑,這個路徑預設放在系統的/dev/shm目錄下。這是shm_open封裝好的,保證檔案一定在tmpfs下。
  • 使用ftruncate改變共享記憶體的大小,實際就是改變檔案的長度。
  • shm_unlink實際就是unlink系統呼叫的封裝。如果不做unlink操作,那麼檔案會一直存在/dev/shm目錄下。
  • 關閉共享記憶體描述符,使用close.

修改共享記憶體核心配置

  1. SHMMAX

一個程序可以在它的虛擬地址空間分配給一個共享記憶體端的最大大小(單位是位元組)

echo 2147483648 > /proc/sys/kernel/shmmax
或
sysctl -w kernel.shmmax=2147483648
或
echo "kenerl.shmmax=2147483648" >> /etc/sysctl.conf
  1. SHMMNI

系統範圍內共享記憶體段的數量


echo 4096 > /proc/sys/kernel/shmmni
或
sysctl -w kernel.shmmni=4096
或
echo "kernel.shmmni=4096" >> /etc/sysctl.conf
  1. SHMALL

這個引數設定了系統範圍內共享記憶體可以使用的頁數。單位是PAGE_SIZE(通常是4096,可以通過getconf PAGE_SIZE獲得)。

echo 2097152 > /proc/sys/kernel/shmall
或
sysctl -w kernel.shmall=2097152
或
echo "kernel.shmall=2097152" >> /etc/sysctl.conf
  1. 移除共享記憶體

執行ipcs -m檢視系統所有的共享記憶體。如果status欄位是dest,表明這段共享記憶體需要被刪除。

ipcs -m -i $shmid