事实上,I/O操作根据设备的不同分为很多种类型,比如内存I/O、网络I/O、磁盘I/O。

一、名词解释:

I/O完成的过程:

一旦发起I/O调用,立即陷入内核模式。首先要把磁盘读进内存。(这段内存是内核访问到的)还需将内核中的内存复制到进程的内存空间中去(这段过程才是真正的I/O过程)。

I/O等待:

造成等待的原因非常多,比如Web服务器在等待用户的访问,这便是等待,因为它不知道谁会来访问,所以只能等。随后当某个用户通过浏览器发出请求,Web服务器与浏览器建立TCP连接后,又要等待用户发出HTTP请求数据,用户的请求数据在网络上传输需要时间,进入服务器接收缓冲区队列以及被复制到进程地址空间都需要时间。另外,假如浏览器和Web服务器采用HTTP长连接模式,那么在超时关闭连接之前,服务器还要等待浏览器发送其他的请求,这也是I/O等待。

同步:

指进程发出一个过程(功能、函数)调用后,在没有得到结果之前,该调用将不会返回。

阻塞:

阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入睡眠状态)。函数只有在得到结果之后才会返回。

非阻塞:

指在不能立刻得到结果之前,被调用函数不会阻塞当前线程,而会立刻返回。


内存映射:

Linux内核提供一种访问磁盘文件的特殊方式,它可以将内存中某块地址空间和我们哟啊指定的磁盘文件相关联,从而把我们对这款内存的访问转换为对磁盘文件的访问,这种技术称为内存映射(Memory Mapping).也就是说将I/O完成过程的第二部操作将内核中的内存复制到进程的内存空间中省去,大大提高了系统性能。nginx就是采用了这种技术。


二、I/O模型

同步阻塞I/O:

指当进程调用某些涉及I/O操作的系统调用或库函数时,比如accept()、send()、recv()等,进程便暂停下来,等待I/O操作完成后再继续运行。这是一种简单而有效的I/O模型,它可以和多进程结合起来有效的利用CPU资源,但是其代价就是多进程的大量内存开销。这种等待也叫做忙等待。


同步非阻塞I/O:

在同步阻塞I/O中,进程实际上等待的时间可能包括两部分,一个是等待数据的就绪,另一个等待数据的复制,对于网络I/O来说,前者的时间可能要更长一些。

与此不同的是,同步非阻塞I/O的调用不会等待数据的就绪,如果数据不可读或者不可写,它会立即告诉进程。比如使用非阻塞recv()接收网络数据的时候,如果网卡缓冲区中没有可接收的数据,函数就及时返回,告诉进程没有数据可读了。相比于阻塞I/O,这种非阻塞I/O结合反复轮询来尝试数据是否就绪,防止进程被阻塞,最大的好处便在于可以在一个进程里同时处理多个I/O操作。


多路I/O就绪通知:

在实际应用中,特别是Web服务器,同时处理大量的文件描述符是必不可少的,但是使用同步非阻塞I/O显然不是最佳的选择,在这种模型下,如果服务器想要同时接收多个TCP连接的数据,就必须轮流对每个socket调用接受数据的方法,比如recv()。不管这些socket有没有可以接收的数据,都要询问一遍,加入大部分socket并没有数据可以接收,那么进程便会浪费很多CPU时间用于检查这些socket,因此多路I/O就绪通知的出现,提供了对大量文件描述就绪检查的高性能方案,它允许进程通过一种方法来同时监视所有文件描述符,并可以快速得所有就绪的文件描述符,然后知针对这些文件描述符进行数据访问。

select:

最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视包含多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。

缺点:单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。所以假如使用了select的服务器已经维持了1024个连接,那么你的请求可能会被拒绝。另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量的TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。


poll:

poll在1986年诞生于System V Release 3,显然UNIX不愿意直接沿用BSD的select,而是自己重新实现一遍,它和select在本质上没有多大差别,但是POLL没有最大文件描述符数量的限制。


水平触发(Level Triggered):

如果进程没有对其进行I/O操作,那么下次调用sleect()或poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息。


边缘触发

只会通知一次,效率更高。


异步I/O

异步则指主动请求数据后便可以继续处理其他任务,随后等待I/O操作完毕的通知,这可以使进程在数据读写是也不发生阻塞。

POSIX1003.1标准为异步方式访问文件定义了一套库函数,这里的异步I/O(AIO)实际上就是指当用户态进程调用库函数访问文件时,进行必要的快速注册,比如进入读写操作队列,然后函数马上返回,这时候真正的I/O传输还没有开始呢。可以看出,这种机制的真正意义上的异步I/O,而且是非阻塞的,它可以是进程的发起I/O操作后继续运行,让CPU处理和I/O操作达到更好的重叠。


同步I/O与异步I/O的区别:

同步和异步、阻塞和非阻塞很容易被混用,其实它们完全不是一回事,而且它们修饰的对象也不同。阻塞和非阻塞是指当进程访问的数据如果尚未就绪,进程是否需要等待,简单说这相当于函数内部的实现区别,即未就绪时是直接返回还是等待就绪;而同步和异步是指访问数据的机制,同步一般指主动请求并等待I/O操作完毕的方式,当数据就绪后在读写的时候必须阻塞,则指主动请求数据后便可以继续处理其他任务,随后等待I/O操作完毕的通知,这可以使进程在数据读写是也不发生阻塞。