Windows 编程/输入输出
前面的许多章节都试图阐明 Windows 图形界面,但本章将开始转向 Windows 操作系统基础的内部工作原理。在本章中,我们将讨论输入和输出例程。这包括(但不限于)文件 I/O、控制台 I/O,甚至设备 I/O。
文件,就像 Windows 平台上的其他所有内容一样,由句柄管理。当您想要读取或写入文件时,您必须首先打开该文件的句柄。句柄打开后,您可以在读/写操作中使用该句柄。事实上,所有 I/O(包括控制台 I/O 和设备 I/O)都是如此:您必须打开用于读/写的句柄,并且必须使用该句柄执行您的操作。
我们将从本章中经常会看到的一个函数开始:CreateFile。CreateFile 是用于在您的系统中打开 I/O 句柄的通用函数。即使名称没有表明,CreateFile 也用于打开控制台句柄和设备句柄。正如 MSDN 文档所说
The CreateFile function creates or opens a file, file stream, directory, physical disk, volume, console buffer, tape drive, communications resource, mailslot, or named pipe. The function returns a handle that can be used to access an object.
现在,这是一个功能强大的函数,并且随着强大而来在使用该函数方面的一定困难。不用说,CreateFile 比标准 C STDLIB fopen 稍微复杂一些。
HANDLE CreateFile( LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile);
正如可以猜测的那样,“lpFileName”参数是要打开的文件的名称。“dwDesiredAccess”指定文件句柄的所需访问权限。在最基本的情况下,对于文件,此参数可以指定读操作、写操作或执行操作。但是,不要被愚弄,这里可以使用许多不同的选项,用于不同的应用程序。最常见的操作是 GENERIC_READ、GENERIC_WRITE 和 GENERIC_EXECUTE。如果需要,这些可以按位或运算来获得读+写访问权限。
文件句柄可以选择共享或锁定。其他进程可以同时打开和访问共享文件。如果文件未共享,则尝试访问该文件的其他程序将失败。“dwShareMode”指定其他应用程序是否可以访问该文件。将 dwShareMode 设置为零表示文件访问不可共享,并且其他应用程序在文件句柄打开时尝试访问该文件将失败。其他常见值是 FILE_SHARE_READ 和 FILE_SHARE_WRITE,它们分别允许其他程序打开读句柄和写句柄。
lpSecurityAttributes 是指向 SECURITY_ATTRIBUTES 结构的指针。此结构可以帮助保护文件免受不必要的访问。我们将在后面的章节中讨论安全属性。现在,您始终可以将此字段设置为 NULL。
dwCreationDisposition 成员最好命名为“dwCreateMode”或类似名称。此位标志允许您根据不同的标志值确定文件将如何打开
- CREATE_ALWAYS
- 始终创建一个新文件。如果文件已经存在,它将被删除并覆盖。如果文件不存在,则创建它。
- CREATE_NEW
- 如果文件存在,则函数失败。否则,创建一个新文件。
- OPEN_ALWAYS
- 打开文件,如果文件存在,则不擦除内容。如果文件不存在,则创建一个新文件。
- OPEN_EXISTING
- 打开文件,如果文件已经存在,则不擦除内容。如果文件不存在,则函数失败。
- TRUNCATE_EXISTING
- 打开文件,只有在文件存在时才打开。当文件打开时,所有内容都被删除,并且文件长度设置为 0 字节。如果文件不存在,则函数失败。使用 TRUNCATE_EXISTING 打开时,您必须指定 GENERIC_WRITE 标志作为访问模式,否则函数将失败。
dwFlagsAndAttributes 成员指定了一系列用于控制文件 I/O 的标志。如果 CreateFile 函数用于创建非文件句柄的内容,则不使用此参数,可以将其设置为 0。对于访问普通文件,应使用标志 FILE_ATTRIBUTE_NORMAL。但是,还有 FILE_ATTRIBUTE_HIDDEN、FILE_ATTRIBUTE_READONLY、FILE_ATTRIBUTE_ARCHIVE 等选项。
最后,如果希望新文件句柄模仿现有文件句柄的属性,则可以指定 hTemplateFile 成员。如果不使用,可以将其设置为 NULL。
一旦文件句柄打开,理想情况下我们希望与指定的文件交互。我们可以最直接地使用 ReadFile 和 WriteFile 函数来做到这一点。它们都采用类似的参数
BOOL ReadFile( HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped);
BOOL WriteFile( HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped);
在这两者中,hFile 参数是我们使用 CreateFile 获取的文件句柄。lpOverlapped 参数仅用于一种称为“重叠 I/O 模式”的特殊 I/O 模式,我们将在后面讨论。对于简单 I/O,lpOverlapped 参数可以设置为 NULL。
在 ReadFile 中,lpBuffer 是指向用于接收数据的通用缓冲区的指针。此数据可能不是字符数据,因此我们不将其称为 LPSTR 类型。“nNumberofBytesToRead”是要读取的字节数,“lpNumberOfBytesRead”是实际读取的字节数。如果 lpNumberOfBytesRead 为零,则文件中不再有数据。
在 WriteFile 中,lpBuffer 参数指向要写入文件的数据。同样,它并不一定是字符数据。nNumberOfBytesToWrite 是要写入的最大字节数,lpNumberOfBytesWritten 返回实际写入文件的字节数。
完成使用文件句柄后,您应该使用 CloseHandle 函数关闭它。CloseHandle 只接受一个参数,即您要关闭的文件句柄。如果您没有关闭句柄,Windows 将在程序关闭时自动关闭句柄。但是,对于 Windows 来说,为您完成它是一个更昂贵的操作,并且会浪费您系统上的时间。在退出程序之前始终显式关闭所有句柄是一个好主意。
无法关闭句柄称为“句柄泄漏”,这是常见的内存泄漏形式,会导致您的程序以及整个系统丢失资源并运行速度变慢。句柄本身只占用 32 位信息,但内核在内部为每个句柄维护大量数据和存储空间。无法关闭句柄意味着内核必须维护有关句柄的所有关联信息。在查找有关当前句柄的信息时,它还会花费内核更多时间和资源来检查所有旧的未使用的句柄。
内存映射文件提供了一种使用常规指针和数组结构读取和写入文件的机制。 您可以使用内存指针读取文件,而不是使用 ReadFile 读取文件。 系统通过将文件读入内存页面,然后将更改写入该页面到物理磁盘来实现这一点。 在开始时将文件读入内存以及在映射完成后将文件写回,存在一定量的额外开销。 但是,如果对文件有很多访问,从长远来看它会方便得多。
重叠 I/O
[edit | edit source]“重叠”I/O 是 Microsoft 用于描述异步 I/O 的术语。 当您要执行 I/O 操作(无论是到文件还是到外部设备)时,您有两个选择
- 同步(非重叠)
- 您向系统请求 I/O,并等待 I/O 完成。 程序将停止运行,直到 I/O 完成。
- 异步(重叠)
- 您向系统发送请求,系统将与您的程序并行完成该请求。 您的程序可以继续执行处理工作,系统将在您的请求完成时自动发送通知。
同步 I/O 使用起来要容易得多,而且更直接。 在同步 I/O 中,事情是按顺序发生的,当 I/O 函数返回时,您就知道事务已完成。 但是,I/O 通常比您程序中的任何其他操作都要慢,等待缓慢的文件读取或缓慢的通信端口会浪费大量宝贵的时间。 此外,如果您的程序正在等待缓慢的 I/O 请求,图形界面将看起来挂起且没有响应,这会让用户感到厌烦。
程序员可以使用专用线程或线程池来执行同步 I/O 操作来避免这些延迟。 但是线程开销很大,创建太多线程会导致系统资源耗尽。 异步 I/O 避免了这种开销,因此是高性能高负载服务器应用程序的首选 API。
异步 I/O 使用起来更复杂:它需要使用 OVERLAPPED 结构,以及创建一个当 I/O 完成时将由系统自动调用的处理程序函数。 但是,这种方法的效率优势是显而易见的。 您的程序可以请求多个事务,而不必等待任何事务完成,它还可以在系统执行所需任务时执行其他任务。 这意味着程序将对用户看起来更具响应性,并且您可以将更多时间花在数据处理上,而将更少的时间花在等待数据上。
控制台 API
[edit | edit source]分配控制台
[edit | edit source]可以通过调用 AllocConsole 函数分配控制台。 通常,如果我们正在创建一个“控制台进程”(它包含 main 函数),我们不需要这样做,因为它们已经附加到控制台。 但是,我们可以为“GUI 进程”(其入口点为 WinMain)创建一个控制台,并在新创建的控制台上执行 I/O 操作。 应该注意的是,每个进程只能与一个控制台关联。 如果进程已经附加到控制台,调用 AllocConsole 将返回 FALSE。
调用 AllocConsole 后,将出现 Windows 命令提示符窗口。
可以通过调用 FreeConsole 释放控制台。
获取控制台句柄
[edit | edit source]创建控制台后,将初始化标准输出、标准输入和标准错误句柄(我们称之为“标准设备”。 这些句柄对于任何控制台 I/O 操作都是必不可少的。 可以通过调用 GetStdHandle 获取它们,它接受一个指定要获取的标准设备句柄的参数。 参数可以是以下任何一个
- STD_OUTPUT_HANDLE
- 指定标准输出设备,用于将数据输出到控制台。
- STD_INPUT_HANDLE
- 指定标准输入设备,用于从控制台读取输入。
- STD_ERROR_HANDLE
- 指定标准错误设备,主要用于输出错误。
如果函数成功,返回值是指定标准设备的句柄。 如果失败,它将返回 INVALID_HANDLE_VALUE。
高级 I/O
[edit | edit source]<stdio.h> 或 <iostream>(仅限 C++)头文件包含通常用于高级控制台 I/O 的函数。 高级 I/O 通常是“缓冲”的。 这些函数包括 printf、scanf、fgets 等。 如果我们希望进行非缓冲 I/O,我们可以使用 fread 或 fwrite 函数,并将 stdin、stdout 或 stderr 分别传递给指定标准输入、标准输出和标准错误设备的参数。 但是,通常不建议将高级 I/O 和低级 I/O 混合使用。
这些函数旨在可移植,并充当低级系统 I/O 函数的抽象。
低级 I/O
[edit | edit source]可以通过使用一些 API 函数(如 WriteConsole、ReadConsole、ReadConsoleInput 等)来完成低级控制台 I/O。
BOOL WriteConsole( HANDLE hConsoleOutput, const VOID *lpBuffer, DWORD dwNumberOfCharsToWrite, LPDWORD lpNumberOfCharsWritten, LPVOID lpReserved );
BOOL ReadConsole( HANDLE hConsoleInput, LPVOID lpBuffer, DWORD dwNumberOfCharsToRead, LPDWORD lpNumberOfCharsRead, LPVOID pInputControl );
请注意,所指的“字符”实际上是 TCHAR 的数量,当定义了 UNICODE 时,它可能是 2 个字节宽。 它不是字节数。
ReadConsoleInput 可用于读取击键,这无法通过 C 或 C++ 标准库完成。 还有更多函数提供强大的 I/O 函数。
颜色和功能
[edit | edit source]有很多令人兴奋的 API 函数提供了对控制台的额外控制。 其中一个最常用的函数是 SetConsoleTitle,它用于设置控制台标题文本。 我们还可以使用 SetConsoleCursorPosition 函数来更改光标的位置。
我们可以通过 SetConsoleTextAttribute 以不同的前景色和背景色输出文本。 我们还可以使用 SetConsoleScreenBufferSize 更改屏幕缓冲区的大小。
有关所有控制台 API 的详细文档,您可以参考 MSDN。
设备 IO API
[edit | edit source]程序和设备驱动程序之间的交互可能很复杂。 但是,有一些标准设备驱动程序可用于访问标准端口和硬件。 在大多数情况下,与端口或硬件交互与打开该设备的句柄然后像文件一样读取或写入一样简单。 在大多数情况下,可以使用 CreateFile 函数打开这些端口和设备,方法是调用设备的名称而不是文件的名称。
获取设备句柄
[edit | edit source]设备 IO 函数
[edit | edit source]设备 IO 函数。
本页是Windows 编程书籍中的一个页面存根。您可以通过扩展它来提供帮助。