多路复用的三种方式特点(看完这篇你应该有一个初步的认识)
算机由CPU、存储器(内存)、网络接口等部件组成。
- 数据的接受:从硬件的角度看计算机怎样接收网络数据
网卡接收数据的过程分3个阶段
一、网卡收到网线传来的数据;
二、经过硬件电路的传输;
三、最终将数据写入到内存中的某个地址上。
整个过程涉及到DMA传输、IO通路选择等硬件有关的知识,但我们只需知道:网卡会把接收到的数据写入内存。
- CPU如何知道接收了数据
这里涉及一个概念叫中断。
计算机执行程序时,会有优先级的需求。比如,当计算机收到断电信号时(电容可以保存少许电量,供CPU运行很短的一小段时间),它应立即去保存数据,保存数据的程序具有较高的优先级。
一般而言,由硬件产生的信号需要cpu立马做出回应(不然数据可能就丢失),所以它的优先级很高。cpu理应中断掉正在执行的程序,去做出响应;当cpu完成对硬件的响应后,再重新执行用户程序。
当网卡把数据写入到内存后,网卡向cpu发出一个中断信号,操作系统便能得知有新数据到来,再通过网卡中断程序去处理数据。
内核态和用户态。在操作系统中,CPU负责执行指令。指令分为特权指令和非特权指令,对于某些特定的指令,只需要操作系统及其相关模块进行调用。因此操作系统内部划分除了内核态和用户态。
- 内核态:内核态拥有完全的底层资源控制权限,可以执行任何的CPU指令,访问任何内存地址,其占有的处理机是不允许被抢占的。
- 用户态:用户程序运行时就称为用户态,用户态不能直接访问底层硬件和内存地址,必须委托系统调用的方式来访问。
用户态和内核态切换有三种方式
- 系统调用,用户态主动要求切换到内核态的一种方式。用户进程申请需要使用内核态才可以提供给的某些服务来完成任务
- 外设中断,如上面的网卡中断信号,当CPU收到中断信号的时候会切换至内核态
- 异常,当CPU执行运行处于用户态的程序时,发生了一些不可知的异常,这个时候就会触发由当前运行进行切换到处理此异常的内核相关程序中,也就是转到了内核态。
Linux的整体架构图如下所示
同步&异步 and 阻塞&非阻塞
- 同步:在发出一个功能调用时,在没有得到结果之前,该调用就不返回。等前一件做完了才能做下一件事。
- 异步:异步的概念和同步相对。当一个异步过程调用发出后,调用者若不能立刻得到结果,此时可以直接返回然后执行其他任务,等到获得了结果之后通过状态、通知或者回调等手段通知调用者。
- 阻塞:阻塞调用是指调用返回之前,当前线程会被挂起,只有当调用得到结果后才返回。
- 非阻塞:与阻塞相反,非阻塞调用是指在不能立即得到结果之前,该函数不会将当前线程阻塞,而是立即返回。
同步和异步关注的是消息通知的机制,阻塞和非阻塞关注的是获取程序运行结果的状态。
五种IO模型IO一般分为磁盘IO和网络IO,这里我们主要关注网络IO。下图为一次完整的网络IO过程
阻塞IO
这个模型应该是最常见的了。应用程序在调用底层将数据委托给系统进行处理,数据处理好之前程序会处于阻塞状态。在内核处理完数据后将数据返回时,将数据从内核空间拷贝至用户空间.涉及用户态和内核态的切换。
这种模型思路清晰比较简单,编程也相对容易。但是缺点也很明显就是每处理一次IO请求就要创建一个线程。
整体流程如下:
非阻塞IO
调用进程在等待数据的过程中不会被阻塞,而是会不断地轮询查看数据有没有准备好。当数据准备好后,将数据从内核空间拷贝到用户空间,完成IO函数的调用。等待数据的过程是非阻塞的,但数据拷贝时仍是阻塞的。
非阻塞IO可以实现一个线程同时处理多个连接的需求,这样减少了线程的数量,但是同时因为要不断的轮询是否已经准备好,因此比较耗费CPU。
IO多路复用
为了解决非阻塞IO不断轮询导致CPU占用升高的问题,出现了IO复用模型。IO复用中,使用其他线程帮助去检查多个线程数据的完成情况,提高效率。
Linux中提供了select、poll和epoll三种方式来实现IO复用。一个线程可以对多个IO端口进行监听,当有读写事件产生时会分发到具体的线程进行处理。
IO复用只需要阻塞在select,poll或者epoll,可以同时处理和管理多个连接。缺点是当select、poll或者epoll 管理的连接数过少时,这种模型将退化成阻塞IO 模型。并且还多了一次系统调用:一次select、poll或者epoll 一次recvfrom。
过程如下所示:
信号驱动IO
应用程序可以创建一个信号驱动程序SIGIO,当数据没有处理好时,应用程序继续运行,不会被阻塞。当数据准备好之后,操作系统向应用程序发送信号,之后信号驱动程序就会执行,在信号处理函数中调用 IO函数处理数据。过程如下所示:
信号驱动IO模型的优点在于非阻塞,缺点在于串行处理信号驱动程序,当前一个SIGIO没有被处理的情况下,后一个信号也不能被处理。在信号量大的时候会导致后面的信号不能被及时感知
异步IO异步IO不是顺序执行的。应用进程在执行aio_read系统调用之后,无论数据是否准备好,都会直接返回给用户进程,然后应用进程可以去做别的事情。当数据准备好之后,内核直接复制数据给用户进程,然后内核向进程发送通知。
信号驱动IO模型中内核通知应用进程数据何时准备好,而在异步IO模型中内核将数据复制完成之后告知应用进程IO操作已完成。
在异步IO模型中,应用进程调用aio_read以及数据被拷贝到用户空间这两个过程都是非阻塞的。
除异步IO外,其他四种IO模型可以分为2部分
- 系统调用
- 数据从内核空间拷贝到用户空间
而异步IO在系统调用和数据拷贝过程都是非阻塞的。
多路复用
上面的五种IO模型中已经大概展示了多路复用的流程。
这里把图再贴一次:
select-poll
select()的思路是启动一个进程给将需要处理的socket放入一个队列中,然后在循环这个队列,当有socket接收到数据的时候在遍历等待列表确认需要唤醒的socket。所谓唤起进程,就是将进程从所有的等待队列中移除,加入到工作队列里面。
select的缺点
- 每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。
- 进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次。
补充说明: 当程序调用select时,内核会先遍历一遍socket,如果有一个以上的socket接收缓冲区有数据,那么select直接返回,不会阻塞。这也是为什么select的返回值有可能大于1的原因之一。如果没有socket有数据,进程才会阻塞。
epollepoll可以理解是对select模式的一种增强
- 功能分离
select低效的原因之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一。
因此 epoll将这俩个过程进行的分离。
- 就绪列表
select低效的另一个原因在于程序不知道哪些socket收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的socket,就能避免遍历。(下面这个图就是个大概意思,具体的实现我这里没有仔细研究)
参考文章:
https://www.cnblogs.com/reecelin/p/13537734.html
https://zhuanlan.zhihu.com/p/63179839
https://zhuanlan.zhihu.com/p/64138532
https://zhuanlan.zhihu.com/p/64746509
封面图,侵权删。(拿图不点赞,丁丁少一寸)
,
免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com