IO多路复用

在聊IO多路复用之前,先简单了解下BIO。BIO即Blocking IO,翻译过来也就是阻塞IO。一般情况下,客户端连接服务端时服务端的逻辑通常是这样的:首先创建一个ServerSocket,并绑定一个端口号。然后监听accept事件,accept事件是阻塞的。当客户端连接服务端,accept事件响应后,服务端就能获取到一个Socket。客户端服务端通过这个socket就可以进行读写了。而读写事件也是阻塞的。由于阻塞的特性,当有多个客户端要连服务器时,我们就需要建立多个线程,来满足客户端的请求。这样就很难满足C10k的问题。因为当有上万个客户端请求连接服务器时,我们需要建立上万个线程去处理客户端的请求,这必然会导致服务器无法正常工作。因此多线程结合BIO的这种方式,无法解决C10K的问题。

为了解决C10K,NIO就问世了。NIO (Non-blocking IO)非阻塞IO。java 中的NIO包提供了一套非阻塞的接口,通过这套接口,服务端就不需要为每个C/S建立一长连接,因此服务端只需要通过一个线程就可以满足多个客户端的连接。这里简单提一下java NIO的原理:NIO包提供了一个selector选择器,然后把socket注册到这个selector中,主线程会阻塞在selector的select方法中,当选择器发现某个socket就绪了,select方法会返回,主线程被唤醒。然后通过selector选择器获取到就绪的socket进行相应的处理。

java层面的NIO大致就写这么多,主要就是了解下。本篇文章主要的硬菜是NIO的底层原理,也就是IO多路复用。首先贴一片知乎上的好文,更好的帮助理解什么是IO多路复用。IO多路复用 - 搜索结果 - 知乎

IO多路复用的底层三种实现分别是 select  poll  epoll.

首先讲讲select的实现。select内部归根结底,其实只做了两件事。1.将 文件描述符 从用户态拷贝到内核态。2.内核判断fd_set中(1024位的bitmap结构)有没有就绪的socket,若有则对socket的fd进行标识,然后返回。没有,则阻塞。select函数返回,对应的就是唤醒了java线程。select函数返回的是一个整形,代表有几个socket就绪了。但是具体哪个socket就绪并不知道。因此,在select函数返回后,需要检查fd_set(文件描述符集合)中的每个socket就绪状态,复杂度又是O(n)。

     那么select函数是怎么发现socket已经就绪的?要回答这个问题,首先需要了解下操作系统中断。中断其实就是让cpu正在执行的进程先保留程序上下文,给中断程序让道。中断程序就会拿到cpu执行权限,进行相应的代码执行。比如用键盘打字,如果cpu一直执行其他程序,不释放,那打字就没法打了。就是因为有了系统中断的存在,按下一个键之后,会给主板发送一个电流信号,主板感知到以后,会触发cpu中断。

    介绍完中断,再回到select函数的执行。select函数第一遍轮询 没发现就绪的socket,就会把当前进程保留在socket的等待队列中,然后将当前进程从工作队列中移除。 这样,当前进程被挂起,select函数阻塞。然后客户端往服务器发送数据,数据通过网线到网卡,最终将数据写到内存里,整个过程CPU是不参与的。当数据完成传输以后,就会触发网络数据传输完毕的中断程序,这个中断程序就会把CPU正在执行的进程顶掉。然后cpu就会执行中断程序的逻辑。。中断程序的逻辑:根据内存中的数据包,分析出数据包是哪个socket的数据。然后把数据导入到socket的读缓冲去里,导入完成以后,就开始检查socket的等待队列,是不是有等待者,若有的话,就把等待者移动到工作队列中,中断程序到这一步就执行完了。进程又回归到工作队列中,又有机会获取到cpu时间片了。当前进程执行select函数,再次检查,就发现有这个就绪的socket了。然后就给就绪的socket的fd 打标记,然后select函数就执行完了。最后就是轮询检查每个socket的fd是否被打标记,然后处理被打了标记的socket就ok了。

poll函数和select函数的区别:最大的区别是传参不一样,select使用的是bitmap,表示需要检查的socket集合。poll使用的是链表结构,表示需要检查的socket集合。主要为了解决bitmap长度的问题。

epoll函数:主要解决了select和poll函数的缺陷。1.系统调用返回后不知道那些socket就绪的问题。2. 涉及用户态-》内核态的拷贝。

select和poll函数的返回值是个int整形,只能代表有几个socket就绪,没办法表示出具体是哪个socket就绪了。这就导致程序被唤醒以后,还需要新一轮的系统调用,去检查哪个socket是就绪状态的。然后再进行socket处理。

epoll是怎么设计的?(怎么解决上面的两个缺陷?)

首先了解下epoll中的三个重要函数以及他们的作用:epoll_create,epoll_ctl,epoll_wait。

系统函数epoll_create的作用是创建了eventpoll对象。创建完之后返回一个eventpoll对象的id。相当于在内核开辟了一小块空间。eventpoll 对象的结构有两块重要的区域:一块是存放需要监听的socket_fd描述符列表。另一块就是就绪列表,存放就绪状态的socket信息。(因为是在内核开辟的,这样就解决了用户态到内核态的拷贝)

epoll_ctl的作用是根据 eventpoll-id去增加或者修改 socket 文件描述符。(可以理解为在开辟的内核空间上维护socket文件描述符)

epoll_wait 的作用是根据eventpoll-id监测eventpoll中的socket_fd列表。epoll_wait函数,默认情况下会阻塞调用线程。直到eventpoll中关联的某个或某些个socket就绪以后,它才返回。

eventpoll中的就绪列表是怎么维护的?

当调用系统函数epoll_ctl时,比如需要新添加一个需要关注的socket,内核程序会把当前eventpoll对象追加到这个socket的等待队列里。当socket对应的客户端发送完数据,数据还是通过网线进入服务器,最终写入到内存。然后触发中断程序,cpu将当前进程让出位置,去执行中断程序。中断程序将网络数据转移到对应的socket的读缓冲区里。然后再去检查这个socket的等待队列,然后发现这个socket的等待队列内等待的不是一个进程,而是一个eventpoll对象引用。这个时候,就根据这个eventpoll引用,将当前的socket引用追加到eventpoll的就绪链表的末尾。(eventpoll还有一块区域是等待队列,保存的就是调用epoll_wait的进程。)然后继续检查eventpoll对象的等待队列,如果有进程,,就把进程转移到工作队列内。转移完毕后,进程就会获取到cpu的执行时间片了。然后就是调用epoll_wait函数。

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

eventpll中需要存放需要监视的socket集合信息,存放socket集合信息,采用的是什么数据结构?

红黑树,socket信息 会经常的增删改查,红黑树能保持一种相对稳定的查找效率,负责度是o(logn)

socket结构的三大核心区域:读缓冲区   写缓冲区   等待队列

(文件描述符(fd)可以理解为socket集合,在linux系统中,一切皆文件。socket也不例外,文件描述符其实就是对应每个socket的一个编号)。

你可能感兴趣的:(IO多路复用)