Linux IO模型详解

蒂卡波湖牧羊犬

IO模型

通常用户进程中的一个完整IO分为两阶段:用户进程空间到内核空间、内核空间到设备空间(磁盘、网络等)。

linux内核IO模式
1.同步阻塞IO模型
流程:线程发起IO系统调用后会被被阻塞,转到内核空间处理,整个IO处理完毕后返回数据。
优缺点:一般需要给每个IO请求分配一个IO线程,因此系统开销大。
典型应用:阻塞socket、Java BIO;
20161028200138896.jpg

2.同步非阻塞IO模型
流程:线程不断轮询读取内核IO设备缓冲区,如果没数据则立即返回EWOULDBLOCK,有则返回数据。
优缺点:CPU消耗多,无效IO多。
典型应用:非阻塞socket(设置为NONBLOCK)
20161028200139219.jpg

3.同步IO复用模型
流程:线程调用select/poll/epoll传入多个设备fd,然后阻塞或者轮询等待。如果有IO设备准备好则返回可读条件,用户主动调IO读写,如果没有则继续阻塞。
优缺点:相比前两种模型,IO复用可以监听多个IO设备,所以一个线程内可以处理多个网络请求。
典型应用:select、poll、epoll。nginx以及Java NIO基于此IO模型。

20161028200139703.jpg

4.异步信号驱动IO模型
流程:利用linux信号机制,用sigaction函数将SIGIO读写信号以及handler回调函数存在内核队列中。当设备IO缓冲区可写或可读时触发SIGIO中断,返回设备fd并回调handler。
优缺点:这种异步回调方式避免用户或内核主动轮询设备造成的资源浪费,但是此方式会存在问题,首先handler是在中断环境下运行,多线程不稳定而且要考虑信号的平台兼容性,其次是SIGIO信号被POSIX定义为standard signals不会进入队列,所以同时存在多个SIGIO会只触发第一个SIGIO。
ref: http://www.man7.org/linux/man-pages/man7/signal.7.html
20161028200140021.jpg

5.异步IO模型
流程:用户传入设备fd跟数据结构体,内核等设备缓冲区准备好后进行数据读写,最后通知用户完成。因为历史原因,linux aio有两种主流实现。

  • glibc aio,在用户空间用多线程封装内核IO实现异步。大致流程是用户调用aio_API传入aiocb("asynchronous I/O control block")结构体,aio将操作入队列并立即返回。当内核拷贝完数据之后,会触发signal通知用户aiocb已经准备好,也可以通过调用aio_error检查aiocb状态是否完成。因为是在用户空间实现的异步,频繁的线程和内核态切换导致明显的性能问题和不可弹性伸缩,Linux Manual也注明这是内核aio成熟前的过渡方案。

ABUIABAEGAAgvMOrwAUoypzSBzCABTjtAg.png

ref: API: http://man7.org/linux/man-pages/man7/aio.7.html
  • libaio/内核aio,libaio在用户空间封装了内核aio的接口,允许应用引入libaio即可使用aio,否则应用需要自己定义syscall。流程是首先调用io_setup获取aio_context,然后构造一个或多个iocb(包含fd,buffer)结构体通过io_submit提交到内核队列中,最后用阻塞或轮询方式调用io_getevents获取io_event判断IO是否完成。这是linux官方方案,但是由于设计上的缺陷,目前只支持O_DIRECT(绕过内核缓冲区直接IO)方式open的文件设备,其他情况要不返回错误,要不退化到同步阻塞方式在io_submit中读写。尽管功能仍然不成熟,但是性能上有人验证一个进程内用aio读写16个fd,与16个进程pread的IO吞吐量一致。
ref: Demo: https://blog.cloudflare.com/io_submit-the-epoll-alternative-youve-never-heard-about/
Linus对aio API的吐槽:https://lwn.net/Articles/671657/
aio一些历史问题的总结:https://www.aikaiyuan.com/4556.html
内核commiter总结aio的问题与演进:《Linux Asynchronous I/O Design: Evolution & Challenges》
glibc与aio更完整的demo: https://oxnz.github.io/2016/10/13/linux-aio/#aio-system-calls
性能测试: http://blog.yufeng.info/archives/741

应用IO模式
1.Reactor模式
主要分为以下几步:应用向reactor注册多个设备监听(epoll/select)以及回调函数,IO ready事件触发,reactor分发事件执行对应的回调函数,应用在回调函数中进行IO操作。
单reactor单回调线程结构图。经典应用:libevent、libuv的network IO、Cronet的底层IO
e3.png

reactor延伸出来的模式还包括
单reactor多回调线程结构。经典应用:libuv的file IO
21215116-d912aa6ee03547e3865d734288368b41.png

多reactor多回调线程结构,每个成功连接后的所有操作由同一个线程处理。这样保证了同一请求的所有状态和上下文在同一个线程,相对于单回调线程避免了不必要的上下文切换。经典应用:netty
21215443-4e028b9ed85542169c62377970d745a2.png

ref: reactor示例代码https://juejin.im/post/5b4570cce51d451984695a9b

2.Proactor模式
流程与reactor类似,区别在于proactor在IO ready事件触发后,完成IO操作再通知应用回调。经典应用例如boost asio异步IO库。虽然在linux平台还是基于epoll/select,但是内部实现了异步操作处理器(Asynchronous Operation Processor)以及异步事件分离器(Asynchronous Event Demultiplexer)将IO操作与应用回调隔离。
boost.asio结构与流程图
proactor.png

demo: https://www.boost.org/doc/libs/1_66_0/doc/html/boost_asio/tutorial/tutdaytime3/src.html

reactor和proactor的主要区别:

以主动写为例:
Reactor将handle放到select(),等待可写就绪,然后调用write()写入数据;写完处理后续逻辑;
Proactor调用aio_write后立刻返回,由内核负责写操作,写完后调用相应的回调函数处理后续逻辑;

Reactor被动的等待指示事件的到来并做出反应;它有一个等待的过程,做什么都要先放入到监听事件集合中等待handler可用时再进行操作;
Proactor直接调用异步读写操作,调用完后立刻返回;

实现
Reactor实现了一个被动的事件分离和分发模型,服务等待请求事件的到来,再通过不受间断的同步处理事件,从而做出反应;
Proactor实现了一个主动的事件分离和分发模型;这种设计允许多个任务并发的执行,从而提高吞吐量;并可执行耗时长的任务(各个任务间互不影响)

优点
Reactor实现相对简单,对于耗时短的处理场景处理高效;
操作系统可以在多个事件源上等待,并且避免了多线程编程相关的性能开销和编程复杂性;
事件的串行化对应用是透明的,可以顺序的同步执行而不需要加锁;
事务分离:将与应用无关的多路分解和分配机制和与应用相关的回调函数分离开来,
Proactor性能更高,能够处理耗时长的并发场景;

缺点
Reactor处理耗时长的操作会造成事件分发的阻塞,影响到后续事件的处理;
Proactor实现逻辑复杂;依赖操作系统对异步的支持,目前实现了纯异步操作的操作系统少,实现优秀的如windows IOCP,但由于其windows系统用于服务器的局限性,目前应用范围较小;而Unix/Linux系统对纯异步的支持有限,应用事件驱动的主流还是通过select/epoll来实现;

适用场景
Reactor:同时接收多个服务请求,并且依次同步的处理它们的事件驱动程序;
Proactor:异步接收和同时处理多个服务请求的事件驱动程序;

ref:https://www.cnblogs.com/losophy/p/9202815.html
阅读 5.1k
23 声望
0 粉丝
0 条评论
23 声望
0 粉丝
文章目录
宣传栏