关于IO——阻塞、非阻塞、同步、异步

来源:互联网 发布:电脑淘宝精选怎么删除 编辑:程序博客网 时间:2024/05/28 15:50

第一部分 IO分类

摘自:http://blog.csdn.net/historyasamirror/article/details/5778378

阻塞IO模型,非阻塞IO模型,IO复用模型,信号驱动IO模型都是同步IO。

select/epoll是IO复用模型(在read、wirte时一般也使用非阻塞IO),应该是同步IO。

http://blog.csdn.net/historyasamirror/article/details/5778378

异步IO:

用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。


同步IO、异步IO的区别:同步IO做”IO operation”的时候会将process阻塞。

解释非阻塞IO是同步IO:non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成(对于读来说,即数据已经从内核空间拷贝至用户空间)。在这整个过程中,进程完全没有被block。


摘来的比喻:

最后,再举几个不是很恰当的例子来说明这四个IO Model(阻塞、非阻塞、IO复用、异步IO):
有A,B,C,D四个人在钓鱼:
A用的是最老式的鱼竿,所以呢,得一直守着,等到鱼上钩了再拉杆;
B的鱼竿有个功能,能够显示是否有鱼上钩,所以呢,B就和旁边的MM聊天,隔会再看看有没有鱼上钩,有的话就迅速拉杆;
C用的鱼竿和B差不多,但他想了一个好办法,就是同时放好几根鱼竿,然后守在旁边,一旦有显示说鱼上钩了,它就将对应的鱼竿拉起来;
D是个有钱人,干脆雇了一个人帮他钓鱼,一旦那个人把鱼钓上来了,就给D发个短信。


第二部分 深入理解异步IO

参考:http://hi.baidu.com/_kouu/item/2b3cfecd49c17d10515058d9

linux下主要有两套异步IO,一套是由glibc实现的(以下称之为glibc版本)、一套是由linux内核实现,并由libaio来封装调用接口(以下称之为linux版本)。

glibc版本:

接口:aio_read(struct aiocb *aiocbp)、aio_write、aio_cancel、aio_error、aio_return、aio_suspend。

随着异步请求的提交,一些异步处理线程被动态创建。异步处理线程处理每一个请求,处理完成后在对应的aiocb中填充结果,然后触发可能的信号通知或回调函数(回调函数是需要创建新线程来调用的);异步处理线程在完成某个fd的所有请求后,进入闲置状态;异步处理线程处于闲置状态一段时间后(没有新的请求),则会自动退出。等到再有新的请求时,再去动态创建。


linux版本:

接口:io_setup、io_destroy、io_submit、io_cancel、io_getevents(aio_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct timespec *timeout)。

io_event结构是用来描述返回结果,保存它的buffer是由内核在用户态地址空间上分配的。按照这样的思路,io_setup时,内核会通过mmap在对应的用户空间分配一段内存,mmap_base、mmap_size就是这个内存映射对应的位置和大小。然后,光有映射还不行,还必须立马分配物理内存,ring_pages、nr_pages就是分配好的物理页面。(因为这些内存是要被内核直接访问的,内核会将异步IO的结果写入其中。如果物理页面延迟分配,那么内核访问这些内存的时候会发生缺页异常。而处理内核态的缺页异常又很麻烦,所以还不如直接分配物理内存的好。其二,内核在访问这个buffer里的信息时,也并不是通过mmap_base这个虚拟地址去直接访问的。既然是异步,那么结果写回的时候可能是在另一个上下文上面,虚拟地址空间都不同。为了避免进行虚拟地址空间的切换,内核干脆直接通过kmap将ring_pages映射到高端内存(用户空间)上去访问好了。)


在《linux文件读写浅析》中可以看到,对于非direct-io的读请求来说,如果page cache不命中,那么IO请求会被提交到底层。之后,do_generic_file_read会通过lock_page操作,等待数据最终读完。这一点跟异步IO是背道而驰的,因为异步就意味着请求提交后不能等待,必须马上返回。而对于非direct-io的写请求,写操作一般仅仅是将数据更新作用到page cache上,并不需要真正的写磁盘。page cache写回磁盘本身是一个异步的过程。可见,对于非direct-io的文件读写,使用linux版本的异步IO接口完全没有意义(就跟使用同步接口效果一样)。
为什么会有这样的设计呢?因为非direct-io的文件读写是只跟page cache打交道的。而page cache是内存,跟内存打交道又不会存在阻塞,那么也就没有什么异步的概念了。至于读写磁盘时发生的阻塞,那是page cache跟磁盘打交道时发生的事情,跟应用程序又没有直接关系。


两个版本的比较:

    从上面的流程可以看出,linux版本的异步IO实际上只是利用了CPU和IO设备可以异步工作的特性(IO请求提交的过程主要还是在调用者线程上同步完成的,请求提交后由于CPU与IO设备可以并行工作,所以调用流程可以返回,调用者可以继续做其他事情)。相比同步IO,并不会占用额外的CPU资源。

    而glibc版本的异步IO则是利用了线程与线程之间可以异步工作的特性,使用了新的线程来完成IO请求,这种做法会额外占用CPU资源(对线程的创建、销毁、调度都存在CPU开销,并且调用者线程和异步处理线程之间还存在线程间通信的开销)。不过,IO请求提交的过程都由异步处理线程来完成了(而linux版本是调用者来完成的请求提交),调用者线程可以更快地响应其他事情。如果CPU资源很富足,这种实现倒也还不错。


第三部分 再次分类

非阻塞IO:Linux中的epoll,BSD系统中的kqueue,Java中的nio;这些属于同步IO。

采用异步的事件通知的IO框架,如C/C++下的ACE、boost::asio、libevent,Java下的MINA。个人觉得这些也属于同步IO,如libevent在回调函数中,还是会使用recv()等读取数据至用户内存;boost::asio不太清楚。

异步IO:glibc版本(aio_*);linux版本(libaio)。这是真正意义上的异步IO。



0 0