|
磁盤文件的正常讀寫與異步讀寫
在Win32系統(tǒng)下文件可以支持平常的同步讀寫和異步讀寫(但在Win9X下,Win32系統(tǒng)不支持磁盤文件的異步讀寫)。本節(jié)在后面部分將會介紹文件的異步讀寫,最后一段內(nèi)容將向大家講解一下文件的區(qū)域加鎖。 在Win32系統(tǒng)中支持64位長度的文件,所以在很多文件操作函數(shù)中需要兩個DWORD參數(shù)來表示文件長度,一個DWORD用來表示低32位,另一個用來表示高32位。 文件的讀寫進行在文件被正確打開后,但請確認在打開文件時設(shè)置了正確的讀寫標記。在Win32的文件操作中沒有了以前類似與以前ANSI C中的fputs fgets fprintf fscanf等函數(shù),只有類似于fread和fwrite的ReadFile和WriteFile函數(shù)。 ReadFile用于文件讀,函數(shù)原型為: BOOL ReadFile( HANDLE hFile, // handle to file LPVOID lpBuffer, // data buffer DWORD nNumberOfBytesToRead, // number of bytes to read LPDWORD lpNumberOfBytesRead, // number of bytes read LPOVERLAPPED lpOverlapped // overlapped buffer );其中各項參數(shù)的含義為:
WriteFile用于文件寫,函數(shù)原型為: BOOL WriteFile( HANDLE hFile, // handle to file LPCVOID lpBuffer, // data buffer DWORD nNumberOfBytesToWrite, // number of bytes to write LPDWORD lpNumberOfBytesWritten, // number of bytes written LPOVERLAPPED lpOverlapped // overlapped buffer );參數(shù)的含義和ReadFile類似。 如果需要移動文件指針到相關(guān)位置(和文件讀寫不同,這個函數(shù)沒有異步版本),使用 DWORD SetFilePointer( HANDLE hFile, // handle to file LONG lDistanceToMove, // bytes to move pointer PLONG lpDistanceToMoveHigh, // bytes to move pointer DWORD dwMoveMethod // starting point );其中各項參數(shù)的含義為:
//第一種情況
DWORD dwPtr = SetFilePointer (hFile, lDistance, NULL, FILE_BEGIN) ;
if (dwPtr == INVALID_SET_FILE_POINTER) // Test for failure
{
// Obtain the error code.
dwError = GetLastError() ;
// 處理錯誤
// . . .
} // End of error handler
//第二種情況
dwPtrLow = SetFilePointer (hFile, lDistLow, & lDistHigh, FILE_BEGIN) ;
// Test for failure
if (dwPtrLow == INVALID_SET_FILE_POINTER && (dwError = GetLastError()) != NO_ERROR )
{
// 處理錯誤
// . . .
} // End of error handler
在Win32中沒有提供得到文件當前指針位置的函數(shù),但通過SetFilePointer也可以確定文件指針當前的位置。在MSDN中提供了兩個宏來得到當前文件的指針位置: //對32位長度的文件
#define GetFilePointer(hFile) SetFilePointer(hFile, 0, NULL, FILE_CURRENT)
//對超過32位長度的文件
#define GetVLFilePointer(hFile, lpPositionHigh) (*lpPositionHigh = 0, SetFilePointer(hFile, 0, lpPositionHigh, FILE_CURRENT))
對了可以通過SetFilePointer來得到文件長度,方法就是從文件位結(jié)束處移動0字節(jié),返回值就是文件的長度。 HANDLE hFile = CreateFile(...);//打開文件進行讀 DWORD dwLen; dwLen = SetFilePointer (hFile, 0, NULL, FILE_END) ; CloseHandle( hFile ) ;當然Win32中也提供了專門的函數(shù)來得到文件的大小 BOOL GetFileSizeEx(
HANDLE hFile, // handle to file
PLARGE_INTEGER lpFileSize // file size
);
typedef union _LARGE_INTEGER {
struct {
DWORD LowPart;
LONG HighPart;
};
LONGLONG QuadPart;
} LARGE_INTEGER, *PLARGE_INTEGER;
其中l(wèi)pFileSize是一個聯(lián)合數(shù)據(jù),用來表明文件的大小。
文件的異步讀寫主要是用在大文件的讀寫上,當使用異步讀寫時,ReadFile和WriteFile會馬上返回,并在讀寫完成時通知應用程序。 要使用異步功能首先需要在打開文件時指定FILE_FLAG_OVERLAPPED作為標記(dwFlagsAndAttributes),在讀寫文件時可以使用ReadFile/WriteFile或者ReadFileEx/WriteFileEx來進行讀寫,當調(diào)用不同的函數(shù)時讀寫完成后通知應用程序的方法有所不同的,。下面分別介紹這兩種方法: 第一種方法,利用ReadFile/WriteFile,這對函數(shù)使用事件信號來進行讀寫完成的通知,由于磁盤讀寫是一個比較耗費時間的操作,而且現(xiàn)在的磁盤子系統(tǒng)可以在磁盤讀寫時脫離CPU而單獨進行,例如使用DMA方式,所以在磁盤讀寫時可以進行其他一些操作以充分利用CPU。關(guān)于事件信號相關(guān)內(nèi)容請參考4.4節(jié) 進程/線程間同步中事件部分內(nèi)容。并且在文件讀寫時提供OVERLAPPED數(shù)據(jù)。 結(jié)構(gòu)定義如下:
typedef struct _OVERLAPPED {
ULONG_PTR Internal; //系統(tǒng)使用
ULONG_PTR InternalHigh; //系統(tǒng)使用
DWORD Offset; // 文件讀或?qū)懙拈_始位置低32位,對于命名管道和其他通信設(shè)備必須設(shè)置為0
DWORD OffsetHigh; // 文件讀或?qū)懙拈_始位置高32位,對于命名管道和其他通信設(shè)備必須設(shè)置為0
HANDLE hEvent; // 事件量,當操作完成時,這個時間會變?yōu)橛行盘枲顟B(tài)
} OVERLAPPED;
//下面的代碼演示了文件異步讀取
//并且比較了同步和異步之間的性能差異
void DoDataDeal(BYTE *pbData,int iLen)
{//對字節(jié)進行操作
Sleep(3*1000);//假設(shè)每次計算需要2秒鐘
}
//下面是使用異步讀的示范代碼,假設(shè)c:\temp\large_file.dat文件有130MB大小()
//每次讀入10MB字節(jié),并且對文件中每個字節(jié)進行操作,由于可以使用異步操作所以可以在下一次讀入數(shù)據(jù)的同時進行計算
void ReadM1(void)
{
HANDLE hFile = CreateFile("c:\\temp\\large_file.dat",GENERIC_READ,0,
NULL,OPEN_EXISTING,FILE_FLAG_OVERLAPPED|FILE_ATTRIBUTE_NORMAL,NULL);
if( INVALID_HANDLE_VALUE != hFile )
{
HANDLE hEvent = CreateEvent(NULL,FALSE,FALSE,"read_event");
BYTE *pbRead = new BYTE[1024*1024*10];//10MB字節(jié)
BYTE *pbBuf = new BYTE[1024*1024*10];
DWORD dwRead,dwCount=0;
OVERLAPPED overlap;
overlap.Offset = 0;
overlap.OffsetHigh =0;
overlap.hEvent = hEvent;
DWORD dwBegin= GetTickCount();//記錄開始時間
ReadFile(hFile,pbRead,1024*1024*10,&dwRead,&overlap);
{//開始計算
for(int i=1;i<13;i++)
{
printf("M1 i=%d\n",i);
WaitForSingleObject(hEvent,INFINITE);//等待上一次完成
memcpy(pbBuf,pbRead,1024*1024*10);
overlap.Offset = i * (1024*1024*10);
overlap.OffsetHigh =0;
overlap.hEvent = hEvent;
ReadFile(hFile,pbRead,1024*1024*10,&dwRead,&overlap);
//在文件進行讀的同時進行計算
DoDataDeal(pbBuf,1024*1024*10);
}
WaitForSingleObject(hEvent,INFINITE);//等待最后一次完成
memcpy(pbBuf,pbRead,1024*1024*10);
//數(shù)據(jù)處理
DoDataDeal(pbBuf,1024*1024*10);
}//結(jié)束計算
printf("耗時 %d\n",GetTickCount()-dwBegin);
//操作完成
CloseHandle(hEvent);
CloseHandle(hFile);
delete pbRead;
delete pbBuf;
}
}
//下面是上面功能的文件同步讀版本
void ReadM2(void)
{
HANDLE hFile = CreateFile("c:\\temp\\large_file.dat",GENERIC_READ,0,
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if( INVALID_HANDLE_VALUE != hFile )
{
DWORD dwRead,dwCount=0;
BYTE *pbRead = new BYTE[1024*1024*10];//10MB字節(jié)
BYTE *pbBuf = new BYTE[1024*1024*10];//10MB字節(jié)
DWORD dwBegin= GetTickCount();//記錄開始時間
for(int i=0;i<13;i++)
{
printf("M2 i=%d\n",i);
if(!ReadFile(hFile,pbRead,1024*1024*10,&dwRead,NULL))
{
printf("error read");
break;
}
memcpy(pbBuf,pbRead,1024*1024*10);
//計算
DoDataDeal(pbBuf,1024*1024*10);
}
printf("耗時 %d\n",GetTickCount()-dwBegin);
//操作完成
CloseHandle(hFile);
delete pbRead;
delete pbBuf;
}
}
在文件的異步讀寫中,如果ReadFile/WriteFile返回FALSE,則需要通過LastError來進一步確認是否發(fā)生錯誤,例如下面的錯誤檢測代碼: bResult = ReadFile(hFile, &inBuffer, nBytesToRead, &nBytesRead,
&gOverlapped) ;
if (!bResult)
switch (dwError = GetLastError())
{
case ERROR_HANDLE_EOF:
{
//到達文件尾
}
case ERROR_IO_PENDING:
{
//正在進行異步操作
} // end case
} // end switch
} // end if
此外可以通過GetOverlappedResult函數(shù)來得到異步函數(shù)的執(zhí)行情況
BOOL GetOverlappedResult(
HANDLE hFile, // handle to file, pipe, or device
LPOVERLAPPED lpOverlapped, // overlapped structure
LPDWORD lpNumberOfBytesTransferred, // bytes transferred
BOOL bWait // wait option
);
如果函數(shù)調(diào)用返回FALSE則可以用GetLastError來得到錯誤,如果返回成功則可以通過lpNumberOfBytesTransferred參數(shù)來確定當前有多少數(shù)據(jù)已經(jīng)被讀或?qū)?。lpOverlapped參數(shù)必須與調(diào)用ReadFile或WriteFile時使用同一個數(shù)據(jù)區(qū)。最后一個參數(shù)bWait表明是否等待異步操作結(jié)束時才返回,如果設(shè)置為TRUE就可以等待文件讀寫完成時返回,否則就會馬上返回,利用這個特點可以利用它來等待異步文件操作的結(jié)束(就如同等待事件變?yōu)橛行盘枲顟B(tài)一樣起到相同的作用)。
在上面的例子中沒有過多的進行錯誤檢查,如果大家有興趣可以自己運行一下這個例子,看看異步文件讀寫對性能的影響。在我自己的計算機PII 450 IBM 30GB硬盤上同步讀比異步讀慢了大約10%左右,這主要時因為數(shù)據(jù)處理時間我設(shè)置為兩秒鐘,如果設(shè)置得足夠長,會顯示出異步和同步處理時的差異極限。此外由于磁盤緩存得作用也會影響結(jié)果,所以如果讀入的數(shù)據(jù)量更大將會產(chǎn)生更明顯的差異,這是因為雖然異步讀寫會在調(diào)用等待函數(shù)上也會耗費一些時間,所以如果數(shù)據(jù)量小就無法分辨出差異。 請記住OVERLAPPED參數(shù)在文件操作執(zhí)行完以前不要手工修改結(jié)構(gòu)內(nèi)的值,因為系統(tǒng)會使用結(jié)構(gòu)中的數(shù)據(jù)。 對于WriteFile操作也可以用相同方法,在WriteFile中磁盤操作將耗費更多的時間,所以使用異步寫更能體現(xiàn)優(yōu)勢,你可以將這個例子改為磁盤寫后自己測試一下。 第二種方法,利用ReadFileEx/WriteFileEx,這對函數(shù)使用回調(diào)函數(shù)來進行讀寫完成的通知。 BOOL ReadFileEx( HANDLE hFile, // handle to file LPVOID lpBuffer, // data buffer DWORD nNumberOfBytesToRead, // number of bytes to read LPOVERLAPPED lpOverlapped, // offset LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // completion routine );
VOID CALLBACK FileIOCompletionRoutine( DWORD dwErrorCode, // completion code DWORD dwNumberOfBytesTransfered, // number of bytes transferred LPOVERLAPPED lpOverlapped // I/O information buffer );
那么如何檢測回調(diào)函數(shù)已經(jīng)被調(diào)用了(文件操作已經(jīng)完成),你可以設(shè)置一個全局的同步量來進行通知,但是系統(tǒng)提供了其他簡便的方法供開發(fā)者使用,這就是SleepEx WaitForMultipleObjectsEx 和WaitForSingleObjectEx。 當線程調(diào)用ReadFileEx或WriteFileEx時,提供了一個告警函數(shù),這個告警函數(shù)會被放入一個隊列,當操作完成時操作系統(tǒng)會從隊列中取出這些函數(shù)進行調(diào)用。所以對于屬于本進程內(nèi)部所產(chǎn)生的需要等待被調(diào)用的告警函數(shù)來說可以直接使用SleepEx對當前線程進行休眠并等待當前提交所有的告警函數(shù)被執(zhí)行。如果希望等待指定文件操作上的告警函數(shù)被調(diào)用你可以使用WaitForMultipleObjectsEx或WaitForSingleObjectEx。這三個函數(shù)的原型為: DWORD SleepEx( DWORD dwMilliseconds, // time-out interval BOOL bAlertable // early completion option ); DWORD WaitForSingleObjectEx( HANDLE hHandle, // handle to object DWORD dwMilliseconds, // time-out interval BOOL bAlertable // alertable option ); DWORD WaitForMultipleObjectsEx( DWORD nCount, // number of handles in array CONST HANDLE *lpHandles, // object-handle array BOOL fWaitAll, // wait option DWORD dwMilliseconds, // time-out interval BOOL bAlertable // alertable option );這三個函數(shù)和Sleep WaitForSingleObject WaitForMultipleObjects的差別就在于多了最后一個參數(shù)bAlertable,這個參數(shù)需要設(shè)置為TRUE表明等待文件異步操作的完成。通過檢測函數(shù)返回值可以得知文件操作是否完成,例如下面的代碼: ReadFileEx(hFile,pbRead,1024*1024*50,&overlap,MyIORoutine);
while(WAIT_IO_COMPLETION != SleepEx(1,TRUE) )//檢測文件操作是否完成
//while (WaitForSingleObjectEx(hFile,1,TRUE) != WAIT_OBJECT_0 )
//在這里WaitForSingleObjectEx和SleepEx具有相同作用
{
DoSomething();
}
對于SleepEx來說如果返回WAIT_IO_COMPLETION則表示異步操作完成,而對于文件對象來說如果異步操作完成文件對象就會變?yōu)橛行盘枲顟B(tài)。下面的例子是一個利用告警回調(diào)函數(shù)實現(xiàn)的文件異步讀寫。 VOID CALLBACK MyIORoutine(
DWORD dwErrorCode, // completion code
DWORD dwNumberOfBytesTransfered, // number of bytes transferred
LPOVERLAPPED lpOverlapped // I/O information buffer
)
{//定義一個簡單的回調(diào)函數(shù)
printf("文件讀完成\n");
}
void DoSomething(void)
{
printf("current time %d\n",GetTickCount());
Sleep(2000);//假設(shè)耗時的操作需要兩秒鐘
}
//下面是使用異步讀的示范代碼,假設(shè)c:\temp\large_file.dat文件有130MB大?。ǎ?
//一次性讀入50MB字節(jié),在讀入的過程中進行一些其他操作
void ReadM(void)
{
HANDLE hFile = CreateFile("c:\\temp\\large_file.dat",GENERIC_READ,0,
NULL,OPEN_EXISTING,FILE_FLAG_OVERLAPPED|FILE_ATTRIBUTE_NORMAL,NULL);
if( INVALID_HANDLE_VALUE != hFile )
{
BYTE *pbRead = new BYTE[1024*1024*50];//50MB字節(jié)
OVERLAPPED overlap;
overlap.Offset = 0;
overlap.OffsetHigh =0;
overlap.hEvent = NULL; //使用告警函數(shù)時無需要使用事件
DWORD dwBegin= GetTickCount();//記錄開始時間
printf("begin time %d\n",dwBegin);
ReadFileEx(hFile,pbRead,1024*1024*50,&overlap,MyIORoutine);
while(WAIT_IO_COMPLETION != SleepEx(1,TRUE) )//檢測文件操作是否完成
//while (WaitForSingleObjectEx(hFile,1,TRUE) != WAIT_OBJECT_0 )
//在這里WaitForSingleObjectEx和SleepEx具有相同作用
{
DoSomething();//在文件讀的執(zhí)行過程中進行其他操作
}
printf("耗時 %d\n",GetTickCount()-dwBegin);
//操作完成
CloseHandle(hFile);
delete pbRead;
}
}
WriteFileEx的用法與ReadFileEx的用法是類似的。 在磁盤操作中磁盤寫比讀需要花更多的時間,并且大文件的異步寫可以更加有效的提高CPU利用率。但是異步操作會給開發(fā)和調(diào)試帶來一些麻煩,所以我建議除非在非常必要(性能要求非常高,文件非常大)的情況下才使用異步的磁盤讀寫。再提一點,對于磁盤文件的異步操作的方式同樣可以用于上章所講的命名管道,或者是串口的異步操作。 文件加鎖時在打開文件后對文件的某個區(qū)域加鎖,加鎖后可以防止其他進程對該區(qū)域數(shù)據(jù)進行讀取。相關(guān)的函數(shù)為: BOOL LockFile( HANDLE hFile, // 文件句柄 DWORD dwFileOffsetLow, // 文件加鎖開始位置低32位 DWORD dwFileOffsetHigh, // 文件加鎖開始位置高32位 DWORD nNumberOfBytesToLockLow, // 區(qū)域長度低32位 DWORD nNumberOfBytesToLockHigh // 區(qū)域長度高32位 ); BOOL UnlockFile( HANDLE hFile, // 文件句柄 DWORD dwFileOffsetLow, // 文件解鎖開始位置低32位 DWORD dwFileOffsetHigh, // 文件解鎖開始位置高32位 DWORD nNumberOfBytesToLockLow, // 區(qū)域長度低32位 DWORD nNumberOfBytesToLockHigh // 區(qū)域長度高32位 );在文件加鎖和解鎖上需要有對應關(guān)系,這種對應關(guān)系就是對A區(qū)域加鎖后必須對A區(qū)域解鎖后才可以對其他區(qū)域解鎖,而且必須是一對一的關(guān)系,也就是說調(diào)用一次對A區(qū)域的加鎖函數(shù)就必須調(diào)用一次對A區(qū)域的解鎖函數(shù),而不能對一個區(qū)域加鎖后分次對該區(qū)域的不同部分解鎖。 在MFC中對文件操作進行了封裝,CFile中封裝了各種文件操作。在CFile中常用的成員函數(shù)有以下這些: CFile( LPCTSTR lpszFileName, UINT nOpenFlags ); //打開文件 virtual BOOL Open( LPCTSTR lpszFileName, UINT nOpenFlags, CFileException* pError = NULL ); //打開文件 uOpenFlags為打開文件時的參數(shù),可以取的以下值的組合: CFile::modeRead / CFile::modeReadWrite / CFile::modeWrite 讀寫模式 CFile::modeCreate 創(chuàng)建文件 CFile::shareDenyNone / CFile::shareDenyRead / CFile::shareDenyWrite 共享設(shè)置 CFile::typeText / CFile::typeBinary 以文本形式還時二進制形式打開文件 virtual void Close( ); //關(guān)閉文件 virtual UINT Read( void* lpBuf, UINT nCount ); //讀文件 virtual void Write( const void* lpBuf, UINT nCount ); // 寫文件 virtual LONG Seek( LONG lOff, UINT nFrom ); //設(shè)置文件指針 void SeekToBegin( );//移動文件指針到文件頭 DWORD SeekToEnd( );//移動文件指針到文件尾 virtual void LockRange( DWORD dwPos, DWORD dwCount ); //鎖定文件 virtual void UnlockRange( DWORD dwPos, DWORD dwCount ); //解鎖文件 CStdioFile是CFile的派生類,主要是完成對文本文件的操作,它只有兩個成員函數(shù): BOOL ReadString(CString& rString); //讀入文件中一行 void WriteString( LPCTSTR lpsz );//將字符串作為一行寫入文件 |
|
|
來自: 昵稱9954619 > 《WINDOWS編程》