IO多路复用

参照:https://blog.csdn.net/sehanlingfeng/article/details/78920423https://mp.weixin.qq.com/s?__biz=MzAxODI5ODMwOA==&mid=2666538919&idx=1&sn=6013c451b5f14bf809aec77dd5df6cff&scene=21#wechat_redirecthttps://www.jianshu.com/p/ef418ccf2f7d

1.  常见的IO模型:同步阻塞IO(BIO),同步非阻塞IO(NIO),IO多路复用,异步IO(AIO)。

2.  同步和异步的概念描述的是用户线程与内核的交互方式:同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。

   阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:阻塞是指IO操作需要彻底完成后才返回到用户空间;而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。

3.  同步阻塞IO:用户线程通过系统调用read发起IO读操作,由用户空间转到内核空间。内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成read操作

   

   即用户需要等待read将socket中的数据读取到buffer后,才继续处理接收的数据。整个IO请求的过程中,用户线程是被阻塞的,这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够。

4.   同步非阻塞IO:用户线程发起IO请求时立即返回。但并未读取到任何数据,用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。

   

   用户需要不断地调用read,尝试读取socket中的数据,直到读取成功后,才继续处理接收的数据。整个IO请求的过程中,虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。

5.  IO多路复用:建立在内核提供的多路分离函数select,poll,epoll基础之上

   

   用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。

6.  异步IO

   相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知(在 Linux 中,通知的方式是 “信号”)。IO两个阶段,进程都是非阻塞的。

   

7.  BIO 和 NIO 作为 Server 端,当建立了 10 个连接时,分别产生多少个线程?

   答案: 因为传统的 IO 也就是 BIO 是同步线程堵塞的,所以每个连接都要分配一个专用线程来处理请求,这样 10 个连接就会创建 10 个线程去处理。而 NIO 是一种同步非阻塞的 I/O 模型,它的核心技术是多路复用,可以使用一个链接上的不同通道来处理不同的请求,所以即使有 10 个连接,对于 NIO 来说,开启 1 个线程就够了。

8.  IO多路复用的几个函数的区别:

   select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024。对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

   poll 函数和select 函数其实没什么区别,但是优化在它是用链表来储存的fd。注意:poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

   epoll函数:epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

   主要使用以下三个函数:

   1.epoll_create在内核层创建了一个数据表,接口会返回一个“epoll的文件描述符”指向这个表。

   2.epoll_ctl接口来注册要监听的事件,通过这个接口可以灵活的注册/取消注册/修改注册某个fd的某些事件。

   3.epoll_wait来等待事件的发生。只有当注册的事件至少有一个发生,或者timeout达到时,该调用才会返回。这与selectpoll几乎一致。但不一样的地方是evlist,它是epoll_wait的返回数组,里面只包含那些被触发的事件对应的fd,而不是像selectpoll那样返回所有注册的fd。

   epoll除了性能优势,还有一个优点——同时支持水平触发(Level Trigger)和边沿触发(Edge Trigger)。

   有两个socket的fd——fd1和fd2。我们设定监听f1的“水平触发读事件“,监听fd2的”边沿触发读事件“。我们使用在时刻t1,使用epoll_wait监听他们的事件。在时刻t2时,两个fd都到了100bytes数据,于是在时刻t3, epoll_wait返回了两个fd进行处理。在t4,我们故意不读取所有的数据出来,只各自读50bytes。然后在t5重新注册两个事件并监听。在t6时,只有fd1会返回,因为fd1里的数据没有读完,仍然处于“被触发”状态;而fd2不会被返回,因为没有新数据到达。

   

   水平触发只关心文件描述符中是否还有没完成处理的数据,边沿触发只关心文件描述符是否有新的事件产生,如果有,则返回;如果返回过一次,不管程序是否处理了,只要没有新的事件产生,epoll_wait不会再认为这个fd被“触发”了。

   那为什么需要边沿触发?

   在边沿触发下,开发者有机会更精细的定制这里的控制逻辑。但是边沿触发也大大的提高了编程的难度而且如果没有很好的根据EAGAIN来“重置”一个fd,就会造成此fd永远没有新事件产生,进而导致饿死相关的处理代码。

原文地址:https://www.cnblogs.com/jkzr/p/10524386.html