• 【Linux】System V 共享内存


    一、共享内存

    1.1 共享内存的原理:

           两个进程,操作系统在内存空间中创建一个共享内存。在之前学习库的时候,有一个共享库的概念。我们可以按照其概念来了解共享内存的概念:将共享内存映射到页表中,和进程的地址空间建立联系。我们可以将共享内存的虚拟地址交给用户。两个进程之间就可以通过虚拟地址找到同一个共享内存进行通信。

    1.2 理解:

    • 上面说的操作,只能是操作系统来做
    • 操作系统必须提供上面步骤的系统调用,供系统A、B进行调用
    • 共享内存在系统中有多份,供不同个数,不同对的操作系统同时进行通信
    • 操作系统要对共享内存进行管理,先描述再组织
    • 共享内存并不是简单的一段空间,也要有描述并管理共享内存的数据结构和匹配的算法
    • 共享内存 = 共享空间(数据) + 共享内存的属性

    二、共享内存的数据结构

    1. struct shmid_ds {
    2. struct ipc_perm shm_perm; /* operation perms */
    3. int shm_segsz; /* size of segment (bytes) */
    4. __kernel_time_t shm_atime; /* last attach time */
    5. __kernel_time_t shm_dtime; /* last detach time */
    6. __kernel_time_t shm_ctime; /* last change time */
    7. __kernel_ipc_pid_t shm_cpid; /* pid of creator */
    8. __kernel_ipc_pid_t shm_lpid; /* pid of last operator */
    9. unsigned short shm_nattch; /* no. of current attaches */
    10. unsigned short shm_unused; /* compatibility */
    11. void *shm_unused2; /* ditto - used by DIPC */
    12. void *shm_unused3; /* unused */
    13. };

    三、共享内存的使用

    3.1 一些有关函数

    3.1.1 shmget函数

    函数的原型:

    函数的参数:

    • key:这个共享内存端的名字
    • size:共享内存的大小
    • shmfig:由九个权限表标志构成,用法和创建文件时使用的mode模式标志是一样的,利用位图进行传参。

    在这里的标识码有两个:IPC_CREAT和IPC_EXCL。

    • IPC_CREAT:如果内核中不存在键值和key相同的共享内存,则新建一个共享内存;如果存在这样的共享内存,则返回该共享内存的标识码(可以获得一个存在的共享内存)
    • IPC_EXCL:单独使用没有意义,只有和IPC_CREAT一起使用才有意义
    • IPC_CREAT | IPC_EXCL:如果内核中不存在键值和key相同的共享内存,则新建一个共享内存;如果存在这样的共享内存,则报错(如果我成功返回,这个shm则是一个新的共享内存)

    函数的返回值:

           成功则返回一个非负整数,即给共享内存段的标识码;失败则返回-1

    3.1.2 ftok函数 

    ftok函数的原型:

    函数的功能:

            系统建立IPC通讯(如消息队列、共享内存等)  必须指定一个ID值。通常情况下,该id值通过ftok函数得到。
    函数的参数:

    • pathname:必须是一个已经存在且程序可范围的文件。
    • proj_id:虽然定义为一个整数,其实实际只有8个bit位有效,即如果该参数大于255,则只有后8bit有效。 

    函数的返回值:

           当函数执行成功,则会返回key_t键值,否则返回-1。在一般的UNIX中,通常是将文件的索引节点取出,然后在前面加上子序号就得到key_t的值。

    3.1.3 stmctl函数

    shmctl函数的原型:

    函数的功能:

           用来控制共享内存

    函数的参数:

    • shmid:有shmget函数返回的共享内存的标识码
    • cmd:将要采取的动作(有三个可取值)
    命令说明
    IPC_STAT把shmid_ds结构中的数据设置为共享内存的当前关联值
    IPC_SET在进程有足够权限的前提下,把共享内粗的当前关联值设为shmid_ds数据结构中给出的值
    IPC_RMID删除共享内存段
    • buf:指向一个保存着恭喜昂内存的模式状态和访问权限的数据结构

    函数的返回值:

           成功返回0,失败返回-1

    3.1.4 shmat函数

    函数的原型:

    函数的功能:

           将创建好的共享内存连接到某个进程,并指定内存空间
    函数的参数:

    • shmid:shmget返回的共享内存标识
    • shmaddr:把共享内存连接到当前进程去的时候准备放置它的那个地址
    1. shmaddr:把共享内存连接到当前进程去的时候准备放置它的那个地址,如果设为NULL为让系统自动选择。
    2. shmaddr为NULL,核心自动选择一个地址
    3. shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
    4. shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式: shmaddr - (shmaddr % SHMLBA)
    • shmflg:其是一组按位OR(或)在一起的标志。它的两个可能取值是SHM_RND和SHM_RDONLY,shmflg=SHM_RDONLY,表示连接操作用来只读共享内存

    函数的返回值:

           如果成功返回一个指针,指向共享内存的第一个字节;如果失败,则返回-1

    3.1.5 shmdt函数

    函数的原型:

    函数的功能:

           将共享内存段与当前进程脱离
    函数的参数

    • shmaddr:由shmat返回的指针

    函数的返回值:

           如果成功返回0;失败则返回-1 

    3.2 获取key值 

           只要拥有相同的文件路径和一个数字,我们可以使用这个函数来使服务端和客户端形成的key是一样的,代码如下:

    1. key_t GetCommonKey(const std::string& pathname, const int proj_id)
    2. {
    3. key_t k = ftok(pathname.c_str(), proj_id);
    4. if(k < 0)
    5. {
    6. perror("GetCommonKey");
    7. }
    8. return k;
    9. }

    3.3 有关共享内存的一些指令 

           共享内存不会随着进程的结束而自动释放,如果不进行释放,该共享内存会一直存在,需要手动进行释放, 共享内存的生命周期随内核 | 文件的生命周期是随进程的。

    1. ipcs -m; // 进行共享内存的查看
    2. ipcrm -m key // 进行共享内存的删除
    • 我们可以使用 ipcs -m指令查看共享内存
    • 我们可以使用 ipcrm -m key指令删除共享内存

    3.4 key VS shmid

           key属于用户形成,内核使用的字段,用户不能使用key来进行shm的管理,内核进行区分shm的唯一性的

           shmid属于内核给用户返回的一个标识符,用来进行用户及对共享内存进行管理的id值。

    3.5下面来进行对shm的准备工作

    3.5.1 shm的创建和销毁工作

    3.5.1.1 使进程之间获取一个相同的key值
    1. // 获取一个相同的key值,需要访问一个相同的文件路径和一个随便的数字
    2. const std::string pathname = "/home/hrx/code/111/lesson3/shm";
    3. const int proj_id = 0x66;
    4. key_t GetKeyCommon()
    5. {
    6. key_t k = ftok(_pathname.c_str(), _proj_id);
    7. if (k < 0)
    8. {
    9. perror("GetCommonKey");
    10. }
    11. return k;
    12. }
    3.5.1.2 通过key值打开共享内存块
    1. int Shmget(int key, int size, int flag) // 将key值的共享内存创建出来
    2. {
    3. int shmid = shmget(key, size, flag);
    4. if (shmid < 0)
    5. {
    6. perror("shmid");
    7. }
    8. return shmid;
    9. }
    10. std::string ToHex(int u) // 将一个数字转换为十六进制的格式
    11. {
    12. char buff[128];
    13. snprintf(buff, sizeof buff, "0x%x", u);
    14. return buff;
    15. }
    16. Shm(const std::string &pathname, int proj_id, int who) // 构造函数
    17. : _pathname(pathname), _proj_id(proj_id), _who(who)
    18. {
    19. _key = GetKeyCommon();
    20. if (who == Creater)
    21. GetShmCeaterUse();
    22. else
    23. GetShmCeaterUse();
    24. }
    25. bool GetShmCeaterUse()
    26. {
    27. if (_who == Creater)
    28. {
    29. _shmid = Shmget(_key, 1024, IPC_CREAT | IPC_EXCL);
    30. sleep(10);
    31. if (_shmid > 0)
    32. return true;
    33. }
    34. return false;
    35. }
    36. bool GetShmForUse()
    37. {
    38. if (_who == User)
    39. {
    40. _shmid = Shmget(_key, 1024, IPC_CREAT);
    41. if (_shmid > 0)
    42. return true;
    43. }
    44. return false;
    45. }
    3.5.1.3 销毁共享内存
    1. ~Shm()
    2. {
    3. if (_who == Creater)
    4. {
    5. // 进行共享库的删除
    6. int res = shmctl(_shmid, IPC_RMID, nullptr);
    7. }
    8. std::cout << "Shm remove done..." << std::endl;
    9. }

    3.5.2 进行shm的挂接和取消挂接

    3.5.2.1 进行shm的挂接
    1. void* Attach()
    2. {
    3. void* attaddr = shmat(_shmid, nullptr, 0);
    4. if(attaddr == nullptr)
    5. {
    6. perror("attaddr");
    7. }
    8. std::cout << "name: " << GetName() << " attach shm..." << std::endl;
    9. return attaddr;
    10. }
    3.5.2.2 进行shm的取消挂接
    1. void Dattach(const char* addr)
    2. {
    3. if(addr == nullptr) return;
    4. shmdt(addr);
    5. std::cout << "name: " << GetName() << "dis attach shm..." << std::endl;
    6. }
    3.5.2.3 进行效果的演示

           刚开始的时候,我们会发现在共享内存的页面没有任何数字的变化,因为共享内存的全西安没有关闭,我们需要在创建共享内存的时候,为共享内存的权限设为可以进行操作。

    1. // 修改共享内存的代码:
    2. _shmid = Shmget(_key, 1024, IPC_CREAT | IPC_EXCL | 0x666);

            当我们开始实行代码后,会发现这个共享内存的挂接数量从0到1到2,最后,又逐渐减少。注意: 将共享内存段与当前进程脱离不等于删除共享内存,只是取消了当前进程与该共享内存之间的联系。

    在服务器端和客户端一起进行挂接,下面是现象:

    3.6 对shm的完成封装

    1. #pragma once
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. #include
    13. #define Creater 1
    14. #define User 2
    15. const std::string pathname = "/home/hrx/code/111/lesson3/shm";
    16. const int proj_id = 0x66;
    17. class Shm
    18. {
    19. public:
    20. key_t GetKeyCommon()
    21. {
    22. key_t k = ftok(_pathname.c_str(), _proj_id);
    23. if (k < 0)
    24. {
    25. perror("GetCommonKey");
    26. }
    27. return k;
    28. }
    29. int Shmget(int key, int size, int flag)
    30. {
    31. int shmid = shmget(key, size, flag);
    32. if (shmid < 0)
    33. {
    34. perror("shmid");
    35. }
    36. return shmid;
    37. }
    38. public:
    39. int getkey()
    40. {
    41. return _key;
    42. }
    43. int getshmid()
    44. {
    45. return _shmid;
    46. }
    47. std::string GetName()
    48. {
    49. if(_who == Creater) return "Creater";
    50. else if(_who == User) return "User";
    51. else return "None";
    52. }
    53. std::string ToHex(int u)
    54. {
    55. char buff[128];
    56. snprintf(buff, sizeof buff, "0x%x", u);
    57. return buff;
    58. }
    59. Shm(const std::string &pathname, int proj_id, int who)
    60. : _pathname(pathname), _proj_id(proj_id), _who(who)
    61. {
    62. _key = GetKeyCommon();
    63. if (who == Creater)
    64. GetShmCeaterUse();
    65. else
    66. GetShmForUse();
    67. _addr = Attach(); // 直接进行挂接
    68. }
    69. bool GetShmCeaterUse()
    70. {
    71. if (_who == Creater)
    72. {
    73. _shmid = Shmget(_key, 1024, IPC_CREAT | IPC_EXCL | 0666);
    74. std::cout << "key:" << _key << ' ' << "shmid:" << _shmid << std::endl;
    75. if (_shmid > 0)
    76. return true;
    77. }
    78. return false;
    79. }
    80. bool GetShmForUse()
    81. {
    82. if (_who == User)
    83. {
    84. _shmid = Shmget(_key, 1024, IPC_CREAT);
    85. std::cout << "key:" << _key << ' ' << "shmid:" << _shmid << std::endl;
    86. if (_shmid > 0)
    87. return true;
    88. }
    89. return false;
    90. }
    91. ~Shm()
    92. {
    93. if (_who == Creater)
    94. {
    95. // 进行共享库的删除
    96. int res = shmctl(_shmid, IPC_RMID, nullptr);
    97. }
    98. std::cout << "Shm remove done..." << std::endl;
    99. }
    100. void* Addr()
    101. {
    102. return _addr;
    103. }
    104. void Zero()
    105. {
    106. if(_addr)
    107. memset(_addr, 0, sizeof _addr);
    108. }
    109. void* Attach()
    110. {
    111. if(_addr != nullptr) Dattach((char*)_addr);
    112. void* attaddr = shmat(_shmid, nullptr, 0);
    113. if(attaddr == nullptr)
    114. {
    115. perror("attaddr");
    116. }
    117. std::cout << "name: " << GetName() << " attach shm..." << std::endl;
    118. return attaddr;
    119. }
    120. void Dattach(const char* addr)
    121. {
    122. if(addr == nullptr) return;
    123. shmdt(addr);
    124. std::cout << "name: " << GetName() << "dis attach shm..." << std::endl;
    125. }
    126. private:
    127. key_t _key;
    128. int _who;
    129. int _shmid;
    130. void* _addr;
    131. const std::string _pathname;
    132. const int _proj_id;
    133. };

    四、进行进程之间的通信

    4.1 直接进行通信

    在将服务端与客户端进行挂接成功后,我们要来进行通信,将共享内存看成数组来进行通信:

    服务端:

    1. Shm shm(pathname, proj_id, Creater);
    2. char* shmaddr = (char*)shm.Addr();
    3. while (true)
    4. {
    5. std::cout << shmaddr << std::endl;
    6. sleep(1);
    7. }

    客户端 

    1. Shm shm(pathname, proj_id, User);
    2. shm.Zero();
    3. char* shmaddr = (char*)shm.Addr();
    4. sleep(3);
    5. char ch = 'a';
    6. while (ch <= 'z')
    7. {
    8. shmaddr[ch - 'a'] = ch;
    9. ch++;
    10. sleep(2);
    11. }

    4.2 共享内存的一些知识点 

           在进行共享内存的使用时,共享内存不提供任何保护机制,数据不一致问题。我在写一个字符串,但是还没有写完就被读取了一半,造成了数据不一致问题。我们在写共享内存没有使用任何系统调用,共享内存是进程间通信速度最快的,因为共享内存大大地减少了拷贝次数。 

                             

    4.3 保护共享内存

           管道提供了保护措施,所以,我们可以建立一个管道,将数据放在管道中,当写完后,直接将数据发送给另一个进程,共享内存的容量大小:如果容量大小为4097,共享内存的大小可以i自己随意定制,但是建议共享内存的大小为4096的整数倍。,如果不是整数倍,操作系统会给你整数倍的容量。

    1. // 服务器端
    2. Shm shm(pathname, proj_id, Creater);
    3. char* shmaddr = (char*)shm.Addr();
    4. // 2.创建管道
    5. NamePipe fifo(comepath, Creater);
    6. fifo.openforread();
    7. while (true)
    8. {
    9. std::string tmp;
    10. fifo.ReadNamePipe(&tmp);
    11. std::cout << shmaddr << std::endl;
    12. }
    1. // 客户端
    2. NamePipe fifo(comepath, User);
    3. fifo.operforwrite();
    4. char ch = 'a';
    5. while (ch <= 'z')
    6. {
    7. std::string tmp = "once";
    8. fifo.WriteNamePipe(tmp);
    9. shmaddr[ch - 'a'] = ch;
    10. ch++;
    11. sleep(2);
    12. }

    五、共享内存与管道的对比

           当共享内存创建好后就不再需要调用系统接口进行通信了,而管道创建好后仍需要read、write等系统接口进行通信。实际上,共享内存是所有进程间通信方式中最快的一种通信方式。

    我们先来看看管道通信:

           从这张图可以看出,使用管道通信的方式,将一个文件从一个进程传输到另一个进程需要进行四次拷贝操作:

    • 服务端将信息从输入文件复制到服务端的临时缓冲区中。
    • 将服务端临时缓冲区的信息复制到管道中。
    • 客户端将信息从管道复制到客户端的缓冲区中。
    • 将客户端临时缓冲区的信息复制到输出文件中。

    我们再来看看共享内存通信:

           从这张图可以看出,使用共享内存进行通信,将一个文件从一个进程传输到另一个进程只需要进行两次拷贝操作:

    • 从输入文件到共享内存。
    • 从共享内存到输出文件。

           所以共享内存是所有进程间通信方式中最快的一种通信方式,因为该通信方式需要进行的拷贝次数最少。

           但是共享内存也是有缺点的,我们知道管道是自带同步与互斥机制的,但是共享内存并没有提供任何的保护机制,包括同步与互斥。

  • 相关阅读:
    【小记】二八十十六,进制团团转
    【数学建模】MATLAB应用实战系列(九十五)-时间序列预测应用案例(附MATLAB代码)
    MySql 查询字段包含指定字符串(locate函数)
    如果让你和你的团队去用yolov8 做一款能检测箭靶自动报靶的系统,你会怎么做?
    L2桥接风险架构
    微软数据库的SQL注入漏洞解析——Microsoft Access、SQLServer与SQL注入防御
    Gorm—Go语言数据库框架
    [附源码]java毕业设计网上招聘系统
    【NSValue Objective-C语言】
    推荐十个优秀的ASP.NET Core第三方中间件,你用过几个?
  • 原文地址:https://blog.csdn.net/2301_77868664/article/details/139360298