http://blog.sina.com.cn/s/blog_56dee71a0100ex0l.html
翻译自:ms-help://MS.MSDNQTR.v80.chs/MS.MSDN.v80/MS.WIN32COM.v10.en/dnfiles/html/msdn_serial.htm
老外写的文章,虽比较全面,但很啰嗦,不如看各个函数的文档来得快。为方便以后查阅,列出本文涉及的主要函数如下:
CreateFile、ReadFile、WriteFile、GetOverlappedResult、WaitForSingleObject
SetCommMask、WaitCommEvent
ClearCommError、GetCommModemStatus、EscapeCommFunction
GetCommState、BuildCommDCB、SetCommState、SetCommTimeouts
此外,新浪的博客系统限制文章最大长度为40000字节,只好把这篇文章分成几部分了。
0 简介
本文仅关注在Windows NT和95间兼容的API。Windows 95支持Telephony API(TAPI),但Windows NT 3.x不支持TAPI,所以本文不讨论它。
本文的示例程序MTTTY(Multithreaded TTY)使用了三个线程:一个进行内存管理的界面线程;控制所有写入操作的写入者线程;读取数据和处理端口状态改变的读取/状态线程。示例采用了一些不同的堆来进行内存管理;还大量使用了同步方法来进行线程间通信。
1 打开端口
使用CreateFile函数打开端口。打开端口时有两种方法:重叠的和非重叠的。下列代码片段以重叠方式打开端口:
HANDLE hComm; hComm = CreateFile( gszPort, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, 0); if (hComm == INVALID_HANDLE_VALUE) // error opening port; abort |
去掉参数中的FILE_FLAG_OVERLAPPED就是非重叠操作方式了。用CreateFile打开通信端口时,有下列限制:
端口名通常是COM1、COM2、COM3和COM4。Win32 API不提供确定系统中有哪些端口可用的机制。Windows NT和Windows 95跟踪系统已安装端口的方法是不同的,所以不太可能提供兼容的确定可用端口的方法。某些系统可能有多于4个端口,而传统的通信端口最大个数是4。硬件厂商和串口驱动编写者可以自由地为端口命名。所以,程序最好可以让用户指定要使用的端口名字。如果端口不存在,则试图打开端口时会返回ERROR_FILE_NOT_FOUND错误,这时应该提示用户端口不可用。
2 读写操作
通信端口的读写操作与文件I/O操作非常相似,它们使用同样的函数。Win32的I/O操作可分为两种:重叠(overlapped)的和非重叠的(nonoverlapped)。平台SDK文档分别使用异步(asynchronous)和同步(synchronous)来表示这两种I/O方式。
很多开发者都熟悉非重叠I/O,因为它就是传统的I/O方式:函数返回时,所请求的操作已经完成。然而在重叠I/O的情况下,系统则可能在操作还没有完成的情形下立即返回,随后才通知调用者操作完成。程序可以在发起I/O请求和请求被完成之间进行一些后台工作。
2.1 非重叠I/O
非重叠I/O的工作方式很简单:I/O操作进行时,调用线程被阻塞;操作完成后,函数返回,调用线程可以继续执行。在多线程应用中,这种I/O方式很有用:一个线程阻塞在某I/O操作上时,其他线程可以继续工作。应用程序应该保证对端口的串行访问。某个线程阻塞在等待某I/O操作上时,其他线程后续的通信API调用也都将阻塞。比如说,一个线程在等待ReadFile调用返回时,另一个线程的WriteFile函数调用将阻塞。
在选择使用非重叠还是重叠方式时,可移植性是要考虑的因素之一。有时候重叠操作并不是好的选择,因为很多操作系统不支持它;然而很多操作系统都支持某种形式的多线程。所以从兼容性方面考虑,多线程非重叠I/O可能是最好的选择。
2.2 重叠I/O
重叠I/O不像非重叠I/O那样简单易懂,但却灵活高效。使用重叠方式打开的端口允许多个线程同时进行I/O操作,并且在操作进行期间可以进行其他的工作。此外,重叠操作的行为方式还允许单个线程提交多个不同的请求,然后在操作进行期间进行其他后台工作。
在单线程和多线程应用中,都必须在提交I/O请求和处理操作结果间进行一些同步操作。线程可能需要在操作结果可用前阻塞;当然也可以进行其他工作。如果没有其他需要进行的工作,则重叠I/O的优点是更好的用户响应性能。
MTTTY使用了重叠I/O。它创建用于读取数据和监测端口状态的线程,并且还定时进行一些后台工作;此外它还另外创建一个线程用于写入数据。
重叠I/O操作分为两个部分:创建I/O操作和检测操作完成。创建I/O操作涉及到建立OVERLAPPED结构体、创建用于同步的手动复位事件、调用恰当的函数(ReadFile或者WriteFile)。I/O操作可能立即完成,也可能不能立即完成,不能认为一个重叠I/O操作请求总是生成一个重叠操作。如果操作立即完成,程序应该可以继续进行通常的处理。检测操作完成涉及到等待事件句柄、检查操作完成结果、处理数据。与重叠I/O相关的工作更多的原因是有更多的失败点。非重叠操作中,简单地通过函数返回值判断操作是否失败;而重叠操作中,则可能在创建操作请求时失败,或者操作阻塞期间失败,也可能是操作超时,或者是等待操作完成信号超时。
2.2.1 读操作
下面的代码片段展示了提交重叠的读操作请求的方法。注意,如果ReadFile返回TRUE,调用了一个函数处理数据。代码定义了fWaitingOnRead标志,它表示是否有重叠的读取操作存在,用于阻止在一个操作进行中时提交另一个读取操作请求。
DWORD dwRead; BOOL fWaitingOnRead = FALSE; OVERLAPPED osReader = {0}; // Create the overlapped event. Must be closed before exiting // to avoid a handle leak. osReader.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); if (osReader.hEvent == NULL) // Error creating overlapped event; abort. if (!fWaitingOnRead) { // Issue read operation. if (!ReadFile(hComm, lpBuf, READ_BUF_SIZE, &dwRead, &osReader)) { if (GetLastError() != ERROR_IO_PENDING) // read not delayed? // Error in communications; report it. else fWaitingOnRead = TRUE; } else { // read completed immediately HandleASuccessfulRead(lpBuf, dwRead); } } |
OVERLAPPED结构体的事件句柄被传递给WaitForSingleObject以等待事件授信,操作完成。注意,事件受信表示操作完成,而不是操作成功完成。应该用GetOverlappedResult来取得操作结果,它返回TRUE表示操作成功完成;FALSE表示有错误发生,用GetLastError可以取得具体的错误码。也可以用GetOverlappedResult来检测操作完成:GetOverlappedResult返回FALSE,GetLastError返回ERROR_IO_INCOMPLETE表示操作进行中。如果对bWait参数传入TRUE,则效果就是重叠操作变成了非重叠的,直到操作完成,函数才返回。下面的代码片段展示了一种检测重叠读取操作完成的方法。注意fWaitingOnRead标志的使用,它是检测代码的控制入口,只有在某操作进行中时,才应该调用检测代码。
#define READ_TIMEOUT 500 // milliseconds DWORD dwRes; if (fWaitingOnRead) { dwRes = WaitForSingleObject(osReader.hEvent, READ_TIMEOUT); switch(dwRes) { // Read completed. case WAIT_OBJECT_0: if (!GetOverlappedResult(hComm, &osReader, &dwRead, FALSE)) // Error in communications; report it. else // Read completed successfully. HandleASuccessfulRead(lpBuf, dwRead); // Reset flag so that another opertion can be issued. fWaitingOnRead = FALSE; break; case WAIT_TIMEOUT: // Operation isn't complete yet. fWaitingOnRead flag isn't // changed since I'll loop back around, and I don't want // to issue another read until the first one finishes. // // This is a good time to do some background work. break; default: // Error in the WaitForSingleObject; abort. // This indicates a problem with the OVERLAPPED structure's // event handle. break; } } |
2.2.2 写入操作
写入操作跟读取操作非常相似。下面的代码片段展示了如何提交写入操作,并等待操作完成。
BOOL WriteABuffer(char * lpBuf, DWORD dwToWrite) { OVERLAPPED osWrite = {0}; DWORD dwWritten; DWORD dwRes; BOOL fRes; // Create this write operation's OVERLAPPED structure's hEvent. osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); if (osWrite.hEvent == NULL) // error creating overlapped event handle return FALSE; // Issue write. if (!WriteFile(hComm, lpBuf, dwToWrite, &dwWritten, &osWrite)) { if (GetLastError() != ERROR_IO_PENDING) { // WriteFile failed, but isn't delayed. Report error and abort. fRes = FALSE; } else // Write is pending. dwRes = WaitForSingleObject(osWrite.hEvent, INFINITE); switch(dwRes) { // OVERLAPPED structure's event has been signaled. case WAIT_OBJECT_0: if (!GetOverlappedResult(hComm, &osWrite, &dwWritten, FALSE)) fRes = FALSE; else // Write operation completed successfully. fRes = TRUE; break; default: // An error has occurred in WaitForSingleObject. // This usually indicates a problem with the // OVERLAPPED structure's event handle. fRes = FALSE; break; } } } else // WriteFile completed immediately. fRes = TRUE; CloseHandle(osWrite.hEvent); return fRes; } |
注意,上面的代码使用WaitForSingleObject时,超时值是INFINITE,这使得函数无限等待直到操作完成。这可能让调用线程似乎是被挂起了;而实际上只是写入操作需要较长的时间,或者流控制阻塞了传输操作。下文将讨论的状态检查可以检测到这种情况,但它也不会让WaitForSingleObject返回。有三种方法可以克服此问题:
上面代码中的WaitForSingleObject使用了INFINITE作为超时值,其效果等同于使用TRUE作为GetOverlappedResult的fWait参数。下面是等效的更简洁的代码:
BOOL WriteABuffer(char * lpBuf, DWORD dwToWrite) { OVERLAPPED osWrite = {0}; DWORD dwWritten; BOOL fRes; // Create this writes OVERLAPPED structure hEvent. osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); if (osWrite.hEvent == NULL) // Error creating overlapped event handle. return FALSE; // Issue write. if (!WriteFile(hComm, lpBuf, dwToWrite, &dwWritten, &osWrite)) { if (GetLastError() != ERROR_IO_PENDING) { // WriteFile failed, but it isn't delayed. Report error and abort. fRes = FALSE; } else { // Write is pending. if (!GetOverlappedResult(hComm, &osWrite, &dwWritten, TRUE)) fRes = FALSE; else // Write operation completed successfully. fRes = TRUE; } } else // WriteFile completed immediately. fRes = TRUE; CloseHandle(osWrite.hEvent); return fRes; } |
GetOverlappedResult并不总是等待重叠操作完成的最好方法。比如说,如果应用需要同时等待另一个事件句柄,则第一个代码片段模型比第二个更好,因为可以很容易地用WaitForMultipleObjects替换WaitForSingleObject,来等待更多的句柄。MTTTY就是这么做的。
在前一个重叠操作完成前重用OVERLAPPED结构是重叠I/O编程中常现的一个错误。如果要在前一个重叠操作完成前提交新的重叠操作请求,则需要分配新的OVERLAPPED结构,其hEvent字段也应该包含新的手动复位事件句柄。只有在重叠操作完成后,OVERLAPPED结构和其事件句柄才可以被重用。
串口通信中使用OVERLAPPED结构时只需要修改hEvent字段,其他字段只需要初始化为零。