System V 共享内存:原理剖析、代码架构分析与双端通信实战

system V

一、基本定义

System V 是早期商用 UNIX 操作系统的一个分支,由贝尔实验室推出,是经典的 UNIX 版本系列。

我们现在 Linux 里说的 System V IPC ,就是这套系统遗留下来的进程间通信标准 ,包含三类:共享内存、消息队列、信号量

本质就是:遵循老式 UNIX System V 规范实现的一套进程间通信机制

二、核心原理

System V 共享内存 = 内核开辟一块物理内存 → 让多个进程同时映射到自己的虚拟地址空间 → 大家直接读写同一块物理内存。

理解

1. 映射到虚拟地址空间的共享区
2. 共享内存原理,是一个简化版本的动态库映射!
3. 共享内存管理结构体 + 共享内存本身 = 共享内存
4. 共享内存使用步骤

1. 创建 2. 关联挂接 3. 使用 4. 去关联 5. 释放共享内存

三、相关接口

1. shmget

c++ 复制代码
int shmget(key_t key, size_t size, int shmflg);

作用:向内核申请一块共享内存,或获取已存在的共享内存。

  1. key:共享内存的编号

  2. size:共享内存大小(必须 >0,内核自动按页对齐)

  3. shmflg

    • IPC_CREAT:不存在则创建,已存在就用之------获取

    • IPC_CREAT | IPC_EXCL:不存在创建,已存在则报错------创建

    • 0664:权限(和文件权限一样)

返回:成功返回 shmid(共享内存 ID),失败 -1

(解析)key 键值

需要共享内存时,如何知道这个内存在不在呢?那么在内核中会对 shm 进行标识,实现共享内存的唯一性。但是我们发现这个跟内核打交道的标识竟然要用户自己填!为什么不能像文件描符fd那样,让内核自己处理好之后,将上层提供给进程,然后把这玩意儿交给进程之间不就好了吗?但是!!!如果是这样,那进程之间也没有通信方式来看到这个上层的东西啊,所以不能让内核自己弄!

所以需要进程之间约定好这个 key 键值

所以,这个key值理论上来说就可以自己随便给,给1、2、3都行但是一般不这么干。一般会通过一个函数生成一个key值。

ftok:key钥匙生成器。

c++ 复制代码
key_t ftok(const char *pathname, int proj_id);

作用把一个文件路径 + 一个数字,转换成一个唯一的 key 值 ,给 shmget / msgget / semget 使用。

参数

  1. pathname

    • 一个真实存在的文件路径(目录 / 普通文件都行)
    • 只要存在就行,内容不重要
  2. proj_id

    • 一个 1 字节的数字(0~255),随便给
    • 同一个文件,不同 proj_id → 生成不同 key

返回值

  • 成功:返回 key_t 类型的 key
  • 失败:返回 -1
c++ 复制代码
// 1. 用当前目录 . 生成 key
key_t key = ftok(".", 66);  // . 一定存在,66 是自定义数字

// 2. 用这个 key 创建共享内存
int shmid = shmget(key, 4096, IPC_CREAT|0666);

此时相当于 server 创建好了一个共享内存,那么 client 端就需要获取这个共享内存。所以 Shm 类内还需要一个获取共享内存的接口,这个接口大致跟创建接口差不多,只是在 shmget() 时传的参数不一样,需要只用 IPC_CRETA 参数获取。

此时运行之后再看看结果:

那么这样就形成了进程间的联系。

(解析)shmget返回值shmid

我们有了key键值,那函数 shmget 返回值shmid是什么呢?我们可以通过代码查看一下:

(解析)共享内存的生命周期

那如果我再运行一下这个./server会发生报错,显示 File exists

此时我们还可以通过一个命令查看当前系统中的IPC: ipcs

问题:为啥进程结束了,这个共享内存还能查到呢?

**结果:**共享内存,包括system V IPC标准下其他的两类通信,生命周期随内核!即:用户如果不主动删除IPC资源,IPC资源会和操作系统一样,一直存在,除非重启系统。

手动命令删除共享内存

ipcrm -m [shmid] 不用key键值删除共享内存

删除之后再次运行./server,shmid就变成了1

再次重复创建就意料之内会失败,但是我们得思考是因为什么冲突了导致重复创建会失败,显而易见:是key键值冲突。

对比 keyshmid

**1. key只在内核中,标识共享内存的唯一性!用户不使用key!**所以命令 ipcrm -m 要用shmid

2. shmid 只在用户中使用,在代码中使用shmid来访问共享内存

2. shmctl (control)

c++ 复制代码
int shmctl(int shmid, int cmd, struct shm_ds *buf);

作用 :获取信息、设置权限、删除共享内存。(因为删除就是控制的一种)

常用 cmd

  • IPC_RMID删除共享内存(最常用)
  • IPC_STAT:获取状态信息
  • IPC_SET:设置权限

第三个参数传:*buf

作用:内核把共享内存的信息(大小、权限、创建时间、挂载进程数)写到 buf

c++ 复制代码
void Delete() {
    int n = shmctl(_shmid, IPC_RMID, nullptr);
    if (n < 0) {
        std::cerr << "shmctl 删除失败:" << strerror(errno) << std::endl;
    }
}

3. shmat (attach)

c++ 复制代码
void *shmat(int shmid, const void *shmaddr, int shmflg);

作用 :把共享内存挂接到当前进程的虚拟地址空间。

参数2:shmaddr

想让共享内存映射到哪个虚拟地址→ 直接填 NULL(让内核自动选,99% 场景)

参数3:shmflg

  • 0 → 可读可写(一般直接填0)
  • SHM_RDONLY → 只读

返回值(重要)

成功:返回一个 void* 类型的指针,指向映射后的共享内存起始地址。跟(malloc差不多)

失败:返回 (void *) -1

所以不能用判断 NULL 的方式检查 shmat 是否失败!

c++ 复制代码
if (p == NULL) { ... }  // 错!错!错!

正确写法:

c++ 复制代码
if (p == (void*)-1) { ... }  // 对!对!对!
c++ 复制代码
int main() {
    Shm sharedmem;
    sharedmem.Create();

    sleep(5);
    sharedmem.Attach();

    sharedmem.Delete();
    return 0;
}
c++ 复制代码
void Attach() {
    _start_addr = shmat(_shmid, nullptr, 0);
    if (_start_addr = (void*)-1) {
        std::cerr << "shmat挂接失败:" << strerror(errno) << std::endl;
        exit(3);
    }
}

nattach :表示此共享内存的挂接数。


perms :表示此共享内存的权限。

c++ 复制代码
// 2. 创建共享内存
_shmid = shmget(k, _size, IPC_CREAT | IPC_EXCL | 0666);
if (_shmid < 0) {
    std::cerr << "shmget创建共享内存失败:" << strerror(errno) << std::endl;
    exit(2);
}

bytes属性

shmget 传参时,传入的size就是设置的共享内存大小,必须是4096的整数倍!

4. shmdt (detach解绑定)

当进程不适用此共享内存之后,最好的办法不是直接删除这个共享内存,而是将自己与这个共享内存解除绑定!需要用到接口 shmdt

c++ 复制代码
int shmdt(const void *shmaddr);

作用:解除当前进程与共享内存的挂接关系。

参数:shmaddr ;填 shmat 返回的那个地址指针

返回值:成功 → 返回 0;失败 → 返回 -1。

c++ 复制代码
void Detach() {
    int n = shmdt(_start_addr);
    if (n == -1) {
        std::cerr << "shmdt解除挂接失败:" << strerror(errno) << std::endl;
        exit(4);
    }
    std::cout << "解除挂接成功" << std::endl;
}
c++ 复制代码
// ./server
#include "shm.hpp"
#include <unistd.h>

int main() {
    Shm sharedmem;
    sharedmem.Create();

    sharedmem.Attach();
    sleep(5);

    sharedmem.Detach();
    sleep(5);

    sharedmem.Delete();
    return 0;
}

四、两端演示

c++ 复制代码
// ./client
#include "shm.hpp"

int main() {
    // 不需要创建内核级共享内存,当然也不需要删除
    Shm sharedmem;
    sharedmem.Get();
    sleep(5);
    
    sharedmem.Attach();
    sleep(5);
    
    sharedmem.Detach();
    return 0;
}
c++ 复制代码
// ./server
#include "shm.hpp"

int main() {
    // 谁创建,谁删除
    Shm sharedmem;
    sharedmem.Create();

    sharedmem.Attach();
    sleep(5);

    sharedmem.Detach();
    sleep(5);

    sharedmem.Delete();
    return 0;
}

挂载数 nattch 0->1->2->1->0->删除。

c++ 复制代码
// ./server
#include "shm.hpp"

int main() {
    // 谁创建,谁删除
    Shm sharedmem;
    sharedmem.Create();
    sharedmem.Attach();

    char* shm_start = (char*)sharedmem.Addr();
    int size = sharedmem.Size();
    while (true) {
        for (int i = 0; i <size; ++i) {
            std::cout << shm_start[i] << ' ';
        }
        std::cout << std:: endl;
        sleep(3);
    }

    sharedmem.Detach();
    sharedmem.Delete();
    return 0;
}
c++ 复制代码
// ./client
#include "shm.hpp"

int main() {
    // 不需要创建内核级共享内存,当然也不需要删除
    Shm sharedmem;
    sharedmem.Get();
    sharedmem.Attach();
    
    char* shm_start = (char*)sharedmem.Addr();
    int size = sharedmem.Size();
    int index = 0;
    while (true) {
        std::cout << "请输入@ ";
        char ch;
        std::cin >> ch;
        shm_start[index++] = ch;
        index %= size;
    }

    sharedmem.Detach();
    return 0;
}

五、共享内存特点

  1. 访问共享内存,不需要系统调用
  2. 写端写入,其他端立即能看到;速度快!拷贝少!
  3. 缺点,没有资源保护机制,没有同步或者互斥(没有阻塞

六、完整代码

c++ 复制代码
// ./shm.hpp
#pragma once
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <iostream>
#include <cstring>
#include <cstdio>

// 用户指明
#define PATHNAME "."
#define PROJ_ID 0x66

const int gsize = 4096;

class Shm {
public:
    Shm()
        : _shmid(-1)
        , _size(gsize)
        , _start_addr(nullptr)
    {}

    ~Shm() {}



    void Delete() {
        int n = shmctl(_shmid, IPC_RMID, nullptr);
        if (n < 0) {
            std::cerr << "shmctl 删除失败:" << strerror(errno) << std::endl;
            exit(5);
        }
        std::cout << "删除成功!" << std::endl;
    }

    void Attach() {
        _start_addr = shmat(_shmid, nullptr, 0);
        if (_start_addr == (void*)-1) {
            std::cerr << "shmat挂接失败:" << strerror(errno) << std::endl;
            exit(3);
        }
    }

    void Detach() {
        int n = shmdt(_start_addr);
        if (n == -1) {
            std::cerr << "shmdt解除挂接失败:" << strerror(errno) << std::endl;
            exit(4);
        }
        std::cout << "解除挂接成功" << std::endl;
    }

    void Get() {
        GetHelper(IPC_CREAT);
    }

    void Create() {
        GetHelper(IPC_CREAT | IPC_EXCL | 0666);
    }

    void* Addr() {
        return _start_addr;
    }

    int Size() {
        return _size;
    }

private:
    key_t Getkey() {
        return ftok(PATHNAME, PROJ_ID);
    }

    void GetHelper(int shmflg) {
        // 1. 构建键值
        key_t k = Getkey();
        if (k < 0) {
            std::cerr << "获取key键值失败:" << strerror(errno) << std::endl;
            exit(1);
        }

        // 2. 创建共享内存
        _shmid = shmget(k, _size, shmflg);
        if (_shmid < 0) {
            std::cerr << "shmget创建共享内存失败:" << strerror(errno) << std::endl;
            exit(2);
        }
        printf("16进制key:0x%x; _shmid:%d\n", k, _shmid);
    }

private:
    int _shmid;
    int _size;
    void* _start_addr;
};
c++ 复制代码
// ./server
#include "shm.hpp"

int main() {
    // 谁创建,谁删除
    Shm sharedmem;
    sharedmem.Create();
    sharedmem.Attach();

    char* shm_start = (char*)sharedmem.Addr();
    int size = sharedmem.Size();
    while (true) {
        for (int i = 0; i <size; ++i) {
            std::cout << shm_start[i] << ' ';
        }
        std::cout << std:: endl;
        sleep(3);
    }

    sharedmem.Detach();
    sharedmem.Delete();
    return 0;
}
c++ 复制代码
// ./client
#include "shm.hpp"

int main() {
    // 不需要创建内核级共享内存,当然也不需要删除
    Shm sharedmem;
    sharedmem.Get();
    sharedmem.Attach();
    
    char* shm_start = (char*)sharedmem.Addr();
    int size = sharedmem.Size();
    int index = 0;
    while (true) {
        std::cout << "请输入@ ";
        char ch;
        std::cin >> ch;
        shm_start[index++] = ch;
        index %= size;
    }

    sharedmem.Detach();
    return 0;
}
相关推荐
wunaiqiezixin10 小时前
如何在C++中创建和管理线程
c++
lolo大魔王10 小时前
Linux 文件系统超全面详解(原理、结构、挂载、分区、inode、日志、管理命令)
linux·运维·服务器
雪度娃娃10 小时前
转向现代C++——在意为改写的函数添加 override
开发语言·c++
王老师青少年编程10 小时前
csp信奥赛C++高频考点专项训练之前缀和&差分 --【一维差分】:[NOIP 2018 提高组] 铺设道路
c++·前缀和·差分·csp·高频考点·信奥赛·铺设道路
星马梦缘11 小时前
aaaaa
数据结构·c++·算法
喵星人工作室11 小时前
C++火影忍者1.1.2
开发语言·c++
磊 子11 小时前
详细讲解一下epoll
linux·io·epoll·io多路复用
basketball61612 小时前
C++ 中的 ptrdiff_t 详解
开发语言·c++
wunaiqiezixin12 小时前
互斥锁与自旋锁的区别
c++