IO读写基础原理

每个应用进程运行在用户空间,进程处于用户态,用户态进程不能访问内核空间数据,不能直接调用内核函数,因此要进行系统调用时,需要将用户进程从用户态切换到内核态。

用户态进程必须通过系统接口( System Call ),才能向内核发出指令,完成调用系统资源之类的操作。

用户程序进行IO操作,依赖底层的IO读写,使用read&write系统调用:

read系统调用:上层程序应用通过操作系统的read系统调用,把数据从内核缓冲区复制到应用程序的进程缓冲区 write系统调用:上层程序应用通过操作系统的write系统调用,把数据从应用程序的进程缓冲区复制到内核缓冲区 设置缓冲区的目的是减少频繁第与设备之前的物理交换

image.png
Redis 持久化:https://www.yuque.com/ixiaoyu/uimvv2/bes7db#dsw9V

IO模型

同步阻塞IO(Blocking IO,BIO)

阻塞IO,指的是需要内核 IO操作彻底完成后,才返回到用户空间执行用户程序的操作指令,阻塞一词所指的是用户程序(发起 IO请求的进程或者线程)的执行状态是阻塞的。 同步IO指用户空间(进程或线程)主动发起,需要等待内核IO操作彻底完成后才返回用户空间的IO操作,发起IO操作的进程或线程处于阻塞状态。

image.png

  1. Java程序进行IO读数据,发起read系统调用,用户进程或线程进入阻塞状态
  2. 系统内核收到read系统调用时,开始准备数据。(等待数据到达内核缓冲区)
  3. 内核等待完整数据到达,会将数据复制到用户本地缓冲区,然后内核返回结果
  4. 内核返回后,用户进程或线程才会解除阻塞状态,进入就绪状态

BIO优点:应用程序开发简单;阻塞等待期间不消耗CPU
BIO缺点:每个连接会创建一个线程,高并发场景会有大量线程,线程、内存的切换CPU消耗很大,性能低

同步步非阻塞IO(Non-Blocking IO,NIO)

非阻塞IO指用户空间的程序不需要等待内核IO操作完成,可以立即返回用户空间去执行后序的指令,即发起IO请求的用户进程或线程处于非阻塞状态,与此同时,内核会立即返回给用户一个IO的状态值。

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

image.png

  1. NIO模式,在内核数据没有准备好的阶段,用户进程或线程发起IO请求,立即返回,所以为了读取到最终的数据,用户进程或线程需要不断发起IO系统调用
  2. 内核数据到达后,用户进程(或者线程)发起系统调用,用户进程(或者线程阻塞。内核开始复制数据,它会将数据从内核缓冲区复制到用户缓冲区,然后内核返回结果(例如返回复制到的用户缓冲区的字节数)。
  3. 用户进程(或者线程)在读数据时,没有数据会立即返回而不阻塞,用户空 间 需要经过多次的尝试,才能保证最终真正读到数据,而后继续执行。

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

IO多路复用(IO Multiplexing)

为了提高性能,操作系统引入了一类新的系统调用,专门用于查询IO文件描述符的(含 socket连接)的就绪状态。 在 Linux系统中,新的系统调用为 select/epoll系统调用。 通过该系统调用,一个用户进程(或者线程)可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓冲区可读 /可写),内核能够将文件描述符的就绪状态返回给用户进程 (或者线程),用户空间可以根据文件描述符的就绪状态,进行相应的 IO系统调用。 IO多路复用( IO Multiplexing)属于经典的 Reactor模式一种实现, 也属于同步非阻塞的类型 。

image.png

  1. 选择器注册。在这种模式中,首先,将需要 read操作的目标文件描述符(socket连接),提前注册到 Linux的 select/epoll选择器中,在 Java中所对应的选择器类是 Selector类。然后,才可以开启整个 IO多路复用模型的轮询流程。
  2. 就绪状态的轮询。通过选择器的查询方法,查询所有的提前注册过的目标文件描述符( socket连接)的 IO就绪状态。通过查询的系统调用,内核会返回一个就绪的 socket列表。当任何一个注册过的 socket中的数据准备好或者就绪了,就是内核缓冲区有数据了,内核就将该 socket加入到就绪的列表中,并且返回就绪事件。
  3. 用户线程获得了就绪状态的列表,根据其 socket连接,发起 read系统调用,用户线程阻塞。内核开始复制数据,将数据从内核缓冲区复制到用户缓冲区。
  4. 复制完成后,内核返回结果,用户线程才会解除阻塞的状态,用户线程读取到了数据,继续执行。

IO多路复用模型的特点: IO多路复用模型涉及两种系统调用,(1)IO操作的系统调用,(2)select/epoll就绪查询系统调用。 IO多路复用模型建立在操作系统的基础设施之上,即操作系统的内核必须能够提供多路分离的系统调用select/epoll。

IO多路复用优点:多路复用模型的优点:一个选择器查询线程,可以同时处理成千上万的网络连接,所以,用户程序不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。
IO多路复用缺点:多路复用模型的缺点:本质上, select/epoll系统调用是阻塞式的,属于同步 IO

异步IO(Asynchronous IO ,AIO)

异步IO,指的是用户空间与内核空间的调用方式反过来。用户空间的线程变成被动接受者,而内核空间成了主动调用者。 在异步 IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户缓冲区内,内核在IO完成后通知用户线程直接使用即可。 异步IO类似于 Java中典型的回调模式,用户进程(或者线程)向内核空间注册了各种IO事件的回调函数,由内核去主动调用。 异步IO包含两种:不完全异步的信号驱动 IO模型 和完全的异步 IO模型。

信号驱动IO(Signal Driven IO,SIGIO)

在信号驱动IO模型中,用户线程通过 IO事 件的回调函数注册,来避免 IO时间查询的阻塞。具体的做法是,用户进程预先在内核中设置一个回调函数,当某个事件发生时,内核使用信号( SIGIO)通知进程运行回调函数。 然后用户线程会继续执行,在信号回调函数中调用 IO读写操作来进行实际的 IO请求操作。

image.png
信号驱动IO优势:优势:用户进程在等待数据时,不会被阻塞,能够用户进程的效率。
信号驱动IO缺点

  1. 在大量 IO事件发生时,可能会由于处理不过来,而导致信号队列溢出。
  2. 对于处理 UDP套接字来讲,对于信号驱动 I/O是有用的。可是,对于 TCP而言,由于致使 SIGIO信号通知的条件为数众多,进行 IO信号进一步区分的成本太高,信号驱动的 I/O方式近乎无用。
  3. 信号驱动 IO仅仅在 IO事的通知阶段是异步的, 而在第二阶段也就是 在将数据从内核缓冲区复制到用户缓冲区这个过程,用户进程是阻塞的、同步的。

    异步IO(Asynchronous IO,AIO)

    AIO的基本流程:用户线程通过系统调用,向内核注册某个IO操作。内核在整个IO操作(包括数据准备、数据复制)完成后,通知用户程序,用户执行后续的业务操作。

image.png
异步IO模型的特点:在内核等待数据和复制数据的两个阶段,用户线程都不是阻塞的。用户线程需要接收内核的 IO操作完成的事件,或者用户线程 需要注册一个 IO操作完成的回调函数。正因为如此,异步 IO有的时候也被称为信号驱动 IO。
异步IO异步模型的缺点:应用程序仅需要进行事件的注册与接收,其余的工作都留给了操作系统,也就是说,需要底层内核提供支持。

文件句柄

文件句柄,也叫文件描述符。在Linux系统中,文件可分为:普通文件、目录文件、链接文件和设备文件。 文件描述符( File Descriptor)是内核为了高效管理已被打开的文件所创建的索引,它是一个非负整数(通常是小整数),用于指代被打开的文件。所有的 IO系统调用,包括 socket的读写调用,都是通过文件描述符完成的。

在Linux下,通过调用 ulimit命令,可以看到一个进程能够打开的最大文件句柄数量,这个命令的具体使用方法是:ulimit -n

ulimit 命令是用来显示和修改当前用户进程一些基础限制的命令,-n选项用于引用或设置当前的文件句柄数量的限制值, Linux的系统默认值为 1024。

ulimit命令只能用于临时修改,如果想永久地把最大文件描述符数量值保存下来,可以编辑 /etc/rc.local开机启动文件,在文件中添加如下内容: ulimit SHn 1000000 两个命令选项。选项 -S表示软性极限值, ,-H表示硬性极限值。