在了解IO模型前,需要先知道部分操作系统的概念。
应用程序需要经过操作系统,才能做一些特殊操作,如磁盘读写。此类操作有较大风险,只能交给操作系统来控制。因此,操作系统将进程占用的内存空间划分为两部分:用户空间和内核空间。内核空间是操作系统内核访问的、受保护的内存空间,用户空间则是用户应用程序访问的内存区域。
如果应用程序需要使用到内核空间的资源,则需要通过系统调用来完成,也就是 CPU 要进行用户态和内核态的切换。
进程从用户态到内核态的转变,需要通过系统调用来完成。系统调用的过程,会发生CPU上下文的切换。CPU上下文切换就是先把前一个任务的CPU上下文(也就是CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
现代操作系统使用虚拟内存,即虚拟地址取代物理地址,使用虚拟内存可以有2个好处:
正是多个虚拟内存可以指向同一个物理地址,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,这样的话,就可以减少IO的数据拷贝次数。
DMA,英文全称是Direct Memory Access,即直接内存访问。DMA 本质上是一块主板上独立的芯片,允许外设设备和内存存储器之间直接进行IO数据传输,不需要CPU参与。
DMA 的工作流程为:
DMA 的主要作用就是将数据从磁盘拷贝到内核缓冲区,这期间不需要占用 CPU 资源。
有了以上概念,下面就可以进一步了解 IO模型了。
BIO 为同步阻塞 IO,blocking queue 的简写,也就是说多线程情况下只有一个线程操作内核的 queue,当前线程操作完 queue后,才能给下一个线程操作。在 BIO 下,一个连接就对应一个线程,如果连接特别多的情况下,就会有特别多的线程,很费资源。
Non-blocking IO的简写,同步非阻塞IO,内核发生了变化,应用程序访问内核的缓冲区时不会阻塞,但是返回值需要用户自己判断;如果连接数特别多的情况下,就需要应用程序不停遍历,一个个进行状态的判断,询问是否有数据到达。当线程未读取到任何数据,线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。
操作系统提供了一类函数(select、poll、epoll等),它们可以同时监控多个fd(文件描述符)。当任何一个 fd 返回内核数据就绪,应用进程再发起 recvfrom 系统调用去读取数据。这也是IO多路复用的核心思路。
但是使用select方式有明显缺点:
因为 select 方式存在连接数限制,所以后来又提出了 poll。与 select 相比,poll 解决了连接数限制问题。但是,select 和 poll 一样,还是需要通过遍历文件描述符来获取已经就绪的 socket。如果同时连接的大量客户端,在一时刻可能只有极少处于就绪状态,伴随着监视的描述符数量的增长,效率也会线性下降。因此经典的多路复用模型 epoll 诞生。
为了解决 select/poll 存在的问题,多路复用模型 epoll 诞生。它采用事件驱动来实现,epoll 先通过 epoll_ctl() 来注册一个 fd,一旦基于某个 fd 就绪时,内核会采用回调机制,迅速激活这个 fd,当进程调用 epoll_wait() 时便得到通知。这里去掉遍历文件描述符的操作,而是采用监听事件回调的机制。
信号驱动IO不再用主动询问的方式去确认数据是否就绪,而是向内核发送一个信号(调用 sigaction 的时候建立一个 SIGIO 的信号),然后应用用户进程可以去做别的事,不用阻塞。当内核数据准备好后,再通过 SIGIO 信号通知应用进程,数据准备好后的可读状态。应用用户进程收到信号之后,立即调用recvfrom,去读取数据。
信号驱动IO模型,在应用进程发出信号后,是立即返回的,不会阻塞进程。但是当数据复制到应用缓冲的时候,应用进程还是阻塞的。回过头来看下,不管是BIO,还是NIO,还是信号驱动,在数据从内核复制到应用缓冲的时候,都是阻塞的。基于这点,Java7 引入了AIO。
AIO 主要是用来解决数据复制阶段的阻塞问题。在进行读写操作时,线程不必等待结果,而是将来由操作系统来通过回调方式,由另外的线程来获得结果。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。