Windows编程系列:设备I/O

Windows设备

在Windows平台下,设备被定义为能够与之进行通信的任何东西。最常见的 I/O 设备包括:文件、文件流、目录、物理磁盘、卷、控制台缓冲区、磁带驱动器、通信资源、mailslot 和管道等。

平常我们使用的文件,目录都可以称之为设备。

 

本文是介绍设备的通用操作,以文件操作进行演示。

 

打开和关闭设备

在Windows中,常见的设备及用途如下:

 在前面的文章中,介绍了mailslot的使用,它就是属于设备的一种,并且它的读取和写入跟文件读取写入用的是一样的函数。

很多设备的数据读取和数据写入都是以相同的方式来实现,具体的方式我们可以看到下表:

 不同的设备有不同的设置函数,以串口为例,设置串口参数使用的是setCommConfig函数,这里不作具体介绍,实际使用时,可以查阅MSDN文档。

 

CreateFile函数

在上表中我们可以看到,设备的打开大部分使用的是CreateFile函数,打开文件也是这个函数,而文件是我们平常使用得比较多的,所以这里重点介绍CreateFile函数。

 

函数声明如下

 1 WINBASEAPI
 2 HANDLE
 3 WINAPI
 4 CreateFileW(
 5     _In_ LPCWSTR lpFileName,
 6     _In_ DWORD dwDesiredAccess,
 7     _In_ DWORD dwShareMode,
 8     _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
 9     _In_ DWORD dwCreationDisposition,
10     _In_ DWORD dwFlagsAndAttributes,
11     _In_opt_ HANDLE hTemplateFile
12     );

 

参数说明:

lpFileName:

指定用于创建或打开的对象的名称。

 

dwDesiredAccess:

请求对文件或设备的访问权限,通常可以取以下值,或GENERIC_READ|GENERIC_WRITE的组合值。

 

dwShareMode:

指定共享方式。如果将此参数设置为0,那么对象不能被共享,后续对该对象进行打开操作将会失败,直到关闭句柄为止。为达到对象共享效果,可以使用

下面的一个或多个值来指定共享方式

含义
0
0x00000000
阻止其他进程在请求删除、读取或写入访问权限时打开文件或设备。
FILE_SHARE_DELETE
0x00000004
启用文件或设备上的后续打开操作以请求删除访问权限。

如果未指定此标志,但已打开文件或设备进行删除访问,则函数将失败。

注意 删除访问权限允许删除和重命名操作。
 
FILE_SHARE_READ
0x00000001
启用文件或设备上的后续打开操作以请求读取访问权限。

否则,如果其他进程请求读取访问权限,则无法打开文件或设备。

如果未指定此标志,但已打开文件或设备进行读取访问,则函数将失败。

FILE_SHARE_WRITE
0x00000002
启用文件或设备上的后续打开操作以请求写入访问权限。

否则,如果其他进程请求写入访问权限,则无法打开文件或设备。

如果未指定此标志,但文件或设备已打开进行写入访问,或者具有具有写入访问权限的文件映射,则函数将失败。

 

lpSecurityAttributes:

指向 SECURITY_ATTRIBUTES 结构的指针,该结构包含两个独立但相关的数据成员:可选的安全描述符,以及一个布尔值,用于确定是否可由子进程继承返回的句柄。

此参数可以为 NULL。

如果此参数为 NULL,则应用程序可能创建的任何子进程都不能继承 CreateFile 返回的句柄,并且与返回的句柄关联的文件或设备将获取默认安全描述符。

结构的 lpSecurityDescriptor 成员指定文件或设备的 SECURITY_DESCRIPTOR 。 如果此成员为 NULL,则会为与返回的句柄关联的文件或设备分配默认安全描述符。

CreateFile 在打开现有文件或设备时忽略 lpSecurityDescriptor 成员,但仍继续使用 bInheritHandle 成员。

结构的 bInheritHandle 成员指定是否可以继承返回的句柄。

 

dwCreationDisposition:

要对存在或不存在的文件或设备执行的操作。

对于文件以外的设备,此参数通常设置为 OPEN_EXISTING。

此参数必须是下列值之一,这些值不能组合使用:

含义
CREATE_ALWAYS
2
始终创建新文件。

如果指定的文件不存在并且是有效路径,则创建新文件,函数成功,最后一个错误代码设置为零。

有关详细信息,请参阅本主题的“备注”部分。

CREATE_NEW
1
仅当文件尚不存在时,才创建新文件。

如果指定的文件存在,则函数将失败,最后一个错误代码设置为 ERROR_FILE_EXISTS (80) 。

如果指定的文件不存在,并且是可写位置的有效路径,则会创建一个新文件。

OPEN_ALWAYS
4
始终打开文件。

如果指定的文件存在,则函数成功,并将最后一个错误代码设置为 ERROR_ALREADY_EXISTS (183) 。

如果指定的文件不存在,并且是可写位置的有效路径,则函数将创建一个文件,并将最后一个错误代码设置为零。

OPEN_EXISTING
3
仅当文件或设备存在时才打开它。

如果指定的文件或设备不存在,则函数将失败,并将最后一个错误代码设置为 ERROR_FILE_NOT_FOUND (2) 。

有关设备的详细信息,请参阅“备注”部分。

TRUNCATE_EXISTING
5
打开一个文件并截断它,使其大小为零字节,仅当它存在时。

如果指定的文件不存在,则函数将失败,最后一个错误代码设置为 ERROR_FILE_NOT_FOUND (2) 。

调用进程必须打开文件,并将 GENERIC_WRITE 位设置为 dwDesiredAccess 参数的一部分。

 

dwFlagsAndAttributes:

文件或设备属性和标志 

dwFlagsAndAttributes参数有两个用途:

一,它允许我们设置一些标志来微调与设备之间的通信;

二,如果设备是一个文件,我们还能够设置文件的属性。这些通信标志中的大多数都是一些信号,用来告诉系统我们打算以何种方式来访问设备。这样系统就可以对缓存算法进行优化,来帮助我们提高应用程序的效率。

 

FILE_ATTRIBUTE_NORMAL 是文件最常见的默认值。

以下某些文件属性和标志可能仅适用于文件,不一定适用于 CreateFile 可以打开的所有其他类型的设备。

Attribute含义
FILE_ATTRIBUTE_ARCHIVE
32 (0x20)
文件应存档。 应用程序使用此属性来标记要备份或删除的文件。
FILE_ATTRIBUTE_ENCRYPTED
16384 (0x4000)
此文件或目录已加密。 对于文件来说,表示文件中的所有数据都是加密的。 对于目录,这意味着加密是新创建的文件和子目录的默认设置。 有关详细信息,请参阅 文件加密

家庭版、家庭高级版、初学者版或 ARM 版本的 Windows 不支持此标志。

FILE_ATTRIBUTE_HIDDEN
2 (0x2)
文件被隐藏。 不要将其包含在普通目录列表中。
FILE_ATTRIBUTE_NORMAL
128 (0x80)
该文件未设置其他属性。 此属性仅在单独使用时有效。
FILE_ATTRIBUTE_OFFLINE
4096 (0x1000)
文件的数据不会立即可用。 此属性指示文件数据以物理方式移动到脱机存储。 远程存储(分层存储管理软件)使用此属性。 应用程序不应随意更改此属性。
FILE_ATTRIBUTE_READONLY
1 (0x1)
文件为只读文件。 应用程序可以读取文件,但不能写入或删除它。
FILE_ATTRIBUTE_SYSTEM
4 (0x4)
该文件是操作系统的一部分或由操作系统独占使用。
FILE_ATTRIBUTE_TEMPORARY
256 (0x100)
该文件用于临时存储。

有关详细信息,请参阅本主题的 缓存行为 部分。

 

hTemplateFile

这个参数既可以标识一个已经打开的文件的句柄,也可以是 NULL。如果hFileTemplate标识一个文件句柄,那么CreateFile会完全忽略dwFlagsAndAttributes参数,并转而使用hFileTemplate所标识的文件的属性。

为了能够让函数以这种方式工作,hFileTemplate标识的文件必须是一个已经用GENERIC_READ标志打开的文件。如果CreateFile要打开已有的文件(而不是创建新文件),那么它会忽略hFileTemplate参数。

 

返回值:

如果CreateFile成功地创建或打开了文件或设备,那么它会返回文件或设备句柄。

如果CreateFile 失败了,那么它会返回INVALID_HANDLE_VALUE。

注意:这里不能通过NULL来判断

 

设置文件指针的位置

使用CreateFile打开文件时,文件指针的读取位置会被默认设置为0,如果调用ReadFile函数从文件读取了10个字节到内存中,那么系统会更新文件指针。这样下一次调用ReadFile时,会从偏移量为10的地方开始读取文件。

1 BYTE buff[10];
2 DWORD dwReadBytes;
3 HANDLE hFile = CreateFile(L"tt.tx",....); //文件指针设置为0
4 ReadFile(hFile,buff,10,&dwReadBytes,NULL); //读取0-9字节
5 ReadFile(hFile,buff,10,&dwReadBytes,NULL); //读取10-19字节

 

每个文件内核对象都有自己的文件指针,所以如果两次打开同一个文件,都会从0开始读取。

如果使用DuplicateHandle函数复制句柄,则会使用同一个内核对象的文件指针。

1 BYTE buffer[10];
2 DWORD dwReadBytes ;
3 HANDLE hFile1 = CreateFile(L"tt.txt",...); //文件指针设置为0 ;
4 HANDLE hFile2;
5 DuplicateHandle(GetcurrentProcess ( ) , hFile1,GetCurrentProcess()  , &hFile2,FALSE,DUPLICATE_SAME_ACCESS);
6 ReadFile (hFile1, buffer,10,&dwReadBytes, NULL); //读取0-9字节
7 ReadFile (hFile2, buffer,10,&dwReadBytes,NULL);  //读取10-19字节

 

可以通过SetFilePointerEx函数来设置文件指针的读取位置,SetFilePointerEx函数声明如下:

1 WINBASEAPI
2 BOOL
3 WINAPI
4 SetFilePointerEx(
5     _In_ HANDLE hFile,
6     _In_ LARGE_INTEGER liDistanceToMove,
7     _Out_opt_ PLARGE_INTEGER lpNewFilePointer,
8     _In_ DWORD dwMoveMethod
9     );

参数说明:

hFile:文件句柄

liDistanceToMove:设置指针移动多少字节,正值向前移动,负值向后移动

lpNewFilePointer:指向LARGE_INTEGER结构中返回文件指针的新值,如果不需要使用,可以设置为NULL

dwMoveMethod:文件指针移动的起点。 此参数的取值可为下列值之一:

含义
FILE_BEGIN
0
起始点为零或文件的开头。 如果指定了此标志,则 liDistanceToMove 参数将解释为无符号值。
FILE_CURRENT
1
起点是文件指针的当前值。
FILE_END
2
起点是当前文件结束位置。

 

需要注意的地方:

1、将文件指针的值设为超过文件当前的大小是允许的 ,不会引发异常。

2、如果SetFilePointerEx 操作的文件是用FILE_FLAG_NO_BUFFERING标志打开的,那么文件指针只能被设置为扇区大小的整数倍。

3、Windows没有提供一个GetFilePointerEx函数,可以调用SetFilePointerEx将文件指针移动0个字节,通过这种方式来达到相同的效果。

设置文件尾

在关闭文件的时候,系统会负责设置文件尾。但是,有时我们可能想要强制使文件变得更小或变得更大。在这些情况下,可以使用SetEndOfFile函数来强制使用文件变大或变小。

SetEndOfFile函数声明如下:

1 WINBASEAPI
2 BOOL
3 WINAPI
4 SetEndOfFile(
5     _In_ HANDLE hFile
6     );

参数说明:

hFile:文件句柄

SetEndOfFile函数会根据文件对象的文件指针当前所在的位置来截断文件的大小或增大文件的大小。

例如,如果想将文件的大小强制设为1024,可以像下面这样使用SetEndOfFile:

1 HANDLE hFile = CreateFile(L"1024.txt", GENERIC_WRITE|GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
2 LARGE_INTEGER liDistanceToMove;
3 liDistanceToMove.QuadPart = 1024;
4 SetFilePointerEx(hFile, liDistanceToMove, NULL, FILE_BEGIN);
5 SetEndOfFile(hFile);
6 CloseHandle(hFile);

 

同步设备I/O

在前面的文章中有使用过ReadFile函数来读取mailslot消息。

设备既可以是文件,也可以是邮件槽(mailslot)、管道、套接字等等。不管使用的是哪种设备,都是使用ReadFile(读取)和WriteFIle(写入)函数来进行I/O操作。

ReadFile函数声明如下:

1 BOOL
2 WINAPI
3 ReadFile(
4     _In_ HANDLE hFile,
5     _Out_writes_bytes_to_opt_(nNumberOfBytesToRead, *lpNumberOfBytesRead) __out_data_source(FILE) LPVOID lpBuffer,
6     _In_ DWORD nNumberOfBytesToRead,
7     _Out_opt_ LPDWORD lpNumberOfBytesRead,
8     _Inout_opt_ LPOVERLAPPED lpOverlapped
9     );

参数说明:

hFile:标识要访问的设备的句柄

pvBuffer:指向一个缓存,函数会把设备数据读取到该缓存中

nNumberOfBytesToRead:指定从设备中读取多少个字节

lpNumberOfBytesRead:接收 实际 读取的字节数

lpOverlapped:在执行同步I/O时,这个参数应该设置为NULL,如果要执行异步I/O,在打开文件时,要指定FILE_FLAG_OVERLAPPED标志。

注意:ReadFile只能用于指定了GENERIC_READ标志打开的设备

 

使用示例如下:

 1 TCHAR szFile[MAX_PATH]{};
 2 TCHAR szText[128]{};
 3 DWORD dwNumberOfBytesToRead = 0;
 4 
 5 //从对话框获取文件路径
 6 ::GetDlgItemText(GetSafeHwnd(), IDC_EDIT1, szFile, MAX_PATH);
 7 
 8 //打开文件
 9 auto hFile = CreateFile(szFile, GENERIC_READ, FILE_SHARE_READ, NULL,OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
10 
11 if (hFile == INVALID_HANDLE_VALUE)
12 {
13     return;
14 }
15 
16 //读取文件 从0-127个字节
17 if (TRUE == ReadFile(hFile, szText, 128, &dwNumberOfBytesToRead, NULL))
18 {
19     AfxMessageBox(szText);
20 }
21 
22 CloseHandle(hFile);

 

WriteFile函数声明如下:

1 BOOL
2 WINAPI
3 WriteFile(
4     _In_ HANDLE hFile,
5     _In_reads_bytes_opt_(nNumberOfBytesToWrite) LPCVOID lpBuffer,
6     _In_ DWORD nNumberOfBytesToWrite,
7     _Out_opt_ LPDWORD lpNumberOfBytesWritten,
8     _Inout_opt_ LPOVERLAPPED lpOverlapped
9     );

参数说明:

WriteFile函数的参数和ReadFile基本一致,只是一个是读取,一个是写入。这里不详细介绍参数了。

 

使用示例如下:

 1 HANDLE hFile = CreateFile(L"1024.txt", GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
 2 
 3 if (INVALID_HANDLE_VALUE == hFile)
 4     return;
 5 
 6 LPCTSTR buffer = _tcsdup(L"HelloWorld");
 7 DWORD nSizeToWrite = lstrlen(buffer) * sizeof(TCHAR);
 8 DWORD dwSizeWritten = 0;
 9 if (WriteFile(hFile, buffer, nSizeToWrite, &dwSizeWritten, NULL))
10 {
11     AfxMessageBox(L"写入成功");
12 }
13 
14 free((void*)buffer);

 

将数据刷新至设备

在前面介绍 CreateFile 函数的时候,提到过可以传一些标志来改变系统对文件数据进行缓存的方式。其他一些设备,比如串口、邮件槽以及管道,也会对数据进行缓存。如果我们想要强制系统将缓存数据写入到设备,那么可以调用FlushFileBuffers函数,声明如下:

BOOL FlushFileBuffers(HANDLE hFile);

FlushFileBuffers函数会强制将与hFile参数所标识的设备相关联的所有缓存数据写入设备。设备必须是通过GENERIC_WRITE标志打开的,这样FlushFileBuffers才能够正常工作。如果调用成功,那么函数会返回TRUE。

 

示例代码

https://github.com/zhaotianff/WindowsProgramming/tree/master/IO

 

参考资料:

《Windows核心编程》第十章

posted @ 2024-04-24 22:52  zhaotianff  阅读(51)  评论(0编辑  收藏  举报