【Linux】进程间通讯-> 共享内存

[复制链接]
发表于 2025-11-17 06:44:28 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
共享内存原理


在C语言/C++中,malloc也可以在物理内存申请空间,将申请的物理内存空间通过页表映射到进程所在空间,将内存空间的起始所在(假造所在)返回,进而进程可以利用假造所在通过页表映射到物理内存的方式访问到申请的内存空间。
那么malloc和共享内存有什么区别呢?
区别就是malloc出来的空间,是进程在自己的堆上申请的空间,没有和其他进程创建映射关系,不能被其他进程共享!

  • 共享内存是用来进程间通讯的!
  • 共享内存是一种通讯方式,全部想通讯的进程都可以利用!
  • OS中肯定会大概存在很多的共享内存!
共享内存概念

共享内存是最快的 IPC 方式之一。答应多个进程共享同一块物理内存地区。进程可以通过将共享内存地区映射到自己的所在空间来访问这块内存。如许,多个进程可以直接在共享内存地区中读写数据,大大进步了通讯服从。
共享内存函数

shmget函数


  • 创建共享内存


乐成,返回共享内存的标识符;失败,返回-1。
shmflg

int shmflg 权限标记
常用选项:

  • IPC_CREAT:如果shm不存在,则创建;存在,则获取。
  • IPC_EXCL:不能单独利用。
IPC_CREAT | IPC_EXCL:如果shm不存在创建,则创建;存在,则堕落返回。(对用户来讲,如果创建乐成,肯定是一个新的shm)
进程要举行通讯,起首要让差异的进程看到同一份资源。怎样包管两个进程看到的就是同一份共享内存呢?
key

通过接口int shmget(key_t key,size_t size,int shmflg),第二个参数key来包管。
key_t范例实在就是一个32位的整数。

key是什么不紧张,紧张的是它能举行唯一性标识一个共享内存段。
怎样形成一个key呢?通过ftok函数来形成一个key。
ftok函数


  • 将文件路径名和项目的识符转换为System V IPC(进程间通讯)key(键)。


乐成,返回乐成创建的key;失败,返回-1。
如果举行通讯的两个进程利用ftok函数,通报的两个参数是一样的,那么形成的key值也是同样的,就能找到同一个共享内存。一个进程通过key举行创建,另一个进程通过key获取。

makefile
  1. .PHONY:all
  2. all:shm_client shm_server
  3. shm_client:shm_client.cc
  4.         g++ -o $@ $^ -std=c++11
  5. shm_server:shm_server.cc
  6.         g++ -o $@ $^ -std=c++11
  7. .PHONY:clean
  8. clean:
  9.         rm -f shm_client shm_server
复制代码
comm.hpp
  1. #ifndef _COMM_HPP_
  2. #define _COMM_HPP_
  3. #include <iostream>
  4. #include <cstdlib>
  5. #include <cstdio>
  6. #include <cerrno>
  7. #include <cstring>
  8. #include <sys/types.h>
  9. #include <sys/ipc.h>
  10. #include <sys/shm.h>
  11. #define PATHNAME "."
  12. #define PROJ_ID 0x88 // 项目标识符,随便写的。
  13. #define MAX_SIZE 4096 //共享内存大小。
  14. //获取key
  15. key_t getKey()
  16. {
  17.     key_t k = ftok(PATHNAME, PROJ_ID);//使用ftok可以获取同样的一个key!
  18.     if (k < 0)
  19.     {
  20.         std::cerr << errno << ":" << strerror(errno) << std::endl;
  21.         exit(1);
  22.     }
  23.     return k;
  24. }
  25. #endif
复制代码
shm_server.cc
  1. #include "comm.hpp"
  2. int main()
  3. {
  4.     key_t k = getKey();
  5.     printf("0x%x\n", k);
  6.     return 0;
  7. }
复制代码
 shm_client.cc
  1. #include "comm.hpp"
  2. int main()
  3. {
  4.     key_t k = getKey();
  5.     printf("0x%x\n", k);
  6.     return 0;
  7. }
复制代码
运行结果

size

共享内存巨细一样寻常发起为4KB的整数倍,体系分配共享内存是以4KB巨细为单元的!这里我们界说了共享内存巨细为4096KB,如果定为,比方:4097KB,那么内核给用户的共享内存巨细会自动向上取整,申请4096*2KB。注意:内核给用户的申请的巨细,和用户能用的巨细是两码事,固然内核会申请4096*2KB,但是用户仍然只能用4097KB(ipcs -m 查察共享内存巨细仍然为4097KB)。
创建共享内存

comm.hpp
  1. //获取key
  2. key_t getKey()
  3. {
  4.     key_t k = ftok(PATHNAME, PROJ_ID);//使用ftok可以获取同样的一个key!
  5.     if (k < 0)
  6.     {
  7.         std::cerr << errno << ":" << strerror(errno) << std::endl;
  8.         exit(1);
  9.     }
  10.     return k;
  11. }
  12. int getShmHelper(key_t k, int flags)
  13. {
  14.     int shmid = shmget(k, MAX_SIZE, flags);
  15.     if (shmid < 0)
  16.     {
  17.         std::cerr << errno << ":" << strerror(errno) << std::endl;
  18.         exit(2);
  19.     }
  20.     return shmid;
  21. }
  22. //获取共享内存
  23. int getShm(key_t k)
  24. {
  25.     return getShmHelper(k, IPC_CREAT);
  26. }
  27. //创建共享内存
  28. int createShm(key_t k)
  29. {
  30.     return getShmHelper(k, IPC_CREAT | IPC_EXCL);
  31. }
复制代码
shm_server.cc 
  1. #include "comm.hpp"
  2. int main()
  3. {
  4.     key_t k = getKey();
  5.     printf("key: 0x%x\n", k);
  6.     int shmid = createShm(k);
  7.     printf("shmid: %d\n",shmid);
  8.    
  9.     return 0;
  10. }
复制代码
shm_client.cc
  1. #include "comm.hpp"
  2. int main()
  3. {
  4.     key_t k = getKey();
  5.     printf("key: 0x%x\n", k);
  6.     int shmid = getShm(k);
  7.     printf("shmid: %d\n",shmid);
  8.    
  9.     return 0;
  10. }
复制代码
 运行结果

怎样明白key和shmid,有什么区别? 
再谈key

基于上述明白,我们熟悉到:



我们先品评两个小题目: 
C语言C++ malloc的时间,比方申请1024字节的内存空间,int *p = malloc(size_t 1024),开释free(p),对空间开释不能局部开释,以是free开释的时间要传入所申请空间的起始所在。

题目是怎么知道从p开始开释多少个字节呢?

固然用户申请了1024个字节,但是OS为了管理用户申请的空间,还会额外申请一些空间,用来记任命户malloc所申请出来的内存空间的属性信息,比方内存块巨细,是否被利用等信息。以是free的时间,会自己去额外申请的空间中搜刮内存块巨细属性,资助用户去开释。
在创建进程的时间,OS为了管理进程,以先形貌,后构造的方式,还创建了一个形貌该进程的内核数据结构PCB,用来记载进程的干系属性,以是OS管理进程就通过PCB来管理。
基于对上面两个题目的明白,那么:
OS中肯定会存在很多共享内存,那么OS也要对共享内存以先形貌,后构造的方式管理起来!

以是,这里并不但单只是用户申请一块物理内存空间,OS也肯定会额外申请一些空间用来记载共享内存的干系属性信息,比方共享内存的权限、容量等。
以是共享内存 = 物理内存块 + 共享内存的干系属性!
实际上,用户在申请共享内存的时间,OS除了给用户申请一个物理内存块之后,还会给共享内存申请一个数据结构对象,OS要管理共享内存,并不是直接通过管理这个物理内存块,而是通过管理这个数据结构对象来举行管理。
创建共享内存的时间,是通过key来包管共享内存在体系中是唯一的。那么key在哪呢?
在创建共享内存时,key就作为共享内存的干系属性被填入到了形貌共享内存干系属性的数据结构对象中。
eg:
struct shm{
//干系属性
//...
key_t k;
}
(下面还会商到)
以是,当我们上层调用ftok创建key,再调用shmget创建共享内存时,把key作为参数传入shmget中,本质是把key设置进创建好的共享内存对应的数据结构对象中某一个属性字段里。另一个进程再去获取共享内存时,并不是通过共享内存对应的物理内存块,而是遍历共享内存所对应的干系属性,查找自己的key和共享内存的key是否相当。
总结:key要通过shmget,设置进共享内存属性中!用来标识该共享内存在内核中的唯一性!
shmid与key雷同于文件概念里的fd与inode。利用具有唯一性的key值来标定特定的共享内存,给上层返回一个特定的整数,利用户可以通过这个特定的整数来访问特定的共享内存。

当我们进程退出,再实验的时间,这里报了一个文件已经存在的错误。进程已经竣事了,共享内存资源仍然存在。

  • 共享内存生命周期随OS,不是随进程的。除非显式的利用对应的下令或接口开释共享内存,否则共享内存会随着OS的运行不绝存在。
  • 生命周期随OS,不随进程,也是全部System V版本进程间通讯的共性!
  • 跟管道不一样,管道的生命周期随进程。
查察IPC资源


  • ipcs -m/q/s


  • ipcs -m 查察体系中共享内存段的干系信息

删除共享内存


  • 利用下令:ipcrm -m + shmid



  • 删除共享内存也有干系接口,利用shmctl函数。
shmctl函数


  • 控制共享内存
获取共享内存的属性、设置共享内存的属性、删除共享内存都利用shmctl接口。

失败返回-1。

  • shmid:共享内存id。
  • cmd:控制共享内存的方式。
包罗选项IPC_STAT、IPC_SET、IPC_RMID、IPC_INFO

此中IPC_RMID选项,就是删除共享内存。

  • buffer:共享内存的干系属性。
comm.hpp
  1. //获取共享内存
  2. //...
  3. //创建共享内存
  4. //...
  5. //删除共享内存
  6. int delShm(int shmid)
  7. {
  8.     if (shmctl(shmid, IPC_RMID, NULL) == -1)
  9.     {
  10.         std::cerr << errno << strerror(errno) << std::endl;
  11.     }
  12. }
  13. //...
复制代码
shm_server.cc
  1. #include "comm.hpp"
  2. int main()
  3. {
  4.     key_t k = getKey();
  5.     printf("key: 0x%x\n", k);
  6.     int shmid = createShm(k);
  7.     printf("shmid: %d\n", shmid);
  8.     sleep(5);
  9.     delShm(shmid);
  10.     return 0;
  11. }
复制代码
一样寻常情况,共享内存谁创建谁删除。

至此,我们完成了共享内存的创建与删除工作,但是,现在所创建的共享内存还没有办法利用,由于共享内存还没有和进程关联起来。接下来,我们要完成共享内存与进程关联的工作。
shmat函数


  • 将共享内存段毗连到进程的所在空间


乐成,返回,共享内存的起始所在;失败,返回-1。

  • shmaddr:指定毗连所在。
要将共享内存映射到哪一个所在空间。大部分情况下,这个参数可以不设置,由于我们并不清楚我们想将共享内存映射到假造所在空间中的哪个地区。设定位为NULL即可,体系会自动选择一个符合的所在来毗连共享内存段。

  • shmflg:标记位,用于控制共享内存段的毗连方式和权限等。
常用的标记位为:
SHM_RDONLY:表现以只读方式毗连共享内存。如果不设置(0),默认以读写方式毗连共享内存段,即进程可以对毗连后的共享内存段举行读和写操纵。
comm.hpp
  1. //获取共享内存
  2. //...
  3. //创建共享内存
  4. //...
  5. //挂接共享内存
  6. void *attachShm(int shmid)
  7. {
  8.     void *mem = shmat(shmid, nullptr, 0);
  9.     if ((long long)mem == -1L) // linux为64位系统,指针大小为8
  10.     {
  11.         std::cerr << errno << ": " << strerror(errno) << std::endl;
  12.         exit(3);
  13.     }
  14. }
  15. //删除共享内存
  16. //...
复制代码
 shm_server.cc
  1. #include "comm.hpp"
  2. int main()
  3. {
  4.     key_t k = getKey();
  5.     printf("key: 0x%x\n", k);
  6.     int shmid = createShm(k);
  7.     printf("shmid: %d\n", shmid);
  8.    
  9.     char *start = (char*)attachShm(shmid);
  10.     printf("attch success,address start:%p\n",start);
  11.     delShm(shmid);
  12.     return 0;
  13. }
复制代码

此时,Permission denied 权限被拒绝了。创建共享内存的时间,添加权限即可。
comm.hpp
  1. int getShmHelper(key_t k, int flags)
  2. {
  3.     int shmid = shmget(k, MAX_SIZE, flags);
  4.     if (shmid < 0)
  5.     {
  6.         std::cerr << errno << ":" << strerror(errno) << std::endl;
  7.         exit(2);
  8.     }
  9.     return shmid;
  10. }
  11. //获取共享内存
  12. int getShm(key_t k)
  13. {
  14.     return getShmHelper(k, IPC_CREAT | 0600);
  15. }
  16. int createShm(key_t k)
  17. {
  18.     return getShmHelper(k, IPC_CREAT | IPC_EXCL | 0600);
  19. }
  20. //挂接共享内存
  21. void *attachShm(int shmid)
  22. {
  23.     void *mem = shmat(shmid, nullptr, 0);
  24.     if ((long long)mem == -1L) // linux为64位系统,指针大小为8
  25.     {
  26.         std::cerr << errno << ": " << strerror(errno) << std::endl;
  27.         exit(3);
  28.     }
  29. }
  30. //删除共享内存
  31. //...
复制代码
shm_server.cc
  1. #include "comm.hpp"
  2. int main()
  3. {
  4.     key_t k = getKey();
  5.     printf("key: 0x%x\n", k);
  6.     int shmid = createShm(k);
  7.     printf("shmid: %d\n", shmid);
  8.    
  9.     sleep(2);
  10.     char *start = (char*)attachShm(shmid);
  11.     printf("attch success,address start:%p\n",start);
  12.    
  13.     sleep(2);
  14.     delShm(shmid);
  15.     return 0;
  16. }
复制代码
下面我们写一个监控监控脚本监测运行一下。 

进程和共享内存挂接工作也已经完成了。接下来就可以举行通讯了,当通讯竣事以后,要先去关联,将进程和共享内存的关联去掉,再删除共享内存。
shmdt函数


  • 去掉进程和共享内存的关联性

乐成,返回0;失败,返回-1。
comm.hpp
  1. //创建共享内存
  2. //...
  3. //挂接共享内存
  4. //...
  5. //共享内存去关联
  6. void detachShm(void *start)
  7. {
  8.     if (shmdt(start) == -1)
  9.     {
  10.         std::cerr << errno << ": " << strerror(errno) << std::endl;
  11.     }
  12. }
  13. //删除共享内存
  14. //...
复制代码
shm_server.cc 
  1. #include "comm.hpp"
  2. int main()
  3. {
  4.     key_t k = getKey();
  5.     printf("key: 0x%x\n", k);
  6.     int shmid = createShm(k);
  7.     printf("shmid: %d\n", shmid);
  8.    
  9.     sleep(2);   
  10.     char *start = (char*)attachShm(shmid);
  11.     printf("attch success,address start:%p\n",start);
  12.    
  13.     sleep(2);
  14.    
  15.     //通信
  16.     //...
  17.    
  18.     //去关联
  19.     detachShm(start);
  20.    
  21.     sleep(2);
  22.     delShm(shmid);
  23.     return 0;
  24. }
复制代码
要通讯,让client也和共享内存挂接起来。
 shm_client.cc
  1. #include "comm.hpp"
  2. int main()
  3. {
  4.     key_t k = getKey();
  5.     printf("key: 0x%x\n", k);
  6.     int shmid = getShm(k);
  7.     printf("shmid: %d\n",shmid);
  8.    
  9.     sleep(5);   
  10.     char *start = (char*)attachShm(shmid);
  11.     printf("attch success,address start:%p\n",start);
  12.    
  13.     sleep(5);
  14.    
  15.     //通信
  16.     //...
  17.    
  18.     //去关联
  19.     detachShm(start);
  20.     sleep(2);
  21.     return 0;
  22. }
复制代码

共享内存直接就可以看成缓冲区,可以直接向共享内存输入输出。
共享内存通讯

comm.hpp
  1. #ifndef _COMM_HPP_
  2. #define _COMM_HPP_
  3. #include <iostream>
  4. #include <cstdlib>
  5. #include <cstdio>
  6. #include <cerrno>
  7. #include <cstring>
  8. #include <unistd.h>
  9. #include <sys/types.h>
  10. #include <sys/ipc.h>
  11. #include <sys/shm.h>
  12. #define PATHNAME "."
  13. #define PROJ_ID 0x66  // 项目标识符,随便写的。
  14. #define MAX_SIZE 4096 // byte
  15. key_t getKey()
  16. {
  17.     key_t k = ftok(PATHNAME, PROJ_ID); // 使用ftok可以获取同样的一个key!
  18.     if (k < 0)
  19.     {
  20.         std::cerr << errno << ":" << strerror(errno) << std::endl;
  21.         exit(1);
  22.     }
  23.     return k;
  24. }
  25. int getShmHelper(key_t k, int flags)
  26. {
  27.     int shmid = shmget(k, MAX_SIZE, flags);
  28.     if (shmid < 0)
  29.     {
  30.         std::cerr << errno << ":" << strerror(errno) << std::endl;
  31.         exit(2);
  32.     }
  33.     return shmid;
  34. }
  35. int getShm(key_t k)
  36. {
  37.     return getShmHelper(k, IPC_CREAT | 0600);
  38. }
  39. int createShm(key_t k)
  40. {
  41.     return getShmHelper(k, IPC_CREAT | IPC_EXCL | 0600);
  42. }
  43. void *attachShm(int shmid)
  44. {
  45.     void *mem = shmat(shmid, nullptr, 0);
  46.     if ((long long)mem == -1L) // linux为64位系统,指针大小为8
  47.     {
  48.         std::cerr << errno << ": " << strerror(errno) << std::endl;
  49.         exit(3);
  50.     }
  51. }
  52. void detachShm(void *start)
  53. {
  54.     if (shmdt(start) == -1)
  55.     {
  56.         std::cerr << errno << ": " << strerror(errno) << std::endl;
  57.     }
  58. }
  59. int delShm(int shmid)
  60. {
  61.     if (shmctl(shmid, IPC_RMID, NULL) == -1)
  62.     {
  63.         std::cerr << errno << strerror(errno) << std::endl;
  64.     }
  65. }
  66. #endif
复制代码
shm_client.cc
  1. #include "comm.hpp"
  2. int main()
  3. {
  4.     key_t k = getKey();
  5.     printf("key: 0x%x\n", k);
  6.     int shmid = getShm(k);
  7.     printf("shmid: %d\n", shmid);
  8.     sleep(5);
  9.     char *start = (char *)attachShm(shmid);
  10.     printf("attch success,address start:%p\n", start);
  11.     //sleep(5);
  12.     // 通信
  13.     const char *message = "Hello server,我是另一个进程,我正在和你通信!";
  14.     pid_t id = getpid();
  15.     int cnt = 1;
  16.     while (true)
  17.     {
  18.         sleep(1);
  19.         snprintf(start, MAX_SIZE, "%s[pid:%d][消息编号:%d]", message, id, cnt++);
  20.     }
  21.     // 不需要再定义缓冲区,写到共享内存中。共享内存就可以当作用户层缓冲区,可以直接写入。跟管道不同!
  22.     // char buffer[1024];
  23.     // while (true)
  24.     // {
  25.     //     snprintf(start, sizeof(buffer), "%s[pid:%d][消息编号:%d]", message, id, cnt++);
  26.     //     memcpy(start, buffer, strlen(buffer) + 1);
  27.     // }
  28.     // 去关联
  29.     detachShm(start);
  30.     //sleep(2);
  31.     return 0;
  32. }
复制代码
 shm_server.cc
  1. #include "comm.hpp"
  2. int main()
  3. {
  4.     key_t k = getKey();
  5.     printf("key: 0x%x\n", k);
  6.     int shmid = createShm(k);
  7.     printf("shmid: %d\n", shmid);
  8.     // sleep(5);
  9.     char *start = (char *)attachShm(shmid);
  10.     printf("attch success,address start:%p\n", start);
  11.     // sleep(5);
  12.     // 通信
  13.     while (true)
  14.     {
  15.         //共享内存当字符串
  16.         printf("client say : %s\n", start);
  17.         sleep(1);
  18.     }
  19.     // 管道读取方式:
  20.     // char buffer[]; read(pipefd, buffer, ...)
  21.     // 去关联
  22.     detachShm(start);
  23.     // sleep(5);
  24.     delShm(shmid);
  25.     return 0;
  26. }
复制代码

至此,就完成了基于共享内存的client、server端的通讯。
共享内存特点


  • 优点:全部进程间通讯,速率最快!
由于,共享内存被进程共享,当一个进程向共享内存地区写入数据后,其他进程可以立即从该地区读取到最新的数据,能大大的镌汰数据拷贝次数,无需像其他通讯方式那样颠末内核空间的中转和数据拷贝,从而大大镌汰了数据传输的时间开销,进步了通讯服从。
同样的代码,利用管道和共享内存的方式通讯,思量键盘输入输出,分别必要反复拷贝?



  • 缺点:没有举行同步与互斥操纵,没有对数据做任何掩护!
将client端改为每3秒写一次,而此时server读端为每1秒读一次,读比写快:

server端读取一次之后,3秒内不绝在重复读取!跟管道差异,管道会壅闭,等候写入。固然,也可以操持出对数据做掩护的共享内存,这里就不演示了。
共享内存的数据结构


shmctl可以答应用户调用特定接口获取指定id的共享内存的属性。
此中shmid_ds结构体是OS袒露给用户的一部分用户级数据结构,存放着共享内存的干系属性,也就是上面我们所说的OS为了管理共享内存额外申请空间创建的数据结构对象,固然,此数据结构是用户级的,跟内核中的另有一点差异,不外雷同。

上面我们已经知道key是要被设置进共享内存属性中的!在shmid_ds中也可以看到key做了封装,封装到了shm_perm结构体中。

我们也可以获取共享内存的干系属性:
  1. //...   
  2. while (true)
  3. {
  4.     printf("client say : %s\n", start);
  5.     struct shmid_ds ds;
  6.     shmctl(shmid, IPC_STAT, &ds);
  7.     printf("获取属性:size:%d,pid:%d,myself:%d,key: ", ds.shm_segsz, ds.shm_cpid, getpid(),ds.shm_perm._key);
  8.     sleep(1);
  9. }
  10. //...
复制代码

回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表