linux笔记—共享内存

共享内存

  • 即多进程之间直接对读写同一段内存.相比较管道及消息队列,显而易见的好处是速度快,所有的 IPC 方法中效率最高.但共享内存并未提供同步机制,需要自行实现.

原理

  • 顾名思义,共享内存就是说两个不同的进程 A、B 可以共同享有一块内存区域
    20150124140604078.png

  • 整个处理流程是

    • 进程 A 第一次访问该页中的数据时, 生成一个缺页中断. 内核读入此页到内存并更新页表使之指向此页.
    • 进程 B 访问同一页面而出现缺页中断,内核只将进程 B 的页表登记项指向次页即可.
    • 进程 A.B 即可访问同一段内存.

使用

  • shmget 函数
    shmget 被用来开辟/初始化一段共享内存.其他进程使用相同的 key 通过 shgat 获取同一段共享内存.,只有 shmget 函数才直接使用信号量键,所有其他的信号量函数使用由 semget 函数返回的信号量标识符.

    函数原型

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

    key : 与信号量的 semget 函数一样,使用共享内存 key(非 0 整数)shmget 函数成功时返回一个与 key 相关的共享内存标识符 (非负整数),用于多进程的共享.调用失败返回 -1.
    size : 开辟的共享内存大小 (字节).
    shmflg : 权限控制,与 IPC_CREAT 做或操作,控制其他进程对共享内存权限. 0644 即代表其他进程只有读的权限.

  • shmat 函数
    用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间

    函数原型

    1
    void *shmat(int shm_id, const void *shm_addr, int shmflg);

    shm_id: 由 shmget 函数返回的共享内存标识.
    shm_addr: 指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址.
    shm_flg: 标志位,通常为 0.

  • shmdt 函数
    将共享内存从当前进程中分离,使该共享内存对当前进程不再可用.

    函数原型:

    1
    int shmdt(const void *shmaddr);

    shmaddr: shmat 函数返回的地址指针,调用成功时返回 0,失败时返回 -1.

  • shmctl 函数
    控制共享内存.

    函数原型

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

    shm_id: shmget 函数返回的共享内存标识符

    command: 要采取的操作,以下三个值 :

    • IPC_STAT:把 shmid_ds 结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖 shmid_ds 的值.
    • IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为 shmid_ds 结构中给出的值
    • IPC_RMID:删除共享内存段

    buf: 结构指针,指向共享内存模式和访问权限的结构.

    1
    2
    3
    4
    5
    6
    7
    shmid_ds结构至少包括以下成员:
    struct shmid_ds
    {
    uid_t shm_perm.uid;
    uid_t shm_perm.gid;
    mode_t shm_perm.mode;
    };

系统限制及规避

大小

  • linux 默认限制共享内存总大小由 SHMMAX 值确定.默认值未 32MB

  • 读取

    1
    cat /proc/sys/kernel/shmmax
  • 当超过系统限制时 提示 unable to attach to shared memory.

  • 规避

    • 直接修改/proc.无需重启

      1
      echo "2147483648" > /proc/sys/kernel/shmmax

      可以将命令写入启动脚本 /etc/rc.local 中.保证重启生效

    • 使用 sysctl 命令修改

      1
      sysctl -w kernel.shmmax=2147483648

      可以将此参数插入到 /etc/sysctl.conf 启动文件中

      1
      echo "kernel.shmmax=2147483648" >> /etc/sysctl.conf

      永久生效.

数量

  • 与大小类似的,共享内存创建的总数量由 SHMMNI 参数确定.

  • 读取

    1
    cat /proc/sys/kernel/shmmni

    默认情况下是 4096.

  • 一般不需要修改.

同一进程多次 Shmat

  • shmat 即挂载共享内存到进程的进程空间.

  • 当同一进程多次调用 shmat 挂载同一共享内存时,shamat 每次返回的地址都不同,相当于在进程的线性空间中存在多个实际指向同一块共享内存.直到最后进程线性空间消耗殆尽.

  • 解决:
    需要在挂载共享内存前,判断申请的共享内存指针是否为空,为 NULL ,则第一次加载此共享内存.否则不再重复加载.

    1
    2
    3
    4
    5
    6
    void* p = NULL;
    /*其他操作*/
    if (NULL == p)
    {
    p = shmat(shmid,p,0666);
    }

多个进程相同 Key 多次创建共享内存

  • 共享内存创建有大小之分.key 相同情况下,容量小的共享内存会获得之前创建的大的共享内存的内容.有可能导致之前创建共享内存的进程崩溃.

  • 解决

    • 在 shmage 使用 IPC_EXCL 标记.现行判断共享内存是否已创建.如果已创建则挂载,没有创建返回失败后,再创建.
    1
    2
    3
    4
    5
    6
    Shmid = Shmget(key, size,IPC_CREATE|IPC_EXCL);
    if (-1 != shmid)
    {
    /*错误处理*/
    Shmid = Shmget(key, size,IPC_CREATE);
    }
    • 不通过 key 标记同一块共享内存.
      shmget 使用 kry = IPC_PRIVATE ,linux 会忽略 key 值,直接新建一块共享内存.返回标识,通过管道/文件方式共享给其他进程使用.

共享内存删除

  • 调用 shmctl 删除共享内存后,共享内存并不会立刻被系统清理.
  • 首先共享内存的 shmid_ds 结构中的 shm_nattch 减一.该共享内存从调用 shmctl 的进程剥离.但 shm_nattch 不为 0 的情况下,即仍然有别进程连接的情况下.共享内存并不会立刻清除. 只有在 shm_nattch 为 0 ,没有任何进程连接的情况下,系统才会清理 这段共享内存.
  • 该段共享内存被任何连接的进程执行 shmctl 删除操作后, 新的进程将无法连接到该段共享内存.
  • 与之对应的 Shmdt ,只是将共享内存由 调用的进程空间剥离.不会影响其他进程连接到该段共享内存.

应用

  • 多进程共享数据.以链表为例.
  • 有两种方式:
    • 开辟一段共享内存,之后存入整个链表.将共享内存连接到各个进程.
      • 优点:
        • 对原有代码改动少.
      • 缺点:
        • 封装链表操作时,各个进程头节点偏移量不同,不能简单的通过 *p->next 访问.
        • 有共享内存大小限制.
    • 每一个链表节点对应一个共享内存.开辟同等数量的共享内存.
      • 优点
        • 可以随用随创,相对节省内存.
        • 链表节点 next 存储相对链表头节点偏移即可,访问操作相对单一共享内存容易.
      • 缺点
        • 受限于 linux 默认 4096 的总数量限制.且总数不易更改.
        • 开辟新链表节点相对复杂.
  • 实际使用中第一种较为常见.

同步

  • 与共享内存类似的信号量,也是跨进程的.信号量是比较一种比较方便配合共享内存同步的方式.