Linux之systemV共享内存方式
作者:s_little_monster_
一、工作原理

操作系统在物理内存上申请一块空间,然后将申请到的空间通过页表映射到进程地址空间mm_struct的共享区中,然后返回虚拟地址供程序使用,如果多个进程申请的是同一块物理空间,那么它们就可以进行通信由于同一时间可能有多组进程进行通信,所以系统当中可能存在多个共享内存块,所以操作系统要把这些内存管理起来,所以内核中会有一个结构体来描述共享内存
二、系统调用接口
1、申请共享内存
(一)key的获取
#include <sys/types.h> #include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id);
- 返回值:成功ftok 会将
proj_id与pathname对应的文件信息结合起来生成最终的key值,失败返回-1 pathname:已经存在的文件或目录的路径名proj_id:由用户指定的非零整数,通常是一个单字符(因为只有低 8 位会被使用),
只要这两个才参数一致,那么生成的key值就一定一致
key值是我们少见的由我们自己传参生成的固定的值,Linux在涉及内存方面的时候,通常是操作系统代为处理,这里其实是因为假设我们有两个进程AB,操作系统生成一个key给到A,但是B也需要这个key,但是操作系统是不知道哪两个进程要建立信道进行通信的,只有程序员知道我们哪两个进程要建立信道进行通信,因为在建立信道之前进程之间是相互独立的,所以就不能由操作系统来分配key值
(二)共享内存的申请
shmget函数的主要作用是在内核中创建或获取共享内存段的标识符
#include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg);
返回值:成功返回一个非负整数,即该共享内存段的标识码(shmid),失败返回-1
key:key是一个数字,它是不同共享内存让自己具备唯一性的标识size:共享内存大小,单位为字节,一般最好是4096的整数倍,它按照页申请,即需要4097字节就申请4096*2个字节
shmflg是一个标识符
| shmflg | 含义 | 作用 | 
|---|---|---|
| IPC_CREAT | 创建标志 | 如果指定的共享内存段不存在,就创建一个新的共享内存段;若已存在,则直接返回其标识符,常用于在不确定共享内存是否存在时创建或获取它 | 
| IPC_EXCL | 排他标志 | 需和 IPC_CREAT 一起使用,若共享内存已存在,shmget 调用失败,errno 设为 EEXIST,可确保新创建共享内存 | 
| IPC_NOWAIT | 非阻塞标志 | 当操作不能立即完成时,不阻塞调用进程,直接返回错误,例如在获取共享内存时,若当前资源无法立即获取,不等待而直接返回错误 | 
| SHM_HUGETLB | 大页标志 | 尝试使用大页内存分配共享内存段,大页内存可减少页表项数量,降低内存管理开销,提高系统性能,适合处理大量数据 | 
| SHM_NORESERVE | 不保留交换空间标志 | 不预先为共享内存段保留交换空间,正常创建共享内存时,系统会预留交换空间以防内存不足,使用此标志可节省交换空间,但可能导致内存紧张时出现页面交换问题 | 
| 权限标志(如 0600、0666 等) | 权限设置 | 类似文件权限设置,用于规定共享内存段的访问权限,0600 表示只有所有者有读写权限,0666 表示所有用户都有读写权限 | 
2、将共享内存段连接到进程地址空间
shmat函数的核心作用是在调用进程的虚拟地址空间和共享内存段的物理内存之间建立映射关系,在调用shmget函数时,虽然创建或获取了共享内存段的标识符,但进程还不能直接访问该共享内存,只有通过shmat函数将共享内存段附加到进程的地址空间后,进程才能像访问普通内存一样访问共享内存段中的数据
#include <sys/types.h> #include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg);
- 返回值:成功返回一个指针,指向共享内存的起始地址,失败返回 -1
 shmid:共享内存段的标识符,用于指定要附加的共享内存段,即shmget的返回值shmaddr:指定共享内存段要附加到的进程地址空间的地址,如果为 NULL,则由系统自动选择合适的地址shmflg:标识符
| 类型 | 含义 | 作用 | 
|---|---|---|
| SHM_RDONLY | 共享内存只读 | 以只读模式将共享内存段附加到进程的地址空间,进程只能读取共享内存中的数据,不能进行写操作,增强数据安全性,适用于多进程共享只读数据的场景 | 
| SHM_RND | 地址舍入 | 当 shmaddr 参数不为 NULL 时,将 shmaddr 向下舍入到一个合适的内存边界(通常是系统页面大小的整数倍),保证共享内存的正确附加 | 
| SHM_REMAP | 重新映射 | 如果共享内存段已经被附加到进程的地址空间,使用此标志可以重新映射该共享内存段,常用于更新共享内存的映射关系 | 
| SHM_EXEC | 可执行权限 | 允许在共享内存段上执行程序指令,不过并非所有系统都支持该标志,适用于需要在共享内存中执行代码的特殊场景 | 
| SHM_COPY | 创建私有副本 | 某些系统支持该标志,尝试创建共享内存段的一个私有副本,后续对该副本的修改不会影响其他进程看到的共享内存内容,用于实现进程对共享内存的独立修改 | 
| SHM_ANON | 匿名共享内存 | 部分系统支持该标志,用于创建匿名共享内存段,此时 shmid 参数会被忽略,可结合 shmaddr 使用,常用于父子进程间的内存共享 | 
3、将内存共享段与当前进程脱离
当进程调用shmat函数将共享内存段附加到自己的地址空间后,系统会在进程的虚拟地址空间和共享内存段的物理内存之间建立映射关系,使得进程可以像访问普通内存一样访问共享内存,而shmdt函数的核心作用就是解除这种映射关系
#include <sys/types.h> #include <sys/shm.h> int shmdt(const void *shmaddr);
- 返回值:成功返回0,失败返回-1
 shmaddr:shmat返回的指针
4、控制共享内存
通过cmd控制共享内存
#include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 返回值:成功返回0,失败返回-1
 shmid:同上cmd:将要采取的动作
| 类型 | 含义 | 作用 | 
|---|---|---|
| IPC_STAT | 获取状态信息 | 将与 shmid 关联的共享内存段的当前状态信息复制到 buf 指向的 struct shmid_ds 结构体中,这个结构体包含了如共享内存段的大小、所有者、权限、创建时间、最后访问时间等信息 | 
| IPC_SET | 设置状态信息 | 使用 buf 指向的 struct shmid_ds 结构体中的值来更新与 shmid 关联的共享内存段的部分状态信息,可以更新的信息包括共享内存段的所有者、权限等 | 
| IPC_RMID | 删除共享内存段 | 标记与 shmid 关联的共享内存段为删除状态,当最后一个使用该共享内存段的进程分离它之后,系统会真正释放该共享内存段所占用的资源,此时 buf 参数会被忽略,通常传递 NULL | 
| IPC_INFO | 获取系统共享内存信息 | 获取系统范围内的共享内存资源信息,这些信息会被存储在一个由系统定义的特定结构体中(通常不是 struct shmid_ds),buf 应指向该结构体,用于接收信息 | 
| SHM_INFO | 获取共享内存段信息 | 获取系统中共享内存段的相关统计信息,返回一个包含这些统计信息的结构体,同样,buf 要指向合适的结构体来接收数据 | 
| SHM_STAT | 通过索引获取共享内存状态 | 类似于 IPC_STAT,但不是通过 shmid 来指定共享内存段,而是通过一个索引值,可以通过这种方式遍历系统中的共享内存段 | 
buf:指向一个保存着共享内存的模式状态和访问权限的结构体struct shmid_ds
三、开始通信
由于共享内存通信没办法做到同步和互斥,我们通过加入命名管道的方式来形成同步与互斥的效果,普通的共享内存通信就调用完接口直接写直接读就行,比较简单,因为共享内存是可以通过指针直接读的,所以我们升级一下
1、comm.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FIFO_FILE "./myfifo"
#define MODE 0664
enum
{
    FIFO_CREATE_ERR = 1,
    FIFO_DELETE_ERR,
    FIFO_OPEN_ERR,
    FIFO_READ_ERR
};
using namespace std;
Log log;
const int size = 4096; 
const string pathname="/home/slm";
const int proj_id = 0x6667;
//封装一个获取Key值的函数
key_t GetKey()
{
	//使用ftok函数获取key值
    key_t k = ftok(pathname.c_str(), proj_id);
    if(k < 0)
    {
        exit(1);
    }
    //返回生成的key值
    return k;
}
//封装一个建立共享内存区的函数
int GetShareMemHelper(int flag)
{
	//调用获取key值,然后在物理内存上申请共享内存
    key_t k = GetKey();
    int shmid = shmget(k, size, flag);
    if(shmid < 0)
    {
        exit(2);
    }
	//返回shmid
    return shmid;
}
//用于创建一个新的共享内存段,如果指定的共享内存段已经存在,函数会失败
int CreateShm()
{
    return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}
//用于获取一个共享内存段,如果指定的共享内存段不存在,函数会创建一个新的共享内存段;如果已经存在,则直接返回该共享内存段的标识符
int GetShm()
{
    return GetShareMemHelper(IPC_CREAT); 
}
class Init
{
public:
    Init()
    {
        // 创建管道
        int n = mkfifo(FIFO_FILE, MODE);
        if (n == -1)
        {
            perror("mkfifo");
            exit(FIFO_CREATE_ERR);
        }
    }
    ~Init()
    {
		// 销毁管道
        int m = unlink(FIFO_FILE);
        if (m == -1)
        {
            perror("unlink");
            exit(FIFO_DELETE_ERR);
        }
    }
};
#endif2、processa.cpp
#include "comm.hpp"
extern Log log;
int main()
{
	//创建管道
    Init init;
    //调用 CreateShm 函数创建一个共享内存段,并返回该共享内存段的标识符shmid
    int shmid = CreateShm();
    //调用 shmat 函数将共享内存段附加到当前进程的地址空间,shmid 是共享内存段的标识符
    //nullptr 表示让系统自动选择附加地址,0 表示默认附加标志
    //返回值是共享内存段在进程地址空间中的起始地址,将其强制转换为char*类型并赋值给shmaddr
    char *shmaddr = (char*)shmat(shmid, nullptr, 0);
    //以只读模式打开命名管道文件FIFO_FILE,由于是以只读模式打开,该操作会阻塞
    //直到有其他进程以写模式打开同一个命名管道
    int fd = open(FIFO_FILE, O_RDONLY); 
    if (fd < 0)
    {
        exit(FIFO_OPEN_ERR);
    }
    //定义shmds用于存储共享内存段的状态信息
    struct shmid_ds shmds;
    while(true)
    {
	//定义一个字符变量c,调用read函数从命名管道文件描述符fd中读取1个字节的数据到c中
	//并将实际读取的字节数存储在s中,如果能读取到说明通信进程给我们发信号了,有内容
	//那我们就读取共享内存部分
        char c;
        ssize_t s = read(fd, &c, 1);
        if(s == 0) break;
        else if(s < 0) 
        {
            exit(FIFO_READ_ERR);
        }
		//可以直接通过指针访问共享内存
        cout << "client say@ " << shmaddr << endl; 
        sleep(1);
		//调用shmctl函数,使用IPC_STAT命令获取共享内存段的状态信息,并将其存储在shmds结构体中
		//然后将结构体shmds部分信息打印出来
        shmctl(shmid, IPC_STAT, &shmds);
        cout << "shm size: " << shmds.shm_segsz << endl;
        cout << "shm nattch: " << shmds.shm_nattch << endl;
        printf("shm key: 0x%x\n",  shmds.shm_perm.__key);
        cout << "shm mode: " << shmds.shm_perm.mode << endl;
    }
	//取消链接
    shmdt(shmaddr);
    //调用shmctl函数,使用IPC_RMID命令将共享内存段标记为待删除状态
    //当所有附加进程都分离后,系统会自动删除该共享内存段
    shmctl(shmid, IPC_RMID, nullptr);
    close(fd);
    return 0;
}3、processb.cpp
#include "comm.hpp"
int main()
{
	//调用GetShm函数获取一个共享内存段的标识符shmid
	//在参数相同、无特殊情况的情况下,shmid也相同
    int shmid = GetShm();
    char *shmaddr = (char*)shmat(shmid, nullptr, 0);
    int fd = open(FIFO_FILE, O_WRONLY); 
    if (fd < 0)
    {
        exit(FIFO_OPEN_ERR);
    }
    // 一旦有了共享内存,挂接到自己的地址空间中,直接把他当成你的内存空间来用即可
    // 不需要调用系统调用接口
    while(true)
    {
        cout << "Please Enter@ ";
        fgets(shmaddr, 4096, stdin);
        //在管道写入一个字符来通知服务器共享内存有新数据
        write(fd, "c", 1); 
    }
	//调用shmdt函数将共享内存段从当前进程的地址空间分离,释放相关的资源
    shmdt(shmaddr);
    close(fd);
    return 0;
}
四、其他问题
key是在操作系统内标定共享内存唯一性的,而shmid是在进程内标定资源唯一性的,二者虽然都是标定唯一性,但是使用范围不同,并且虽然共享内存属于文件系统,但是shmid和文件描述符兼容性不好,共享内存这方面单独搞了一套类似于文件描述符表的规则
共享内存的生命周期随内核,除非用户释放或者内核重启,共享内存才会释放
ipcs -m命令可以查看操作系统中所有的共享内存,其中perms是权限位,nattch是和当前共享内存关联的进程个数

共享内存是没有存在同步互斥这样的保护机制的,它是所有进程通信方式中最快的,因为相较其他方式,它的拷贝更少
共享内存内部的数据由用户自己维护
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
