虚拟内存抽象使应用程序能够拥有一个独立而连续的虚拟地址空间,其通过页表与硬件的配合能够在对应用程序透明的前提下自动地进行虚拟地址到物理地址的翻译。除此之外,虚拟内存抽象还带来了其它的功能。
一、共享内存
共享内存允许通一个物理页在不同的应用程序中共享,如下图
应用程序A的虚拟页V1和应用程序B的虚拟页V2都被映射到物理页P,则物理页P是应用程序A和B的共享内存。此时程序A读取虚拟页V1和程序B读取虚拟页V2将得到相同的内容。互相也能看到对方所修改的内容。其中最重要的一个用途就是可以让不同的应用程序之间传递数据。基于共享内存的思想,操作系统又从中衍生出,写时拷贝、内存去重等功能。
下面是一个c++的例子
服务端:
#include <windows.h>
#include <iostream>
using namespace std;
#define BUF_SIZE 4096
int main()
{
// 定义共享数据
char szBuffer[] = "锄禾日当午!";
// 创建共享文件句柄
HANDLE hMapFile = CreateFileMapping(
INVALID_HANDLE_VALUE, // 物理文件句柄
NULL,// 默认安全级别
PAGE_READWRITE, // 可读可写
0,// 高位文件大小
BUF_SIZE,// 低位文件大小
L"ShareMemoryTy" // 共享内存名称
);
// 映射缓存区视图 , 得到指向共享内存的指针
LPVOID lpBase = MapViewOfFile(
hMapFile, // 共享内存的句柄
FILE_MAP_ALL_ACCESS, // 可读写许可
0,
0,
BUF_SIZE
);
// 将数据拷贝到共享内存
strcpy((char*)lpBase, szBuffer);
cout << "服务端:" << (char*)lpBase << endl;
// 线程挂起等其他线程读取数据
Sleep(20000);
// 解除文件映射
UnmapViewOfFile(lpBase);
// 关闭内存映射文件对象句柄
CloseHandle(hMapFile);
return 0;
}
客户端:
#include <iostream>
#include <windows.h>
using namespace std;
#define BUF_SIZE 4096
int main()
{
// 打开共享的文件对象
HANDLE hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS, NULL, L"ShareMemoryTy");
//cout << hMapFile << endl;
if (hMapFile) {
// 映射缓存区视图 , 得到指向共享内存的指针
LPVOID lpBase = MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 0);
// 将共享内存数据拷贝出来
char szBuffer[BUF_SIZE] = { 0 };
strcpy_s(szBuffer, (char*)lpBase);
cout << "客户:" << szBuffer << endl;
// 解除文件映射
UnmapViewOfFile(lpBase);
// 关闭内存映射文件对象句柄
CloseHandle(hMapFile);
}
else {
// 打开共享内存句柄失败
cout << "打开共享失败!" << endl;
}
system("pause");
return 0;
}
结果:
二、写时拷贝
写时拷贝技术允许应用程序A和应用程序B以只读的方式共享一段物理内存,一旦某个应用程序对该内存区域进行修改,就会触发缺页异常。注意,这里的缺页异常是否与违反权限导致的,不是之前所说的换页机制下的缺页异常是由于未映射导致的。在触发了缺页异常后,CPU同样会将控制流传递给操作系统预先设置的缺页异常处理函数,在该函数中操作系统会发现是缺页异常由于写了只读内存造成的,而且相应的内存区域又被系统标记为写时拷贝。于是操作系统会在物理内存中将缺页异常对应的物理页重新拷贝一份,并且将新拷贝的物理页以可读可写的方式重新映射给触发异常的应用程序,此后再恢复应用程序的执行。比如两个应用程序拥有很多相同的内存数据(比如加载了相同的动态链接库),如果把这些数据相同的内存页在物理内存中仅存一份,然后以只读的方式映射给两个应用程序,那么就能显著地节约物理内存。
接下来看例子:
#include <iostream>
#include <Windows.h>
#pragma warning (disable:4996)
using namespace std;
class String
{
public:
String(const char* pStr = "") //构造函数
:_pStr(new char[strlen(pStr) + 4 + 1])//+4多创建的四个字节用来保存当前地址有几个对象
{
if (NULL == pStr)
{
*((int*)_pStr) = 1;//前4个字节用来计数
_pStr += 4;//向后偏移4个字节
*_pStr = '\0';
}
else
{
*((int *)_pStr) = 1;//前4个字节用来计数
_pStr += 4;//向后偏移4个字节
strcpy(_pStr, pStr);//拷贝字符串
}
}
String(const String& s)//拷贝构造函数
:_pStr(s._pStr)
{
++(*(int*)(_pStr - 4));//向前偏移4个字节将计数+1
}
~String()//析构函数
{
if (NULL == _pStr)
{
return;
}
else
{
if (--(*(int*)(_pStr - 4)) == 0)
{
delete[](_pStr - 4);
_pStr = NULL;
}
}
}
//重载赋值运算符=
String& operator=(const String& s)
{
if (_pStr != s._pStr)
{
if (--(*(int*)(_pStr - 4)) == 0)//释放旧空间
{
delete[](_pStr - 4);
_pStr = NULL;
}
_pStr = s._pStr;//指向新空间
++(*(int*)(_pStr - 4));//计数+1
}
return *this;
}
//重载下标访问操作符
char& operator[](size_t t)
{
if (t >= 0 && t < strlen(_pStr))//下标非法判断
{
if ((*(int*)(_pStr - 4)) > 1)//多个对象指向同一块空间
{
char *pTemp = new char[strlen(_pStr) + 4 + 1];//开辟临时空间
pTemp += 4;//向后偏移4个字节
strcpy(pTemp, _pStr);//拷贝字符串
--(*(int*)(_pStr - 4));//计数-1
_pStr = pTemp;//将当前的对象指向临时空间
*((int*)(_pStr - 4)) = 1;//将新空间的计数置为1
}
return _pStr[t];
}
}
friend std::ostream & operator<<(std::ostream & out, String & A)
{
printf("%s %p\n", A._pStr, A._pStr);
return out;
}
private:
char *_pStr;
};
void FunTest()
{
String s1("Hello world");
std::cout << s1;
String s2(s1);
std::cout << s2;
String s3 = s2;
s3[3] = 'm';
std::cout << s3;
}
int main()
{
FunTest();
system("pause");
return 0;
}
结果:
三、内存去重
基于写时拷贝机制,操作系统进一步设计了内存去重的功能。操作系统可以定期地在内存中扫描具有相同内容的物理页,并且找到映射到这些物理页的虚拟页,然后只保留其中一个物理页,并将具有相同的内容的其它虚拟页都用写时拷贝的方式映射到这个物理页,然后释放其它物理页以供将来使用。该功能通常由操作系统主动发起。