《Windows核心编程 5th》部分读书笔记----第10章 同步设备I/O与异步设备I/O
2013-03-23 15:12
239 查看
异步设备I/O基础
假设一个线程向设备发出一个异步I/O请求。这个请求被传给设备驱动程序,后者负责完成实际I/O的操作。当驱动程序在等待设备响应的时候,应用程序的线程并没有因为要等待I/O请求完成而被挂起,线程会继续运行并执行其他有用的任务。到了某一时刻,设备驱动程序完成了对队列中的I/O请求的处理,这时它必须通知应用程序数据已发送,数据已收到,或发生了错误,这些通知称之为“接收I/O请求完成通知”
接收I/O请求完成通知
用来接收I/O完成通知的方法
触发设备内核对象
当ReadFile和WriteFile函数在将I/O请求添加到队列之前,会先将设备内核对象设为未触发状态。当设备驱动程序完成了请求之后,驱动程序会将设备内核对象设为触发状态。因此我们可以利用设备内核对象进行线程同步。
触发事件内核对象
异步I/O中的OVERLAPPED结构中的最后一个成员hEvent用来标识一个事件内核对象。当一个异步I/O请求完成的时候,设备驱动程序会检查OVERLAPPED结构的hEvent成员是否为NULL。如果hEvent不为NULL,那么驱动程序会调用SetEvent来触发事件。与等待设备内核对象类似,我们可以等待OVERLAPPED结构中的hEvent事件对象。
可提醒I/O
当系统创建一个线程的时候,会同时创建一个与线程相关联的队列。这个队列被称为异步过程调用(asynchronous procedure call,APC)队列。当发出一个I/O请求的时候,我们可以告诉设备驱动程序在调用线程的APC队列中添加一项。为了将I/O完成通知添加到线程的APC队列中,我们应该调用ReadFileEx和WriteFileEx函数:
上面两个函数都包含一个最重要的参数,它要求我们传一个回调函数的地址,这个回调函数被称为完成函数(completion routine)。它的原型必须符合以下形式:
当线程处于可提醒状态的时候(调用Windows提供的6个函数,可以将线程置于可提醒状态),系统会检查它的APC队列,对队列中的每一项,系统会调用完成函数,并传入I/O错误码,已传输的字节数,以及OVERLAPPED结构的地址。在调用那6个函数时,需要注意的是,当调用这些函数中的任何一个时,只要线程的APC队列中至少有一项,线程就不会进入睡眠状态。在调用这些函数的时候,当且仅当线程APC队列中一项都没有的时候,这些函数才会将线程挂起。当线程被挂起的时候,如果我们正在等待的那个(或那些)内核对象被触发,或线程的APC队列中出现了一项,那么线程将会被唤醒。因为我们的线程处于可提醒状态,所以一旦APC队列中出现一项,系统会唤醒我们的进程并(通过调用回调函数来)清空队列。然后函数会立即返回---线程不会再次进行睡眠状态来等等内核对象被触发。
可提醒I/O的优劣
回调函数:可提醒I/O要求我们必须创建一个回调函数,这使得代码的实现变得更加复杂。由于这些回调函数一般来说并没有足够的与某个问题有关的上下文信息,因此我们最终不得不将大量的信息放在全局变量中。
线程问题:实际上可提醒I/O的大问题是:发现I/O请求的线程必须同时对完成通知进行处理。如果一个线程发出多个请求,那么即使其他线程完全处于空闲状态,该线程也必须对每个请求的完成通知做出响应。由于不存在负载均衡机制,因此应用程序的伸缩性不太好。
I/O完成端口
1、创建I/O完成端口
I/O完成端口背后的理论是并发运行的线程的数量必须有一个上限--也就是说,同时发出的500个客户请求不应该允许出现500个可运行的线程。一旦可运行线程的数量大于可用的CPU数量,系统就必须花时间来执行线程上下切换,而这会浪费宝贵的CPU周期--这也是并发模型的一个潜在缺点。
并发模型的另一个缺点是需要为每个客户请求创建一个新的线程,这样仍然会花费一定的开销。如果能在应用程序初始化的时候创建一个线程池,并让线程池中的线程在应用程序运行期间一直保持可用状态,那么服务应用程序的性能就能够得到提高。I/O完成端口的设计初衷就是与线程池配合使用。
2、I/O完成端口的周边架构
当服务应用程序初始化的时候,应用程序接着应该创建一个线程池来处理客户请求。就标准的经验法则而言,线程池中线程的数量应取主机的CPU的数量并将其乘以2。
3、I/O完成端口如何管理线程池(工作机理)
首先,当我们创建I/O完成的时候,需要指定允许多少个线程并发运行。正如前面已经提到过,我们通常会将这个值设为主机的CPU数量(和线程池里面有多少的线程不一样)。当已完成的I/O项被添加到队列中的时候,I/O完成端口想要唤醒正在等待的线程。但是,完成端口唤醒的线程数量最多不会超过我们指定的数量。因此,如果有4个I/O请求已完成,有4个线程正在等待GetQueuedCompletionStatus,那么I/O完成端口只会唤醒两个线程,而让其他两个线程继续睡眠。当每个线程处理完一个已完成的I/O项时,会再次调用GetQueuedCompletionSatus。这时系统发现队列中还有其他的项,于是会唤醒同一个线程来对剩余的项进行处理。
当完成端口唤醒一个线程的时候,会将该线程的线程标识符保存在与完成端口相关联的第4个数据结构中,也就是已释放线程列表(released thread list)。这使得完成端口能够记住哪些线程已经被唤醒,并监视它们的执行情况。如果 一个已释放的线程调用的任何函数将该线程切换到了等待状态,那么完成端口会检测到这一情况,此时它会更新内部的数据结构,将该线程的线程标识符从已释放线程列表中移除,并将其添加到已暂停线程列表(paused
thread list)中。
完成端口的目标是根据在创建完成端口时指定的并发线程的数量,将尽可能多的线程保持在已释放线程列表中。如果一个已释放线程由于任何原因而进入等待状态,那么已释放线程列表会缩减,完成端口就可以释放另一个正在等待的线程。如果一个已暂停的线程被唤醒,那么它会离开已暂停线程列表并重新进入已释放线程列表。这意味着此时已释放线程列表中的线程数量将大于最大允许的并发线程数量。
假设我们在一台有两个CPU的机器上运行。我们创建一个同时最多只允许两个线程被唤醒的完成端口,还创建了4个线程来等待已完成的I/O请求。如果3个已完成的I/O请求被添加到端口的队列中,只有两个线程会被唤醒来对请求进行处理,这降低了可运行的数量,并节省了上下文切换的时间。现在,如果一个可运行线程调用了Sleep、WaitForSingleObject、WaitForMultipleObjects、SignalObjectAndWait,一个异步I/O调用或任何能够导致线程不可运行状态的函数,I/O完成端口会检测到这一情况并立即唤醒第3个线程。完成端口的目标是使CPU保持在满负荷状态下工作。
简单总结一下:
IOCP的机制大概就是:当有一个线程调用GetQueuedeCompletionStatus去等待完成队列中一项时,系统会将该线程添加到IOCP的等待线程队列中。当I/O完成队列中出现一项,系统会先唤醒最近等待的线程(LIFO),并将该线程添加到已释放线程队列中。当I/O完成队列中继续出现一项时,如果没有线程池中还有线程在等待,且已释放的线程没有大于创建完成端口设定的数量,IOCP则会唤醒新的线程去处理新一项。否则等待已在运行的线程处理完前一项,然后再让处理完数据的线程来处理I/O完成队列中这新的一项(这样做可以减少不同线程进行上下文切换带来的花销)。当一个正在运行的线程调用其它函数,将自己挂起或处于等待状态,IOCP会发现这种情况,并将这些线程放到自己的“已暂停线程队列”中,也就是说,已释放的线程数减少了,当I/O完成队列中出现新的一项时,IOCP可以唤醒一个线程。与ICOP相关联的4个数据结构(黑体字),大概就是这样运作的。由此可以看出,IOCP既能让CPU处理满负荷状态工作,也可以尽可能减少线程间切换上下文带来的花销。
假设一个线程向设备发出一个异步I/O请求。这个请求被传给设备驱动程序,后者负责完成实际I/O的操作。当驱动程序在等待设备响应的时候,应用程序的线程并没有因为要等待I/O请求完成而被挂起,线程会继续运行并执行其他有用的任务。到了某一时刻,设备驱动程序完成了对队列中的I/O请求的处理,这时它必须通知应用程序数据已发送,数据已收到,或发生了错误,这些通知称之为“接收I/O请求完成通知”
接收I/O请求完成通知
用来接收I/O完成通知的方法
技术 | 摘要 |
触发设备内核对象 | 当向一个设备同时发出多个I/O请求的时候,这种方法没什么用。它允许一个线程发出I/O请求,另一个线程对结果进行处理。 |
触发事件内核对象 | 这种方法允许我们向一个设备同时发出多个I/O请求。它允许一个线程发出I/O请求,另一个线程对结果进行处理。 |
使用可提醒I/O | 这种方法允许我们向一个设备同时发出多个I/O请求。发出I/O请求的线程必须对结果进行处理 |
使用I/O完成端口 | 这种方法允许我们向一个设备同时发出多个I/O请求。它允许一个线程发出I/O请求,另一个线程对结果进行处理。这项技术具有高度的伸缩性和最佳的灵活性。 |
当ReadFile和WriteFile函数在将I/O请求添加到队列之前,会先将设备内核对象设为未触发状态。当设备驱动程序完成了请求之后,驱动程序会将设备内核对象设为触发状态。因此我们可以利用设备内核对象进行线程同步。
触发事件内核对象
异步I/O中的OVERLAPPED结构中的最后一个成员hEvent用来标识一个事件内核对象。当一个异步I/O请求完成的时候,设备驱动程序会检查OVERLAPPED结构的hEvent成员是否为NULL。如果hEvent不为NULL,那么驱动程序会调用SetEvent来触发事件。与等待设备内核对象类似,我们可以等待OVERLAPPED结构中的hEvent事件对象。
可提醒I/O
当系统创建一个线程的时候,会同时创建一个与线程相关联的队列。这个队列被称为异步过程调用(asynchronous procedure call,APC)队列。当发出一个I/O请求的时候,我们可以告诉设备驱动程序在调用线程的APC队列中添加一项。为了将I/O完成通知添加到线程的APC队列中,我们应该调用ReadFileEx和WriteFileEx函数:
BOOL ReadFileEx( HANDLE hFile, PVOID pvBuffer, DWORD nNumBytesToRead, OVERLAPPED* pOverlapped, LPOVERLAPPED_COMPLETION_ROUTINE pfnCompletionRoutine) ; BOOL WriteFileEx( HANDLE hFile, CONST VOID *pvBuffer, DWORD nNumBytesToWrite, OVERLAPPED* pOverlapped, LPOVERLAPPED_COMPLETION_ROUTINE pfnCompletionRoutine) ;
上面两个函数都包含一个最重要的参数,它要求我们传一个回调函数的地址,这个回调函数被称为完成函数(completion routine)。它的原型必须符合以下形式:
VOID WINAPI CompletionRoutine( DWORD dwError, DWORD dwNumBytes, OVERLAPPED * po ) ;
当线程处于可提醒状态的时候(调用Windows提供的6个函数,可以将线程置于可提醒状态),系统会检查它的APC队列,对队列中的每一项,系统会调用完成函数,并传入I/O错误码,已传输的字节数,以及OVERLAPPED结构的地址。在调用那6个函数时,需要注意的是,当调用这些函数中的任何一个时,只要线程的APC队列中至少有一项,线程就不会进入睡眠状态。在调用这些函数的时候,当且仅当线程APC队列中一项都没有的时候,这些函数才会将线程挂起。当线程被挂起的时候,如果我们正在等待的那个(或那些)内核对象被触发,或线程的APC队列中出现了一项,那么线程将会被唤醒。因为我们的线程处于可提醒状态,所以一旦APC队列中出现一项,系统会唤醒我们的进程并(通过调用回调函数来)清空队列。然后函数会立即返回---线程不会再次进行睡眠状态来等等内核对象被触发。
可提醒I/O的优劣
回调函数:可提醒I/O要求我们必须创建一个回调函数,这使得代码的实现变得更加复杂。由于这些回调函数一般来说并没有足够的与某个问题有关的上下文信息,因此我们最终不得不将大量的信息放在全局变量中。
线程问题:实际上可提醒I/O的大问题是:发现I/O请求的线程必须同时对完成通知进行处理。如果一个线程发出多个请求,那么即使其他线程完全处于空闲状态,该线程也必须对每个请求的完成通知做出响应。由于不存在负载均衡机制,因此应用程序的伸缩性不太好。
I/O完成端口
1、创建I/O完成端口
I/O完成端口背后的理论是并发运行的线程的数量必须有一个上限--也就是说,同时发出的500个客户请求不应该允许出现500个可运行的线程。一旦可运行线程的数量大于可用的CPU数量,系统就必须花时间来执行线程上下切换,而这会浪费宝贵的CPU周期--这也是并发模型的一个潜在缺点。
并发模型的另一个缺点是需要为每个客户请求创建一个新的线程,这样仍然会花费一定的开销。如果能在应用程序初始化的时候创建一个线程池,并让线程池中的线程在应用程序运行期间一直保持可用状态,那么服务应用程序的性能就能够得到提高。I/O完成端口的设计初衷就是与线程池配合使用。
2、I/O完成端口的周边架构
当服务应用程序初始化的时候,应用程序接着应该创建一个线程池来处理客户请求。就标准的经验法则而言,线程池中线程的数量应取主机的CPU的数量并将其乘以2。
3、I/O完成端口如何管理线程池(工作机理)
首先,当我们创建I/O完成的时候,需要指定允许多少个线程并发运行。正如前面已经提到过,我们通常会将这个值设为主机的CPU数量(和线程池里面有多少的线程不一样)。当已完成的I/O项被添加到队列中的时候,I/O完成端口想要唤醒正在等待的线程。但是,完成端口唤醒的线程数量最多不会超过我们指定的数量。因此,如果有4个I/O请求已完成,有4个线程正在等待GetQueuedCompletionStatus,那么I/O完成端口只会唤醒两个线程,而让其他两个线程继续睡眠。当每个线程处理完一个已完成的I/O项时,会再次调用GetQueuedCompletionSatus。这时系统发现队列中还有其他的项,于是会唤醒同一个线程来对剩余的项进行处理。
当完成端口唤醒一个线程的时候,会将该线程的线程标识符保存在与完成端口相关联的第4个数据结构中,也就是已释放线程列表(released thread list)。这使得完成端口能够记住哪些线程已经被唤醒,并监视它们的执行情况。如果 一个已释放的线程调用的任何函数将该线程切换到了等待状态,那么完成端口会检测到这一情况,此时它会更新内部的数据结构,将该线程的线程标识符从已释放线程列表中移除,并将其添加到已暂停线程列表(paused
thread list)中。
完成端口的目标是根据在创建完成端口时指定的并发线程的数量,将尽可能多的线程保持在已释放线程列表中。如果一个已释放线程由于任何原因而进入等待状态,那么已释放线程列表会缩减,完成端口就可以释放另一个正在等待的线程。如果一个已暂停的线程被唤醒,那么它会离开已暂停线程列表并重新进入已释放线程列表。这意味着此时已释放线程列表中的线程数量将大于最大允许的并发线程数量。
假设我们在一台有两个CPU的机器上运行。我们创建一个同时最多只允许两个线程被唤醒的完成端口,还创建了4个线程来等待已完成的I/O请求。如果3个已完成的I/O请求被添加到端口的队列中,只有两个线程会被唤醒来对请求进行处理,这降低了可运行的数量,并节省了上下文切换的时间。现在,如果一个可运行线程调用了Sleep、WaitForSingleObject、WaitForMultipleObjects、SignalObjectAndWait,一个异步I/O调用或任何能够导致线程不可运行状态的函数,I/O完成端口会检测到这一情况并立即唤醒第3个线程。完成端口的目标是使CPU保持在满负荷状态下工作。
简单总结一下:
IOCP的机制大概就是:当有一个线程调用GetQueuedeCompletionStatus去等待完成队列中一项时,系统会将该线程添加到IOCP的等待线程队列中。当I/O完成队列中出现一项,系统会先唤醒最近等待的线程(LIFO),并将该线程添加到已释放线程队列中。当I/O完成队列中继续出现一项时,如果没有线程池中还有线程在等待,且已释放的线程没有大于创建完成端口设定的数量,IOCP则会唤醒新的线程去处理新一项。否则等待已在运行的线程处理完前一项,然后再让处理完数据的线程来处理I/O完成队列中这新的一项(这样做可以减少不同线程进行上下文切换带来的花销)。当一个正在运行的线程调用其它函数,将自己挂起或处于等待状态,IOCP会发现这种情况,并将这些线程放到自己的“已暂停线程队列”中,也就是说,已释放的线程数减少了,当I/O完成队列中出现新的一项时,IOCP可以唤醒一个线程。与ICOP相关联的4个数据结构(黑体字),大概就是这样运作的。由此可以看出,IOCP既能让CPU处理满负荷状态工作,也可以尽可能减少线程间切换上下文带来的花销。
相关文章推荐
- 第10章 同步设备I/O和异步设备I/O(1)_常见设备及CreateFile函数
- 第10章 同步设备I/O和异步设备I/O(3)_接收I/O请求完成通知的4种方法
- 第10章 同步设备I/O和异步设备I/O(4)_利用I/O完成端口实现Socket通信
- 第10章 同步设备I/O和异步设备I/O(2)_同步IO和异步IO基础
- 同步和异步设备I/O
- 字符设备驱动--异步通知、同步互斥阻塞
- Chapter10-“I/O设备的同步和异步”之打开和关闭设备
- 同步设备IO与异步设备IO之异步IO介绍
- 应用程序对设备 + IRP 的同步异步学习
- Chapter10-“I/O设备的同步和异步”之打开和关闭设备
- Windows核心编程学习笔记(20)--同步设备I/O与异步设备I/O1
- 应用程序对设备 + IRP 的同步异步学习
- 《Windows核心编程系列》九谈谈同步设备IO与异步设备IO之同步设备IO
- 《Windows核心编程》学习笔记(10)– 同步设备I/O与异步设备I/O
- 《Windows Via C/C++》学习之同步设备I/O与异步设备I/O
- windows核心编程--同步设备I/O与异步设备I/O
- Windows核心编程:同步设备I/O与异步设备I/O
- 《Windows核心编程系列》十谈谈同步设备IO与异步设备IO之异步IO
- 《Windows核心编程系列》九谈谈同步设备IO与异步设备IO之同步设备IO
- 同步和异步设备I/O(Synchronous and asynchronous device IO)