shm 共享内存简单学习

作者: | 更新日期:

之前了解cache的时候, 我曾说过:共享内存也仅仅只是一片内存而已. 现在来看看共享内存的基本操作吧.

本文首发于公众号:天空的代码世界,微信号:tiankonguse

背景

前段时间, 我了解了共享内存,并写了一篇笔记: cache 的简单认识与思考.

简单的说就是cache的数据储存在共享内存中, 而这片内存和程序中的内存其实是一样的,只是操作方式不同而已.

我今天花费了几个小时学习了一下共享内存,并简单的封装了一下, 然后为之后的 cache 做准备吧.代码在github

下面我们就来简单的学习一下这些操作方式吧, 这是菜鸟级的记录, 大神直接忽视吧!

下面主要分两部分:

  1. 共享内存的基本操作
  2. 共享内存的实践与应用

基本操作

共享内存大概由四个操作组成: 申请查询内存、程序连接映射内存、程序断开映射内存、管理共享内存.

申请查询内存

shmget 函数用来申请查询内存.

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

int shmget(key_t key, size_t size, int shmflg);

可以看到, 这个操作需要三个参数.

  • key 用来唯一确定这片内存, 比如我们申请了三片内存, 我们怎么区分这三片内存呢? 就是依靠这个key.
    如果我们传入0, 那么含义大概就是让系统帮我们申请一片内存. 一般不建议这样做.
  • size 就是我们申请内存的大小
    假设 key 代表的内存不存在, 则我们可以申请 size 大小的内存.
    假设 key 代表的内存已存在, 如果我们的size和存在的size相同, 则可以申请成功, 如果size不同, 则申请失败.
    简单的理解就是 key 和 size 唯一确定一片内存, 但是 key 不能重复.
  • shmflg flag 参数.
    对于linux中的flag参数, 一般都是使用位压缩把一些含义储存在一个参数上的.
    比如 IPC_CREAT 代表内存不存在时创建, 没这个参数时不存在就返回错误啦.
  • 返回值 这个操作会返回一个id, 我们一般称为 shmid. 可以理解为这个内存的内部唯一标识吧

程序连接映射内存

我们申请了共享内存并不代表我们可以直接使用这片内存.

大家都知道, 一个进程只能操作自己范围内的地址空间, 而申请的那片内存就不在自己的管辖范围内.
这个时候我们就需要通过一种方法, 把那片内存映射到自己的范围内了.

shmat 函数用于把共享内存映射到进程的地址空间.

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

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

这个操作同样需要三个参数.

  • shmid : 我们申请内存时, 返回的shmid
  • shmaddr 定共享内存出现在进程内存地址的什么位置,直接指定为NULL让内核自己决定一个合适的地址位置.
  • shmflg flag 参数. 比如 SHM_RDONLY 代表只读.
  • 返回值 映射后进程内的地址指针, 代表内存的头部地址

程序断开映射内存

我们使用完了内存, 可能需要断开映射内存来节省资源.

shmdt 函数可以实现这个功能.

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

int shmdt(const void *shmaddr);

这个操作只需要一个参数,就是这个内存地址的指针.

管理共享内存

管理内存大概分这么三个部分: 查询共享内存的状态, 更新共享内存的状态, 删除这片共享内存.

shmctl 函数可以用来管理共享内存.

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

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

这里只来看看 cmd 参数可以使用的值吧.如下

  • IPC_STAT 查询共享内存的状态, 结果会填充在第三个参数 buf 内
  • IPC_SET 更新共享内存的状态, 根据第三个参数来更新
  • IPC_RMID 删除这片共享内存

实践与应用

学完了共享内存的基本知识, 如果不来敲几行代码怎么可以呢?

仔细想想, 共享内存的作用就是用来临时储存数据的, 说的高大上点就是进程间通信的.

于是我是实现了这么几个小程序, 可以加深对共享内存的理解.

父子进程通信

源代码见github

核心代码如下:

Shm shm;

pid = fork();

if (pid == 0) {
    char *shmaddr = shm.getAdr();
    if (NULL == shmaddr) {
        printf("getAdr error. err=%s\n", shm.getLastError());
        return -1;
    }
    
    snprintf(shmaddr, SIZE, "this is parent, but will print in child !");
    printf("father end\n");
    return 0;
    
} else if (pid > 0) {
    sleep(3);
    char *shmaddr = shm.getAdr();
    if (NULL == shmaddr) {
        printf("getAdr error. err=%s\n", shm.getLastError());
        return -1;
    }
    
    printf("%s\n", shmaddr);
    printf("child end\n");
    shm.delShm();
}

上面代码片段的含义是: 创建一个共享内存类的实例, 这样这个实例在 fork 的时候就会复制给子进程了, 进而可以和父进程共享这个内存了.
在父进程内, 先把内存映射到父进程,然后填充进一些信息.
在子进程内, 先等几秒, 这样就可以保证父进程把信息写进内存了, 然后子进程也把内存映射到子进程, 最后输出内存中的信息.

输出大概如下

tiankonguse:fork $ ./fork 
iShmID = 10813468
father end
this is parent, but will print in child !
child end

进程间通信

进程间通信和父子进程通信没什么大的区别.

只要知道共享内存的key, 就可以读写同一片内存了, 进而就可以通信了.

源代码见github

核心代码如下:

写进程:

Shm shm;
Time *p_time;
p_time = (Time *) shm.getAdr();
if (NULL == p_time) {
    printf("getAdr error. err=%s\n", shm.getLastError());
    return -1;
}
srand(time(NULL));
for (int i = 0; i < TIME_NUM; i++) {
    struct timeval start;
    gettimeofday(&start, NULL);
    (p_time + i)->sec = start.tv_sec;
    (p_time + i)->usec = start.tv_usec;
    (p_time + i)->val = rand() % 100;
}

读进程:

Shm shm;
Time *p_time;
p_time = (Time *) shm.getAdr();
if (NULL == p_time) {
    printf("getAdr error. err=%s\n", shm.getLastError());
    return -1;
}

for (int i = 0; i < TIME_NUM; i++) {
    printf("i=%d sec=%lld usec=%lld val=%lld\n", i, (p_time + i)->sec,
            (p_time + i)->usec, (p_time + i)->val);
}

大家可以看到, 这个和上面的父子进程没什么区别.
只是这里储存的不是字符串了, 而是若干个 sizeof(Time) 大小的内存了.

输出大概如下:

tiankonguse:readwrite $ make
g++ -O0 -o read read.cpp -I../../../include/  -L../../../lib/ -static -lshm -pthread
g++ -o write write.cpp -I../../../include/  -L../../../lib/ -static -lshm -pthread

tiankonguse:readwrite $ ./write 
key=50462721
iShmID = 10944523

tiankonguse:readwrite $ ./read 
key=50462721
iShmID = 10944523
i=0 sec=1441286802 usec=81659 val=56
i=1 sec=1441286802 usec=81673 val=31
i=2 sec=1441286802 usec=81674 val=57

尾记

现在我对共享内存的接触还不多, 理解也可能有误, 欢迎大家吐槽.

下一步就开始进入cache 中 hash 的实现了.

本文首发于公众号:天空的代码世界,微信号:tiankonguse
如果你想留言,可以在微信里面关注公众号进行留言。

关注公众号,接收最新消息

tiankonguse +
穿越