深入理解內存映射:高效處理大型文件和數據

內存映射是一種操作系統提供的機制,它將磁盤文件或其他設備的數據映射到進程的虛擬地址空間中的一段連續內存區域。通過內存映射,這些數據可以像訪問內存一樣直接讀取和寫入,而無需通過傳統的讀寫操作進行 IO 訪問。

具體而言,內存映射允許將一個文件或設備上的數據塊作爲一個連續的內存區域來對待。當應用程序需要訪問該文件或設備時,它可以直接從該內存區域中讀取或寫入數據,而不需要通過使用 read()、write() 等系統調用進行 IO 操作。

一、內存映射系統調用 mmap

1.1mmap 系統調用概述

mmap 是實現內存映射的關鍵系統調用。它創建了文件內容和進程地址空間之間的直接映射,使得文件的一部分或全部可以直接映射到進程的地址空間中。這樣,文件的讀寫就變得像內存訪問一樣高效。

在繼續深入探索之前,我們需要理解,技術的每一次進步都是對人類思維方式的挑戰和擴展。內存映射不僅僅是一種技術手段,它更是一種思維方式的革新。如同愛因斯坦所言:“邏輯會帶你從 A 點到 B 點,想象力會帶你到任何地方。” 當我們學習和應用內存映射這樣的技術時,我們不僅是在學習一種新的編程技能,更是在擴展我們對計算和數據處理的整體理解。

1.2mmap 的系統調用

mmap() 系統調用使得進程之間通過映射同一個普通文件實現共享內存。普通文件被映射到進程地址空間後,進程可以向訪問普通內存一樣對文件進行訪問,不必再調用 read(),write()等操作。

注:實際上,mmap() 系統調用並不是完全爲了用於共享內存而設計的。它本身提供了不同於一般對普通文件的訪問方式,進程可以像讀寫內存一樣對普通文件的操作。而 Posix 或系統 V 的共享內存 IPC 則純粹用於共享目的,當然 mmap() 實現共享內存也是其主要應用之一。

mmap() 系統調用形式如下:

void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )

參數 fd 爲即將映射到進程空間的文件描述字,一般由 open() 返回,同時,fd 可以指定爲 - 1,此時須指定 flags 參數中的 MAP_ANON,表明進行的是匿名映射(不涉及具體的文件名,避免了文件的創建及打開,很顯然只能用於具有親緣關係的進程間通信)。len 是映射到調用進程地址空間的字節數,它從被映射文件開頭 offset 個字節開始算起。prot 參數指定共享內存的訪問權限。可取如下幾個值的或:PROT_READ(可讀) , PROT_WRITE (可寫), PROT_EXEC (可執行), PROT_NONE(不可訪問)。

flags 由以下幾個常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE 必選其一,而 MAP_FIXED 則不推薦使用。offset 參數一般設爲 0,表示從文件頭開始映射。參數 addr 指定文件應被映射到進程空間的起始地址,一般被指定一個空指針,此時選擇起始地址的任務留給內核來完成。函數的返回值爲最後文件映射到進程空間的地址,進程可直接操作起始地址爲該值的有效地址。這裏不再詳細介紹 mmap() 的參數,讀者可參考 mmap() 手冊頁獲得進一步的信息。

系統調用 mmap() 用於共享內存的兩種方式:

(1)使用普通文件提供的內存映射:適用於任何進程之間;此時,需要打開或創建一個文件,然後再調用 mmap();典型調用代碼如下:

fd=open(name, flag, mode);
if(fd<0
 ...

ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 通過 mmap() 實現共享內存的通信方式有許多特點和要注意的地方,我們將在範例中進行具體說明。

(2)使用特殊文件提供匿名內存映射:適用於具有親緣關係的進程之間;由於父子進程特殊的親緣關係,在父進程中先調用 mmap(),然後調用 fork()。那麼在調用 fork() 之後,子進程繼承父進程匿名映射後的地址空間,同樣也繼承 mmap() 返回的地址,這樣,父子進程就可以通過映射區域進行通信了。注意,這裏不是一般的繼承關係。一般來說,子進程單獨維護從父進程繼承下來的一些變量。而 mmap() 返回的地址,卻由父子進程共同維護。
對於具有親緣關係的進程實現共享內存最好的方式應該是採用匿名內存映射的方式。此時,不必指定具體的文件,只要設置相應的標誌即可,參見範例 2。

系統調用 munmap()

int munmap( void * addr, size_t len )

該調用在進程地址空間中解除一個映射關係,addr 是調用 mmap() 時返回的地址,len 是映射區的大小。當映射關係解除後,對原來映射地址的訪問將導致段錯誤發生。

系統調用 msync()

int msync ( void * addr , size_t len, int flags)

一般說來,進程在映射空間的對共享內容的改變並不直接寫回到磁盤文件中,往往在調用 munmap()後才執行該操作。可以通過調用 msync() 實現磁盤上文件內容與共享內存區的內容一致。

1.3mmap 系統調用和直接使用 IPC 共享內存之間的差異

mmap 系統調用用於將文件映射到進程的地址空間中,而共享內存是一種不同的機制,用於進程間通信。這兩種方法都用於數據共享和高效的內存訪問,但它們有一些關鍵區別:

(1) 數據源和持久化

(2) 使用場景

(3) 性能和效率

(4) 同步和一致性

二、存儲映射 I/O

在現在的項目中需要用到 mmap 建立內存映射文件,順便把存儲映射 I/O 看了一下,這個東西還真是加載索引的良好工具,存儲映射 I/O 可以使一個磁盤文件與存儲空間中的一個緩衝區相映射,這樣可以從緩衝區中讀取數據,就相當於讀文件中的相應字節,而當將數據存入緩衝區時,最後相應字節就自動寫入文件中。

利用 mmap 建立內存映射文件一般會分爲兩條線:寫文件,讀文件,在分別介紹這兩條線之前首先將存儲映射 I/O 的常用函數介紹一下。

2.1 存儲映射 I/O 基本函數

(1) mmap 函數, 這個函數會告訴內核將一個給定的文件映射到一個存儲區域中,其函數原型爲:

void* mmap(void *addr,size_t len,int prot,int flags,int fields,off_t off);

其中,參數 addr 用於指定存儲映射區的起始地址,通常設定爲 0,這表示由系統選擇該映射區的起始地址,參數 len 是指定映射的字節數,參數 port 指定映射區的方式,如 PROT_READ,PROT_WRITE,值得注意的是映射區的保護不能超過文件 open 模式訪問權限。參數 flags 是設置映射區的屬性,一般設爲 MAP_SHARED, 這一標誌說明本進程的存儲操作相當於文件的 write 操作,參數 fields 是指定操作的文件描述符,參數 off 是要映射字節在文件中的起始偏移量。如果函數調用成功,函數的返回值是存儲映射區的起始地址;如果調用失敗,則返回 MAP_FAILED。

(2) msync 函數,這個函數會將存儲映射區的修改沖洗到被映射的文件中,其函數原型爲:

int msync(void *addr,size_t len,int flags)

其中,參數 flags 參數設定如何控制沖洗存儲區,可以選擇 MS_ASYNC,這表明是異步操作,函數調用立即返回,而選擇 MS_SYNC,函數調用則等待寫操作完成後纔會返回。

(3) munmap 函數,這個函數會解除文件和存儲映射區之間的映射。

int munmap(caddr_t addr,size_t len)

2.2 寫入映射緩衝區

當我們想向映射緩衝區中寫入數據時,首先需要確定映射文件的大小,在打開文件後,可以利用修改文件大小的函數重新設定文件的大小,接下來就可以對該緩衝區進行寫操作。

int fd = open(file_name,O_RDWR|O_CREAT);
ftruncate(fd,size);
mmap(0,size,PROT_WRITE,MAP_SHARED,fd,0);

2.3 從映射緩衝區讀取

當我們想從映射緩衝區中讀取數據時,需要利用 stat 系列函數得到文件大小,進行利用在映射存儲區中打開該文件。

int fd = open(file_name,O_RDONLY);
struct stat stat_buf;
fstat(fd,&stat_buf);
void *data = mmap(0,stat_buf.st_size,PROT_READ,
                  MAP_SHARED,fd,0);

2.4 實例:用存儲映射 I/O 複製文件

#include "apue.h"
#include <fcntl.h>
#include <sys/mman.h>

#define COPYINCR (1024*1024*1024)	/* 1 GB */

int
main(int argc, char *argv[])
{
	int			fdin, fdout;
	void		*src, *dst;
	size_t		copysz;
	struct stat	sbuf;
	off_t		fsz = 0;

	if (argc != 3)
		err_quit("usage: %s <fromfile> <tofile>", argv[0]);

	if ((fdin = open(argv[1], O_RDONLY)) < 0)
		err_sys("can't open %s for reading", argv[1]);

	if ((fdout = open(argv[2], O_RDWR | O_CREAT | O_TRUNC,
	  FILE_MODE)) < 0)
		err_sys("can't creat %s for writing", argv[2]);

	if (fstat(fdin, &sbuf) < 0)			/* need size of input file */
		err_sys("fstat error");

	if (ftruncate(fdout, sbuf.st_size) < 0)	/* set output file size */
		err_sys("ftruncate error");

	while (fsz < sbuf.st_size) {
		if ((sbuf.st_size - fsz) > COPYINCR)
			copysz = COPYINCR;
		else
			copysz = sbuf.st_size - fsz;

		if ((src = mmap(0, copysz, PROT_READ, MAP_SHARED,
		  fdin, fsz)) == MAP_FAILED)
			err_sys("mmap error for input");
		if ((dst = mmap(0, copysz, PROT_READ | PROT_WRITE,
		  MAP_SHARED, fdout, fsz)) == MAP_FAILED)
			err_sys("mmap error for output");

		memcpy(dst, src, copysz);	/* does the file copy */
		munmap(src, copysz);
		munmap(dst, copysz);
		fsz += copysz;
	}
	exit(0);
}

三、內存映射

mmap 內存映射的實現過程,總的來說可以分爲三個階段:

(一)進程啓動映射過程,並在虛擬地址空間中爲映射創建虛擬映射區域

1、進程在用戶空間調用庫函數 mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

2、在當前進程的虛擬地址空間中,尋找一段空閒的滿足要求的連續的虛擬地址

3、爲此虛擬區分配一個 vm_area_struct 結構,接着對這個結構的各個域進行了初始化

4、將新建的虛擬區結構(vm_area_struct)插入進程的虛擬地址區域鏈表或樹中

(二)調用內核空間的系統調用函數 mmap(不同於用戶空間函數),實現文件物理地址和進程虛擬地址的一一映射關係

5、爲映射分配了新的虛擬地址區域後,通過待映射的文件指針,在文件描述符表中找到對應的文件描述符,通過文件描述符,鏈接到內核 “已打開文件集” 中該文件的文件結構體(struct file),每個文件結構體維護着和這個已打開文件相關各項信息。

6、通過該文件的文件結構體,鏈接到 file_operations 模塊,調用內核函數 mmap,其原型爲:int mmap(struct file *filp, struct vm_area_struct *vma),不同於用戶空間庫函數。

7、內核 mmap 函數通過虛擬文件系統 inode 模塊定位到文件磁盤物理地址。

8、通過 remap_pfn_range 函數建立頁表,即實現了文件地址和虛擬地址區域的映射關係。此時,這片虛擬地址並沒有任何數據關聯到主存中。

(三)進程發起對這片映射空間的訪問,引發缺頁異常,實現文件內容到物理內存(主存)的拷貝

注:前兩個階段僅在於創建虛擬區間並完成地址映射,但是並沒有將任何文件數據的拷貝至主存。真正的文件讀取是當進程發起讀或寫操作時。

9、進程的讀或寫操作訪問虛擬地址空間這一段映射地址,通過查詢頁表,發現這一段地址並不在物理頁面上。因爲目前只建立了地址映射,真正的硬盤數據還沒有拷貝到內存中,因此引發缺頁異常。

10、缺頁異常進行一系列判斷,確定無非法操作後,內核發起請求調頁過程。

11、調頁過程先在交換緩存空間(swap cache)中尋找需要訪問的內存頁,如果沒有則調用 nopage 函數把所缺的頁從磁盤裝入到主存中。

12、之後進程即可對這片主存進行讀或者寫的操作,如果寫操作改變了其內容,一定時間後系統會自動回寫髒頁面到對應磁盤地址,也即完成了寫入到文件的過程。

注:修改過的髒頁面並不會立即更新迴文件中,而是有一段時間的延遲,可以調用 msync() 來強制同步, 這樣所寫的內容就能立即保存到文件裏了。

3.1 內存映射分類

按文件分:

按權限分:

將上面兩兩組合:

進程執行 exec() 時映射會丟失,但通過 fork() 的子進程會繼承映射

3.2API 函數

(1) 創建一個映射

#include <sys/mman.h>
void *mmap( void *addr, size_t length, int prot, int flags, int fd, off_t offset );

成功返回新映射的起始地址,失敗返回 MAP_FAILED。

參數 addr:映射被放置的虛擬地址,推薦爲NULL(內核會自動選擇合適地址)
參數 length:映射的字節數
參數 prot:位掩碼,可以取OR

違反了保護信息,內核會向進程發送 SIGSEGV 信號。

參數 flags:位掩碼,必須包含下列值中的一個
MAP_PROVATE:創建私有映射
MAP_SHARED:創建共享映射

參數 fd:被映射的文件的文件描述符(調用之後就能夠關閉文件描述符)。在打開描述符 fd 引用的文件時必須要具備與 prot 和 flags 參數值匹配的權限。特別的,文件必須總是被打開允許讀取。

參數 offset:映射在文件中的起點

(2) 解除映射區域

#include <sys/mman.h>
int munmap( void *addr, size_t length );

可以解除一個映射的部分映射,這樣原來的映射要麼收縮,要麼被分成兩個,這取決於在何處開始解除映射。還可以指定一個跨越多個映射的地址範圍,這樣的話所有在範圍內的映射都會被解除。

(3) 同步映射區域

#include <sys/mman.h>
int msync( void *addr, size_t length, int flags );

參數 flags:

(4) 重寫映射一個映射區域

#define _GNU_SOURCE
#include <sys/mman.h>
void *mremap( void *old_address, size_t old_size, size_t new_size, int fflags, ... );

參數 old_address 和 old_size 指既有映射的位置和大小。

參數 new_size 指定新映射的大小

參數 flags:
0
MREMAP_MAYMOVE:爲映射在進程的虛擬地址空間中重新指定一個位置
MREMAP_FIXED:配合 MREMAP_MAYMOVE 一起使用,mremap 會接收一個額外的參數 void *new_address

(5) 創建私有文件映射

創建一個私有文件映射,並打印文件內容

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

int main( int argc, char **argv )
{
    int fd = open( argv[1], O_RDONLY );
    if( fd == -1 ) {
        perror("open");
    }

    /*獲取文件信息*/
    struct stat sb;
    if( fstat( fd, &sb ) == -1 ) {
        perror("fstat");
    }

    /*私有文件映射*/
    char *addr = mmap( NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0 );
    if( addr == MAP_FAILED ) {
        perror("mmap");
    }

    /*將addr的內容寫到標準輸出*/
    if( write( STDOUT_FILENO, addr, sb.st_size ) != sb.st_size ) {
        perror("write");
    }

    exit( EXIT_SUCCESS );
}

(6) 創建共享匿名映射

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>



int main( int argc, char **argv )
{
    /*獲取虛擬設備的文件描述符*/
    int fd = open( "/dev/zero", O_RDWR );
    if( fd == -1 ) {
        perror("open");
    }

    int *addr = mmap( NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 );
    if( addr == MAP_FAILED ) {
        perror("mmap");
    }

    if( close( fd ) == -1 ) {
        perror("close");
    }

    *addr = 1;

    switch( fork() ) {
        case -1:
            perror("fork");
            break;
        case 0:
            printf("child *addr = %d\n", *addr);
            (*addr)++;

            /*解除映射*/
            if( munmap(addr, sizeof(int)) == -1 ) {
                perror("munmap");
            }
            _exit( EXIT_SUCCESS );
            break;
        default:
            /*等待子進程結束*/
            if( wait(NULL) == -1 ) {
                perror("wait");
            }

            printf("parent *addr = %d\n", *addr );
            if( munmap( addr, sizeof(int) ) == -1 ) {
                perror("munmap");
            }
            exit( EXIT_SUCCESS );
            break;
    }
}

案例:分塊內存映射處理大文件

內存映射文件可以用於 3 個不同的目的

• 系統使用內存映射文件,以便加載和執行. exe 和 DLL 文件。這可以大大節省頁文件空間和應用程序啓動運行所需的時間。

• 可以使用內存映射文件來訪問磁盤上的數據文件。這使你可以不必對文件執行 I/O 操作,並且可以不必對文件內容進行緩存。

• 可以使用內存映射文件,使同一臺計算機上運行的多個進程能夠相互之間共享數據。Windows 確實提供了其他一些方法,以便在進程之間進行數據通信,但是這些方法都是使用內存映射文件來實現的,這使得內存映射文件成爲單個計算機上的多個進程互相進行通信的最有效的方法。

使用內存映射數據文件

若要使用內存映射文件,必須執行下列操作步驟:

  1. 創建或打開一個文件內核對象,該對象用於標識磁盤上你想用作內存映射文件的文件。

  2. 創建一個文件映射內核對象,告訴系統該文件的大小和你打算如何訪問該文件。

  3. 讓系統將文件映射對象的全部或一部分映射到你的進程地址空間中。

當完成對內存映射文件的使用時,必須執行下面這些步驟將它清除:

文件操作是應用程序最爲基本的功能之一,Win32 API 和 MFC 均提供有支持文件處理的函數和類,常用的有 Win32 API 的 CreateFile()、WriteFile()、ReadFile() 和 MFC 提供的 CFile 類等。一般來說,以上這些函數可以滿足大多數場合的要求,但是對於某些特殊應用領域所需要的動輒幾十 GB、幾百 GB、乃至幾 TB 的海量存儲,再以通常的文件處理方法進行處理顯然是行不通的。所以可以使用內存文件映射來處理數據,網上也有鋪天蓋地的文章,但是映射大文件的時候又往往會出錯,需要進行文件分塊內存映射,這裏就是這樣的一個例子,教你如何把文件分塊映射到內存。

//
// 該函數用於讀取從CCD攝像頭採集來的RAW視頻數據當中的某一幀圖像,
// RAW視頻前596字節爲頭部信息,可以從其中讀出視頻總的幀數,
// 幀格式爲1024*576*8
/* 
參數:
  pszPath:文件名
  dwFrame: 要讀取第幾幀,默認讀取第2幀
*/
BOOL  MyFreeImage::LoadXRFrames(TCHAR *pszPath, DWORD dwFrame/* = 2*/ )
{

  // get the frames of X-Ray frames
  BOOL bLoop = TRUE;
  int i;
  int width = 1024;
  int height = 576;
  int bitcount = 8;     //1, 4, 8, 24, 32

  //
  //Build bitmap header
  BITMAPFILEHEADER bitmapFileHeader; 
  BITMAPINFOHEADER bitmapInfoHeader; 
  BYTE       rgbquad[4];      // RGBQUAD
  int        index = 0;

  DWORD widthbytes = ((bitcount*width + 31)/32)*4;    //每行都是4的倍數  DWORD的倍數  這裏是 576-
  TRACE1("widthbytes=%d\n", widthbytes);

  switch(bitcount) { 
  case 1: 
    index = 2; 
    bitmapFileHeader.bfOffBits = (DWORD)(sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + 2*4); 
    break; 
  case 4: 
    index = 16; 
    bitmapFileHeader.bfOffBits = (DWORD)(sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + 16*4); 
    break; 
  case 8: 
    index = 256; 
    bitmapFileHeader.bfOffBits = (DWORD)(sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + 256*sizeof(RGBQUAD)); 
    break; 
  case 24: 
  case 32: 
    index = 0; 
    bitmapFileHeader.bfOffBits = (DWORD)(sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER)); 
    break; 
  default:
    break;
  } 

  //構造Bitmap文件頭BITMAPFILEHEADER 
  bitmapFileHeader.bfType = 0x4d42;    // 很重要的標誌位  BM 標識
  bitmapFileHeader.bfSize = (DWORD)(bitmapFileHeader.bfOffBits + height * widthbytes);    //bmp文件長度  
  bitmapFileHeader.bfReserved1 = 0; 
  bitmapFileHeader.bfReserved2 = 0; 

  //構造Bitmap文件信息頭BITMAPINFOHEADER 
  bitmapInfoHeader.biSize = sizeof(BITMAPINFOHEADER); 
  bitmapInfoHeader.biWidth = width; 
  bitmapInfoHeader.biHeight = height; 
  bitmapInfoHeader.biPlanes = 1; 
  bitmapInfoHeader.biBitCount = bitcount;
  bitmapInfoHeader.biCompression = BI_RGB;  // 未壓縮
  bitmapInfoHeader.biSizeImage = height * widthbytes; 
  bitmapInfoHeader.biXPelsPerMeter = 3780; 
  bitmapInfoHeader.biYPelsPerMeter = 3780; 
  bitmapInfoHeader.biClrUsed = 0; 
  bitmapInfoHeader.biClrImportant = 0; 

  //創建BMP內存映像,寫入位圖頭部
  BYTE *pMyBmp = new BYTE[bitmapFileHeader.bfSize];   // 我的位圖pMyBmp
  BYTE *curr = pMyBmp;                  // curr指針指示pMyBmp的位置
  memset(curr, 0, bitmapFileHeader.bfSize); 

  //寫入頭信息 
  memcpy(curr, &bitmapFileHeader,sizeof(BITMAPFILEHEADER));
  curr = pMyBmp + sizeof(BITMAPFILEHEADER); 
  memcpy(curr, &bitmapInfoHeader,sizeof(BITMAPINFOHEADER)); 
  curr += sizeof(BITMAPINFOHEADER);

  //構造調色板 , 當像素大於8位時,就沒有調色板了。
  if(bitcount == 8) 
  {
    rgbquad[3] = 0;                   //rgbReserved
    for(i = 0; i < index; i++) 
    { 
      rgbquad[0] = rgbquad[1] = rgbquad[2] = i; 
      memcpy(curr, rgbquad, sizeof(RGBQUAD)); 
      curr += sizeof(RGBQUAD); 
    } 
  }else if(bitcount == 1) 
  { 
    rgbquad[3] = 0;                   //rgbReserved
    for(i = 0; i < index; i++) 
    { 
      rgbquad[0] = rgbquad[1] = rgbquad[2] = (256 - i)%256; 
      memcpy(curr, rgbquad, sizeof(RGBQUAD)); 
      curr += sizeof(RGBQUAD); 
    } 
  } 

  //
  // 文件映射,從文件中查找圖像的數據
  //Open the real file on the file system
  HANDLE hFile = CreateFile(pszPath, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  if (hFile == INVALID_HANDLE_VALUE)
  {
    DWORD dwError = GetLastError();
    ATLTRACE(_T("MapFile, Failed in call to CreateFile, Error:%d\n"), dwError);
    SetLastError(dwError);
    bLoop = FALSE;
    return FALSE;
  }

  //Create the file mapping object
  HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL);
  if (hMapping == NULL)
  {
    DWORD dwError = GetLastError();
    ATLTRACE(_T("MapFile, Failed in call to CreateFileMapping, Error:%d\n"), dwError);

    // Close handle
    if (hFile != INVALID_HANDLE_VALUE)
    {
      CloseHandle(hFile);
      hFile = INVALID_HANDLE_VALUE;
    }

    SetLastError(dwError);
    bLoop = FALSE;
    return FALSE;
  }

  // Retrieve allocation  granularity
  SYSTEM_INFO sinf;
  GetSystemInfo(&sinf);
  DWORD dwAllocationGranularity = sinf.dwAllocationGranularity;

  // Retrieve file size
  // Retrieve file size
  DWORD dwFileSizeHigh;
  __int64 qwFileSize = GetFileSize(hFile, &dwFileSizeHigh);
  qwFileSize |= (((__int64)dwFileSizeHigh) << 32);

  CloseHandle(hFile);

  // Read Image
  __int64 qwFileOffset = 0;                 // 偏移地址
  DWORD   dwBytesInBlock = 0,                 // 映射的塊大小
      dwStandardBlock = 100* dwAllocationGranularity ;  // 標準塊大小
  DWORD   dwFrameSize = height*width;                 // 計算一幀圖像的數據量,不包括頭部信息
  DWORD   dwCurrentFrame = 1;

  dwBytesInBlock = dwStandardBlock;
  if (qwFileSize < dwStandardBlock)
    dwBytesInBlock  = (DWORD)qwFileSize;

  //Map the view  
  LPVOID lpData = MapViewOfFile(hMapping,  FILE_MAP_ALL_ACCESS, 
    static_cast<DWORD>((qwFileOffset & 0xFFFFFFFF00000000) >> 32), static_cast<DWORD>(qwFileOffset & 0xFFFFFFFF), dwBytesInBlock);
  if (lpData == NULL)
  {
    DWORD dwError = GetLastError();
    ATLTRACE(_T("MapFile, Failed in call to MapViewOfFile, Error:%d\n"), dwError);

    // Close Handle
    if (hMapping != NULL)
    {
      CloseHandle(hMapping);
      hMapping = NULL;
    }
    SetLastError(dwError);
    bLoop = FALSE;
    return FALSE;
  }


  BYTE  *lpBits = (BYTE *)lpData;
  BYTE  *curr1, *curr2, *lpEnd;
  curr1 = lpBits;             // seek to start
  curr2 = lpBits + 596;         // seek to first frame
  lpEnd = lpBits + dwBytesInBlock;    // seek to end

  // Read video infomation
  KMemDataStream streamData( curr1, dwBytesInBlock);
  ReadXRHeader(streamData);

  while(bLoop)
  {
    DWORD dwTmp = lpEnd - curr2;    //內存緩衝剩餘的字節
    if ( dwTmp >= dwFrameSize )  
    {
      if(dwCurrentFrame == dwFrame)
      {
        memcpy(curr, curr2, dwFrameSize);
        bLoop = FALSE;
      }
      curr2 += dwFrameSize;
    }else   //內存中不夠一幀數據
    {
      DWORD dwTmp2 = dwFrameSize - dwTmp;     // 一副完整的幀還需要dwTmp2字節

      if (dwCurrentFrame == dwFrame)
      {
        memcpy(curr, curr2, dwTmp);
        curr += dwTmp;
      }

                        
      //1、首先計算文件的偏移位置
                        qwFileOffset += dwBytesInBlock;
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/CPvvTjSQUjqIuHH0sMKGDw