IO多路复用(总结)

目录

IO多路复用

1、捋顺

2、BIO

3、NIO

4、IO多路复用

1、select

2、poll

3、epoll

4、总结


1、捋顺

IO多路复用:多个网络连接IO复用同一个线程

BIO:无法做到,每个网络阻塞在每个线程中

NIO:可以做到,自旋,一个线程中自旋遍历所有网络IO

EPOLL:可以做到,无需自旋,采用事件监听机制+fd,实现一个线程复用多个网络IO

1、多个客户端(socket)连接到redis,此时redis会将每个socket对应的fd(文件描述符,每一个网络连接其实都对应一个文件描述符)注册select、poll、epoll,由这些函数帮我们监听每个socket是否有读写操作/消息

(socket注册)

2、注册完之后,只会阻塞一个socket连接,当其他socket有读写操作的时候,epoll会马上监听到,然后从当前的阻塞状态返回,将事件放到事件队列中,然后马上回到阻塞状态。

(epoll监听与线程阻塞)

3、当多个socket同时有读写请求的时候,redis会将每个读写请求放到事件派发器中,由事件派发器处理每个socket的读写请求,因此可以知道epoll只是监听fd,将fd变化的socket放到事件队列中,等待事件派发器进行处理

(事件队列)

4、事件派发器会处理事件队列中的每个事件,因此处理每个事件是单线程的,因此redis也就称为了单线程模型。

(事件派发器消费事件队列)

总:至此io多路复用,复用的是网络io的阻塞状态,将每个socket的fd交给epoll进行监听,当epoll监听到了(这里就是io多路复用了,正常情况肯定会一直阻塞的),会马上从阻塞状态返回,将事件放到事件队列中,由事件派发器处理每个事件(这里就是redis单线程,消费每个事件是单线程的)

问:redis中的单线程和多线程是怎么回事?

答:其实从上面就可以知道,我们从最开始建立socket连接,监听请求,事件放入事件队列,事件派发器消费事件,这里有很多步(多线程就是在这里,这么多步骤需要处理,肯定是由多线程来完成的),(单线程也是在这里,事件派发器消费事件,处理每个socket的读写请求依旧还是单线程的)

2、BIO

BIO = blocking io(阻塞式io)

介绍:

当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。所以,BIO的特点就是在IO执行的两个阶段都被block了。

 

场景描述

1、启动一个服务器,监听socket连接,当socket连接的inputStream中有数据的时候会拿到input流并打印

2、启动两个客户端,连接socket服务器,给socket服务器发送消息

现象:

会发现,只有第一个连接到socket服务器的客户端输入的值才会被打印,而第二个连接上的客户端就会进入阻塞,直到第一个客户端退出,socket服务器就会马上打印第二个客户端输入的值。

问题:

       无法做到一个服务器打印多个客户端输入的值,会一直阻塞在一个客户端上面

解决:

       每个客户端连接,我们都分配一个socket来监听(启动一个线程),这样每个线程就打印自己内部的input流数据,这样就可以做到一个服务器端监听多个客户端的请求。

接着出现的问题:

       要是有一千个客户端连接服务器,那岂不是要启动一千个线程,服务器不是基本分分钟就垮掉了。

再解决:

       使用NIO来避免多个线程实现的socket通信。

3、NIO

NIO = Non-Blocking io(非阻塞式io)

介绍:

当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system.call,那么它马上就将数据拷贝到了用户内存,然后返回。所以,NIO特点是用户进程需要不断的主动询问内核数据准备好了吗?

 

在非阻塞式I/O模型中,应用程序把一个套接口设置为非阻塞,就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠而是返回一个“错误”,应用程序基于I/O操作函数将不断的轮询数据是否已经准备好,如果没有准备好,继续轮询,直到数据准备好为止。

问题针对:

       由BIO引出的问题,如果想要实现一个服务器端监听多个客户端连接,那么就需要启动相应数量的线程来单独执行,这样当客户端数量多→线程多→服务器崩溃

解决:

       采用NIO模式解决,nio的模式就是服务器与每个客户端连接之后,会将客户端放入到list中,然后遍历这个list,拿到每一个客户端连接,使用nio特有的read方法,可以从用户态转换到内核态,查看该socket是否有数据,无论是否有数据都会马上返回(有数据会拷贝会用户态),所以不会阻塞在任意一个socket上面。

问题:

       底层还是采用while自旋查看每个socket是否有数据,每次查看都要进行一次系统调用(用户程序无法执行内核代码,如果我们要获取socket是否有数据,就得调用内核的read方法,但是我们程序是无法直接调用的,因此我们一般是利用封装好的api,该api会调用底层的c函数,由底层c函数来调用系统内核的方法),其实很浪费性能。

方案:

       其实对于NIO,我们可以将每次自旋来系统调用内核方法获取socket数据的过程,封装为一次调用,也就是我们一次系统调用,其实就是要遍历所有socket来进行read,我们可以将在用户进程的循环read,转换为内核的循环read,那么我们只需要用户进程一次调用,系统内核进行循环read,那岂不是省去了每次用户进程的系统调用。

解决:

       针对于将所有socket的遍历放入到内核中,该操作将有linux独有的select、poll、epoll来进行遍历处理。

4、IO多路复用

1、select

优点:

select 其实就是把NIO中用户态要遍历的fd数组(我们的每一个socket链接,安装进ArrayList里面的那个)拷贝到了内核态,让内核态来遍历,因为用户态判断socket是否有数据还是要调用内核态的,所有拷贝到内核态后,这样遍历判断的时候就不用一直用户态和内核态频繁切换了

从代码中可以看出,select系统调用后,返回了一个置位后的&rset,这样用户态只需进行很简单的二进制比较,就能很快知道哪些socket需要read数据,有效提高了效率

问题:

1、bitmap最大1024位,一个进程最多只能处理1024个客户端

2、&rset不可重用,每次socket有数据就相应的位会被置位

3、文件描述符数组拷贝到了内核态(只不过无系统调用切换上下文的开销。(内核层可优化为异步事件通知)),仍然有开销。select 调用需要传入 fd 数组,需要拷贝一份到内核,高并发场景下这样的拷贝消耗的资源是惊人的。(可优化为不复制)

4、select并没有通知用户态哪一个socket有数据,仍然需要O(n)的遍历。select 仅仅返回可读文件描述符的个数,具体哪个可读还是要用户自己遍历。(可优化为只返回给用户就绪的文件描述符,无需用户做无效的遍历)

我们自己模拟写的是,RedisServerNIO.java,只不过将它内核化了。

2、poll

优点:

1、poll使用pollfd数组来代替select中的bitmap,数组没有1024的限制,可以一次管理更多的client。它和 select 的主要区别就是,去掉了 select 只能监听 1024 个文件描述符的限制。

2、当pollfds数组中有事件发生,相应的revents置位为1,遍历的时候又置位回零,实现了pollfd数组的重用

问题:

poll 解决了select缺点中的前两条,其本质原理还是select的方法,还存在select中原来的问题

1、pollfds数组拷贝到了内核态,仍然有开销

2、poll并没有通知用户态哪一个socket有数据,仍然需要O(n)的遍历

3、epoll

事件通知机制:

1、当有网卡上有数据到达了,首先会放到DMA(内存中的一个buffer,网卡可以直接访问这个数据区域)中

2、网卡向cpu发起中断,让cpu先处理网卡的事

3、中断号在内存中会绑定一个回调,哪个socket中有数据,回调函数就把哪个socket放入就绪链表中

总结:

多路复用快的原因在于,操作系统提供了这样的系统调用,使得原来的 while 循环里多次系统调用,

变成了一次系统调用 + 内核层遍历这些文件描述符。

epoll是现在最先进的IO多路复用器,RedisNginxlinux中的Java NIO都使用的是epoll

这里多路指的是多个网络连接,复用指的是复用同一个线程。

1、一个socket的生命周期中只有一次从用户态拷贝到内核态的过程,开销小

2、使用event事件通知机制,每次socket中有数据会主动通知内核,并加入到就绪链表中,不需要遍历所有的socket

在多路复用IO模型中,会有一个内核线程不断地去轮询多个 socket 的状态,只有当真正读写事件发送时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有真正有读写事件进行时,才会使用IO资源,所以它大大减少来资源占用。

多路I/O复用模型是利用 selectpollepoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。 采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈

4、总结

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值