操作系统

操作系统的对IO操作有很复杂的处理,这里对基本原理简介。

内存分配

为了避免用户进程直接操作内核,保证内核安全,操作系统将内存(虚拟内存)划分为两部分:一部分是内核空间(Kernel-Space),另一部分是用户空间(User-Space)。在Linux系统中,内核模块运行在内核空间,对应的进程处于内核态;用户程序运行在用户空间,对应的进程处于用户态。

程序划分

操作系统的核心是内核程序,它独立于普通的应用程序,既有权限访问受保护的内核空间,也有权限访问硬件设备。总是驻留在内存中。
应用程序权限较低,不允许直接在内核空间区域进行读写,也不允许直接调用内核代码定义的函数。每个应用程序进程都有一个单独的用户空间,对应的进程处于用户态,用户态进程不能访问内核空间中的数据,也不能直接调用内核函数,因此需要将进程切换到内核态才能进行系统调用。
应用程序的IO操作实际上不是物理设备级别的读写,而是缓存的复制,即内核缓冲区和进程缓冲区之间进行的数据交换。

内核缓冲区与进程缓冲区

为什么设置那么多的缓冲区,导致读写过程那么麻烦呢?
缓冲区的目的是减少与设备之间的频繁物理交换。为了减少底层系统的频繁中断所导致的时间损耗、性能损耗,出现了内核缓冲区。
内核缓冲区与应用缓冲区在数量上也不同。在Linux系统中,操作系统内核只有一个内核缓冲区。每个用户程序(进程)都有自己独立的缓冲区,叫作用户缓冲区或者进程缓冲区。
drawio.svg

IO模型

常见的IO模型有四种。

  • 同步阻塞
  • 同步非阻塞
  • IO多路复用
  • 异步IO

    基本概念解析

    在IO中,阻塞指的是内核IO操作彻底完成后才返回到用户空间执行用户程序的操作指令的现象。简单地说,用户程序发起IO操作到系统内核把数据处理完成这个过程是需要用户程序一直等待的。
    在IO中,同步与异步的区别主要体现在IO请求发起方的不同,同步IO是指用户空间(进程或者线程)是主动发起IO请求的一方,系统内核是被动接收方。异步IO则反过来,系统内核是主动发起IO请求的一方,用户空间是被动接收方。具体表现为,用户程序发起IO请求后,不必等待内核程序完成数据操作,只需轮询内核消息,接收到完成消息后再处理IO相关的操作。
    Java中的NIO是“New IO”,并非是 Non-Blocking IO 的缩写,因为主要用的是IO多路复用模型。

    同步阻塞IO

    在阻塞式IO模型中,从Java应用程序发起IO系统调用开始,直到系统调用返回,这段时间内发起IO请求的Java进程(或者线程)是阻塞的。直到返回成功后,应用进程才能开始处理用户空间的缓冲区数据。
    image.png

  • 优点:应用程序开发非常简单;在阻塞等待数据期间,用户线程挂起,基本不会占用CPU资源。

  • 缺点:一般情况下会为每个连接配备一个独立的线程,一个线程维护一个连接的IO操作。在并发量小的情况下,这样做没有什么问题。在高并发的应用场景下,阻塞IO模型需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常大,性能很低,基本上是不可用的。

    同步非阻塞IO

    在同步非阻塞IO模型中,应用程序一旦开始IO系统调用,就会出现以下两种情况:
  1. 在内核缓冲区中没有数据的情况下,系统调用会立即返回一个调用失败的信息。
  2. 在内核缓冲区中有数据的情况下,在数据的复制过程中系统调用是阻塞的,直到完成数据从内核缓冲区复制到用户缓冲区。复制完成后,系统调用返回成功,用户进程(或者线程)可以开始处理用户空间的缓冲区数据。

image.png
同步非阻塞IO的特点是应用程序的线程需要不断地进行IO系统调用,轮询数据是否已经准备好,如果没有准备好就继续轮询,直到完成IO系统调用为止。

  • 优点:每次发起的IO系统调用在内核等待数据过程中可以立即返回,用户线程不会阻塞,实时性较好。
  • 缺点:是不断地轮询内核,这将占用大量的CPU时间,效率低下。

总体来说,在高并发应用场景中,同步非阻塞IO是性能很低的,也是基本不可用的。

IO多路复用

为了提高性能,操作系统引入了一种新的系统调用,专门用于查询IO文件描述符(含socket连接)的就绪状态。在Linux系统中,新的系统调用为select/epoll系统调用。通过这个特性,用户程序只需要用一个线程即可以监视多个文件描述符,其他线程用于处理IO具体操作。
IO多路复用(IO Multiplexing)属于一种经典的Reactor模式实现,有时也称为异步阻塞IO,Java中的Selector属于这种模型。

读写过程是阻塞的,轮询是非阻塞的。

image.png

异步IO

异步IO(Asynchronous IOAIO)指的是用户空间的线程变成被动接收者,而内核空间成为主动调用者。在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕并放在了用户缓冲区内,内核在IO完成后通知用户线程直接使用即可。
用户线程通过系统调用向内核注册某个IO操作。内核在整个IO操作(包括数据准备、数据复制)完成后通知用户程序,用户执行后续的业务操作。
image.png
理论上来说,异步IO是真正的异步输入输出,它的吞吐量高于IO多路复用模型的吞吐量。
但目前IO多路复用模型是主流。

系统配置

在Linux操作系统中,文件句柄数是有限制的,默认为1024,即一个进程最多可以接受1024个socket连接,大多数情况下是够用的。当链接超过上限时,会有Socket/File:Can't openso many files的错误提示。
临时修改限制:
ulimit -n 1000000
退出会话就会恢复默认值。永久生效则需要修改配置文件:/etc/security/limits.conf

  1. soft nofile 1000000
  2. hard nofile 1000000

soft nofile表示软性极限,hard nofile表示硬性极限。