看这一篇IO多路复用面试专题就够了!最全面最详细的解答!

多路复用问题汇总

  1. BIO有什么缺陷?
  2. 针对C10K这样的需求,NIO靠什么解决问题?
  3. 多路复用操作系统函数select(…)工作原理?
  4. 多路复用操作系统函数select(…)默认监听socket数量为什么是1024?
  5. 多路复用操作系统函数select(…)第一遍O(N)未发现就绪socket,后续再有某个socket就绪后,select(…)如何感知的?是不停轮询么?
  6. 多路复用操作系统函数poll(…) 和select(…)主要区别是什么?
  7. 为什么会有epoll这个技术,它产生的背景是什么呢?
  8. epoll函数的工作原理是什么?
  9. eventpoll 对象的就绪列表数据是如何维护的呢?
  10. eventpoll 对象中存放需要检查的socket信息是采用的什么数据结构?为什么?
一、面试官:BIO有什么缺陷?

面试者: BIO中的"B",它表示的是blocking的意思,就是"阻塞"。

作为服务端开发的话,我们使用severSocket绑定完端口号之后,我们会进行监听该端口,等待accept事件,accept会阻塞当前主线程,当我们收到accept事件时,程序就会拿到一个客户端与当前服务端连接的socket。

针对这个socket我们可以进行读写,但是呢…这个socket读写方法都是会阻塞当前线程的。

一般我们会使用多线程的方式来进行C/S交互,但是这样就很难做到C10K。
比如说:1w客户端就需要服务端1w个线程去支持,这样的话且不说cpu肯定就会爆炸了…然后就线程上下文切换,也会把机器负载 给拉飞的。

二、面试官:NIO它靠什么解决的C10K问题呢?靠什么去解决的多客服端的问题?

面试者:阻塞IO之所以需要给每个socket长连接指定一个线程,就是因为它阻塞嘛。NIO它具备了非阻塞特性了,就可以用1个线程去检查N个socket。

在Java代码层面,NIO包提供了一个选择器selector,我们需要把检查的socket注册到这个selector中,然后主线程会阻塞在selector#select方法里,当选择器发现某个socket就绪了,就会唤醒主线程。然后我们可以通过selector获取到就绪状态的socket进行相应的处理。

站在Java层面聊IO这件事,没有太大意义。

三、面试官:多路复用操作系统函数select(…)工作原理?

面试者:我们每次调用kernel#select函数,它都会涉及到 用户态/内核态的切换,还需要传递需要检查的socket集合,其实就是需要检查的fd。
因为咱们的程序都是运行在Linux或者Unix操作系统上,这种操作系统上,一切皆文件,socket也不例外。这里传递的fd其实就是文件系统中对应socket生成的文件描述符。

当操作系统的select函数被调用以后,首先会按照fd集合,去检查内存中的socket套接字状态,这个复杂度是O(N)的。然后检查完一遍之后,发现有就绪状态的socket那么直接返回,不会阻塞当前调用线程,否则就说明当前指定fd集合对应的socket没有就绪状态的。

那么就需要阻塞当前调用线程了,直到有某个socket有数据之后,才会唤醒线程。

四、 面试官:监听socket数量有没有限制?

面试者:它默认最大可以监听1024个socket。

因为fd_set这个结构它是一个bitmap位图结构,是一个长二进制数,这个bitmap默认长度时1024个bit,想要修改这个长度的话非常麻烦。还需要重新编译系统内核。这种针线活一般人操作不来。

另外认为默认值给1024个bit,它是处于性能考虑吧。因为select函数它检查到就绪状态的socket后,做了两件事:

  1. 遍历fd_set集合,将有数据的FD置位,表示当前对应的socket就绪了。
  2. 返回select函数,对应的也就是唤醒线程。返回int结果值,表示有几个socket处于就绪状态。

但是具体哪个并不知道,所以接下来又是一个O(N)系统调用,检查fd_set集合中每一个socket的就绪状态。这也涉及到用户态/内核态的来回切换。
系统调用涉及到参数的数据拷贝, 如果bitmap数据太庞大,也不利于系统调用速度。

五、多路复用操作系统函数select(…)第一遍O(N)未发现就绪socket,后续再有某个socket就绪后,select(…)如何感知的?是不停轮询么?

面试者:我觉得要回答这个问题,还得先铺垫一些东西。
操作系统调度和操作系统中断的一些知识。
咱先说下这个调度吧:
CPU同一个时刻,它只能运行一个进程,这个毫无疑问了(单核心),操作系统最主要的任务就是系统调度嘛,就是有N个进程,然后让这个N个进程在CPU上切换执行,未挂起的进程都在工作队列内,都要机会获取CPU执行权,挂起的进程,就会从这个工作队列内移除出去,反映到Java层面就是线程阻塞了,Linux系统线程其实就是轻量级进程。

再说下这个操作系统中断:
这个非常重要。就比如说,咱们用键盘打字,如果CPU正执行着其他程序一直不释放,那咱们这个打字是不是也没办法打了呢?但是咱们都知道,不是这样的。 因为就是有了这个系统中断的存在,在你按下一个键之后会给这个主板,发送一个电流信号,主板感知到以后,它就会触发这个CPU中断。

所谓中断,其实就是让CPU正在执行的进程先保留程序上下文,然后避让出CPU给中断程序让道,中断程序就会拿到CPU执行权限,进行相应的操作。

回到这个问题:
这个select函数,它第一遍轮询,它没有发现就绪状态的socket,它就会把当前进程,保留给需要检查的socket的等待队列中。也就是说这个socket结构,它有三块核心区域,分别就是:读缓存、写缓存还有这个等待队列。
select函数,它把当前进程保留到每个需要检查的socket#等待队列之后,就会把当前进程从工作队列移除了,其实就是挂起当前进程。所以select函数也就不会再运行了嘛。

这个阶段结束了,然后再说下一个阶段:
假设我们客户端往当前服务器发送了数据,数据通过网线到网卡,网卡再到DMA硬件的这种方式直接将数据写到内存里,整个过程CPU它是不参与的。当数据完成传输以后,它就会触发网络数据传输完毕的中断程序了,这个中断程序会把CPU正在执行的进程顶掉,然后CPU就会执行咱们这个中断程序的逻辑了。

根据内存中它有的数据包,分析出数据是哪个socket的数据,通过端口号就能找到它对应的socket实例。找到socket实例以后,就会把数据导入到socket的读缓冲区里,导入完成以后,它就开始去检查socket的等待队列,是不是有等待者?如果有就把等待者移动到工作队列,中断程序到这一步就执行完了。咱们的进程又回到工作队列, 又有机会获取到CPU时间片了。然后当前进程执行select函数再次检查就会发现有这个就绪的socket了。

六、多路复用操作系统函数poll(…) 和select(…)主要区别是什么?

select缺点:

  1. 1024 bitmap
  2. fd_set不可重用
  3. 用户态<->内核态 开销
  4. O(n)再次遍历

面试者:其实最大的区别就是传参不一样了,select它使用的是bitmap表示需要检查的socket集合,poll使用的是数组结构,表示需要检查的socket集合,主要就是为了解决这个select bitmap长度时1024的这个问题嘛。poll使用数组就没有这个限制了,它就可以让咱们线程监听超过1024个socket限制。

poll数组的结构体:
struct pollfd { int fd; short events; short revents;}
解决了select 1和2 两个缺点

七、为什么会有epoll这个技术,它产生的背景是什么呢?

面试者:epoll它主要就为了解决select和poll函数的缺陷。先说说select和poll的缺点:

  1. 这两系统函数每次都是需要我们提供它所有需要监听的socket文件描述符集合,而且咱们的程序主线程都是死循环调用 select/poll函数的,这里涉及到用户空间数据到内核克空间拷贝的过程,这个相对来讲,还是比较消耗新性能的。
  2. select和poll函数它的返回值是个int整形值,只能代表有几个socket就绪或者是有错误了,它没办法表示具体是哪个socket就绪了。这就导致程序唤醒以后,还需要新的一轮系统调用去检查哪个socket是就绪状态的,然后再进行socket数据处理逻辑,在这已经走了不少弯路了。

这也是epoll的产生背景吧。

八、epoll函数的工作原理是什么?

epoll_create
epoll_ctl
epoll_wait

面试者:解决上面说的问题,就需要epoll函数再内核空间内,创建一个对应的数据结构去存储一些数据,这个数据结构其实就是epoll_event对象,它是通过系统函数epoll_create()去创建,就会得到epfd文件号,相当于我们再内核开辟了一小块空间,并且我们也知道这块空间的位置。

先说一下 epoll_event的结构,它主要是两块重要的区域,其中一块是存放需要监听的socket_fd描述符列表,另一块就是就绪列表,存放就绪状态的socket信息。

还另外两个重要的函数epoll_ctl()和epoll_wait()。
epoll_ctl它可以根据epfd号去增删改内核空间上的eventpoll对象列表;epoll_wait()它主要的参数就是epfd,表示此次系统调用需要监测的socket_fd集合,是eventpoll中已经指定好的那些socket信息。epoll_wait函数默认情况下会阻塞调用线程,直到某个socket就绪以后epoll_wait才会返回。

九、eventpoll对象的就绪列表数据是如何维护的呢?

面试者:前面已经说了socket对象,它有三块区域嘛,读缓冲区、写缓冲区和等待队列。epoll跟select调用流程非常相似,当我们调用epoll_ctl添加一个需要关注的socket,其实内核程序就会把当前eventpoll对象追加到这个socket#等待队列里,当socket对应的客户端发送完数据,写入内存之后触发中断程序,最后检查这个socket的等待队列,发现这个socket#不是进程,是一个eventpoll对象引用,它会根据这个引用讲当前socket引用追加到eventpoll的就绪链表的末尾。

还有一个是eventpoll 有一块空间是eventpoll#等待队列,这个等待队列它保存的就是调用了epoll_wait的进程了。

十、epollevent对象中存放需要检查的socket信息是采用的什么数据结构?为什么?

epoll_wait返回 是int类型,也没有表示出来哪个socket是就绪的,获取就绪的socket是怎么实现的呢?

面试者:调用epoll_wait函数的时候会传入一个epoll_event事件数组指针,epoll_wait函数正常返回之前,会把就绪的socket事件信息拷贝到这个数组里,返回到上层程序,这样就可以通过这个数组拿到就绪列表了。

epoll_wait默认是阻塞的,也可以设置成非阻塞。

存放的集合信息是采用 红黑树数据结构。

  • 4
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值