web 服务器用到的底层技术为什么大多是线程阻塞和多路复用?

2015-07-21 11:42:48 +08:00
 aiqier

《unix网络编程》,一共提到五种io模型:

1.阻塞IO模型
2.非阻塞IO模型
3.IO复用模型
4.信号驱动IO
5.异步IO模型

可是为什么我见过的web服务器,要么是用阻塞io+线程(比如apache,来请求开线程),要么是多路复用(比如epoll,tornado),但在我看来后两种io(4和5)才是最完美的解决方式,因为我认为数据好了再通知我去操作要比我去轮询好,可是为什么web服务器没有使用呢?

是因为发送信号,这个操作太浪费了?所以对于web服务器这种要接受大量请求的,每一个io都发送信号代价太大?还是因为别的,比如实现起来太难?

3289 次点击
所在节点    服务器
7 条回复
dorentus
2015-07-21 11:49:18 +08:00
select/poll 是 3
epoll 是 4 吧
tftk
2015-07-21 12:00:28 +08:00
信号描述能力不足。
nalaginrut
2015-07-21 17:24:13 +08:00
@aiqier

我想有必要先梳理下几个概念,不然是无法讨论的。如果一问什么是IO复用,回答就是select/poll,那就没法谈了,因为作为poll增强版的epoll也是IO复用模型,只不过edge-trigger为异步提供了可能。

1、什么是IO复用:一次查询可以获取多个文件描述符,是IO复用的典型特征。符合这个描述的都可称为IO复用模型。相对的是accept,一次查询最多得一个结果(你可以称其为IO单用,但实际上没有这样一个名词)。值得注意的是,查询无论阻塞或者非阻塞都可以,看你怎么玩。

在这个前提下,楼主说的阻塞IO+线程,跟IO复用的方式并不是互斥的。并且在使用IO复用模型的时候,“数据好了再通知操作”是很容易实现并且也是目前已经流行起来的用法,否则epoll就没啥意义了——只不过每N个连接(N由backlog决定)需要一次查询。表面上看是在轮询,其实在大量连接请求的情况下已经不再是以往那种受人诟病的轮询等待的方式了。所以这种用法仍然是高效并且容易实现的,相对于复杂的消息模型构建要单纯得多。

2、什么是异步IO:就是非阻塞IO[2],这是同一个概念的两种表达。程序在IO操作未完成的情况下也能继续往下运行而非等待,是其特征,符合这个描述都属于异步IO。
但是我们也看到这样的一个描述非常宽泛,所以可以有许多具体的实现方式,非阻塞式IO复用就是其中一种方式。
还有一种很容易让人混淆的就是“托管异步IO”,一般就简称异步IO,但其实只是异步IO的实现形式之一,比如win下的IOCP,不需要使用者处理传输中断,发出IO请求后只需要等待完成的通知即可。这种方式并不比异步的IO复用高级多少,只不过是隐藏了底层机制,让使用者更方便而已,但对于有把控能力的编程者来说灵活性是大大降低了。
信号驱动(SIGIO)同样是其具体实现的一种。

3、信号驱动的问题:SIGIO实质上是中断驱动,只不过注册了的特定IO中断一定会产生SIGIO信号,所以说信号驱动也没问题。信号的问题在于可能会丢失,请求数密集了你就只能加队列先把事件记录下来然后逐个处理了,本质上跟轮询也没啥区别。即便如此,SIGIO对于TCP而言也就是理论上咱可以玩玩,其实没人真的敢拿来用,UDP还凑合[3]。
轮询也没什么不好的,在Linux里处理高速网络传输使用的是用软中断以类似轮询的方式处理的[4](跟前面描述的队列处理SIGIO是相似的原理),这是个很有趣的事情,事务少的时候使用轮询,当事务变多了人们开始喜欢中断,而事务足够多的时候人们却返回去使用轮询了。曾经有段时间许多人对于green thread也难以理解,好不容易掌握了多线程怎么这个世界开始玩儿单线程了。所以不要对一些东西单纯地贴标签,量变引起质变的时候你会觉得世界变化太快的。

4、没有什么是完美的,这几种模型都是shit,仍然不能解放苦逼的码奴,该你遭的时候你就知道了,人类还需要继续努力...

楼主觉得4和5更好,其实理论到了工程领域就必须要考虑复杂度和稳定性,复杂度高了稳定性就下来了,维护成本也是个问题。为什么这几年流行FP?因为不用费力担心state的变化,谁也不愿意费心费力最后搞出个维护不起的东西。相对来说性能如果可以靠堆硬件和改进算法解决是最好的。恩扯远了...

[1] https://en.wikipedia.org/wiki/Asynchronous_I/O
[2] http://www.masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch25lev1sec2.html
[3] 《I’ll Do It Later: Softirqs, Tasklets, Bottom Halves, Task Queues, Work Queues and Timers》. Matthew Wilcox
hellogmh
2015-07-21 21:56:49 +08:00
信号这种玩意儿容易打断程序执行逻辑,造成程序状态异常,如果正在执行一个系统调用,系统调用会被信号打断,重启系统调用又浪费CPU资源
aiqier
2015-07-21 23:31:13 +08:00
@nalaginrut 感谢您如此详细认真的回答,我还想请教一下,我只知道epoll与select,poll不同的地方在于,epoll每次阻塞在那里,每次返回,都只轮寻那些已经准备好了的io,那么是不是说,epoll除了有一个像select那样的“数组“,还有一个单独的队列,每次准备好的io就扔到这个队列中,然后返回,使得epoll每次只轮寻准备好的io。还有这个完成这个“如果io准备好,扔到队列里。”的操作是不是操作系统级别的中断完成的,而非进程级别的信号?
aiqier
2015-07-21 23:38:07 +08:00
@hellogmh 那epoll内部难道不是用信号这种东西,保证自己每次只轮寻准备好的io?
nalaginrut
2015-07-22 19:01:24 +08:00
@aiqier
1、有一点要注意,查询操作可以是阻塞也可以非阻塞,epoll_wait把timeout置零其实是非阻塞的

2、epoll的优势是每次只返回准备好了的一组fd,所以这个理解你是对的

3、epoll的在底层已经实现了事件机制,每当有数据到达某个fd的时候就会产生一个事件,然后事件会触发一个callback将该fd加入就绪队列(熟悉JS的同学对这种玩法应该非常熟悉)。当上层调用epoll_wait的时候就直接返回当前的就绪队列[1];
而select则要将上层传入的fd-set遍历一次,看哪些fd可以置位的就置位,然后返回,用户还得自己再遍历一次把置位了的fd找出来[2]——在这种实现方式下我们可以很容易猜到,数组内fd数量跟select的性能是成反比的,而epoll则几乎不受fd数量影响。

4、你的理解已经很接近了,epoll在底层维护两个队列,一个是就绪队列,还有一个是软中断用的队列,原理之前已经简单介绍过了。“如果io准备好,扔到队列里”这个步骤是基于操作系统级别的中断完成的,但跟平常的中断处理是不同的,如果想了解的话可以看我之前给的第三个连接。

[1] https://www.quora.com/Network-Programming/How-is-epoll-implemented
[2] https://www.quora.com/Network-Programming/How-is-select-implemented

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/207186

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX