IO
最初级的 I/O 复用
所谓的 I/O 复用,就是多个 I/O 可以复用一个进程。
采用非阻塞的模式,当一个连接过来时,我们不阻塞住,这样一个进程可以同时处理多个连接了。
比如一个进程接受了 10000 个连接,这个进程每次从头到尾的问一遍这 10000 个连接:“有 I/O 事件没?有的话就交给我处理,没有的话我一会再来问一遍。”然后进程就一直从头到尾问这 10000 个连接,如果这 1000 个连接都没有 I/O 事件,就会造成 CPU 的空转,并且效率也很低,不好不好。
升级版的 I/O 复用
上面虽然实现了基础版的 I/O 复用,但是效率太低了。于是伟大的程序猿们日思夜想的去解决这个问题…终于!
我们能不能引入一个代理,这个代理可以同时观察许多 I/O 流事件呢?
当没有 I/O 事件的时候,这个进程处于阻塞状态;当有 I/O 事件的时候,这个代理就去通知进程醒来?
于是,早期的程序猿们发明了两个代理—select、poll。
select、poll 代理的原理是这样的:
当连接有 I/O 流事件产生的时候,就会去唤醒进程去处理。但是进程并不知道是哪个连接产生的 I/O 流事件,于是进程就挨个去问:“请问是你有事要处理吗?”……问了 99999 遍,哦,原来是第 100000 个进程有事要处理。那么,前面这 99999 次就白问了,白白浪费宝贵的 CPU 时间片了!痛哉,惜哉…
- select 是第一个实现 (1983 左右在 BSD 里面实现)
- 1997 年实现了 poll.
- select 与 poll 原理是一样的,只不过 select 只能观察 1024 个连接,poll 可以观察无限个连接。
上面看了,select、poll 因为不知道哪个连接有 I/O 流事件要处理,性能也挺不好的。
那么,如果发明一个代理,每次能够知道哪个连接有了 I/O 流事件,不就可以避免无意义的空转了吗?
于是,超级无敌、闪闪发光的 epoll,于 5 年以后, 在 2002 年被大神 Davide Libenzi 发明出来了。
epoll IO 多路复用
epoll 代理的原理是这样的:
当连接有 I/O 流事件产生的时候,epoll 就会去告诉进程哪个连接有 I/O 流事件产生,然后进程就去处理这个进程。如此,多高效!
epoll 可以说是 I/O 多路复用最新的一个实现,epoll 修复了 poll 和 select 绝大部分问题, 比如:
epoll 现在是线程安全的。epoll 现在不仅告诉你 sock 组里面数据,还会告诉你具体哪个 sock 有数据,你不用自己去找了。
可是 epoll 有个致命的缺点,只有 linux 支持。于是其他的平台实现类型的多路复用,比如 BSD 上面对应的是 kqueue, win 下对应的 iocp。
epoll 和 select/poll 区别
简单说 epoll 和 select/poll 最大区别是
- epoll 内部使用了 mmap 共享了用户和内核的部分空间,避免了数据的来回拷贝
- epoll 基于事件驱动,epoll_ctl 注册事件并注册 callback 回调函数,epoll_wait 只返回发生的事件避免了像 select 和 poll 对事件的整个轮寻操作。
Nginx 异步,非阻塞,IO 多路复用
Nginx 这样出众,正是他采用了异步,非阻塞,IO 多路复用。
Nginx 之前是单进程的。看下他的进程。1 个 master 进程,2 个 work 进程。
$ pstree |grep nginx |-+= 81666 root nginx: master process nginx | |— 82500 nobody nginx: worker process | -– 82501 nobody nginx: worker process
每进来一个 request,会有一个 worker 进程去处理。但不是全程的处理,处理到什么程度呢?处理到可能发生阻塞的地方,比如向上游(后端)服务器转发 request,并等待请求返回。那么,这个处理的 worker 不会这么傻等着,他会在发送完请求后,注册一个事件:“如果 upstream 返回了,告诉我一声,我再接着干”。于是他就休息去了。这就是异步。此时,如果再有 request 进来,他就可以很快再按这种方式处理。这就是非阻塞和 IO 多路复用。而一旦上游服务器返回了,就会触发这个事件,worker 才会来接手,这个 request 才会接着往下走。这就是异步回调。