Linux的IO模型

微信公众号:郑尔多斯
关注可了解更多的Nginx知识。任何问题或建议,请公众号留言;
关注公众号,有趣有内涵的文章第一时间送达!

同步/异步与阻塞/非阻塞的理解

线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。用线程执行程序流的过程去理解同步异步,阻塞非阻塞。同步异步关注的是流执行过程需不需要等待外部调用的结果,而阻塞非阻塞关注的是外部调用对流本身产生的影响。

同步与异步

线程的执行过程中,产生一个外部调用,如果需要等待该调用返回才能继续线程流则叫做同步,不需要等待结果返回线程流可以继续往下执行的情况叫做异步。
区分:线程流向下执行需不需要等待系统调用的结果。

阻塞与非阻塞

线程执行过程中,产生一个外部调用后,会不会把该线程流“堵”住,会“堵”对应的是阻塞,反之为非阻塞。

IO模型

我们先看一下linux中存在的几种IO模型,如下图所示:
Linux的IO模型
每个 I/O 模型都有自己的使用模式,它们对于特定的应用程序都有自己的优点。本节将简要对其一一进行介绍。

同步阻塞IO

最常用的一个模型是同步阻塞 I/O 模型。在这个模型中,用户空间的应用程序执行一个系统调用,这会导致应用程序阻塞。这意味着应用程序会一直阻塞,直到系统调用完成为止(数据传输完成或发生错误)。调用应用程序处于一种不再消费 CPU 而只是简单等待响应的状态,因此从处理的角度来看,这是非常有效的。
下图给出了传统的阻塞 I/O 模型,这也是目前应用程序中最为常用的一种模型。其行为非常容易理解,其用法对于典型的应用程序来说都非常有效。在调用 read 系统调用时,应用程序会阻塞并对内核进行上下文切换。然后会触发读操作,当响应返回时(从我们正在从中读取的设备中返回),数据就被移动到用户空间的缓冲区中。然后应用程序就会解除阻塞(read 调用返回)。
同步阻塞IO模型
从应用程序的角度来说,read 调用会延续很长时间。实际上,在内核执行读操作和其他工作时,应用程序的确会被阻塞。即在调用read()的时候,应用程序处于阻塞状态,什么也不能干。

同步非阻塞 I/O

同步阻塞 I/O 的一种效率稍低的变种是同步非阻塞 I/O。在这种模型中,设备是以非阻塞的形式打开的。这意味着 I/O 操作不会立即完成,read操作可能会返回一个错误代码,说明这个命令不能立即满足(EAGAIN 或 EWOULDBLOCK),如下图所示。
同步非阻塞IO模型
非阻塞的实现是 I/O 命令可能并不会立即满足,需要应用程序调用许多次来等待操作完成(简单地说就是轮询)。这可能效率不高,因为在很多情况下,当内核执行这个命令时,应用程序必须要进行忙碌等待,直到数据可用为止,或者试图执行其他工作。正如图 3 所示的一样,这个方法可以引入 I/O 操作的延时,因为数据在内核中变为可用到用户调用 read 返回数据之间存在一定的间隔,这会导致整体数据吞吐量的降低。
这种情况虽然调用read()的时候会立刻返回数据是否可用的反馈,这样的话应用程序就不会被阻塞,在多次轮询的时间间隔中,应用程序可以做一些其他的事情。但是如果数据不可用,我们就需要多次调用read();

while(read(fd) == EAGAIN){
     //do something ,然后再次调用read()进行查询状态
}

异步阻塞 I/O

另外一个阻塞解决方案是IO多路复用(复用的select线程)。I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,(这一点要知道,我们调用select,poll,epoll的时候,应用进程是阻塞的,很多文章说epoll是非阻塞的,完全是胡扯。)但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。对于每个提示符来说,我们可以获取这个描述符可以写数据、有读数据可用以及是否发生错误的通知。
异步阻塞I/O模型
epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现.
select 调用的主要问题是它的效率不是非常高。尽管这是异步通知使用的一种方便模型,但是对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低,对于高性能的 I/O 操作来说不建议使用。而且需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大.
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历.它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:大量的fd的数组被整体复制于用户态和内核地址空间之间. poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知.1. 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)2. 效率提升,不是轮询的方式,只管你“活跃”的连接,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数 3. 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销.
BUT: 表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

异步非阻塞 I/O(AIO)

最后,异步非阻塞 I/O 模型是一种CPU处理与 I/O 重叠进行的模型。读请求会立即返回,说明 read 请求已经成功发起了。在后台完成读操作时,应用程序然后会执行其他处理操作。当 read 的响应到达时,就会产生一个信号或执行一个基于线程的回调函数来完成这次 I/O 处理过程。
IO操作的两个步骤,请求和执行。其中请求是查询设备的状态,看一下数据是否准备好,执行指的是数据准备好之后,内核将数据从内和缓冲区复制到应用程序的缓冲区中。
上面介绍的各种情况都是阻塞的,因为当数据准备好之后,应用程序必须自己调用read()函数将数据从内核缓冲区复制到应用程序缓冲区,在调用read()的过程中,应用程序不能做其他的事情。但是AIO则是异步非阻塞的,因为在当数据数据准备好之后,内核会自动将数据从内核缓冲区复制到应用程序缓冲区中,在这个过程中,应用程序是无感知的,应用程序可以做其他的事情,当内核已经完成了数据复制的工作,会通知应用程序进行处理。所以AIO是真正的异步非阻塞。
异步非阻塞AIO模型
在一个进程中为了执行多个 I/O 请求,利用CPU处理速度与 I/O 速度之间的差异, 而对CPU计算操作和 I/O 处理进行重叠处理。当一个或多个 I/O 请求挂起时,CPU 可以执行其他任务;或者更为常见的是,在发起其他 I/O 的同时对已经完成的 I/O 进行操作。


喜欢本文的朋友们,欢迎长按下图关注订阅号郑尔多斯,更多精彩内容第一时间送达
郑尔多斯

猜你喜欢

转载自blog.csdn.net/weixin_43797048/article/details/84950256