大家好呀,我是码财同行。
在网络开发中,I/O复用是经常被提及的一个技术名词。今天,我们就来聊聊它。
来看一组I/O模型的示意图:
上图中,I/O 模型有3种,他们是
- 阻塞 I/O
- 非阻塞 I/O
- I/O 复用
无论哪种类型,都分为2个阶段:
- 等待网卡数据到达
- 将数据从内核空间拷贝到程序调用方的用户空间
现在思考一下,这里的阻塞
是阻塞谁?
答案是调用方的线程或进程。
假如有个客户端连接过来,看看调用方的线程或进程是什么表现。
第一种,阻塞I/O,全程阻塞,线程只要调用 socket 的I/O 函数(如read),线程或进程就卡住了,其他什么事情都干不了:
- 做不了其他逻辑,如协议包的逻辑处理、发送Ack包等;
- 服务不了其他连接,读不了连接数据。这对于一个服务器而言是不可接受的,服务器的基本宗旨是服务大量的连接(玩家),不能让一个玩家做无效的独占。
那这个卡住(阻塞)会持续多久?
不知道。可能很快,如这条连接上立即有新的数据过来;也可能很慢,客户端用户半天没什么操作,也就没有交互数据。
实际上,这种情况一个进程或线程只能为一个连接服务,是对系统资源的很大浪费。
事实上一个连接上,大部分时间是没有数据需要处理的。
因此,一个线程或者进程资源,服务多个连接完全没有问题。这样,也能节约服务器的系统资源。这相当于通信上的时分复用
,一段时间服务这个连接,一段时间服务那个连接,交替进行,看起来像是同时(并发)服务多个连接。
要达到时分复用的效果,I/O方案就必须是:
第二种,非阻塞I/O。这个时候,如果线程调用 socket 的I/O 函数(如read),有数据就读取,没有数据就返回。
这种方式能解决卡住(阻塞)的问题么?
可以。因为读不到数据就返回了嘛,调用方线程可以处理其他逻辑,如读取其他连接上的数据或者做逻辑处理。
现在,假如有多个连接,都被设置成了非阻塞I/O,服务器同时服务这些连接。
那应该采取什么样的方式及时得读取所有连接上过来的请求数据呢?
这时候,有几种方案可以解决这个问题:
1)轮询
第一种方法,对多个连接进行人工轮询,读到数据就处理,读不到就遍历下一个。
那这种方式最大的问题是什么?
效率低。
效率为何低呢?因为I/O操作(如read)一般是系统调用,轮询时候需要不停的在用户空间和内核空间进行切换;此外,轮询本身消耗 CPU,连接数如果成千上万,依次遍历的效率可想而知。
所以非阻塞I/O一般不单独使用。
2)I/O复用
第二种方法是让操作系统帮我们探测有数据的连接,通知我们,然后我们去处理。这就是I/O 复用,也是服务器主流的处理高并发的方案。
I/O复用本质上,是让操作系统对多个连接上I/O事件的并发探测,所谓复用,就是复用同一个调用方资源(如同一个线程或进程)来并发服务多个连接,也类似时分复用的思想。 如果不复用,像前面说的阻塞I/O模式,那只能让一个调用方资源对应一个连接了。
这里的处理只是数据上的并发通知,实际数据的读取还不是并发的,当然我们可以用多个工作线程或工作进程来并发读数据和处理消息。
最后再提一点,回过头看文章一开始的图,细心的同学可能发现了:
I/O复用虽然从使用上是两个阶段(探测有无数据,读取数据),但也都是阻塞的啊?如果一直阻塞,什么时候做逻辑处理?
其实,这里的阻塞是对所有连接的阻塞
,当所有连接上都没有数据时,才是阻塞的。
即使所有连接上都没有新数据传输过来,也有几种方案可以解决这个问题:
- 设置依次I/O复用的超时,短时间I/O超时(没有数据)之后,就可以做逻辑处理了;
- 可以把一些逻辑转化成I/O的形式,例如linux上定时器就可以用文件fd来实现(muduo);
- 可以把I/O操作放在单独的一个网络线程中,逻辑处理再开一个线程或进程处理,这就是大名鼎鼎的 Reactor 模式;
这篇文章到这里就结束啦。
好了,看了这么多,一定很费脑力吧。来个笑话放松一下 :)
【笑话一则】我每天都会做仰卧起坐,每天晚上一个仰卧,每天早上一个起坐。
感谢您花时间阅读这篇文章!如果觉得有趣或有收获,请来个关注、评论、点赞吧,您的鼓励是我持续创作的动力,蟹蟹!
| 往期推荐