Java IO:操作系统的IO处理过程以及5种网络IO模型

操作系统如何处理IO


Linux 会把所有的外部设备都看成一个文件来操作,对外部设备的操作可以看成是对文件的操作。

我们对一个文件的读写,都会通过内核提供的系统调用,内核会给我们返回一个 File Descriptor,这个描述符是一个数字,指向内核的一个结构体,我们应用程序对文件的读写就是对描述符指向的结构体的读写。

系统调用是如何完成IO操作?

Linux 会把内存分为 内核区和用户区。Linux 的内核区会帮我们管理所有的硬件资源,并且会提供系统调用,我们应用程序的读操作,就会通过系统调用 read 发起一个读操作,这个时候,内核就会创建一个文件描述符,通过驱动向硬件发送读指令,并且把读的数据放在描述符指向的结构体的缓冲区中。当这个数据传到用户区的时候,就完成了一次 IO。

Linux 系统调用的 read,是一个阻塞函数。这个我们应用程序在发起read系统调用的时候,就必须要阻塞,进程挂起,等待文件描述符的读就绪。

从上面我们可以知道,应用程序的一个 read系统调用,需要经过:

  • 硬件读取文件数据到文件描述符指向的结构体的缓冲区。
  • 结构体的缓冲区的数据 传输到 用户区。

五种网络 IO 模型


阻塞式 IO

在进行网络IO的时候,进程发起一个 recvform 系统调用,然后进程就会被阻塞,什么也不干,等待上述 所说的 1 和 2步骤完成(也就是硬件读取内容到内核的结构体缓冲区,结构体缓冲区的内容到用户区),最后进程再被处理。大致如下图:

非阻塞式 IO

在上述的 阻塞式 IO中,可以看到,进程在发起了 recvform 系统调用的时候,就会阻塞,那么非阻塞式 IO 和 阻塞式 IO 的一个区别就在这里,它并不会阻塞,而是马上返回一个错误码,进程在得到错误码之后,可以干点别的事情,然后再重复上述的步骤,也就是又发起一个 recvform 系统调用。这个过程就成为轮询。

IO 复用

由上面的非阻塞式IO可以看出,轮询占了很大一部分过程,这个过程会消耗很多CPU时间。这个轮询是由用户区发起的,但是,这个轮询如果是由内核区发起的,那么效率就有提升。IO复用提供了两个 系统调用 :select 和 poll。select 系统调用是内核级别的,它和 非阻塞式的轮询 有一个区别,就是select 轮询 可以等待多个 socket,任何一个socket 的数据准备好了,那么就可以返回进行读。 select 和 poll调用之后,会阻塞进程,那么这种阻塞进程不同于 第一种 阻塞式IO的阻塞,此时的select不是等到socket数据全部到达后再处理,而是有一部分数据就会调用用户进程处理。

打个比方:钓鱼的时候,雇佣一个帮手,他可以同时抛下多个鱼竿,任何一杆的鱼上钩,他就会拉杆,他只负责帮我们,不负责帮我们处理,所有我们还得在一边等着,等到他拉杆,我们再处理。 如下图:

IO复用图

Ps:实际上 select/poll 是落后的,因为 select 的句柄数有限,为1024 个,也就是说,同次检测 1025 个句柄死 是不可能。另外,内核的select是采用轮询的方法,select检测的句柄数越大就会越耗时,这些都是问题。那么epoll就能够解决这些问题,epoll 实际上也是 select/poll 的增强版。

信号驱动式IO

也就是说,数据未准备就绪的时候,那么就进行等待,但是这个等待不会进行轮询,也不会阻塞。而是在某一时刻如果数据准备好了,那么内核会通知进程启动IO操作,将数据从内核区复制到用户区。如下图4 所示:
信号驱动式IO图

异步 IO

异步 IO 是指 相当于 信号式驱动 IO 的一个升级版本。异步 IO 是等 内核完成整个操作之后再通过应用程序,这整个过程包括了 硬件读取数据到 内核结构体的缓冲区中,以及缓冲区的数据复制到 用户区中,这整个过程进程都不会阻塞。如下图:
异步IO图

总结:

以上就是 操作系统如何处理IO,以及常用的 5种 网络 IO模型。包括(阻塞式IO(java的传统IO),非阻塞式IO(NIO),多路复用IO,信号驱动IO,异步IO)。

上一篇:《 自动化测试最佳实践:来自全球的经典自动化测试案例解析》一一1.9 团队成功


下一篇:系列文章深度解读|SwiftUI 背后那些事儿