前言
本文详细讲解了共享内存的原理和使用,并且通过实例代码角度来深度理解共享内存,下面就让我们开始吧。
进程间通信目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
通信背景
1.由于进程是具有独立性的,进程想交互数据,成本会非常高。但是有些情况下需要多进程处理一件事情。
2.进程独立并不是彻底独立,有时候我们需要双方能够进行一定程度的信息交互。
我们要学的进程间通信,不是告诉我们如何通信,是他们两个如何先看到同一份资源。(文件,内存块…等方式)
两个进程同时访问磁盘上的一个文件进行读写
但由于进程在磁盘上读写太慢,所以进程间通信一般读写内存中的文件。
两个进程同时访问内存上的一个文件进行读写
共享内存
一. 原理
共享内存是指多个进程可以同时访问同一块内存区域的机制。在共享内存中,多个进程可以把同一块内存映射到它们自己的地址空间中,并且可以直接读写这块内存,就好像它们都拥有这块内存一样。这样可以实现进程间高效的数据共享,而不需要通过进程间通信机制进行数据传输,从而提高程序的性能。
那么内存中是如何实现共享内存的呢?
首先我们要明白,进程间通信的前提是:先让不同的进程,看到同一份资源。而共享内存就是通过进程可以看到同一块内存而实现的。
共享内存原理图
- 每个进程都有一个虚拟地址空间,在地址空间的栈区和堆区中间有一块很大的空内存,名叫共享区。
- 在用户需要申请共享内存时,操作系统在物理内存中申请一块空间,然后映射到页表中,页表建立连接之后,再映射到进程各自的共享区中。两个进程共同映射的物理内存的操作就叫做共享内存。
二. 基本编写
要实现共享内存,我们需要四个接口:
操作系统角度:
①创建共享内存
②删除共享内存
进程角度:
③关联共享内存
④去关联共享内存
2.1 创建共享内存
int shmget(key_t key, size_t size, int shmflg);
allocates a System V shared memory segment
分配System V标准的共享内存段。
参数列表:
size: 要申请的共享内存大小。
由于操作系统对内存管理的最小单位是页(4KB),所以size
建议设置成为页的整数倍。
shmflg: 创建共享内存的选项。
为了解决两个问题
1.如果共享内存存在怎么办
2.如果共享内存不存在怎么办
常见的有两个选项
- IPC_CREAT:创建共享内存,如果已经存在,就获取之。不存在,就创建之
- IPC_EXCL:不单独使用,必须和IPC_CREAT配合使用。如果不存在指定的共享内存,创建之。如果存在了,出错返回
- 并且可以按位或文件权限,设置共享内存权限。
int shmid = shmget(key, SHM_SIZE, flags | 0666);
IPC_EXCL
可以保证,如果shmget
函数调用成功,一定是一个全新的share memory。如果调用失败,则报错。
这里会有两个问题:
1. 共享内存存在哪里?
内核中 – 内核会给我们维护共享内存的结构,共享内存也要被管理起来,所以一定会有管理共享内存的结构体。
2. 我怎么知道,这个共享内存属于存在还是不存在?
这个问题稍后回答。
共享内存结构体shmid_ds
可以看到在shmid_ds
中,存在ipc_perm
结构体,保存的是共享内存的权限。
而在ipc_perm
中,存在一个key
值,由shmget
提供。
由此可以引出最后一个参数:
key:共享内存的唯一值,这个参数需要由用户提供。
共享内存要被管理 -> struct shmid_ds
-> struct ipc_perm
-> key
(shmget
)(共享内存唯一值)
1. 为什么key值需要由用户提供?
进程间通信的前提是,先让不同的进程,看到同一份资源。如果由操作系统提供,创建共享内存的进程可以知道key值,但是使用共享内存的进程无法获取。所以key值必须由用户获取,然后在使用时标定key值,则能让使用共享内存的进程获取到。
共享内存,在内核中,让不同的进程看到同一份共享内存,做法是:让他们拥有同一个key即可。
匿名管道 --> 约定好使用同一个文件
共享内存 --> 约定好使用同一个唯一key
2.为什么key值要有唯一性?
操作系统中可能有很多个共享内存在被使用,所以我们就需要用一个唯一值来标识每一个共享内存。
3. 那么如何保证key值唯一性呢?
生成唯一key值函数:ftok函数。
key_t ftok(const char *pathname, int proj_id);
将文件路径和一个项目标识符,转化为唯一key值。
返回值:一个整数,创建成功,返回一个合法的共享内存标识符。失败,返回 -1。
生成唯一key值并且让服务端和客户端可以获取
通过唯一key值的标识,我们就可以知道共享内存是否存在。
2.2 删除共享内存
当我们运行完毕创建全新的共享内存代码后(进程退出),创建成功,但是第二(n
)次的时候,该代码无法运行,告诉我们文件存在(共享内存存在)。
创建共享内存
所以system V下的共享内存,生命周期是随内核的。如果不显示的删除,只能通过操作系统重启来解决。
这里有两个问题:
1. 我怎么知道有哪些IPC资源?
ipcs -m
查看共享内存列表
key:shmget传入的key值
shmid:shmget的返回值,共享内存id
owner:所有者
perms:共享内存权限
bytes:共享内存大小
nattch:共享内存的进程链接数
status:共享内存状态
可以看到刚刚创建的shmid为1的共享内存确实存在。
2. 如何显示删除?
指令实现
ipcrm -m shmid
删除共享内存
可以看到shmid为1的共享内存被删除了。
代码实现
用指令可以删除共享内存,但是我们需要编写代码,代码中有没有删除共享内存的函数呢?操作系统为我们提供了接口。
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
System V shared memory control
控制System V标准的共享内存段。当前我们只用它来实现删除。
参数列表:
shmid:创建的共享内存id。
cmd:如何控制共享内存。我们只用其中的一个选项IPC_RMID
,删除共享内存。
buf:当我们要删除时,只要指定了shmid
,这个参数设置为NULL
即可。
返回值:-1为失败。
shmctl(shmid, IPC_RMID, nullptr);
下面写了一个服务器代码,功能是生成key值开始创建共享内存,等待五秒。创建共享内存,等待五秒,删除共享内存。
右侧监控脚本
while :; do ipcs -m; sleep 1; echo "####################"; done
当我们运行服务端代码时,先生成key值并开始创建,此时共享内存还未被创建。服务端创建成功时,右侧可以看到共享内存被创建了。服务端删除成功时,右侧可以看到共享内存被删除了。
2.3 关联共享内存
void *shmat(int shmid, const void *shmaddr, int shmflg);
System V shared memory attach
关联System V标准的共享内存段
参数列表:
shmid:共享内存id
shmaddr:要关联到共享内存的哪个地址上。当前无法操作,把它设置为NULL即可
shmflg:读或者写。设置为默认0,即为读。
返回值:void* 类型,如果成功,返回要关联的共享内存起始地址,失败为-1。类似于molloc,使用时需要强转为需要的指针类型。
char* str = (char*)shmat(shmid, nullptr, 0);
可以看到,当加入shmat之后,共享内存的nattch变为1,表示进程与共享内存产生了关联。
服务端加入shmat代码
2.4 去关联共享内存
int shmdt(const void *shmaddr);
System V shared memory detach
去关联System V标准的共享内存段。
参数列表:
shmaddr:共享内存起始地址,即为
shmat
的返回值
可以看到,当加入shmdt之后,nattch从1变0,再从0变1,代表进程与共享内存先关联,再去关联。
服务端加入shmdt代码
加入客户端,让服务端创建共享内存并关联,然后运行客户端,也让它关联共享内存,然后都去关联,可以看到如下现象。
nattch从1变2,再变成1,再变成0。代表服务端关联,客户端关联,去关联。
加入客户端
三、实例代码
3.1 头文件
Comm.hpp
包含需要的库文件,并且封装CreateKey
函数,用来生成key
值。
#pragma once
#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATH_NAME "/home/ydp/blog"
#define PROJ_ID 0x14
#define SHM_SIZE 4096
key_t CreateKey()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key < 0)
{
std::cerr << "ftok: " << strerror(errno) << std::endl;
exit(0);
}
}
Log.hpp
封装Log
函数,用来打印Debug信息
#pragma once
#include <iostream>
#include <ctime>
std::ostream &Log()
{
std::cout << "For Debug | " << " timestamp: "
<< (uint64_t)time(nullptr) << " | ";
return std::cout;
}
3.2 服务端
服务端实现以下操作:
①创建key
②创建共享内存
③将共享内存和进程关联
④持续循环打印共享内存中的内容
⑤解除共享内存和进程之间的关联
⑥删除共享内存
#include "Comm.hpp"
#include "Log.hpp"
using namespace std;
// 我想创建全新的共享内存
const int flags = IPC_CREAT | IPC_EXCL;
// 充当创建共享内存的角色
int main()
{
// 1.创建key
key_t key = CreateKey();
Log() << "key: " << key << endl;
// 2.创建共享内存
Log() << "create share memory begin" << endl;
int shmid = shmget(key, SHM_SIZE, flags | 0666);
if(shmid < 0)
{
Log() << "shmget: " << strerror(errno) << endl;
return 2;
}
Log() << "create shm success, shmid: " << shmid << endl;
// 用它
// 3.将共享内存和进程关联
char* str = (char*)shmat(shmid, nullptr, 0);
Log() << "attach shm: " << shmid << " success" << endl;
// 4.持续循环打印共享内存中的内容
while(true)
{
printf(".%s\n", str);
sleep(1);
}
// 5.解除共享内存和进程之间的关联
shmdt(str);
Log() << "detach shm: " << shmid << " success" << endl;
// 6.删除共享内存
shmctl(shmid, IPC_RMID, nullptr);
Log() << "delete shm: " << shmid << " success" << endl;
return 0;
}
3.3 客户端
客户端实现以下操作:
①创建相同的key
②获取共享内存
③将共享内存和进程关联
④将26个字母写入共享内存
⑤解除共享内存和进程之间的关联
#include "Comm.hpp"
#include "Log.hpp"
using namespace std;
// 充当使用共享内存的角色
int main()
{
// 1.创建相同的key
key_t key = CreateKey();
Log() << "key: " << key << endl;
// 2.获取共享内存
int shmid = shmget(key, SHM_SIZE, IPC_CREAT);
if(shmid < 0)
{
Log() << "shmget: " << strerror(errno) << endl;
return 2;
}
// 3.将共享内存和进程关联
char *str = (char*)shmat(shmid, nullptr, 0);
// 用它
// 竟然没有使用任何的系统调用接口!
// 4.将26个字母每隔一秒写入共享内存
int cnt = 0;
while(cnt <= 26)
{
str[cnt] = 'A' + cnt;
++cnt;
// 保证str为字符串
str[cnt] = '\0';
sleep(1);
}
// 5.解除共享内存和进程之间的关联
shmdt(str);
return 0;
}
在客户端与服务端中可以发现,输入输出不需要调用系统接口。
由此我们可以得出三个结论:
1.我们把共享内存实际上是映射到了我们进程地址空间的用户空间了(堆->栈之间),对每一个进程而言,挂接到自己的上下文中的共享内存,属于自己的空间,类似于堆空间或者栈空间,可以被用户直接使用,不需要调用系统接口。
2.共享内存,因为它自身的特性,它没有任何访问控制。共享内存被双方直接看到,属于双方的用户空间,可以直接通信,但是不安全。
3.共享内存是所有进程间通信,速度最快的。
对比管道:
管道从用户到操作系统需要四次拷贝。
管道拷贝次数
共享内存由于客户端和服务端看到的是同一份内容,不需要拷贝,所以从用户到操作系统只需要两次拷贝。
共享内存拷贝次数
3.4 输出样例
可以看到,当客户端每隔一秒写入字母,服务器也每隔一秒读取到了字母
四、特征总结
共享内存特征文章来源:https://www.toymoban.com/news/detail-781885.html
- 共享内存可以被用户直接使用,不需要调用系统接口。
- 共享内存没有任何访问控制。共享内存被双方直接看到,属于双方的用户空间,可以直接通信,但是不安全。
- 共享内存是所有进程间通信,速度最快的。
总结
本文从原理和代码编写角度详细介绍了进程间通信的一种方式 – 共享内存。大家也可以尝试去使用一下共享内存,这样可以更深入地了解其中的细节。喜欢的话,欢迎点赞支持和关注~文章来源地址https://www.toymoban.com/news/detail-781885.html
到了这里,关于进程间通信--共享内存详解【Linux】的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!