一、前概
ps:这里主要讲解的是一些有关操作系统IO有关的知识,如果你对这一块已经比较熟悉了,那你可以跳过这一块,去阅读后面的知识。如果你对这一块有所不了解的话,请静下心来阅读!
一、虚拟内存
1.什么是虚拟内存?
虚拟内存是计算机系统内存管理的一种技术。它使得每个进程都有自己独立的虚拟空间,并且在进程角度来看的话,该虚拟空间是一个线性空间(即一片连续的内存空间),进程可自由使用这片空间。
进程的虚拟空间可以划分为内核空间和用户空间,如下图所示:
(x86 的32位地址空间示意图,2^32=4G)
由图中可知,每个进程拥有4GB的虚拟地址空间。每个进程有各自的私有用户空间(0-3GB),这个空间对系统中的其他进程是不可见的。最高的1GB内核空间为所有进程以及内核所共享。
2.为什么要用虚拟内存?OR 虚拟内存的好处是什么?
我们可以先设想不用虚拟内存的情况,即进程直接对应物理内存进行操作。最容易想到和发生的事情就是:进程之间缺乏隔离性,造成了数据混乱。
举个小栗子💡:
进程A要对物理内存地址为0x123456写入数据10,此后,进程B也对物理内存地址为0x123456写入数据20。随后进程A对相应地址进行读取,最终发现,what?,怎么变成了20?

引入了虚拟内存后,进程需要多大的内存只需要跟操作系统进行申请,由操作系统来进行安排(操作系统就像一个管家,来负责资源的提供。这种管家思想在分布式中十分普遍,一出现冲突,那就找个管家来解决)。
以上面例子为例,进程A要对内存地址(虚拟内存地址)为0x123456写入数据10,那OS就找个空闲的物理空间(比如0x11111)写入数据10。此后,进程B也对内存地址为0x123456写入数据20。那OS就找个空闲的物理空间(比如0x22222)写入数据20。后面,进程A要进行数据读取,那OS就到对应的物理地址(比如0x11111)去读取数据。通过这种方式就解决了上述所说的数据混乱。
请原谅我长篇大论,我只是想尽可能把东西给清楚,而我觉得举例子是最能形象地把问题描述清楚。
好了,有了上面的知识铺垫后,我们就可以讲讲虚拟内存的好处了。
- 提高内存的利用率和可用性。使得程序能够获得比实际物理内存更多的空间,同时,也使得在有限的物理内存上能够运行更多的程序。
- 实现进程之间相互独立,相互隔离。使得程序在运行时不会去干扰到另外的程序。
(至于进程是如何拥有自己的虚拟空间?主要是通过地址转换和每个进程都拥有自己的页目录来实现的。)
二、应用程序IO操作
应用程序通常是跑在用户空间的,它不存在实质的IO过程,真正的IO操作是在操作系统执行的。
应用程序发起的一次IO操作包含两个阶段:
- IO调用:应用程序进程向操作系统内核发起调用。
- IO执行:操作系统内核完成IO操作。
操作系统内核完成IO操作还包括连个两个过程:
- 准备数据阶段:内核等待I/O设备准备好数据,将数据放到内核缓冲区
- 拷贝数据阶段:将数据从内核缓冲区拷贝到用户空间缓冲区
小总结:
IO本质就是把进程的内部数据转移到外部设备,或者把外部设备的数据迁移到进程内部。外部设备一般指硬盘、socket通讯的网卡。
一个完整的IO过程包括以下几个步骤(以读取数据为例):
- 应用程序进程向操作系统发起IO调用请求
- 操作系统准备数据,把IO外部设备的数据,加载到内核缓冲区
- 操作系统拷贝数据,即将内核缓冲区的数据,拷贝到进程缓冲区
二、阻塞/非阻塞与同步/异步
阻塞/非阻塞
结论:阻塞/非阻塞,关注的是接口调用后等待数据返回时的状态。
- 阻塞型:调用接口后被挂起无法执行其他操作;
- 非阻塞型:调用接口后不会被挂起,可以继续执行
以读取文件为例:
如果应用层调用的是阻塞型I/O,那么在调用之后,应用层即刻被挂起,直到系统内核从磁盘读取完数据并返回给应用层,应用层才用获得的数据进行接下来的其他操作。
如果应用层调用的是非阻塞I/O,那么调用后,系统内核会立即返回(虽然还没有文件内容的数据),应用层并不会被挂起,它可以做其他任意它想做的操作。(至于文件内容数据如何返回给应用层,这已经超出了阻塞和非阻塞的辨别范畴。)
同步/异步
结论:同步/异步,关注的是任务完成时消息通知的方式。
- 异步:调用者调用某操作后,会立即返回。被调用方执行完操作后,会主动通知调用方,然后执行回调。(图中关于异步描述稍微有点错误)
- 同步:调用方盲目主动问询结果(++这里多对比异步去理解,不要对比阻塞和非阻塞理解,同步跟阻塞就是两个完全不同的概念,记住,不要把同步跟阻塞、非阻塞去对比理解,你会绕进一个没有意义的奇怪领域中去。你可以理解为异步的反义词++)
以读取文件为例:
对于同步型的调用,应用层需要自己去向系统内核问询,如果数据还未读取完毕,那此时读取文件的任务还未完成,应用层根据其阻塞和非阻塞的划分,或挂起或去做其他事情(++所以同步和异步并不决定其等待数据返回时的状态++);如果数据已经读取完毕,那此时系统内核将数据返回给应用层,应用层即可以用取得的数据做其他相关的事情。
而对于异步型的调用,应用层无需主动向系统内核问询,在系统内核读取完文件数据之后,会主动通知应用层数据已经读取完毕,此时应用层即可以接收系统内核返回过来的数据,再做其他事情。
小总结:
应用程序调用某个操作后,若处于被挂起状态直至数据返回,即阻塞型调用;若不被挂起,可继续执行,则为非阻塞型调用。
应用程序调用某个操作后,若需要主动去查询任务完成状态,则为同步调用;
若是由被调用方进行任务完成通知,则为异步调用。
场景举例:
- 同步阻塞:小明点击下载按钮之后,就一直干瞪着进度条不做其他任何事情直到软件下载完成。
- 异步阻塞:如果小明点击下载按钮之后,就一直干瞪着进度条不做其他任何事情直到软件下载完成,但是软件下载完成其实是会「叮」的一声通知的(但小明依然那样干等着)(不常见)
- 同步非阻塞:小明点击下载按钮之后,就去做其他事情了,不过他总需要时不时瞄一眼屏幕看软件是不是下载完成了。
- 异步非阻塞:小明点击下载按钮之后,就去做其他事情了,软件下载完之后「叮」的一声通知小明,小明再回来继续处理下载完的软件。
三、IO模型
阻塞IO模型
进程发起IO调用后便被挂起,阻塞至数据返回。
如进程调用IO后,如果内核的数据还没准备好的话,那应用程序进程就一直在阻塞等待,一直等到内核数据准备好了,从内核拷贝到用户空间,才返回成功提示,此次IO操作,称之为阻塞IO。
- 应用:阻塞socket、Java BIO。
- 缺点:如果内核数据一直没准备好(若外部没有传递数据进来,还得等待外部传递数据),那用户进程将一直阻塞,浪费性能,可以使用非阻塞IO优化。
非阻塞IO模型
进程发起IO调用后,如果内核数据还没准备好,可以先返回错误信息给用户进程,让它不需要等待,而是通过轮询的方式再来请求,这就是非阻塞IO。流程图如下:
非阻塞IO的流程如下:
- 应用进程向操作系统内核,发起recvfrom读取数据。
若操作系统内核数据没有准备好,立即返回EWOULDBLOCK错误码。
(若是阻塞调用,则进程会被挂起,直到数据返回) - 应用程序轮询调用,继续向操作系统内核发起recvfrom读取数据。
- 操作系统内核数据准备好了,从内核缓冲区拷贝到用户空间,完成调用,返回成功提示。
非阻塞IO模型,简称NIO,Non-Blocking IO。它相对于阻塞IO,通过轮询去完成数据的返回,在准备数据阶段,进程不会被挂起,它会一直在cpu上运行。缺点也很明显,导致频繁的系统调用,同会消耗大量的CPU资源。可以考虑IO复用模型,来解决这个问题。
IO多路复用模型
一句话:单线程/进程可以同时处理多个IO请求。
一个线程/进程能够监视多个fd(文件描述符),一旦某个fd就绪,就能够通知应用程序进行相应的读写操作;若没有fd就绪时,应用程序会陷入阻塞状态,让出cpu。
系统给我们提供一类函数(如select、poll、epoll函数),它们可以同时监控多个fd的操作,任何一个返回内核数据就绪,应用进程再发起recvfrom系统调用。
Select
应用进程通过调用select函数,可以同时监控多个fd,在select函数监控的fd中,只要有任何一个数据状态准备就绪了,select函数就会返回可读状态,这时应用进程再发起recvfrom请求去读取数据。
深入一点认识select:
(1)select函数如下:
(2)调用select过程:
- 当应用程序调用select函数时,会将需要监听的fd集合拷贝到内核空间中;
- 内核会遍历fd集合,判断某个fd是否就绪(即数据已到达内核空间中),若就绪则打上标志。最后返回就绪的fd数量;
- 应用程序会遍历fd集合,查询到就绪的fd,进行相应的处理。
"
在这里可以想想,就绪的文件描述符是如何返回的?
其实,select函数在返回时,复用了fd_set,即在入参和回参时,表示了不同的意思。如下图:
在入参时,表示了1号,3号fd是我们要进行监听的。在回参时,表示了3号fd是就绪的。
(3)select函数的优缺点:
poll
为解决连接数限制,提出了poll。与select相比,poll解决了连接数限制问题。但是呢,select和poll一样,还是需要通过遍历文件描述符来获取已经就绪的socket。
poll优缺点:
底层通过采用链表,打破了监听fd数量的限制。
epoll
epoll先通过epoll_ctl()来注册一个fd(文件描述符),一旦基于某个fd就绪时,内核会采用回调机制,迅速激活这个fd,当进程调用epoll_wait()时便得到通知。这里去掉了遍历文件描述符的坑爹操作,而是采用监听事件回调的的机制。
深入一点了解epoll
(1)epoll函数:
(2)执行过程:
- 先通过epoll_create创建一个epoll;
等待队列存放阻塞的进程(进程调用时发现没有就绪的fd则陷入阻塞,加入等待队列中),就绪队列存储就绪的fd,红黑树存储我们要监听的fd。
- 调用epoll_ctl,将要监听的fd拷贝到内核空间中去;
- 调用epoll_wait,获取就绪事件;
若调用epoll_wait时,发现就绪队列为空,则当前进程会进入阻塞状态,加入到等待队列中。等数据到达时,再去等待队列中唤醒相应的进程。此后,进程再去检测就绪队列情况。
若调用epoll_wait时,发现就绪队列不为空,则会直接返回,不会进入阻塞状态。
(3)优缺点:
epoll通过epoll_ctl将fd添加到内核空间中,此后一直维护着这fd,避免了每次需将fd从用户态拷贝到内核态中。
epoll 通过就绪列表通知应用程序哪些fd是就绪的,避免了遍历查询。
拓展:
异步IO模型(AIO)
细心的同学会发现BIO,NIO中,在数据从内核复制到应用缓冲的时候,都是阻塞的,因此都不是真正的异步。
AIO实现了IO全流程的非阻塞。应用进程发出系统调用后,是立即返回的,但是立即返回的不是处理结果,而是表示提交成功类似的意思。等内核数据准备好,将数据拷贝到用户进程缓冲区,发送信号通知用户进程IO操作执行完毕。
比如抢购某个东西,但是抢购处理比较耗时,这时候后端可以先告知前端正在排队中,等到结果处理完,再通知前端结果即可。
四、总结:
本文主体上讲解了虚拟内存,在虚拟内存的基础的上,讲述了阻塞/非阻塞,同步/异步之间的区别。
在了解上述知识后,又引出了IO模型,分别是阻塞IO模型,非阻塞IO模型,异步IO模型。在非阻塞IO模型中,讲解了IO多路复用,分别有select、poll、epoll。
------------ 本文到此结束! 写这篇文章前前后后花了好几天的时间,好久没写过这么长的博文。 网上关于阻塞/非阻塞,同步/异步的说法众说纷纭,请读者辩证性地看文章,若有错误或不足之处,也欢迎大家积极指出!
参考资料:
看一遍就理解:IO模型详解