github 地址https://github.com/liujunsheng0/notes/blob/master/linux/io模型.md
概念说明
-
用户空间和内核空间(user space and kernel space)
**操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。**为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
简单来说,内核空间是 Linux 内核的运行空间,仅供内核使用,用户空间是用户程序的运行空间,供各个进程使用。为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。
-
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。
从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
- 保存处理机上下文,包括程序计数器和其他寄存器
- 更新PCB信息
- 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列
- 选择另一个进程执行,并更新其PCB
- 更新内存管理的数据结构。
- 恢复处理机上下文
-
进程阻塞
在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。
可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。
-
文件描述符(file descriptor,简称fd)
文件描述符是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
linux下一切皆文件
-
缓存IO(标准IO)
缓存 IO 又被称作标准 IO,大多数文件系统的默认 IO 操作都是缓存 IO。
在 Linux 的缓存 IO 机制中,操作系统会将 IO 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
Linux IO 模型
对于一次IO访问(以read举例),数据先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间(用户空间)。所以说,当一个read操作发生时,它会经历两个阶段:
- 等待内核中的数据准备完成
- 将内核中的数据拷贝到进程(用户空间)中
网络IO本质上就是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作。
对于socket而言,一次IO访问过程如下,
- 等待网络上的数据到达,然后被复制到内核的缓冲区
- 将数据从内核缓冲区复制到应用进程中
网络应用需要处理的无非就是两大类问题:网络IO,数据计算。两者相比,网络IO的延迟,给应用带来的性能瓶颈大于后者。
网络IO的模型大致有如下几种:
- 同步模型(synchronous IO)
- 阻塞IO(bloking IO)
- 非阻塞IO(non-blocking IO)
- 多路复用IO(multiplexing IO)
- 信号驱动式IO(signal-driven IO)
- 异步IO(asynchronous IO)
同步阻塞IO(blocking IO)
-
场景
我和女友点完餐后,不知道什么时候能做好,只好坐在餐厅里面等,直到做好,然后吃完才离开。女友本想逛街,但是不知道饭能什么时候做好,只好和我一起在餐厅等,而不能去逛街,直到吃完饭才能去逛街,中间等待做饭的时间浪费掉了。这就是典型的阻塞。
-
网络模型
同步阻塞 IO 模型是最常用的一个模型,也是最简单的模型。 在linux中,默认情况下所有的socket都是阻塞的。它符合人们最常见的思考逻辑。
阻塞就是进程挂起,CPU处理其它进程去了。
在这个IO模型中,用户空间的应用程序执行系统调用recvform,这会导致应用程序阻塞,什么也不干,直到内核数据准备好,并且将数据从内核复制到应用进程,最后应用进程再处理数据。在等待数据到处理数据的两个阶段,整个进程都被阻塞。不能处理别的网络IO。调用应用程序处于一种不再消费 CPU 而只是简单等待响应的状态。
在linux中,默认情况下所有的socket都是阻塞的,一个典型的读操作流程大概是这样
-
流程描述
-
用户进程发起系统调用 recv/recvfrom,内核开始准备数据,数据拷贝到内和缓冲区中是需要时间的,在此时间内,用户进程会被阻塞(进程自己主动选择的阻塞)
以网络IO为例,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候内核就要等待数据
-
内核数据准备好,将数据从内核拷贝至用户空间(也就是应用进程),拷贝也是需要时间的…
-
数据从内核-> 用户空间完成后,内核返回结果,应用进程解除阻塞,开始处理数据
-
-
特点
IO执行的两个阶段都被阻塞了
缺点:性能差
同步非阻塞
-
场景
我女友不甘心白白在这等,又想去逛商场,又担心饭好了。所以我们逛一会,回来询问服务员饭好了没有,来来回回好多次,饭都还没吃都快累死了啦。这就是非阻塞。需要不断的询问,是否准备好了,即通过轮询的方式询问不断询问
-
网络模型
发起recvform系统调用后,进程并没有阻塞,内核会马上返回结果给内核,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环进行recvform系统调用。这个过程被称之为轮询。
轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,将数据从内核拷贝至用户空间,进程仍然是属于阻塞的状态。
在linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程如图所示:
-
流程描述
当用户进程发起非阻塞的系统调用recvfrom时,如果
-
内核中的数据没有准备好
并不会阻塞用户进程,而是立刻返回一个error。从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好。
-
内核中的数据准备好了
立即返回数据准备好了,应用进程会再次发起系统调用,将数据由内核拷贝到用户空间,然后返回,应用进程处理数据
应用进程不断重复上述过程
-
-
特点
用户进程需要不断的主动询问内核数据好了没有。
优点:应用进程阻塞时间减少
缺点:任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。
IO多路复用
-
场景
如果每个人都像我和女友一样,等饭期间出去逛街,过一阵就来询问服务员饭好没,服务员估计会累死。所以
餐厅开发了一款名为"开饭了"的app,可查看当前饭菜准备状态,当饭菜全部做好了时,app会发送通知,这样我和女友逛街时,就不用去询问服务员了,直接安装这个app就好了。这就是典型的IO多路复用,每个用餐人员看app就能知道自己的饭是否已经做完。
-
网络模型
**问题:**由于同步非阻塞方式需要不断主动轮询,轮询会消耗大量的CPU时间。而 “后台” 可能有多个任务在同时进行,如果每个进程都去轮询,会消耗更多的CPU时间。
**解决方案:**大神们想到只要有一个专门的进程(暂时叫中间进程)去轮询内核中每个进程的数据准备状态,每个进程再与这个中间进程交互,从而获取内核数据准备状态,这就是所谓的IO多路复用。
阻塞I/O只能阻塞一个I/O操作,而I/O复用模型能够阻塞多个I/O操作,所以才叫做多路复用
UNIX/Linux 下的 select、poll、epoll 就是干这个的,select、poll、poll 的好处在于单个进程能处理多个网络连接的IO。
以select为例,处理过程大致如下
-
流程描述,以select为例
- 用户进程调用select,用户进程阻塞
- 数据准备好,通知用户进程(并不是通知所有用户进程- -,而是通知发起调用的用户进程)
- 无数据准备好,阻塞直到超时
- 用户进程发起系统调用recvfrom,将数据由内核复制到用户空间
IO多路复用多了一个select的系统调用,所以,如果处理的连接数不是很高的话,使用select的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。
初看好像IO多路复用没什么用,其实select、poll、epoll的优势在于可以以较少的代价来同时监听处理多个IO,select、poll、epoll的优势在于能处理更多的连接
- 用户进程调用select,用户进程阻塞
-
特点
- 阻塞I/O只能阻塞一个I/O操作,而I/O复用模型能够阻塞多个I/O操作
- 在IO多路复用中,如上图所示,整个用户进程其实是一直被阻塞的。只不过进程是被select这个函数阻塞,而不是被recvfrom给阻塞。所以IO多路复用是阻塞在select,poll,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用如recvfrom之上。将数据从内核空间复制到用户空间也是阻塞的。
所以从整个IO过程来看,他们是顺序执行的,因此可以归为同步模型(synchronous)。都是进程主动等待且向内核检查状态。
同步是需要主动等待消息通知,而异步则是被动接收消息通知,通过回调、通知、状态等方式来被动获取消息。IO多路复用在阻塞到select阶段时,用户进程是主动等待并调用select函数获取数据就绪状态消息,并且其进程状态为阻塞。所以,把IO多路复用归为同步阻塞模式。
select就像一个中介,每个人都找中介获取自己所需要的信息。
异步非阻塞IO
-
场景
女友不想逛街,餐厅又太吵了,回家好好休息一下。于是我们叫外卖,打个电话点餐,然后我和女友可以在家好好休息一下,饭好了送货员送到家里来。这就是典型的异步,只需要打个电话说一下,然后可以做自己的事情,饭好了就送来了。
-
网络模型
相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。
Linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv。异步过程如下图所示:
-
流程描述
用户进程发起aio_read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何阻塞。然后,内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,内核会给用户进程发送一个signal或执行一个基于线程的回调函数来完成这次 IO 处理过程,告诉它read操作完成了。
在 Linux 中,通知的方式是"信号":
-
如果这个进程正在用户态忙着做别的事(例如在计算两个矩阵的乘积),那就强行打断之,调用事先注册的信号处理函数,这个函数可以决定何时以及如何处理这个异步任务。由于信号处理函数是突然闯进来的,因此跟中断处理程序一样,有很多事情是不能做的,因此保险起见,一般是把事件 “登记” 一下放进队列,然后返回该进程原来在做的事。
-
如果这个进程正在内核态忙着做别的事,例如以同步阻塞方式读写磁盘,那就只好把这个通知挂起来了,等到内核态的事情忙完了,快要回到用户态的时候,再触发信号通知。
-
如果这个进程现在被挂起了,例如无事可做 sleep 了,那就把这个进程唤醒,下次有 CPU 空闲的时候,就会调度到这个进程,触发信号通知。
异步 API 说来轻巧,做来难,这主要是对 API 的实现者而言的。Linux 的异步 IO(AIO)支持是 2.6.22 才引入的,还有很多系统调用不支持异步 IO。Linux 的异步 IO 最初是为数据库设计的,因此通过异步 IO 的读写操作不会被缓存或缓冲,这就无法利用操作系统的缓存与缓冲机制。
-
IO多路复用
IO多路复用是指内核一旦发现进程(上文所说的中间进程)指定的一个或者多个IO条件准备读取,它就通知该进
程(通知的是上文所说的那个中间进程,而不是用户进程)。IO多路复用适用如下场合:
(1)当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
(2)当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
(3)如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
(4)如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
(5)如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
Linux支持IO多路复用的系统调用有select、poll、epoll,这些调用都是内核级别的。但select、poll、epoll本质上
都是同步I/O,先是阻塞住等待就绪的socket,然后阻塞将数据从内核拷贝到用户内存的过程。
select,poll,epoll区别
select | poll | epoll | |
---|---|---|---|
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | 数组 | 链表 | 哈希表 |
IO效率 | 每次调用都进行线性遍历,时间复杂度为O(n) | 每次调用都进行线性遍历,时间复杂度为O(n) | 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到rdllist里面。时间复杂度O(1) |
最大连接数 | 1024(x86)或 2048(x64) | 无上限 | 无上限 |
fd拷贝 | 每次调用select,都需要把fd集合从用户态拷贝到内核态 | 每次调用poll,都需要把fd集合从用户态拷贝到内核态 | 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝 |
学习链接
https://segmentfault.com/a/1190000003063859
https://www.jianshu.com/p/486b0965c296
https://blog.csdn.net/wjtyy/article/details/46373089
https://www.jianshu.com/p/dfd940e7fca2
http://www.cnblogs.com/Anker/archive/2013/08/14/3258674.html
https://blog.csdn.net/lixungogogo/article/details/52219951