java的IO模型和IO多路复用的底层原理(select,poll,epoll)

在这里插入图片描述虽然有很多种io模型,但是对于java来说有三种,分别是BIO,NIO,AIO三种模型。是java语言对操作系统的各种IO模型的封装。先了解什么是同步异步,阻塞非阻塞。

同步:就是调用者调用被调用者时,被调用者没有处理完调用之前什么结果都不返回,没有反馈。
异步:当调用者调用被调用者时,被调用者会立即给调用者一个反馈,表示已经收到请求,但是并不会返回结果。此时调用者可以做其他的事情,当被调用者处理完请求之后,就会将结果通过事件或者回调机制返回给调用者。

两者最大的区别就是,异步时:调用者不需要获取到结果之后才进行其他操作,被调用者会通过回调机制来向调用者返回结果。

阻塞:当调用者发起一个请求时,调用者一直等待被调用者返回结果的过程中,当前线程会被挂起,无法执行其他的任务,也就是只有条件就绪时才能继续执行。
非阻塞:当调用者发起一个请求时,不需要去等待结果的返回,可以先干其他的事情。

也就是说:同步异步是相对于被调用者来说的,被调用者收到请求后,根据被调用者处理结果时是否需要调用者等待,如果调用者等待被调用者 的结果,就是同步的,如果调用者不等待而去干其他的事情,就是异步的。

阻塞非阻塞是相对于调用者来说的。调用者如果等待返回结果,就是阻塞状态,如果调用者此时发情请求后,调用者不等待结果而去执行其他结果,那么就是非阻塞的。
以烧水为例。当用普通水壶烧水时,烧水的人(调用者)用水壶烧水(被调用者),如果烧水的人一直等待着水烧开才去看电视,就是同步阻塞的,

而将水壶烧水,自己去看电视,并且时不时的去查看水有没有烧开,就是同步非阻塞的(因为此时被调用者可以去干其他事情了,所以是非阻塞的,并且普通的水壶没有提醒功能,所以是同步的)。

如果买了一个高档的水壶。水烧开之后会报警提醒。如果此时烧水的人可以直接去看电视,等待水壶响了之后,就可以判断烧开了(对于被调用者高档水壶,当收到调用请求时开始烧水,烧水开了之后,就通过声音给调用者一个提示,所以是异步的)就是异步非阻塞的。

如果使用高档的水壶,调用者还再水壶面前傻等的水烧开,就是异步阻塞的

1BIO阻塞式IO

在这里插入图片描述阻塞式io就是一个请求一个响应的模型,只有被调用者返回结果时,才接着处理下一个请求,所以当要解决多个请求时,为了提高效率,就需要多个线程来处理。一般池化技术来解决BIO,如FixThreadPool这种固定线程池大小,来保证有限的资源控制。

2 NIO同步非阻塞式IO

非阻塞就是相对于调用者来说不需要等待结果的返回,可以处理其他的事情。服务器的实现模式为一个线程处理多个请求,客户端发送的请求都会注册到多路复用器上,然后多路复用器轮询到有io请求就进行处理。对于低并发的应用程序使用同步阻塞式io,使用多个线程处理多个请求,而对于高负载和高并发的应用,应使用NIO的非阻塞模式来开发。
对于NIO有三个核心的组件,分别是channel,buffer和selector.
在这里插入图片描述在这里插入图片描述

channel类似流,每一个channnel都对应一个buffer缓冲区,而每一个channel由注册到selector上,由selector来决定哪个请求由线程处理,select可以对应多个线程也可以单个线程,NIO的buffer和channnel可以读也可以写。

问题:IO和NIO的区别?
1io流是阻塞式的io,NIO是非阻塞的io
,阻塞是相对于调用者来说的,当调用时不能干其事情,就是阻塞的。非阻塞的是调用者发起请求后,不需要立马等到结果就可以去做其他的请求,直接提高了效率。对于非阻塞的io,单线程从channel中读取数据到缓冲buffer中的同时可以做其他的事情,当数据完全读取到buffer后,线程再继续处理数据。非阻塞写也是这样,一个线程请求写入一些数据到某个通道中,步需要等待它完全写入,当前线程可以去做别的事情。而io是阻塞的。就意味着进行读写时线程就会被阻塞。完全读入时才可以做其他的事情。
2IO是面向流的,NIO是面向缓冲区的。
再面向流的io中,直接将数据写入或者读到Stream对象中而NIO读到Buffer中进行操作,再NIO库中,所有的数据都是用缓冲区处理的,再读写数据时直接读写到缓冲区。都是通过缓冲区来实现的
3NIO通过channel进行读写。
通道是双向的,可以读也可以写,而流是单项的。无论是读还是写,channel都是和buffer进行交互的,因为buffer的存在,channel可以实现异步的读写。
4NIO有选择器selector,可以使用单线程来处理多个通道
所以NIO一般速度很快,避免多线程下线程的切换问题。
综上:NIO适合于连接数目多且连接事件比较短的场景下,如聊天服务器,弹幕系统等。

3AIO异步非阻塞

异步就是针对被调用者来说,收到调用请求后,只回复一个收到调用的响应,然后完成处理之后,再将结果返回给调用者。当收到请求之后,操作系统才会通知相应的线程(调用者)进行后续的操作。因此适用于连接时间长且连接数目多的场景。

IO多路复用的引入思路

首先针对于服务器而言,如果客户端有多个请求的解决方案?
方案1,最直接最简单的实现就是创建多个线程来对应多个请求,提高请求的处理速度,但是一些特定的场景下,由于多个线程之间来回的上下文切换,会导致系统开销加大,时间长。
方案2,使用单线程,使用单线程来处理的话,可以避免多个线程之间来回的切换,如redis就是单线程的,底层是一个文件事件处理器,采用io多路复用模型,有多个socket来对请求进行监听,当有多个socket都有请求来的时候,需要判断哪些socket有请求,需要针对有请求的socket进行一个一个的处理当前socket中的请求,就涉及到io多路复用的原理。

再linux系统中,一切皆文件,每一个请求都会有一个网络连接,每一个网络连接再linux系统中都以文件描述符fd的方式进行识别,如有连接A,B,C,D,E,5个连接socket,供请求连接。
在这里插入图片描述此时可以接简单粗暴的写出伪代码

//1先获取到所有请求文件的文件描述符,记录下有哪些请求描述符
int[] fds=new int[5];
//2对所有的文件请求进行扫描,看看有没有请求
while(true){
    
    
	for(int i=0;i<5;i++){
    
    
		if(fds[i] 有请求数据){
    
    
			1读取到文件描述符,定位到具体的请求
			2处理请求
		}
	}
}

因此基于以上思路,就有select,poll,epoll等这几个具体的实现

select模式实现下的io多路复用

在这里插入图片描述
红线以上是对请求描述符的处理,将所有的文件描述符(并不是按照顺序的)进行记录到数组中,并且记录到最大的文件描述符,然后就开始执行select函数。

五个参数(最大的文件描述符,读文件的描述符,写文件的描述符,异常的文件描述符集合,超时的时间)后三个一般都有默认值。

其中读文件描述符集合rset,具体是一个bitmap位数组,1024位,记录下了当前位的状态,下标为文件描述符的位置状态为1,表示当前文件描述符正在发送请求。也就是此rset包含了所有的文件信息。执行select函数的细节:
在这里插入图片描述在执行select时,程序会把rset由用户态来拷贝到内核态,由内核态来判断有哪些文件描述符处于活跃的状态,如果没有活跃的会一直阻塞在此处,当有活跃的请求时,即有数据时,内核会将rset的bitmap中的对应的位置置1,此时就不会阻塞,进入下面的处理请求的过程。
处理请求时,先遍历到所有的socket,判断socket中的bitmap,因为已经记录下的最大的文件描述符max,所以只需要读取到bitmap中max之前的的数组返回即可,找出有哪些文件描述符进行了请求,然后读取到对应的数据,进行处理

select优缺点分析

优点是将对于rsetcopy到了内核态,由内核态来对文件描述符的活跃程度进行判断,比伪代码中直接在用户态进行判断效率高。
缺点:
1对于rset中的bitmap默认大小1024,大小有限。
2对于一次遍历之后,有可能下一次会有其他的文件描述符进行修改,因此在下一次遍历时,需要将rest进行重置,全部重置为0.即rset不可重用
3用户态rsetcopy到用户态也有一个开销
4即使select返回了,也就是有rest对应的位被置位了(可以理解为新的请求了,此时只是知道有请求)但是不知道是哪一个位置或者哪几个位置进行了修改,所以在接下来,还需要再次进行遍历,找到变化的位置,做对应的请求。

poll模式实现io多路复用

在这里插入图片描述此时相比于select,没有使用bitmap来记录每一个fd的信息,而是自定义了一个结构pollfd,这个结构体有三个属性,第一个fd表示文件描述符,events表示当先处理的事件是读还是写,最后一个参数revents就表示当前是否是活跃的。当调用poll方法时,依然是一个阻塞函数,如果fd没有数据时,就阻塞,当有数据时,就需要把pollfds中有数据的pollfd的revents进行置位,是否是POLLIN,如果是就表明是有数据了,然后下面再对其进行处理。
1相比于select的1024位的限制,使用pollfd结构体可以没有限制
2相比于每一次事件监听时fdset中bitmap的不可重用问题,poll只需要对结构体内的属性进行修改即可。

epoll模式的io多路复用

在这里插入图片描述
1首先用epoll_create函数创建一个epfd参数,供下面最重要的epollwait方法参数。
epoll_create函数就是开辟了一个白板,
在这里插入图片描述

2当执行完epoll_ctl函数,白板上面就会记录下每一个fd,和events,类似于poll的pollfd结构体。但是不在存放revents
在这里插入图片描述
3执行epoll_wait函数,此时内核态和用户态共享epfd,仍然由内核态判断哪个fd有事件到来,当没有数据时,就阻塞。当有数据时,就会触发events事件,也就是用事件驱动代替轮询,就需要对有数据的fd进行**“置位”**。此时这个置位并不是前面两个那种做一个标记,而是通过重排的方式来实现标记,把有数据的fd放在最前面的位置进行返回,此时返回的值也不一样,返回值为触发事件的个数m,此时只需要对epfd中前m个个数进行遍历。所以此时时间复杂度是1。
优点:相比于select,解决了select的后两个问题
1不需要进行copy,2再次遍历的时间复杂度为1

总结

select模式
fd_set 使用数组实现
1.fd_size 有限制 1024 bitmap
fd【i】 = accept()
2.fdset不可重用,新的fd进来,重新创建
3.用户态和内核态拷贝产生开销
4.O(n)时间复杂度的轮询
成功调用返回结果大于 0,出错返回结果为 -1,超时返回结果为 0
具有超时时间

	poll
基于结构体存储fd
struct pollfd{
	int fd;
	short events;
	short revents; //可重用
}
解决了select的1,2两点缺点

epoll 解决select的1,2,3,4 不需要轮询,时间复杂度为O(1)
epoll_create 创建一个白板 存放fd_events 和epoll_ctl
用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上 epoll_wait
通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符

两种触发模式:
	LT:水平触发
		当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
	ET:边缘触发
		和 LT 模式不同的是,通知之后进程必须立即处理事件。
		下次再调用 epoll_wait() 时不会再得到事件到达的通知。很大程度上减少了 epoll 事件被重复触发的次数,
		因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

猜你喜欢

转载自blog.csdn.net/m0_56184347/article/details/124361021