• Linux操作系统~基于systemV共享内存的进程间通信


    目录

    一.进程间通信有哪些方式

    二.什么是systemV

    三.共享内存-双向通信-大致实现思路

    四.4个函数about共享内存

    1.shmget函数-创建

     ftok函数

    ​编辑

     e.g.

    ipcs/ipcrm指令(ipc资源会被回收吗)

    2.shmctl函数-删除/释放

    3.shmat函数-挂接

    4.shmdt函数-去挂接

    5.e.g.

    五.总结/共享内存的特性

    Q:为什么共享内存的大小建议是4KB整数倍?

    六.共享内存的内核数据结构

    Q:为什么我们看到的shmid是0,1,2,3,4递增的呢?


    一.进程间通信有哪些方式

    首先我们先来看一下进程间通信总共有哪些方式?

    管道

    1. 匿名管道pipe
    2. 命名管道

    System V IPC

    1. System V 消息队列
    2. System V 共享内存
    3. System V 信号量

    POSIX IPC

    1. 消息队列
    2. 共享内存
    3. 信号量
    4. 互斥量
    5. 条件变量
    6. 读写锁

    这里我讨论的是基于systemV共享内存的进程间通信


    二.什么是systemV

            命名管道和匿名管道都是基于文件的通信方式,还有systemV标准的通信方式:

    1. OS层面专门为进程间通信设计的一个方案
    2. OS不相信任何用户,给用户提供功能的时候,采用系统调用
    3. System V进程间通信,一定会存在专门用来通信的接口(system call)

    同一主机内的进程间通信方案——system V方案

    注意进程间通信的本质:让不同进程看到同一份资源


    三.共享内存-双向通信-大致实现思路

    1.通过某种调用,在内存中创建一份内存空间

    2.通过某种调用,让进程(参与通信的多个进程)“挂接”到这份新开辟的内存空间上(挂接就是通过页表映射到这片物理空间)-让不同的进程看到了同一份资源

    3.不用共享内存后,我们需要去关联(去掉页表中的映射)

    4.释放共享内存


    四.4个函数about共享内存

    1.shmget函数-创建

    用于创建一个共享内存

    • key:用来唯一表示这个共享内存块(就像路径+文件名),需要用户自己设定。我们可以调用ftok函数,将自定义路径名和项目id,经过算法转换成一个唯一的key,这个key会设置到管理shm共享内存的数据结构中(注意这里的key和shmid是不一样的)。

    注:

    key_id实际上是一个有符号整数

    key只是用来在系统层面进行标识唯一性的,不能用来管理shm

    shmid是OS给用户返回的id,用来在用户层面对shm进行管理

            如果硬是要类比的话,shmid就像是文件描述符,key就像是struct_file的地址,OS是使用shmid对struct_file进行管理的

    • size:共享内存大小,建议是4KB的整数倍
    • shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
    • 返回值:shmid,管理当前共享内存的id;成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

    这里常用两个权限标志:

    1. IPC_CREATE:不存在则创建:如果单独使用IPC_CREAT,或者flg为0:创建一个共享内存,如果创建的共享内存已经存在,则直接返回当前已经存在的共享内存。(基本不会空手而归)
    2. IPC_EXCL(单独使用没有意义)

     ftok函数

    pathname:路径名

    proj_id:自定义项目名ID

            路径名+项目名ID应该是唯一的,通过这两个参数生成的key也就是唯一的了,用于唯一标识共享内存块


     e.g.

    1.生成同一个key

    我们在server和client中使用同一个获取key值的方法ftok(并且传入相同的路径和项目ID),这样也就保证了两个不同的进程可以看到同一份资源。

    1. int main()
    2. {
    3. key_t key = ftok(PATH_NAME, PROJ_ID);
    4. if(key < 0){
    5. perror("ftok");
    6. return 1;
    7. }
    8. printf("%u\n", key);
    9. return 0;
    10. }

    2.创建一个共享内存

    int shmid = shmget(key, SIZE, IPC_CREAT|IPC_EXCL|0666); //创建全新的shm,如果和系统已经存在ID冲突,我们出错返回,这个0666表示共享内存的权限

    1. if(shmid < 0){
    2. perror("shmget");
    3. return 2;
    4. }

    ipcs/ipcrm指令(ipc资源会被回收吗)

    ipcs可以看到systemV进程通信设施状态的指令(信号量,共享内存,消息队列都属于ipc资源)

    1. ipcs -s可以显示信号量的状态
    2. ipcs -m显示共享内存的状态
    3. ipcs -q显示消息队列的状态

            systemV的IPC资源,生命周期是随内核的,只能通过,程序员显示的释放(释放IPC资源的指令或者system call系统调用)或者是OS重启。文件,堆空间,进程退出的时候,这些资源都会被操作系统回收,但是IPC资源不会。

    ipcrm指令:

    用于删除ipc资源参数:

    -M   以shmkey删除共享内存

    -m   以shmid删除共享内存

    -Q   以msgkey删除消息队列

    -q    以msgid删除消息队列

    -S    以semkey删除信号量

    -s    以semid删除信号量


    2.shmctl函数-删除/释放

    • shmid:前面shmget函数的返回值,描述共享内存数据结构中的shmid
    • cmd:cmd指令,我们这里只用删除指令IPC_RMID
    • *buf:操作系统用于描述当亲共享内存的数据结构

    shmctl(shmid, IPC_RMID, NULL);

    printf("key: 0x%x, shmid: %d -> shm delete success\n", key, shmid);

    这样就能够删除shmid对应的共享内存(释放空间)


    3.shmat函数-挂接

    • shmid:前面shmget函数的返回值,描述共享内存数据结构中的shmid
    • shmaddr:挂接到哪个位置,一般设为NULL(我们自己不太清楚,但是操作系统清楚)
    • shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的,我们一般传0
    • 返回值:成功返回创建共享内存的起始地址(虚拟地址),就像是malloc的返回值

    char *mem = (char*)shmat(shmid, NULL, 0);

    printf("attaches shm success\n");

    这样就得到了一个指向共享内存的地址mem


     4.shmdt函数-去挂接

            并不是释放共享内存,而是删除共享内存和进程的关系(删除进程页表上进程虚拟地址和共享内存物理地址映射的页表项)

    • shmid:前面shmget函数的返回值,描述共享内存数据结构中的shmid
    • shmaddr:去挂接哪个位置,一般不设置,用缺省值(我们自己不太清楚,但是操作系统清楚)
    • shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的,我们一般不传

        shmdt(mem);

    这样当前进程共享内存的挂接就解除了


     5.e.g.

    这里模拟实现一个用共享队列进行通信的服务端和客户端。服务端读数据,客户端写数据

    server.c

    1. #include
    2. #include
    3. #include
    4. #include
    5. #define PATH_NAME "./" //用于ftok函数,表示当前的路径名,路径名+项目ID应该是唯一的,随之生成的key也就是唯一的
    6. #define PROJ_ID 0x1234 //用于ftok函数,表示当前的路径名,路径名+项目ID应该是唯一的,随之生成的key也就是唯一的
    7. #define SIZE 4097
    8. int main()
    9. {
    10. key_t key = ftok(PATH_NAME, PROJ_ID);
    11. if(key < 0){
    12. perror("ftok");
    13. return 1;
    14. }
    15. int shmid = shmget(key, SIZE, IPC_CREAT|IPC_EXCL|0666);
    16. //创建全新的shm,加上了IPC_EXCL选项,如果和系统已经存在ID冲突,则出错返回,这个0666表示共享内存的权限
    17. if(shmid < 0){
    18. perror("shmget");
    19. return 2;
    20. }
    21. printf("key: %u, shmid: %d\n", key, shmid);
    22. //sleep(1);
    23. char *mem = (char*)shmat(shmid, NULL, 0); //将共享内存挂接到当前进程
    24. printf("attaches shm success\n");
    25. //sleep(15);
    26. //此处进行通信逻辑,直接像数组一样对共享内存进行操作即可,服务端进行读操作
    27. while(1){
    28. sleep(1);
    29. printf("%s\n", mem); //server 认为共享内存里面放的是一个长字符串
    30. }
    31. shmdt(mem); //去挂接
    32. printf("detaches shm success\n");
    33. //sleep(5);
    34. shmctl(shmid, IPC_RMID, NULL); //释放共享内存
    35. printf("key: 0x%x, shmid: %d -> shm delete success\n", key, shmid);
    36. //sleep(10);
    37. return 0;
    38. }

    client.c

    1. #include
    2. #include
    3. #include
    4. #include
    5. #define PATH_NAME "./"
    6. #define PROJ_ID 0x1234
    7. #define SIZE 4097
    8. int main()
    9. {
    10. key_t key = ftok(PATH_NAME, PROJ_ID);
    11. if(key < 0){
    12. perror("ftok");
    13. return 1;
    14. }
    15. printf("%u\n", key);
    16. //client这里只需要进行获取即可,只需要加上IPC_CREATE选项即可
    17. int shmid = shmget(key, SIZE, IPC_CREAT);
    18. if(shmid < 0){
    19. perror("shmget");
    20. return 1;
    21. }
    22. char *mem = (char*)shmat(shmid, NULL, 0);
    23. //sleep(5);
    24. printf("client process attaches success!\n");
    25. //此处进行通信逻辑,直接像数组一样对共享内存进行操作即可,客户端进行写操作,每隔两秒,多写入一个字母
    26. char c = 'A';
    27. while(c <= 'Z'){
    28. mem[c-'A'] = c;
    29. c++;
    30. mem[c-'A'] = 0;
    31. sleep(2);
    32. }
    33. shmdt(mem);
    34. //sleep(5);
    35. printf("client process detaches success\n");
    36. //client不需要释放共享内存的空间,由服务端释放
    37. return 0;
    38. }

    运行结果:我们2s写一次,1s读一次,我们读的时候并没有把数据拿走,所以会读两次

            这里server会一直读取共享内存中的内容,即使共享内存中没有内容可以读,所以不会退出while循环,也不会执行去挂接和释放共享内存的操作。(需要后期自己用ipcrm指令删除,也可以考虑在循环中定义一个计数器,几s后自动退出循环,进行去挂接,释放共享内存的操作)

            这里我们读的时候直接读的是mem这个连续的地址空间,和前面管道中的不同,管道是基于文件的,在一次打开中,读过的内容不会再读了。管道中的内容都不会像普通文件一样保存在磁盘中,读取过的数据就会失效,不能重复读取(包括匿名管道和命名管道)


    五.总结/共享内存的特性

    1.我们在使用共享内存的时候,没有调用类似pipe or fifo中的read过样的接口,因为匿名管道和命名管道都是基于文件的,read和write也是基于文件的,本质是将数据从内核拷贝到用户,或者从用户拷贝到内核。共享内存并不是基于文件的。

    2.共享内存一旦建立好并映射进自己进程的地址空间,进行通信的进程就可以直接看到该共享内存,就如同自己malloc的空间一般,不需要任何系统调用接口,直接当数组用都行。所以共享内存是所有的进程间通信中速度最快的!(共享内存最多拷贝一次,管道可能要4次或者更多)

    3.当client没有写入,甚至没有启动的时候,server端也会直接读取shm,不会像管道那样等待client写入。共享内存不提供任何司步或者互斥儿制,需要程序员自行保证数据的安全!

    Q:为什么共享内存的大小建议是4KB整数倍?

            共享内存在内核中申请的基本单位是页,内存页,大小为4KB,内核给你的大小是4KB的整数倍。比如我申请4097个字节->内核会给你4096byte*2的空间


    六.共享内存的内核数据结构

    用户层:是操作系统层的子集

    ipc_perm中存放的uid是用户id,gid是组id,key就是我们传入的用于唯一标识共享内存块的id。

    除此之外还有当前共享内存的一些基本信息

    在内核中,所有的ipc资源都是通过数组组织起来的,这三种资源结构体都不一样,怎么放在同一个数组中呢?原因就是下面的ipc_perm

    SystemV三种通信方式的ipc资源,描述ipc资源的数据结构很相似,而且第一个成员都是ipc_perm,这个ipc_perm是完全相同的。

    这个数组实际上是一个ipc_perm*类型的数组,也就是一个指针数组,然后根据对应的类型强制类型转换。(用C语言实现的切片技术

    Q:为什么我们看到的shmid是0,1,2,3,4递增的呢?

            shmid为什么是0,1,2,3,4递增的呢?因为说白了这个shmid就是ipc_perm*数组的下标,共享内存是ipc资源,所有ipc资源都是用一个ipc_perm*数组管理的,shmid就是直接取了这个数组的下标。

  • 相关阅读:
    vue3 自定义组件 v-model 原理解析
    低代码/无代码平台盘点:Notion Like 产品、简道云、伙伴云
    不仅仅是钥匙防丢器,Numy防丢器还支持苹果Find My功能
    【DDIM】DENOISING DIFFUSION IMPLICIT MODELS【论文精读】【视频讲解】【公式推导】
    负载均衡技术全景:理论、实践与案例研究
    JAVA Stream流
    信息安全技术 关键信息基础设施安全保护要求 2022版附下载地址
    打破一万小时定律--20个小时学会任何事情的五个步骤
    @Import :Spring Bean模块装配的艺术
    Java中的super关键字
  • 原文地址:https://blog.csdn.net/qq_24016309/article/details/128169193