网络传输大数据-内存映射

内存映射技术相关知识

由于内存映射技术与操作系统有关系,这里简单的介绍下段页式与逻辑地址
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 <stdio.h>
#include<winsock2.h>
#include<windows.h>
#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 <stdio.h>
#include<winsock2.h>
#include<windows.h>
#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 <tchar.h>
#include <windows.h>

enum MSG_TYPE
{
    file_size = 0x123,//数据大小
    file_begin,//开始接受数据
};

struct stMsgHead
{
    MSG_TYPE msg_type;
};

struct stMsgInfo 
{
    stMsgHead head;
    LONGLONG size;
    TCHAR szFileName[MAX_PATH];
};

内存映射简单流程:
内存映射


相关MSDN解释,方便后续学习:
CreateFile

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值