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
相关文章
相关标签/搜索