内存映射文件与虚拟内存有些类似,通过内存映射文件可以保留一个地址空间的区域,同时将物理存储器提交给此区域,只是内存文件映射的物理存储器来自一个已经存在于磁盘上的文件,而非系统的页文件,而且在对该文件进行操作之前必须首先对文件进行映射,就如同将整个文件从磁盘加载到内存。由此可以看出,使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,这意味着在对文件进行处理时将不必再为文件申请并分配缓存,所有的文件缓存操作均由系统直接管理,由于取消了将文件数据加载到内存、数据从内存到文件的回写以及释放内存块等步骤,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。另外,实际工程中的系统往往需要在多个进程之间共享数据,如果数据量小,处理方法是灵活多变的,如果共享数据容量巨大,那么就需要借助于内存映射文件来进行。实际上,内存映射文件正是解决本地多个进程间数据共享的最有效方法
由于内存映射技术与操作系统有关系,这里简单的介绍下段页式与逻辑地址
1、段页式存储管理简述
在段页式存储中,一个PE文件(.exe,.dLL等)由各种段组成,而一个段由由一些页面组成,其中每页面的大小为4KB。
比如:一个exe文件分为主程序段I、主程序段II、…、数据段及栈段(线程栈)等,而数据段的大小为15KB,也就是说数据段包含了4个页面。
为什么不是3.25个页面?因为页面是一个内存单元,与之相对应的物理内存的块大小也为4KB。
2、何为逻辑地址?
逻辑地址是假设程序都是从0地址开始,到程序大小的一个地址区间(其实并不是)。这样从逻辑上容易理解一个可执行文件的空间分布,也方便我们对程序的开发,而不用无关心它的实际地址,比如数据段在偏移0地址多远的地方,我们只需要知道偏移地址即可在程序内调用。
逻辑地址一般又称之为虚拟地址,为什么说虚拟呢?因为程序运行在内存的时候,程序总能调用到指定(比如指针)地址处的数据,给我们的感觉好像就是程序直接能在内存中的实际地址调用到数据,其实并不是这样,这个过程的实现最先是由CPU给我们所需要调用的数据的地址(该地址就是逻辑地址),然后通过在自身程序查找相应段表,然后查段表中的页表,最后查找到对应的页面并加载到相关的数据…
3、运行中的程序加载数据的过程
在程序加载到内存并运行一段时间后,此时CPU需要调用程序数据段中某一个数据(在数据段中某个页面里),同时CPU会给出该数据所在的逻辑地址。接下来就查找存储在内存中为该程序创建的段表(段页式存储),然后根据逻辑地址换算出是段表中的第几个段,接着换算出是该段对应页表中的第几个页面,最后结合逻辑地址中的页内偏移地址,找出对应的物理内存地址(数据真正在物理内存中的地址)。此时又分为如下三种情况:
(1)运气好的话,可能相应页的数据加载到了对应的物理内存块中,这样程序就能直接调用对应物理内存地址处的数据。
(2)可是偏偏运气不好,相应页的数据没有加载到物理块中,此时会产生访问违规(或者说中断,就是没有找到数据的意思),然后就会去页交换文件(也就是我们的虚拟内存)查找对应的的数据是否存在,如果存在页交换文件中,就将页交换文件中对应页加载到物理内存对应的块。如果此时物理内存块不足够,就通过页面置换算法(LRU、FIFO等),将闲置的页面置换到页交换文件中,然后再将页交换文件中的页加载到物理内存中。
(3)可能此时运气很不好,在页交换文件中也没有找到相应的页,此时就要到程序的磁盘地址空间去查找对应的数据,然后将查找的数据直接加载到物理内存中,如果物理内存不足,就通过置换算法将闲置的块置换到页交换文件中,然后再将磁盘中的数据加载到物理内存。
4、内存映射技术的理解
内存映射技术,常常指虚拟内存映射与内存映射文件。
虚拟内存映射:物理内存与页交换文件(虚拟内存)的数据操作,就是通过内存映射实现,此时的内存映射一般称之为虚拟内存映射。在运用程序在查找页表并在物理内存中没有查找到页面数据时,就会进行中断,保存CPU现场,然后在虚拟内存中查找到相应页面后替换到物理内存中(前提是虚拟内存中已经有相关的数据)。
内存映射文件:物理内存直接与磁盘中的文件进行数据操作时,此时称之为内存映射文件,通俗理解就是将文件映射到内存,映射指的是将文件的逻辑地址映射到磁盘中的实际地址。如果内存映射文件的文件是可执行程序,那么当可执行程序需要调用某部分的数据时,首先是到物理内存中查找,如果没有找到目标数据,此时中断后会先去虚拟内存中查找数据,然后依旧没有找到,就会直接去磁盘中的文件提取数据到物理内存中,供可执行程序继续运行。
5、内存映射文件的优点
很明显,我们在加载一个程序的时候,不用全部加载我们的程序,只需要将需要运行的部分加载到物理内存即可,因为有映射关系,直接提取在磁盘中指定的数据即可。
可能你会觉得文件不都是这样加载的吗?确实,现在可执行文件都是这样加载的,可是我们在程序中加载文件的时候,可不是使用的内存映射技术,而是先将数据从磁盘空间加载到内核缓冲区,再从内核缓冲区加载到我们应用程序的用户空间(用户缓冲区),很明显,这里进行了两次拷贝,分别是磁盘到内核缓冲区与内核缓冲区到用户空间。同样,我们在往磁盘写入数据的时候,也需要经历从用户空间到内核缓冲区再到磁盘空间。这样大量的数据读写操作,会大量的降低效率。
所以,推荐凡是大文件读写都采用内存映射技术,这样对文件的操作只有从磁盘空间到用户空间的一次拷贝。
内存映射技术在网络传输中的C++实现
实现了使用内存映射实现网络高效传输大文件的示例,分别包括服务端与客户端。文章末尾有关于内存映射的相关MSDN解释,详细的参数意义请参阅MSDN。
服务端:
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include
#include
#include
#include "MsgDef.h"
#pragma comment(lib,"Ws2_32.lib")
using namespace std;
#define PORT 6000
#define MAX_SIZE (1024*1024)
BOOL _beginRecvByMapping(SOCKET sockClient, LONGLONG filesize, TCHAR* filename);
int _tmain()
{
WSAData wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
{
WSACleanup();
return -1;
}
SOCKET sockServ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == sockServ)
{
WSACleanup();
closesocket(sockServ);
return -1;
}
SOCKADDR_IN addrServ;
addrServ.sin_family = AF_INET;
addrServ.sin_port = htons(PORT);
addrServ.sin_addr.S_un.S_addr = htonl(ADDR_ANY);
int ret = bind(sockServ, (sockaddr*)&addrServ, sizeof(addrServ));
if (SOCKET_ERROR == ret)
{
WSACleanup();
closesocket(sockServ);
return -1;
}
ret = listen(sockServ, SOMAXCONN);
if (SOCKET_ERROR == ret)
{
WSACleanup();
closesocket(sockServ);
return -1;
}
SOCKADDR_IN addrClient = { 0 };
int len = sizeof(SOCKADDR_IN);
SOCKET *sockClient = new SOCKET;
*sockClient = accept(sockServ, (sockaddr*)&addrClient, &len);
if (INVALID_SOCKET == *sockClient)
{
WSACleanup();
closesocket(sockServ);
return -1;
}
puts("接收到客户端连接");
// 接收到数据传输的字节数
stMsgInfo msg;
ret = recv(*sockClient,(char*) &msg, sizeof(msg), 0);
if (ret == SOCKET_ERROR)
{
WSACleanup();
closesocket(sockServ);
closesocket(*sockClient);
return -1;
}
puts("接收到 文件大小 信息");
if (msg.head.msg_type != file_size)
{
WSACleanup();
closesocket(sockServ);
closesocket(*sockClient);
return -1;
}
stMsgHead head = { file_begin };
send(*sockClient, (char*)&head, sizeof(head), 0);
puts("发送 可以传输数据 指令");
// 开始接受数据
if (!_beginRecvByMapping(*sockClient, msg.size, msg.szFileName))
{
int err = GetLastError();
printf("err code:%d", err);
}
puts("数据接收完成");
getchar();
WSACleanup();
closesocket(sockServ);
return 0;
}
BOOL _beginRecvByMapping(SOCKET sockClient, LONGLONG filesize, TCHAR* filename)
{
HANDLE hFile = CreateFile(L"test.MP4",//非本机测试,这里填filename
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
CREATE_ALWAYS,
FILE_FLAG_SEQUENTIAL_SCAN,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
return FALSE;
HANDLE hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE,
(DWORD)(filesize >> 32), (DWORD)(filesize & 0xFFFFFFFF), NULL);
if (hFileMapping == NULL)
return FALSE;
char* pViwOfFile = (char*)MapViewOfFile(hFileMapping,
FILE_MAP_ALL_ACCESS,
0,
0,
filesize);//延伸到映射文件的末尾(因为此处我们已经知道映射文件的大小)
char* buf = new char[MAX_SIZE];
ZeroMemory(buf, MAX_SIZE);
puts("开始接收数据");
while (true)
{
int ret = recv(sockClient, pViwOfFile, MAX_SIZE, 0);
if (ret == 0 || ret == SOCKET_ERROR)
break;
pViwOfFile += ret;
}
delete[] buf;
UnmapViewOfFile(pViwOfFile);
CloseHandle(hFile);
CloseHandle(hFileMapping);
return true;
}
客户端
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include
#include
#include
#pragma comment(lib,"Ws2_32.lib")
#include "MsgDef.h"
#define PORT 6000
#define MAX_SIZE (1024*1024)
BOOL SendFileByMapping(SOCKET sock);
int _tmain()
{
WSAData wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
{
WSACleanup();
return -1;
}
SOCKET sockClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == sockClient)
{
WSACleanup();
closesocket(sockClient);
return -1;
}
SOCKADDR_IN addrServ;
addrServ.sin_family = AF_INET;
addrServ.sin_port = htons(PORT);
addrServ.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
int ret = connect(sockClient, (sockaddr*)&addrServ, sizeof(addrServ));
if (SOCKET_ERROR == ret)
{
printf("connect() error:%d", WSAGetLastError());
WSACleanup();
closesocket(sockClient);
return -1;
}
puts("连接成功");
if (!SendFileByMapping(sockClient))
{
int err = GetLastError();
printf("err code:%d", err);
}
puts("数据传输完成");
getchar();
WSACleanup();
closesocket(sockClient);
return 0;
}
BOOL SendFileByMapping(SOCKET sock)
{
TCHAR* fileName = L"h:\\TEST\\video.mp4";
HANDLE hFile = CreateFile(fileName,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
return FALSE;
LARGE_INTEGER lFileSize;
if (!GetFileSizeEx(hFile, (PLARGE_INTEGER)&lFileSize))
return FALSE;
stMsgInfo msg;
ZeroMemory(&msg, sizeof(stMsgInfo));
msg.head.msg_type = file_size;
msg.size = lFileSize.QuadPart;
_tcscpy(msg.szFileName, fileName);
puts("发送传输文件的大小");
int ret = send(sock, (char*)&msg, sizeof(stMsgInfo), 0);
if (SOCKET_ERROR == ret)
return FALSE;
ZeroMemory(&msg, sizeof(stMsgInfo));
ret = recv(sock, (char*)&msg, sizeof(stMsgInfo), 0);
if (SOCKET_ERROR == ret)
return FALSE;
puts("接收到 可以传输数据 指令");
if (msg.head.msg_type != file_begin)
return FALSE;
// 接收到传输数据的命令,开始数据传输
HANDLE hMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE,
0, 0,// 创建与文件相同大小的文件映射
NULL);// 文件映射name(名字不可以与其它内核对象(事件、信号、互斥量等)相同)
if (hMap == NULL)
return FALSE;
char* pViwOfFile = (char*)MapViewOfFile(
hMap,// 文件映射句柄
FILE_MAP_READ,//文件映射对象的访问类型
0, 0, 0);
if (pViwOfFile == NULL)
return FALSE;
int len = 0;
puts("开始文件传输");
while (true)
{
if (lFileSize.QuadPart - len < MAX_SIZE)
{
send(sock, pViwOfFile, lFileSize.QuadPart - len, 0);
break;
}
ret = send(sock, pViwOfFile, MAX_SIZE, 0);
if (ret != MAX_SIZE)
break;
len += MAX_SIZE;
pViwOfFile += ret;// 指针后移
}
UnmapViewOfFile(pViwOfFile);
CloseHandle(hFile);
CloseHandle(hMap);
}
MsgDef.h->定义简单的传输协议
#pragma once
#include
#include
enum MSG_TYPE
{
file_size = 0x123,//数据大小
file_begin,//开始接受数据
};
struct stMsgHead
{
MSG_TYPE msg_type;
};
struct stMsgInfo
{
stMsgHead head;
LONGLONG size;
TCHAR szFileName[MAX_PATH];
};
内存映射简单流程:
HANDLE CreateFile(
LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);
lpFileName:需要创建或者打开的文件名字
dwDesiredAccess:文件的打开方式,GENERIC_READ(只读), GENERIC_WRITE(只写), GENERIC_READ | GENERIC_WRITE(读写)
dwShareMode:文件的共享方式,如:FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE
lpSecurityAttributes:文件的安全属性,通常为空
dwCreationDisposition:文件的访问方式,如:CREATE_ALWAYS, CREATE_NEW, OPEN_ALWAYS, OPEN_EXISTING, or TRUNCATE_EXISTING(只能是其中一种,不能组合使用)
dwFlagsAndAttributes:文件属性和标志
hTemplateFile:模板文件句柄
补充:
dwFlagsAndAttributes
此参数可以包含可用文件属性(FILE_ATTRIBUTE_ )的任意组合,还可以包含用于控制文件或设备缓存行为,访问模式和其他特殊用途标志的标志组合(FILE_FLAG_ )。这些与任何FILE_ATTRIBUTE_ *值结合使用。
FILE_FLAG_NO_BUFFERING
FILE_FLAG_RANDOM_ACCESS
FILE_FLAG_SEQUENTIAL_SCAN
FILE_FLAG_WRITE_THROUGH
FILE_ATTRIBUTE_TEMPORARY
如果没有指定这些标志,则系统使用默认的通用缓存方案。否则,系统缓存将按照每个标志的规定运行。
其中一些标志不应合并。例如,将FILE_FLAG_RANDOM_ACCESS与FILE_FLAG_SEQUENTIAL_SCAN组合起来是自我毁灭的。
指定FILE_FLAG_SEQUENTIAL_SCAN标志可以提高使用顺序访问读取大文件的应用程序的性能。对于大部分按顺序读取大文件的应用程序,性能提升可能会更加明显,但偶尔会跳过小字节范围。如果应用程序移动文件指针进行随机访问,则最有可能不会发生最佳缓存性能。但是,正确的操作仍然有保证。
通过FILE_FLAG_WRITE_THROUGH的直写请求还会导致NTFS清除处理请求导致的所有元数据更改,例如时间戳更新或重命名操作。因此,FILE_FLAG_WRITE_THROUGH标志通常与FILE_FLAG_NO_BUFFERING标志一起使用,作为每次写入后调用FlushFileBuffers函数的替代品,这会导致不必要的性能损失。
当FILE_FLAG_NO_BUFFERING与FILE_FLAG_OVERLAPPED结合使用时,这些标志提供了最大的异步性能,因为I / O不依赖于内存管理器的同步操作。但是,一些I / O操作需要更多的时间,因为数据没有被保存在缓存中。另外,文件元数据可能仍然被缓存(例如,创建空文件时)。为确保将元数据刷新到磁盘,请使用FlushFileBuffers函数。
如果有足够的高速缓存可用的话,指定FILE_ATTRIBUTE_TEMPORARY属性将导致文件系统避免将数据写回大容量存储器,因为应用程序在句柄关闭后会删除临时文件。在这种情况下,系统可以完全避免写入数据。虽然它不像前面提到的标志那样直接控制数据缓存,但是FILE_ATTRIBUTE_TEMPORARY属性确实告诉系统尽可能地保留在系统缓存中,而不用写入,因此可能会对某些应用程序造成影响。
当应用程序通过网络创建文件时,最好使用GENERIC_READ | GENERIC_WRITE for dwDesiredAccess比单独使用GENERIC_WRITE。由此产生的代码更快,因为重定向器可以使用缓存管理器,并发送更少的SMB数据更多。这种组合还避免了通过网络写入文件偶尔会返回ERROR_ACCESS_DENIED的问题。
CreateFileMapping
HANDLE WINAPI CreateFileMapping(
_In_ HANDLE hFile,
_In_opt_ LPSECURITY_ATTRIBUTES lpAttributes,
_In_ DWORD flProtect,
_In_ DWORD dwMaximumSizeHigh,
_In_ DWORD dwMaximumSizeLow,
_In_opt_ LPCTSTR lpName
);
hFile:需要创建文件内存映射的文件句柄
lpAttributes:安全属性指针
flProtect:文件内存映射访问模式
dwMaximumSizeHigh:内存映射大小的高32位
dwMaximumSizeLow:内存映射大小的低32位
lpName:内存映射的名字
MapViewOfFile
LPVOID WINAPI MapViewOfFile(
_In_ HANDLE hFileMappingObject,
_In_ DWORD dwDesiredAccess,
_In_ DWORD dwFileOffsetHigh,
_In_ DWORD dwFileOffsetLow,
_In_ SIZE_T dwNumberOfBytesToMap
);
hFileMappingObject:文件内存映射句柄
dwDesiredAccess:访问方式,如FILE_MAP_ALL_ACCESS、FILE_MAP_COPY等
dwFileOffsetHigh:视图开始处的文件偏移量的高位DWORD。
dwFileOffsetLow:视图开始处的文件偏移量的低位DWORD。 高偏移量和低偏移量的组合必须在文件映射中指定偏移量。 它们还必须匹配系统的内存分配粒度。 也就是说,偏移量必须是分配粒度的倍数。 要获得系统的内存分配粒度,请使用GetSystemInfo函数,该函数填充SYSTEM_INFO结构的成员。
注意:文件偏移必须是系统粒度的整数倍
dwNumberOfBytesToMap:映射到视图的文件映射的字节数。 所有字节必须在由CreateFileMapping指定的最大大小内。 如果此参数为0(零),则映射从指定的偏移量延伸到文件映射的结尾。
提醒:
CreateFile、CreateFileMapping、MapViewOfFile的访问类型尽量要相同,否则会出现ERROR_ACCESS_DENIED(5L),遇到这个问题的时候,多半是上下文中设置的访问类型有不同
示例源码:http://pan.baidu.com/s/1sl2tIRb