吐槽
早上五点半胃疼醒了,去医院噼里啪啦检查了一天花了1500啥也没检查出来,继续疼。在排队的时候刷到知乎上的这个问题感觉似懂非懂,这里总结一下看到的几个比较好的解释。
一. I/O模型
- 阻塞式I/O
- 非阻塞式I/O
- I/O复用(select,poll,epoll…)
- 信号驱动式I/O(SIGIO)
- 异步I/O(POSIX的aio_系列函数)
一个输入操作一般有两个不同的阶段:
- 等待数据准备好
- 从内核到进程拷贝数据
对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所有等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用程序缓冲区。
1. 阻塞式I/O模型
默认情况下,所有套接字都是阻塞的。下面我们以阻塞套接字的recvfrom的的调用图来说明阻塞。 标红的这部分过程就是阻塞,直到阻塞结束recvfrom才能返回。
2.非阻塞式I/O
以下这句话很重要:进程把一个套接字设置成非阻塞是在通知内核,当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把进程投入睡眠,而是返回一个错误。看看非阻塞的套接字的recvfrom操作如何进行,可以看出recvfrom总是立即返回。
3.I/O多路复用
虽然I/O多路复用的函数也是阻塞的,但是其与以上两种还是有不同的,I/O多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用如recvfrom之上。
4.信号驱动式I/O
见得少,直接给出原图
5. 异步I/O
这类函数的工作机制是告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到用户空间)完成后通知我们。如图所示,注意红线标记处说明在调用时就可以立马返回,等函数操作完成会通知我们。
区别
前四种I/O模型都是同步I/O操作,他们的区别在于第一阶段,而他们的第二阶段是一样的:在数据从内核复制到应用缓冲区期间(用户空间),进程阻塞于recvfrom调用。相反,异步I/O模型在这两个阶段都要处理。书上也有一副很好的图解释了
- 同步I/O操作:导致请求进程阻塞,直到I/O操作完成
- 异步I/O操作:不导致请求进程阻塞
阻塞,非阻塞:进程/线程要访问的数据是否就绪,进程/线程是否需要等待;
同步,异步:访问数据的方式,同步需要主动读写数据,在读写数据的过程中还是会阻塞;异步只需要I/O操作完成的通知,并不主动读写数据,由操作系统内核完成数据的读写。
这里有个来自知乎的例子:
老张爱喝茶,废话不说,煮开水。出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
- 老张把水壶放到火上,立等水开。(同步阻塞)老张觉得自己有点傻
- 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。
- 老张把响水壶放到火上,立等水开。(异步阻塞)老张觉得这样傻等意义不大
- 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
老张觉得自己聪明了。所谓同步异步,只是对于水壶而言。普通水壶,同步;响水壶,异步。虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。所谓阻塞非阻塞,仅仅对于老张而言。立等的老张(被挂起),阻塞;看电视的老张,非阻塞。情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。
二. IO复用
这里一开始可能看不出来IO复用比之前阻塞IO有哪些好处,这里也给出一个在知乎上看的解释:
假设你是一个机场的空管, 你需要管理到你机场的所有的航线, 包括进港,出港, 有些航班需要放到停机坪等待,有些航班需要去登机口接乘客。
你会怎么做?
最简单的做法,就是你去招一大批空管员,然后每人盯一架飞机, 从进港,接客,排位,出港,航线监控,直至交接给下一个空港,全程监控。
那么问题就来了:
很快你就发现空管塔里面聚集起来一大票的空管员,交通稍微繁忙一点,新的空管员就已经挤不进来了。 空管员之间需要协调,屋子里面就1, 2个人的时候还好,几十号人以后 ,基本上就成菜市场了。空管员经常需要更新一些公用的东西,比如起飞显示屏,比如下一个小时后的出港排期,最后你会很惊奇的发现,每个人的时间最后都花在了抢这些资源上。 现实上我们的空管同时管几十架飞机稀松平常的事情, 他们怎么做的呢?
他们用这个东西
这个东西叫flight progress strip. 每一个块代表一个航班,不同的槽代表不同的状态,然后一个空管员可以管理一组这样的块(一组航班),而他的工作,就是在航班信息有新的更新的时候,把对应的块放到不同的槽子里面。这个东西现在还没有淘汰哦,只是变成电子的了而已。。是不是觉得一下子效率高了很多,一个空管塔里可以调度的航线可以是前一种方法的几倍到几十倍。
如果你把每一个航线当成一个Sock(I/O 流), 空管当成你的服务端Sock管理代码的话.第一种方法就是最传统的多进程并发模型 (每进来一个新的I/O流会分配一个新的进程管理。)第二种方法就是I/O多路复用 (单个线程,通过记录跟踪每个I/O流(sock)的状态,来同时管理多个I/O流 。)其实“I/O多路复用”这个坑爹翻译可能是这个概念在中文里面如此难理解的原因。所谓的I/O多路复用在英文中其实叫 I/O multiplexing. 如果你搜索multiplexing啥意思,基本上都会出这个图:
于是大部分人都直接联想到”一根网线,多个sock复用” 这个概念,包括上面的几个回答, 其实不管你用多进程还是I/O多路复用, 网线都只有一根好伐。多个Sock复用一根网线这个功能是在内核+驱动层实现的。
重要的事情再说一遍: I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态(对应空管塔里面的Fight progress strip槽)来同时管理多个I/O流. 发明它的原因,是尽量多的提高服务器的吞吐能力。 是不是听起来好拗口,看个图就懂了
在同一个线程里面, 通过拨开关的方式,来同时传输多个I/O流。
- 顺便提一句:
- select和poll都只返回可用集合,线程不安全,区别是select有1024个连接的限制
- epoll 现在是线程安全的,返回具体可用的连接,现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。
引用
https://www.zhihu.com/question/19732473
UNIX网络编程(卷一)
https://www.zhihu.com/question/32163005