• linux共享内存介绍


    一、什么是共享内存

    1、共享内存的定义

    由于进程通信的本质是要让两个不同的进程看到同一份资源,我们可以在物理内存上开辟一块空间,这块空间被称为共享内存,然后让这两个进程通过某种方式都能访问到这块内存,这样的话,两个进程之间就可以通信了。

    注意:共享内存操作默认不阻塞,如果多个进程同时读写共享内存,可能出现数据混乱,共享内存需要借助其他机制来保证进程间的数据同步,比如:信号量,共享内存内部没有提供这种机制。

    2、共享内存的特点

    第一,和创建进程类似,进程被创建的时候,会被分配一个pid来标识这个进程的唯一性,同时也方便OS管理这些进程,因此共享内存在被创建的时候,会被分配一个“ID”来标识唯一性。

    在这里插入图片描述
    第二,共享内存可以允许存在多个,为了区分这些共享内存,我们上面引入了“ID”的概念,但是要如何让两个进程连上同一个共享内存呢??

    就好比,我要和人solo(通信),我创建了一个房间(共享内存),这个房间就有了房间号(共享内存的ID),是个人都能进这个房间,根本没法通信,所以我们要设置房间密码。因此为了通信,我们需要两样东西,一个是房间号,一个是房间密码。

    二、使用共享内存的准备和收尾工作

    当我们需要使用共享内存时,我们要做的准备工作是:

     a.通过某种调用,在内存中开辟一块空间(shmget)
    
     b.通过某种调用,让两个进程挂接到这个新开辟的空间上(shmat)
    
    • 1
    • 2
    • 3

    当我们不需要使用共享内存时,我们需要做的收尾工作是:

    a. 断开进程和共享内存之间的关联(shmdt)
    
    b.释放共享内存(shmctl)
    
    • 1
    • 2
    • 3

    每一步都对应着一个系统调用接口,下面要说的就是这四个系统调用接口。

    三、shmget函数(shared memory get)

    这是共享内存的创建函数,调用以后会向内核申请内存,但是需要注意的是,共享内存是以“页”为单位的,一页是4KB = 4096bytes,所以一般建议申请共享内存的大小是4KB的整数倍!

    如果申请了4097个字节,那么OS会给你4096*2个字节的空间。
    函数原型:

    #include 
    #include 
    int shmget(key_t key, size_t size, int shmflg);
    
    • 1
    • 2
    • 3

    (1) 第一个参数 key
    第一个参数是唯一标识编号,也就是前面说到的房间密码,这个是由用户自己设置的,一般是通过ftok函数来,也可以自己随意设置一个整数。

    // ftok函数原型
    #include 
    #include 
    // 将两个参数作为种子, 生成一个 key_t 类型的数值
    key_t ftok(const char *pathname, int proj_id)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ftok函数的第一个参数:路径名
    ftok函数的第二个参数:项目ID
    关于这个函数无需想的太复杂,简单来说就是,从路径名中取出一部分,然后再从ID中取出一部分,最后再把两部分组合一下形成一个整数,我们就把这个整数当作“房间密码”。
    注意:ftok被不同进程调用,只要路径名和ID是一样的,生成的整数就是一样的。

    (2) 第二个参数 size
    第二个参数是开辟共享内存的大小,一般建议是4KB的整数倍(原因详见第三部分开头)。

    (3) 第三个参数 shmget
    第三个参数是创建共享内存的方式以及设置权限。属于位运算输入

    IPC_CREAT:可以单独使用,如果共享内存不存在,则重新开辟,函数返回值是新开辟的共享内存的ID;如果已经存在,则沿用已有的共享内存,函数返回值是已有的共享内存的 ID。
    
    IPC_EXCL:检测共享内存是否已经存在了,无法单独使用,要配IPC_CREAT使用,即 IPC_CREAT | IPC_EXCL。IPC_CREAT | IPC_EXCL:如果共享内存不存在,则重新开辟,函数返回值是新开辟的共享内存的ID;如果已经存在,则报错。
    
    IPC_CREAT | IPC_EXCL | 0664:开辟共享内存的同时,设置共享内存的访问权限.
    
    • 1
    • 2
    • 3
    • 4
    • 5

    (4)返回值解析
    如果共享内存开辟成功,则返回共享内存的ID(即房间号);否则返回 -1。

    四、关联和解除关联函数shmat

    创建 / 打开共享内存之后还必须和共享内存进行关联,这样才能得到共享内存的起始地址,通过得到的内存地址进行数据的读写操作,关联函数的原型如下

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

    参数:

    • shmid: 要操作的共享内存的 ID, 是 shmget () 函数的返回值
    • shmaddr: 共享内存的起始地址,用户不知道,需要让内核指定,写 NULL
    • shmflg: 和共享内存关联的对共享内存的操作权限
      SHM_RDONLY: 读权限,只能读共享内存中的数据
      0: 读写权限,可以读写共享内存数据
    • 返回值:关联成功,返回值共享内存的起始地址,关联失败返回 (void *) -1

    五、shmdt函数

    当进程不需要再操作共享内存,可以让进程和共享内存解除关联,另外如果没有执行该操作,进程退出之后,结束的进程和共享内存的关联也就自动解除了。

    int shmdt(const void *shmaddr);
    
    • 1

    参数:shmat () 函数的返回值,共享内存的起始地址
    返回值:关联解除成功返回 0,失败返回 - 1

    六. shmctl函数,删除共享内存

    shmctl () 函数是一个多功能函数,可以设置、获取共享内存的状态也可以将共享内存标记为删除状态。当共享内存被标记为删除状态之后,并不会马上被删除,直到所有的进程全部和共享内存解除关联,共享内存才会被删除。因为通过 shmctl () 函数只是能够标记删除共享内存,所以在程序中多次调用该操作是没有关系的。

    // 共享内存控制函数
    int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    
    // 参数 struct shmid_ds 结构体原型          
    struct shmid_ds {
    	struct ipc_perm shm_perm;    /* Ownership and permissions */
    	size_t          shm_segsz;   /* Size of segment (bytes) */
    	time_t          shm_atime;   /* Last attach time */
    	time_t          shm_dtime;   /* Last detach time */
    	time_t          shm_ctime;   /* Last change time */
    	pid_t           shm_cpid;    /* PID of creator */
    	pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
        // 引用计数, 多少个进程和共享内存进行了关联
    	shmatt_t        shm_nattch;  /* 记录了有多少个进程和当前共享内存进行了管联 */
    	...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 参数:
      shmid: 要操作的共享内存的 ID, 是 shmget () 函数的返回值
    • cmd: 要做的操作
      IPC_STAT: 得到当前共享内存的状态
      IPC_SET: 设置共享内存的状态
      IPC_RMID: 标记共享内存要被删除了
    • buf:
      cmdIPC_STAT, 作为传出参数,会得到共享内存的相关属性信息
      cmd
      IPC_SET, 作为传入参,将用户的自定义属性设置到共享内存中
      cmd==IPC_RMID, buf 就没意义了,这时候 buf 指定为 NULL 即可
    • 返回值:函数调用成功返回值大于等于 0,调用失败返回 - 1

    七、相关shell命令

    使用 ipcs 添加参数 -m 可以查看系统中共享内存的详细信息

    ipcs  -m
    
    ------------ 共享内存段 --------------
    键        shmid      拥有者  权限     字节     nattch     状态      
    0x00000000 425984     oracle     600        524288     2          目标       
    0x00000000 327681     oracle     600        524288     2          目标       
    0x00000000 458754     oracle     600        524288     2          目标 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    使用 ipcrm 命令可以标记删除某块共享内存

    # key == shmget的第一个参数
    $ ipcrm -M key 
    
    # id == shmget的返回值
    $ ipcrm -m id 
    
    • 1
    • 2
    • 3
    • 4
    • 5

    八、共享内存的状态

    // 参数 struct shmid_ds 结构体原型          
    struct shmid_ds {
    	struct ipc_perm shm_perm;    /* Ownership and permissions */
    	size_t          shm_segsz;   /* Size of segment (bytes) */
    	time_t          shm_atime;   /* Last attach time */
    	time_t          shm_dtime;   /* Last detach time */
    	time_t          shm_ctime;   /* Last change time */
    	pid_t           shm_cpid;    /* PID of creator */
    	pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
        // 引用计数, 多少个进程和共享内存进行了关联
    	shmatt_t        shm_nattch;  /* 记录了有多少个进程和当前共享内存进行了管联 */
    	...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    通过 shmctl() 我们可以得知,共享内存的信息是存储到一个叫做 struct shmid_ds 的结构体中,其中有一个非常重要的成员叫做 shm_nattch,在这个成员变量里边记录着当前共享内存关联的进程的个数,一般将其称之为引用计数。当共享内存被标记为删除状态,并且这个引用计数变为 0 之后共享内存才会被真正的被删除掉。

    当共享内存被标记为删除状态之后,共享内存的状态也会发生变化,共享内存内部维护的 key 从一个正整数变为 0,其属性从公共的变为私有的。这里的私有是指只有已经关联成功的进程才允许继续访问共享内存,不再允许新的进程和这块共享内存进行关联了。下图演示了共享内存的状态变化:
    在这里插入图片描述
    示例如下:
    server.cpp

    #include 
    #include 
    #include 
    
    int main()
    {
        const char* path = "./";
        int proId = 1000;
        int shmKey = ftok(path, proId);
        // 1. 创建共享内存, 大小为4k
        int shmid = shmget(shmKey, 4096, IPC_CREAT|0664);
        if(shmid == -1)
        {
            perror("shmget error");
            return -1;
        }
    
        // 2. 当前进程和共享内存关联
        void* ptr = shmat(shmid, NULL, 0);
        if(ptr == (void *) -1)
        {
            perror("shmat error");
            return -1;
        }
    
        // 3. 写共享内存
        const char* p = "hello, world, 我是共享我是共享内存,我有4k大小...";
        memcpy(ptr, p, strlen(p)+1);
    
        // 阻塞程序
        printf("按任意键继续, 删除共享内存\n");
        getchar();
    
        shmdt(ptr);
    
        // 删除共享内存
        shmctl(shmid, IPC_RMID, NULL);
        printf("共享内存已经被删除...\n");
    
        return 0;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    client.cpp

    #include 
    #include 
    #include 
    
    int main()
    {
        const char* path = "./";
        int proId = 1000;
        int shmKey = ftok(path, proId);
        //1.读取共享内存段,唯一差别在于 flags 参数,此时传0说明只读
        int shmId = shmget(shmKey, 0, 0);
        printf("shmId is %x\n",shmId);
        if(shmId == -1)
        {
            perror("shmget error");
            return -1;
        }
        //2.将本进程和共享内存关联
        void *ptr = shmat(shmId, NULL, 0);
        if (ptr == (void*)-1)
        {
            perror("shmat error");
            return -1;
        }
        // 3. 读共享内存
        printf("共享内存数据: %s\n", (char*)ptr);
        // 阻塞程序
        printf("按任意键继续, 删除共享内存\n");
        getchar();
        shmdt(ptr);
        // 删除共享内存
        shmctl(shmId, IPC_RMID, NULL);
        printf("共享内存已经被删除...\n");
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
  • 相关阅读:
    如何做好一个配置中心
    对Mysql数据表查询出来的结果进行排序
    <Linux>(极简关键、省时省力)《Linux操作系统原理分析之Linux 进程管理 4》(8)
    Hive数据库动态分区和静态分区插入区别
    【5G NR】RRC连接释放
    linux作业整理
    python从数据库导出电子表格
    docker部署neo4j
    判断用户输入的密码是否正确,如果是123,则为正确,如果不是,就错误
    SDVO:LDSO+语义,直接法语义SLAM(RAL 2022)
  • 原文地址:https://blog.csdn.net/qq_34888036/article/details/126356904