> 文章列表 > 【详解 进程通信】之 System V 共享内存

【详解 进程通信】之 System V 共享内存

【详解 进程通信】之 System V 共享内存

  • 简介
  • 大致操作步骤介绍
  • 结果展示
  • key值的获取
  • 创建 | 获取共享内存
  • 使用共享内存段
  • 解除共享内存段
  • 销毁共享内存段(重点)
    • 命令行方式销毁共享内存段
    • 系统调用销毁共享内存
  • 使用共享内存进行进程间通信

简介

共享内存可以让多个进程共享同一块内存,也就是说这块内存能映射到多个进程的页表中供不同的进程读写。由于用户对内存的操作是非常快的,因此共享内存是其中一种速度最快的IPC方法,一个进程一旦更新了共享内存,那么这个变更会立即对共享同一个内存段的其他进程可见

大致操作步骤介绍

第一步 :
调用 ftok ( )函数获取一个接近唯一的 key 值供shmget函数使用

第二步:
调用 shmget ( ) 函数创建或获取一个已有的共享内存,并返回该内存标识符

第三步:
调用 shmat ( ) 函数将指定标识符的共享内存映射到该进程的页表中,并返回指针供进程使用 ,换句话说就是将指定的共享内存与进程关联起来

第四步:
调用 shmdt ( ) 函数 解除对应共享内存与该进程间的关联

第五步:
调用shmctl ( ) 函数 将指定表示符的共享内存删除

结果展示

启动两个程序(也就是两个进程),由其中一个进程读取共享内存中的内容,另一个进程写入。实现进程间数据传输
【详解 进程通信】之 System V 共享内存

key值的获取

为啥key还需要获取?

1:由于获取共享内存时,shmget () 会根据 key 值转换成对应的 IPC 标识符(本文就暂时把他称为共享内存表示符)。

2:也就意味着shmget () 函数只要我们传相同的key值就能获得相同的共享内存标识符,所以key值和共享内存标识符是一一对应的

3:共享内存标识符又和共享内存块一一对应,所以当不同进程使用相同的标识符将对应共享内存块映射到进程时,就会让不同进程访问同一个空间,达到数据交互的目的-

4 : 但是当想创建一个新的共享内存块供进程使用时,就需要一个接近唯一的key值,这时就可以使用 ftok () 函数帮助我们获取随机数,让我们无需指定key值并无需思考key值是否重复

#include <sys/types.h>
#include <sys/ipc.h>key_t ftok(const char *pathname, int proj_id);

参数解释
pathname : 可以是文件或目录的路径 ,但该路径是必须存在的

proj_id : 虽然是一个 int 值但 ftok () 函数只会取其第八位的值 , 所以 proj_id 的有效参数为 1 ~ 255 ,注意 proj_id 不要设为0 ,在1 ~ 255中选择即

返回值:成功返回一个key_t类型的值,失败返回-1

实际使用演示

ftok(., 30);

如上:pathname是 “.” ,也就是当前路径 ,proj_id 是 30

原理:
在Linux上,ftok() 返回的key是一个32位的值,它通过取proj参数的最低8个有效位,以及 pathname所引用的文件的i-node号的最低16个有效位组合经一定算法后形成。

创建 | 获取共享内存

shmget()系统调用会创建一个新的共享内存段或获取一个已经存在的共享内存段标识符

#include <sys/ipc.h>
#include <sys/shm.h>int shmget(key_t key, size_t size, int shmflg);

注意:
shmget()新创建的内存段中的内容会被初始化为0 , 只要 shmget ()中的key 参数相同就会获取到同 一个共享内存段标识符

作用 :
创建
一个新的共享内存段或获取一个已经存在的共享内存段标识符

返回值
成功返回获取到的内存段的描述符,失败返回-1

参数解释:
key : 想创建新的共享内存段应使用 ftok() 获取 key 值,想获取已有的共享内存段就使用开辟内存时所传递的key值

size: 是一个正整数,表示所申请共享内存段的字节数。如果shmget () 是用来获取一个已有的的共享内存段标识符,则size参数不起作用,但必须小于或等于已有共享内存段的实际大小。

shmfly:可以是 IPC_CREAT 或者是 IPC_CREAT | IPC_EXCL

IPC_CREAT : 如果该key值的共享内存段已存在,则直接获取其内存段标识符并返回,但如果不存在与该key值相同的内存段则创建一个新的共享内存段,并返回新内存段的标识符。

IPC_CREAT | IPC_EXCL :注意 IPC_EXCL 不能单独使用,需或上 IPC_CREAT 使用。表示但如果不存在与该key值相同的内存段则创建一个新的共享内存段,并返回新内存段的标识符。如果该key值的共享内存段已存在,则直接返回错误

注意:-
IPC_CREAT参数不挑内存,有了直接拿标识符,没有才创建
IPC_CREAT | IPC_EXCL参数很挑,一定要自己新创建的共享内存段的标识符,也就意味着这个参数能保证程序获取的内存段是最新的,不可能有其他人使用

使用共享内存段

上述 shmget() 系统调用只是获取到了共享共存段的标识符,但要使标识符对应的内存段还需将内存段映射进进程的虚拟地址空间中

#include <sys/types.h>
#include <sys/shm.h>void *shmat(int shmid, const void *shmaddr, int shmflg);

作用:将标识符对应的内存段映射进进程的虚拟地址空间中(也可以理解为关联共享内存段)

返回值:成功返回共享内存段的地址,失败返回-1

参数解释:

shmid : 要关联的目标共享内存段的描述符

shmaddr : 建议设置为NULL,这样共享内存会自动被映射到一个合适的虚拟地址空间。(不建议传递非NULL,传递的是其他指针时,系统会根据这个指针及其地址边界,内存对齐等分配地址。)

shmflg : 如果参数为 0 ,则进程对共享内存段有读写权限

如果指定为:SHM_RDONLY 则只要只读权限

解除共享内存段

调用 shmdt()来分离共享内存段,执行了该步骤后进程将无法再引用这块共享内存。

#include <sys/types.h>
#include <sys/shm.h>int shmdt(const void *shmaddr);

参数解释:

shmaddr :为使用 shmat()获取到的指针

作用:取消 shmaddr 指向的共享内存与进程的映射(取消关联)

销毁共享内存段(重点)

这步是必须要做的,申请了就必须释放,多个进程一起使用的共享内存也只需销毁一次即可。共享内存的生命不会随进程结束而结束,只要没接收到销毁命令就会一直存在,且不报错

销毁共享内存段的方式有两种:
1 : 命令行使用指令
2 :程序中使用系统调用

命令行方式销毁共享内存段

首先写一个不规范的代码给大家见见共享内存段

#include<iostream>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
#include<sys/stat.h>//                  假设这是客户端写对应指令
int main()
{key_t key = ftok( "." , 33);//获取key值umask(0);												//0666是创建文件的权限int id = shmget(key , 1314 , IPC_CREAT | IPC_EXCL | 0666);//获取一个新的共享内存段,char* str = (char*)shmat(id , NULL , 0);//将id(描述符)对应的共享内存段映射到进程的虚拟地址空间shmdt(str);//取消 str 指向的共享内存与进程的映射(取消关联)//并未销毁共享内存段,直接返回了return 0 ; 
}

可以看到上述代码,申请共享内存段后,关联了进程,后面又取消关联了。但是没有销毁共享内存段

可想而知程序运行结束后,这个共享内存段就被遗留了下来

但是别慌,可以通过命令来查找他并且也可以用命令销毁他

命令:ipcs -m
作用:显示当前系统中运行的共享内存
使用如下:
【详解 进程通信】之 System V 共享内存
可以看这个就是我们刚刚创建的共享内存,有兴趣的小伙伴可以再程序中将共享内存标识符打印出来确定一下,我这里片面认为就是这个描述符为17的共享内存块了,块大小1314也符合咱的开辟大小

命令:ipcrm -m shmid
作用: 销毁 shmid(内存描述符)对应的内存段

实际使用:
【详解 进程通信】之 System V 共享内存
可以看到执行后,指定内存就被销毁了。但这样未免太麻烦了,所以可以在程序中使用系统调用来销毁共享内存段,命令方式只是用于不时之需

系统调用销毁共享内存

#include <sys/ipc.h>
#include <sys/shm.h>int shmctl(int shmid, int cmd, struct shmid_ds *buf);

作用:这个函数除了销毁共享内存,还有其他用处。但现在只了解删除功能即可
返回值:成功返回0 , 失败返回 -1

参数解释:
shmid : 共享内存段描述符
cmd : 传 IPC_RMID 即为删除功能
buf : 这里咱不用传NULL即可

测试代码:

#include<iostream>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
#include<sys/stat.h>
#include<stdio.h>//                  假设这是客户端写对应指令
int main()
{key_t key = ftok( "." , 33);//获取key值umask(0);int id = shmget(key , 1314 , IPC_CREAT | IPC_EXCL|0666);//获取一个新的共享内存段printf("创建了一个描述符为 %d 的共享内存段\\n" , id);char* str = (char*)shmat(id , NULL , 0);//将id(描述符)对应的共享内存段映射到进程的虚拟地址空间shmdt(str);//取消 str 指向的共享内存与进程的映射(取消关联)shmctl(id , IPC_RMID , NULL);return 0 ; 
}

结果如下,内存创建后也被销毁了,无需在命令行手动销毁
【详解 进程通信】之 System V 共享内存

使用共享内存进行进程间通信

共享内存通信原理

ftok传参一致的程序(进程)就会回到相同key值,shmget使用相同key值就会获得同一个内存段描述符,不同进程使用相同描述符就能映射到同一个共享内存段,就达到了通信的前提 ———— 让不同进程看到同一份资源

下面是俩进程间实现通信的代码

Client.cpp

#include<iostream>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
#include<sys/stat.h>
#include<stdio.h>
//                  假设这是客户端写对应指令
int main()
{key_t key = ftok( "." , 33);//获取key值umask(0);int id = shmget(key , 1314 , IPC_CREAT | IPC_EXCL|0666);//获取一个新的共享内存段char* str = (char*)shmat(id , NULL , 0);//将id(描述符)对应的共享内存段映射到进程的虚拟地址空间sleep(5);//延时一下方便其他进程读取for(int i = 0 ; i < 26 ; i++)//向共享内存循环写入数据{str[i] = 'A' + i;//写入数据str[i+1] = '\\0';//可不写因为刚开辟默认为0sleep(1);//防止写入太快,需测试看效果}shmdt(str);//取消 str 指向的共享内存与进程的映射(取消关联)shmctl(id , IPC_RMID , NULL);return 0 ; 
}

Server.cpp

 #include<iostream>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
#include<sys/stat.h>
#include<stdio.h>
//                  假设这是服务端读对应指令
int main()
{key_t key = ftok( "." , 33);//获取key值umask(0);int id = shmget(key , 1314 , IPC_CREAT );//获取一个共享内存段char* str = (char*)shmat(id , NULL , 0);//将id(描述符)对应的共享内存段映射到进程的虚拟地址空间for(int i = 0 ; i < 35 ; i++)//读取内存中数据{std::cout << str << std::endl;sleep(1);//防止读入太快,需测试看效果}shmdt(str);//取消 str 指向的共享内存与进程的映射(取消关联)shmctl(id , IPC_RMID , NULL);return 0 ; 
}

运行如下:
【详解 进程通信】之 System V 共享内存
代码写的比较简单,有兴趣的小伙伴可以这些调用封装成类,然后直接调用就能返回关联好的指针,直接使用即可,销毁调用放在析构函数中即可实现自动销毁,进程结束共享内存段也会自动销毁。