Netty基础---IO多路复用知识(附高频面试题)

引子

对于现在网络上的资源,当我们打开浏览器搜索的时候,就会启动一个线程去请求该地址,然后将我们需要的数据返回给我们,随着时间的发展,资源越来越多,一开始碰到10个资源我们可以启动10个线程去获取,但是100个呢?100W个呢?不可能起100W个线程吧,直接能把你的电脑干报废了,首先我们要想到,不是时时刻刻都去获取资源的,所以给每一个资源都单独开一个线程不太现实,所以就需要一个线程来管理多个资源,达到资源的有效利用,就出现了我们IO多路复用,后面的面试题也涉及到了一点Linux底层的知识…

举个栗子吧

比如你开了一家酒店,正好100间房都住满人了,你们酒店特色服务就是半夜提供夜宵,这就需要你去问这些客人需要吃什么了,有三种方法

1.一个个打电话去询问,也就是轮询的机制,如果中间有个客人要求过多,你将会阻塞在这里

2.请100个员工,分别对应着100组客人,直接有效,也就是来一个客人我招一个员工

3.还有一种方法,就是你发一个公告,到了晚上你就监视着电话,想夜宵的在20:00 ~ 20:10、21:00 ~ 21:10才可以联系你,你做登记再去准备夜宵给客人送过去

看着上面的例子可能有些钢筋是不是说第二种方法高效,但是你有没有想过如果我有1000个房间呢?招1000个员工,我觉得是个正经酒店都干不出来这事,上面的例子只有第三种是比较符合实际的,也就是我们下面要讲的IO多路复用

什么是IO多路复用

多路:多条网络

复用:一个线程重复使用

IO多路复用与其它IO获取数据时的时序图

同步阻塞IO:

在这里插入图片描述
在我们使用此类型IO来获取数据时,当调用了read方法,会一直阻塞到获取到数据,期间用户线程处于阻塞状态

同步非阻塞IO

在同步阻塞的基础上,将socket设置为非阻塞,发出read请求后直接返回,然后用户线程将不断轮询去询问是否有数据可以传输
在这里插入图片描述
上面的缺点还是很明显的,不断轮询的过程也是对cpu的一种无端损耗

IO多路复用

也就是我么上面说的酒店的那个例子
在这里插入图片描述

高频面试题

1.什么是IO多路复用

我们线程通过监听某几个socket,当socket有数据时则通知用户线程做好读写准备,当没有socket就会阻塞该线程,交还cpu所占用的资源

2.为什么IO多路复用需要与非阻塞IO搭配使用

首先我们看看Linux文档对这个问题的描述
在这里插入图片描述
我在这结合网上的我查询到的答案以及上面的这段话,我这里给出一个结论,如果有问题欢迎大家指正

首先,select与read不是原子操作,当我调用select获取到的socket可读,然后我才去调用read去读,即使几率很小,但还是很有可能是读取不到数据的,这是其一,还有就是我的socket读缓冲里存在比较多的数据,使用非阻塞的IO可以循环读取,不用担心阻塞在read这里,转而去处理其它socket的数据,read读完之后再去处理;

还有对于**ET模式(边沿触发)的文件描述符(fd)LT模式(水平触发)的文件描述符(fd)**我在这里扩展一下

ET模式(边沿触发)的文件描述符(fd):ET模式是当epoll_wait检测到fd上有数据时只会通知客户端一次,后面则不再通知这一事件;也就是说服务端发送数据,I/O函数只会提醒一次客户端fd上有数据,以后将不会再提醒,在ET模式下必须一次将buffer里面的数据读取完毕或者遇到EAGAIN错误,所以当使用ET+阻塞IO时,很有可能会造成数据无人可读,造成资源浪费
LT模式(水平触发)的文件描述符(fd):只要这个fd内还有数据可读,每次epoll_wait都会返回它的事件

3.BIO的缺点

BIO的这个B,意为Blocking(阻塞),当进行服务端开发时,绑定端口号之后,我们会监听该端口,看看有没有客户端连接上这个服务端,也就是等待accept事件,这个accept会阻塞当前主线程,然后当有一个客户端连接上的话,程序就会拿到一个C/S连接的一个socket,对这个socket我们可以进行读写,但是这个读写也是会阻塞当前线程的,所以一般会使用多线程的方式来进行C/S的交互也就是一个客户端一个线程处理,但是这里又会出现一个大问题了,也就是我们常说的C10K问题,也就是说10W个客户端我难道要起10W个线程吗?还有就是不可能这10W个客户端的线程上下文切换也是吃不消的,所以这样肯定是不可以的,CPU直接炸裂,负载直接起飞

4.Java NIO怎么解决C10K问题

使用一个线程去监视多个socket,也就是我们的多路复用,Java为我们提供了一个NIO的包,包里有一个Selector(选择器),有客户端连接上服务端,也就是说我得到了socket,我将其注册进Selector,一个Selector可以注册多个socket,然后我的Selector去调用select方法去阻塞当前线程,然后查看有没有就绪的socket,如果当Selector发现了就绪的socket,就会唤醒主线程进行读写事件

5.kernel中select()原理

首先我给大家普及内存态与用户态的区别:当一个进程在执行用户自己的代码时处于用户运行态(用户态),然后我进程中的代码需要操作系统介入的话就会设计到对内核态的操作了,当数据从内核态拷贝到用户态或者用户态拷贝到内核态对于CPU来说都是一笔不小的开销,CPU可是很宝贵的,所以这里又涉及到一个零拷贝的知识点,后面我会单独开一篇文章来讲零拷贝

回到正题,当调用select(),会发生两件事,第一、检测到就绪状态的socket,跑到socket做个标记,代表这个socket已经就绪;第二、唤醒线程,因为select()只返回就绪socket的就绪数量,但是我线程并不知道哪个就绪了,所以需要重新遍历fd_set,寻找select()做的标记,再进行下面的步骤

每次调用select(),都涉及到用户态与内核态的切换,也需要传递(对应socket生成的文件描述符集合)fd_set,也就是fd的一个set集合,根据这个fd集合去检查socket的状态,时间复杂度为O(n),检查完后如果有就绪状态的socket则直接返回,不会阻塞当前调用线程,如果没有则需要阻塞当前调用线程,直到有就绪的socket才会唤醒当前调用线程执行下面的步骤

select()对监听的socket数量也是有限制的,最多可以监听小于1024个socket,因为fd_set是一个bigmap,默认为1024个bit,因为要遍历2次fd_set,所以1024应该是开发者所能找到数量与性能的临界点

操作系统调度:CPU同一时间段只能运行一个进程,未挂起的线程都在工作队列中,都有机会争取到CPU的时间片执行程序,挂起的线程(java的阻塞线程)会直接移除工作队列,无法竞争CPU时间片

操作系统中断:让CPU正在执行的程序保留上下文,先让出CPU给中断程序,然后中断程序执行相应的权限

socket结构:读缓存、写缓存和等待队列

扩展:对于select()函数是否会一直轮询socket问题,

第一阶段:select第一遍轮询,如果没有发现就绪的socket,会把该线程保存进socket的等待队列中,然后把当前线程从CPU的工作队列移除,也就是把当前线程挂起(阻塞)

第二阶段:数据通过TCP/IP(带端口号)网络传输,最后通过DMA(Direct Memory Access)存到内存中,整个过程可以做到CPU不参与,也就是我们说的零拷贝方式,当传输完毕了之后,触发中断,让CPU正在执行的程序保留上下文,先让出CPU给中断程序,然后中断程序执行分析刚刚网络传输过来的数据包步骤,通过端口号分析出来这个数据包是哪个socket的数据,将数据放到socket的读缓冲区中,再去检查等待队列,看看有没有等待中的线程,如果有则把等待队列中挂起的线程加入工作队列,去争夺CPU时间片,执行上面轮询socket的步骤,然后读取数据

6.poll()原理与epoll()原理

select()返回的是bigmap的fd_set,而poll()返回的是一个数组,主要是为了解决select()只能监听1024个以下的问题,其它的与select()没什么区别

这个epoll()就厉害了,它是总结了select()与poll()的缺点而开发出来的

1.select()与poll()每次调用都需要提供需要监听fd_set,而且程序中我们是死循环调用select()与poll(),况且不是每个socket都在变化,可能一段时间只有那么两三个有数据变化,但是这两个函数是把所有的fd_set都拷贝了且kernel层面不会保存任何信息,也就是不断的经历用户态与内核态的切换,这非常的耗费性能

2.select()与poll()每次调用返回的是有几个socket就绪,但是你不知道是哪几个,还需要线程去检测,就相当于遍历了两次,然后再去执行下面的逻辑

Eventpoll对象:通过epoll_create()去创建,有两块区域,一块负责存储fd_set,一块存储socket就绪的列表,等待队列(存储的是调用epoll_wait的线程)

epoll_ctl:增删改socket_fd_set

epoll_wait:默认阻塞调用线程可以设置非阻塞,直到有socket就绪,它才会返回(返回0表示没有就绪进程、n表示有n个就绪的进程、-1表示异常),在调用epoll_wait时,会传入一个epoll_event事件数组,在它正常返回之前,他会把就绪列表内的socket拷贝到数组中,然后上层应用就可以拿到就绪的socket了

为了解决这两个问题,Eventpoll对象相当于我们再内核区开辟了一块区域,

1.当我们调用epoll_ctl来添加一个新的socket,内核程序会把当前Eventpoll对象添加到socket的等待队列

2.等到socket对应的客户端(网络->DMA->内存)发送完数据后,触发中断程序,让CPU正在执行的程序保留上下文,先让出CPU给中断程序

3.中断程序还是将内存中刚刚获取到的数据拷贝到读缓冲区中,直接去检查socket的等待队列,发现等待队列中存在的是Eventpoll对象引用,然后它就把socket引用追加到Eventpoll就绪链表的末尾,再去检测Eventpoll对象的等待队列,有没有epoll_wait,有的话就去竞争CPU时间片,去执行Java层面的代码

对于多路复用这一块的知识点我们先讲到这里,有什么错误欢迎指正

完成:2021/3/28 13:58 ALiangX

转载请标注原作者,谢谢你们的支持,能给个小心心吗?
在这里插入图片描述

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沉淀顶峰相见的PET

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值