03-水平触发和边缘触发
63.1.1 水平触发和边缘触发
在深入讨论多种可选的I/O机制之前,我们需要先区分两种文件描述符准备就绪的通知模式。
- 水平触发通知:如果文件描述符上可以非阻塞地执行 I/O 系统调用,此时认为它已经就绪。
- 边缘触发通知:如果文件描述符自上次状态检查以来有了新的I/O活动(比如新的输入),此时需要触发通知。
表63-1总结了 I/O 多路复用、信号驱动I/O以及epoll所采用的通知模型。epoll API同其他两种I/O模型的区别在于它对水平触发(默认)和边缘触发都支持。
| I/O模式 | 水平触发 | 边缘触发 | | :----- | :----- | :----- | :----- | :----- | | select(),poll() | ● | | 信号驱动I/O | | ● | | epoll | ● | ● |
有关这两种通知模型区别的细节将在本章的学习中逐渐清晰。现在我们讨论一下通知模型的选择是如何影响我们设计程序的方式的。
当采用水平触发通知时,我们可以在任意时刻检查文件描述符的就绪状态。这表示当我们确定了文件描述符处于就绪态时(比如存在有输入数据),就可以对其执行一些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操作,可能会让其他文件描述符处于饥饿状态。在63.4.6节中,我们对epoll API的边缘触发通知做介绍时再深入讨论这个问题。
- 如果程序采用循环来对文件描述符执行尽可能多的I/O,而文件描述符又被置为可阻塞的,那么最终当没有更多的I/O可执行时,I/O系统调用就会阻塞。基于这个原因,每个被检查的文件描述符通常都应该置为非阻塞模式,在得到I/O事件通知后重复执行I/O操作,直到相应的系统调用(比如read(),write())以错误码EAGAIN或EWOULDBLOCK的形式失败。