文章目录
- 1.阻塞IO模型(BIO)和 非塞IO模型(NIO)
- 2.什么是IO多路复用?
- 3.IO多路复用的演进?
1.阻塞IO模型(BIO)和 非塞IO模型(NIO)
阻塞IO模型(BIO):
如果我们为每一个连接创建一个线程,连接结束时做对应的销毁,这种情况下,如果连接中没有数据可读的话,线程就不得不阻塞,直到连接可读。这个时候就算其他的连接有数据可读,阻塞的线程也是没有机会去处理的。
服务端整个过程只有一个线程,依次循环处理客户端的socket,这种情况下,客户端的某个socket阻塞住了,是会影响到其他的客户端的处理的,必须等待阻塞的客户端处理结束才能接着处理接下来的请求.
非塞IO模型(NIO):
在线程读取数据的时候,如果数据不可读,线程会立即拿到返回,然后去处理别的逻辑。等一会线程再尝试读取数据,这样反复处理,这里就需要一个轮询的逻辑,因为读数据的一端,也不知道数据什么时候可读,所以就需要每间隔一段时间去看看。
非塞IO模型(NIO)相较于阻塞IO最大的特点就是同步非阻塞在进行系统调用的时候,也就是accept和read调用的时候,是不会被阻塞的。socket的相关操作(read、write)都是需要在内核态进行完成的,不能在用户态进行完成,内核态通过将这些函数进行封装,通过像read跟write这种函数提供出来
优点:单个socket阻塞的话,是不会影响到其他的socket
缺点:要不断的在客户端遍历需要建立连接的fd,不断的进行系统调用(会涉及到用户态和内核态的切换),需要一定的开销的。
2.什么是IO多路复用?
IO多路复用(出发点就是要设计一个高性能的网络服务器,这个网络服务器可以供多个客户端建立连接,并且能处理多个客户端的请求。。能想到的就是写多线程去处理,其实现在很多的rpc框架就是用到了这种方式,多线程的弊端就是需要不断地进行上下文的切换,需要处理一些上下文的切换(占用内存的瓶颈),这个过程很繁琐,并且会造成资源的浪费,比如有一千个客户端和服务端建立连接,就需要创建一千个线程,但在同一时刻可能就只有三四个建立连接,这就会造成资源的浪费,因而多线程并不是一种最好的解决方式,那么如何用单线程的方式处理):
就是一个线程如何处理多个连接请求的过程和技术,我们不需要为每个连接创建一个处理线程等待数据可读或可写,IO多路复用会在IO准备好的时候主动通知我们,我们用一个线程就可以完成对全部IO通讯的很监听。。IO多路复用被用来解决性能的问题,解决的办法1:资源复用,多个网络IO复用一个或者多个线程来处理请求. 2.线程不需要等待时间被触发,IO事件被触发的时候能直接通知应用程序,要实现这种线程池的处理方式,就需要引入了一个中间层,所有网络连接在中间层上进行注册,而程序也需要阻塞在这个中间层上,等待他的事件通知,这个中间层便是经常提及的SELECT、EPOLL。
FD:文件描述符,非负整数。linux下一切皆文件,linux中的一切资源都可以通过文件的方式访问和管理,fd就是文件的索引,指向某个文件资源,内核利用fd来访问和管理资源。
同步阻塞:服务端每次只会和一个客户端建立连接,要么就阻塞等待,客户端建立连接
同步非阻塞:服务端每次只会和一个客户端建立连接,他不会被阻塞,如果没有就绪的事件时,非阻塞IO会马上返回一个负数的fd,如果服务端没有和客户端建立上连接,他会返回一个非负的fd继续进行轮询,见下图
总结:同步非阻塞IO其实就是操作系统底层,对同步阻塞IO做了一些优化,提供了一种解决问题的方式,从而避免了单个socket影响到其他socket的情况
缺点:同步非阻塞IO需要我们在用户空间不断的去遍历(涉及到用户态到内核态的一个调用,会是一个问题)调用read函数来检查是否有数据到来。
3.IO多路复用的演进?
- select:
下面的函数中nfds传入的是三个集合描述符的最大值加一,也就是告诉内核,三个集合里面的fd就是这么大,,你只需要检查到这个地方就可以了,而不用做一些无用的检查。
timeval代表的是超时事件,就是马上判断,判断没有就绪的时候,经过多长时间进行返回。如果是-1,那么就一直等待,直到有就绪的时候才进行返回
服务端监听4个socket,我们拿到四个socket对应的一个fd,当我们在用户空间调用select函数的时候,首先会先将这四个fd拷贝一份到内核空间,接着内核空间会来遍历这4个fd,就是会依次检查每个fd上对应的socket有没有数据到来,就是有没有就绪,如果没有的话,接着往下检查,直到他检查到某个fd有数据可读了,这个时候,它会将这个fd打上一个标记,然后返回fd的就绪的数量。返回之后,用户空间知道有fd就绪了。,但是他不知道具体是哪一个,所以用户空间就是遍历fd集合找到最终的就绪的fd,然后对就绪的fd进行数据的处理。然后继续下一个fd的调用。
如果在内核态遍历一次发现4个fd都没有就绪,这个时候内核空间一种就是继续下一次的遍历,一直遍历,直到有一个fd就绪了才终止遍历,将这个fd打标返回给用户空间,这种方法会占用大量的CPU。
具体的实现其实是遍历一次之后发现没有就绪的,他会把当前的一个用户进程阻塞起来,当客户端向服务端发送数据的时候,数据通过网络到服务端的网卡,网卡通过DMA的方式将这个数据包写入到指定的内存,处理完之后通过中断信号告诉CPU有新的数据到达了,CPU收到中断信号后会进行响应中断,调用中断的处理程序进行处理,首先就是根据这个数据包的ip跟端口号找到对应的这个socket,然后将数据保存到socket队列,然后在检查socket对应的等待队列里是不是有进程在阻塞等待 ,如果有的话,唤醒该进程,用户进程唤醒后,再继续检查一遍这个fd集合,检查到某个fd就绪后,就给这个fd打标,然后结束阻塞,返回给用户
就绪的文件描述符复用了 readset。用的是bitmap位图,发送的时候代表的是哪些文件描述符是需要检查的,返回的是准备就绪的文件描述符的标记。
- Poll:
poll整体的话跟select是比较类似的,主要是在数据结构上进行了一些优化,
优化一:存储监听的事件和就绪的事件,所以就不用每次调用完之后进行重置,的限制的
优化二:层是传入的一个链表,用户传入的时候是一个数组,但是拷贝到内核,它是通过链表存储要监听的文件描述符,这个链表他是没有
问题:
1.调用的时候都需要将fd从用户态拷贝到内核态,涉及到用户到内核态的切换,并且说如果fd比较大的时候是需要一定的开销,
2.返回就绪事件的时候,poll和select是不知道具体是哪个fd事件就绪的,需要进行一个O(n)的遍历
- epoll:
epoll主要有三个函数,epoll_create(int size),创建一个epoll,size是epoll想要监听的文件符的数量,返回值就是epoll对应的文件描述符,后面我们可以通过返回的文件描述符来操作我们这个epoll。
第二个函数就是epoll_ctl,就是对我们创建的epoll进行操作。
epoll创建之后,底层内核空间是一个eventpoll,有三个元素,等待队列、就绪队列、红黑树
红黑树的话就是把想要监听的这些文件描述符通过红黑树的形式存储起来,通过红黑树进行高效的新增、删除、查询,
就绪列表:就是说如果有一些fd对应的socket已经是就绪的,就是数据已经到来了,这个时候就要把对应的fd添加到就序列表来,可以很快的知道就绪队列
等待队列:当我们调用的时候发现没有事件就绪,这个时候,我们就将我们的进程进行阻塞,阻塞的时候我们就会把这个进程关联到我们的这个等待队列里面,以便后续有事件到来的时候唤醒我们的进程。
通过在内核空间维护一个fd的红黑树(将文件描述符维护到内核态去,这样就不需要每次将我们想要监听的fd拷贝到内核态)和一个就绪链表(poll和select返回的时就绪的文件描述符),解决了poll和select的问题。