Linux共享內存

--摘自窮佐羅的Linux書node

共享內存用處

使用文件或者管道進行進程間通訊會有不少侷限性。管道只能在父進程和子進程間使用;經過文件共享,在處理效率上又差一些,並且訪問文件描述符不如訪問內存地址方便。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);
}

這段共享內存的使用是有競爭條件的。進程間通訊不只僅是通訊這麼簡單,還要處理相似的這樣的臨界區代碼。在這裏,能夠採用文件鎖進行處理。可是共享內存使用文件鎖顯得不太協調。除了不方便和效率低下之外,文件鎖還不能進行更高級的進程控制。這裏可使用信號量這種更高級的進程同步控制原語來實現相關功能。bash

下面這段程序用來幫助理解mmap的內存佔用狀況。ide

#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.觀察內存變化函數

[zorro@zorrozou-pc0 sharemem]$ free -g
              total        used        free      shared  buff/cache   available
Mem:             15           2           2           0          10          11
Swap:            31           0          31
[zorro@zorrozou-pc0 sharemem]$ ./mmap_mem &
[1] 32036
[zorro@zorrozou-pc0 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產生的子進程間使用,除此以外的其它進程沒法獲得共享內存段的地址。code

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
相關文章
相關標籤/搜索