小言_互联网的博客

以后谁再问你什么是多路复用io,那他就算踢到钢板上了

330人阅读  评论(0)

io是很多Java / python / go开发人员的重灾区,如果平时开发没接触过,可能就只知道个阻塞/非阻塞、同步/异步,厉害一点的再来个多路复用

很多同学对这些概念根本没有明确的理解,真就是朗读并背诵全文~

今天,我就带着你探索一下io的发展史,以后再有人问你io,那他就是纯纯踢到钢板上了

网络上关于io的文章多如牛毛,但是下面这段话你可能是第一次看到(看得懂就看,看不懂就跳过,该你懂的时候自然会懂):

不管是windows还是linux,所有牵涉到 io 的操作,都无法由应用程序直接完成,把文件操作权限开放给用户是很危险的,想执行io操作,必须使用操作系统内核提供的函数,但这些函数不需要我们亲自调用,Java已经帮我们做好了封装,我们在开发时调相关api即可,如下图这两个包

io就是简单的read / write,而nio引入三个新的概念:

  • Buffer:数据容器;
  • Channel:这个东西太抽象了,中文名叫通道,知道通过它能完成内核间的io操作就行;
  • Selector:nio 实现多路复用的基础;

ok废话说完,我们先把相关概念梳理清楚

  • 同步或异步:同步即有序运行,当前任务执行完,才会执行下个任务;异步则相反,其他任务不需要等待当前任务执行完,通常依靠事件、回调机制来实现任务间次序关系;
  • 阻塞与非阻塞:在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如数据读取、写入;而非阻塞则是不管 IO 操作是否结束,直接返回。

光看概念脑瓜子嗡嗡的吧,没关系,请看大屏幕~

干饭

公司附近有家网红饭店,每天去他们家吃饭的小姐姐特别多,当然你不要误解我的意思,我是想说他们家饭很好看,啊…不对,我的意思是他们家小姐姐很好吃,啊呸…也不对

总之我经常去,期间我的点餐方式经历了以下几个版本

  • v1.0:刚开始去的时候看别人都在排队点餐,身为老实人的我只能等前面人点完了再点,然后去取餐口等着取餐,有时候需要等15分钟才会取到餐,但是在取餐口站着可以看到更多小姐姐,所以最初也没在意;
  • v2.0:后面某一天发现取餐口上面有一个屏幕,显示了当前的取餐号顺序,哇 我恍然大明白,从那开始,我每次点完餐就找个小姐姐多的地方坐着,每隔5分钟就来取餐口看下是否轮到我取餐;
  • v3.0:久而久之,每天看小姐姐也看腻了,有一天发现桌子上有个二维码,哇 我又恍然大明白,原来可以扫码点餐啊,而且通过扫码点的餐,服务员会把餐送到你的位置;

取餐和io之间的关系

  • v1.0(bio,blocking io):排队点餐、等餐,类似bio,同步阻塞,线程提交io操作后,在io操作完成前不能返回,也不能去干其它事,只能憨憨地等
  • v2.0(nio,non-blocking io):排队点餐,时不时去取餐口查看是否轮到自己取餐,类似nio,同步非阻塞,线程提交io操作后可以先返回,但是需要主动找操作系统获取结果(nio可以构建多路复用io,这是本文的重点);
  • v3.0(nio2,或者叫aio,asynchronous io):扫码点餐,点好餐后找个位置坐着看小姐姐即可,餐好了服务员会送过来,类似aio,异步非阻塞,在nio基础上增加了事件和回调机制,操作系统准备好数据后,会主动通知线程。

关于操作系统对于io到nio的函数支持,演变过程比较复杂,你需要先搞懂操作系统如何实现零拷贝,请移步 零拷贝技术浅浅析

关于io / nio,其实就上面这点东西,千万别被绕晕了,今天我们的主题是 多路复用io

老规矩,先抛几个兄弟们最最最常见的疑惑,这些疑惑在下文都会找到答案:

  • 什么是多路复用?
  • 为什么需要多路复用?
  • 多路复用解决了什么问题?
  • 到底什么是多路复用,复用的是什么?

传统的网络通信都是使用socket,流程如下:

  1. 创建socket
  2. 将当前服务器ip和端口绑定到socket
  3. 监听端口,通过accept处理请求

流程来看没啥毛病,但是你要知道,accept是阻塞的,也就是说同时只能处理一个请求,如果你想同时处理多个,可以用多线程,但这样又会引发另一个问题:操作系统中的线程多了后,需要耗费大量资源来管理线程和上下文切换(这里啰嗦一句,不管什么场景,线程并不是越多越好,到达某个阈值后,线程越多效率反而越低)

所以,想优雅地同时处理多个请求,操作系统内核必须实现单个线程监听多个socket,即 io多路复用

linux系统对多路复用的支持有三种:

  • 多路复用:select
  • 多路复用pro:poll
  • 多路复用pro max:epoll

来看看这三种函数的实现,关于他们三个的性能也就大概清楚了

select

//返回值是已就绪文件描述符个数
int select (int __nfds, fd_set *__readfds, fd_set *__writefds, fd_set *__exceptfds, struct timeval *__timeout)

参数解释:

  • _ _nfds:监听的文件描述符数量(不要纠结文件描述符是什么东西,可以理解成每个io操作都对应一个文件描述符);
  • _ _readfds、__writefds、__exceptfds:指定多路复用机制监听的事件类型,这三个分别是读数据事件、写数据事件、异常事件;
  • _ _timeout:监听时阻塞等待的超时时长;

select通过监听多个文件描述符来管理多个连接,一次能监听1024个(默认值),当select函数返回后,遍历描述符集合找到已就绪的描述符进行下一步处理

至此,一个线程只能管理一个连接的痛点算是解决了,但还存在两个问题:

  • 每次监听的描述符数量有限,虽然可以通过修改宏文件修改,但这种做法太极端;
  • 我们需要不断执行select函数来获取已就绪的文件描述符,遍历过程也需要消耗cpu性能;

所以linux引入了poll,解决文件描述符限制的问题

poll

//返回值也是已就绪文件描述符个数
int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);

参数解释:

  • _ _fds:pollfd 结构体数组,包含了要监听的文件描述符和要监听的事件类型;
  • _ _nfds: pollfd 结构体数组的数量,没有大小限制;
  • _ _timeout:监听时阻塞等待的超时时长;

相对于select,poll其实就突破了文件描述符的限制,但仍然需要我们遍历每个文件描述符来检测是否就绪

所以,Linux2.6引入了epoll

epoll

epoll有三个函数epoll_create、epoll_ctl 和 epoll_wait

//创建epoll实例,size表示监听多少个文件描述符
int epoll_create(int size)//将连接加入epoll监听列表,参数分别表示:
//epoll_create()的返回值
//需要执行的修改操作
//需要监听的文件描述符
//监听的事件类型
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)//阻塞等待返回已就绪的文件描述符,参数分别表示:
//epoll_create()的返回值
//事件的集合
//events大小
//超时时间
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

 

epoll模型支持自定义监听的描述符数量,也可以直接返回就绪的描述符

大名鼎鼎的redis在Linux系统的实现就是用的epoll模型,所以即使是单线程,也能轻松应对高并发的客户端访问


说了这么多,那到底什么是io多路复用?复用的是什么?

这两个问题我觉得很难解释,可能等以后真正接触到操作系统层面的知识了,才会真正悟透吧

个人对于复用的看法:

首先肯定不是对socket的复用,一个请求对应一个socket耶稣来了也改变不了;

也不是对线程的复用,但好像也可以这么理解,因为毕竟是一个线程管理多个socket;

所谓复用,可能并不是非得复用某一个东西,我觉得是通过一次到内核的请求实现对多个socket的管理这个行为


关于上面提到的各种函数了解即可,不用太深入研究,我也是翻了好些文档才得到这些信息,没必要在上面花太多时间,不然脑瓜子嗡嗡的~,我现在脑瓜子就嗡嗡的,果然懂得越多不懂得就越多。。。

ok我话说完


转载:https://blog.csdn.net/qq_33709582/article/details/123137787
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场