Skip to content

Latest commit

 

History

History
173 lines (139 loc) · 20.6 KB

1_操作系统IO.md

File metadata and controls

173 lines (139 loc) · 20.6 KB

IO-从操作系统角度

  • 主要参考自:
  • 从个人理解角度记录,如有误:望通知,必修改!
  • IO的分类:大部分都是从是否阻塞、是否异步的角度进行分类!
  • 网络IO如何读取?
    • 两个过程:产生一个IO的进程,系统内核进行将数据拷贝到这个IO进程的缓冲区
    • 也就是:当一个read操作发生时,它会经历两个阶段:
      • **阶段1:**等待数据准备
      • **阶段2:**CPU将数据从内核空间拷贝到用户进程缓冲区的过程
  • 阅读完后最好需要理解:
    • 用户空间,也可以称为用户进程空间,主要存储用户进程需要的数据
    • 内核空间,也可以称为内核态空间,主存
    • 计算机从socket接口中获取数据是在内核态,需要cpu将内核空间中的数据拷贝到用户空间,用户进程再从用户空间中读取数据进行处理
    • IO模型就是针对:用户进程如何读取数据?什么时候读取数据?耗时操作在哪里?
      • 解决:合理利用CPU资源、进程资源!
      • 同步IO:用户进程最少都需要等待CPU将内核空间中的数据拷贝到用户空间
      • 异步IO:用户进程不需要等待CPU将内核空间中给的数据拷贝到用户空间(此时也许:不是CPU进行拷贝工作,因为这样用户进程才可以获取CPU去做其他事情),当内核空间中的数据全部拷贝到用户空间之后,给用户进程发送一个通知即可!完全避免的两阶段浪费的时间

1. 阻塞IO模型

  • 前置知识?
    • 什么是recvfrom?百度!
      • 是一个函数,可以从对应socket接口上接收别人发送过来的数据,并且能够知道是谁发送过来的(数据发送源的地址)
      • 它用于从套接字上接收一个消息,因为是一个系统调用,所以调用时会从用户进程空间切换到内核空间运行一段时间再切换回来。默认情况下recv会等到网络数据到达并且复制到用户进程空间或者发生错误时返回,而不同的flags参数可以改变返回的时间。
  • 简单理解下图:应该可以这样理解
    • 服务器内部模型
    • 此时客户端向服务器发送数据(网络IO场景),服务器内部的用户进程希望获取用户的数据进行处理,但是需要等待用户数据发送完毕才能获取到数据,所以是阻塞的!
  • image-20211119094432651
  • 1、当用户进程使用recvfrom时,期望能够从绑定的socket接口中获取数据,发送一个系统调用请求,此时操作系统内核接收到请求,开始进行IO的第一个阶段:等待数据准备
  • 2、对应网络IO来说,此时需要接收的数据并没有到达,比如,还没有收到一个完整的TCP包,这个时候内核就要等待足够的数据到来。
  • 3、而在用户进程这边,整个进程会被阻塞。当内核一直等到数据准备好了,它就会将数据从内核中拷贝到用户内存,然后内核返回结果,用户进程才解除 block的状态,重新运行起来。
  • 另一种理解:A同学用杯子装水,打开水龙头装满水然后离开。这一过程就可以看成是使用了阻塞IO模型,因为如果水龙头没有水,他也要等到有水并装满杯子才能离开去做别的事情。很显然,这种IO模型是同步的。
  • 所以,blocking IO的特点就是在IO执行的两个阶段(等待数据、数据拷贝这个两个阶段用户线程都是阻塞的!)都被block了。

2. 非阻塞IO模型

  • image-20211119095252414
  • 1、当用户进程发出读操作时,使用recvfrom,从对应的socket接口中获取数据,发送一个系统调用请求,如果内核中的数据没有准备好,就直接返回一个error
  • 2、在用户进程这边,发起一个读操作后,不需要等待可以马上从操作系统内核中获取一个结果:
    • 如果是error,就知道数据还没有准备好,然后用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。
  • 另一种理解:B同学也用杯子装水,打开水龙头后发现没有水,它离开了,过一会他又拿着杯子来看看……在中间离开的这些时间里,B同学离开了装水现场(回到用户进程空间),可以做他自己的事情。但是它**只有是检查无数据的时候是非阻塞的,**在数据到达的时候依然要等待复制数据到用户空间(等着水将水杯装满),因此它还是同步IO。
  • 虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。
  • 所以,用户进程第一个阶段不是阻塞的,需要不断的主动询问内核数据好了没有;第二个阶段依然总是阻塞的。因此还是同步的

3. IO多路复用模型

  • 前面两个模型存在的问题?
    • 非阻塞IO是阻塞IO的改进,但是非阻塞IO在第一阶段需要轮询内核的 I/O 是否准备好,在轮询的过程中,用户进程啥也做不了,只是在循环。
  • 为了解决以上问题:
    • I/O 多路复用:如 select、poll,它是通过 I/O 事件分发,当内核数据准备好时,再以事件通知应用程序进行操作。
    • 大大改善了应用进程对 CPU 的利用率,在没有被通知的情况下,CPU可以做其他的事情。
  • image-20211119095845917
  • 1、IO多路复用模型是建立在内核提供的多路分离函数基础之上的(Select),使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。利用了新的select系统调用,由内核来负责本来是请求进程该做的轮询操作
    • 也就是之前:用户进程要做轮询操作
    • 现在:用户进程使用系统调用的方式,让操作系统内核使用select函数做轮询操作,(用户进程叫操作系统内核的另一个进程来对操作系统内核的收取数据的进程进行轮询)
    • 基本原理:就是select/epoll这个函数会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程,正式发起read请求。
  • 2、从流程上来看,使用了select函数进行IO请求和阻塞模型似乎并没有什么区别,只不过是让系统调用来完成阻塞等待的情况,并且还需要新增监控socket、以及调用select函数的额外操作,效率更差!
    • 但是:使用select的最大优势:用户进程可以在一个线程内同时处理多个socketIO请求,用户可以注册多个socket,即:即可达到在同一个线程内同时处理多个IO请求的目的。而在阻塞模型中,必须通过多线程的方式才能达到这个目的。
  • 多路IO复用是阻塞于select或poll,而没有阻塞于recv,有人将非阻塞IO定义成在读写操作时没有阻塞于系统调用的IO操作(不包括数据从内核复制到用户空间时的阻塞,因为这相对于网络IO来说确实很短暂),如果按这样理解,这种IO模型也能称之为非阻塞IO模型,但是按POSIX来看,它也是同步IO,因此是同步非阻塞IO。
  • 多路IO复用的IO模型比较特别,分个段。因为它能同时监听多个文件描述符(fd)。
  • **另一种理解:**这个时候C同学来装水,发现有一排水龙头,舍管阿姨告诉他这些水龙头都还没有水,等有水了告诉他。于是等啊等(select调用中),过了一会阿姨告诉他有水了,但不知道是哪个水龙头有水,自己看吧。于是C同学一个个打开,往杯子里装水(recv)。这里再顺便说说鼎鼎大名的epoll(高性能的代名词啊),epoll也属于IO复用模型,主要区别在于舍管阿姨会告诉C同学哪几个水龙头有水了,不需要一个个打开看(当然还有其它区别)。

3.1 select函数

  • handle_events:实现事件循环
  • handle_event:进行读/写等操作
  • 使用select函数的优点并不仅限于此。虽然上述方式允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的(在select函数上阻塞),平均时间甚至比同步阻塞IO模型还要长。
  • 如果用户线程只注册自己感兴趣的socket或者IO请求,然后去做自己的事情,等到数据到来时再进行处理,则可以提高CPU的利用率。
    • 没有什么是加一层不能解决的!!!加一层去管理IO请求!
    • IO多路复用模型使用了Reactor设计模式实现了这一机制。通过Reactor的方式,可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时(就是数据准备好的时候),则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。由于select函数是阻塞的,因此多路IO复用模型也被称为异步阻塞IO模型。
    • 注意,这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。(一般在使用IO多路复用模型时,socket都是设置为NONBLOCK的,不过这并不会产生影响,因为用户发起IO请求时,数据已经到达了,用户线程一定不会被阻塞。)
  • image-20211119101158934

4. 信号驱动IO模型

  • image-20211119103233821
  • 通过调用sigaction注册信号函数,等内核数据准备好的时候系统中断当前程序,执行信号函数(在这里面调用recv)。
  • **另一种理解:**D同学让舍管阿姨等有水的时候通知他(注册信号函数),没多久D同学得知有水了,跑去装水。是不是很像异步IO?很遗憾,它还是同步IO(省不了装水的时间啊)。
  • 和IO多路复用模型很像!

5.异步IO模型

  • 实际上,无论是阻塞 I/O、非阻塞 I/O,还是基于非阻塞 I/O 的多路复用都是同步调用。因为它们在 read 调用时,内核将数据从内核空间拷贝到应用进程空间,这个阶段都是需要等待的。
  • 而真正的异步 I/O 是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待!
  • image-20211119103609574
  • 1、真正”的异步IO需要操作系统更强的支持。在IO多路复用模型中,由用户线程自行读取数据、处理数据。在异步IO模型中,用户进程发起read操作之后,立刻就可以开始去做其它的事。
  • 2、从内核的角度,当它受到一个异步读之后,首先它会立刻返回,所以不会对用户进程产生任何阻塞。然后,内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都 完成之后,内核会给用户进程发送一个信号,告诉它read操作完成了,用户线程直接使用即可。 在这整个过程中,进程完全没有被阻塞。
  • 也就是调用aio_read,让内核等数据准备好,并且复制到用户进程空间后执行事先指定好的函数
  • **另一种理解:**E同学让舍管阿姨将杯子装满水后通知他。整个过程E同学都可以做别的事情(没有recv),这才是真正的异步IO。
  • 异步IO模型使用了Proactor设计模式实现了这一机制

6.总结

  • image-20211119104002767
  • IO分两阶段:
    • 1.数据准备阶段
    • 2.内核空间复制回用户进程缓冲区阶段
  • 阻塞IO模型、非阻塞IO模型、IO复用模型(select/poll/epoll)、信号驱动IO模型都属于同步IO,因为阶段2是阻塞的(尽管时间很短)。
  • 只有异步IO模型是符合POSIX异步IO操作含义的,不管在阶段1还是阶段2都可以干别的事。

7.再次理解同步异步阻塞非阻塞

  • 同步:用户进程触发IO操作,并且需要等待或者轮询的查看操作系统内核是否完成了数据拷贝到用户空间
  • 异步:用户进程触发IO操作后,可以去处理其他业务,等操作系统内核完成了数据拷贝到用户空间并且完成了指定的函数处理后,再通知用户进程获取结果即可,用户进程这两个阶段都可以处理其他业务
  • 阻塞:当一个进程调用 read() 或 write()时,该进程被阻塞,直到有一些数据被读取或写入,该进程在此期间不能执行其他任务
  • 非阻塞:当一个进程调用 read() 或 write()时,等待数据阶段是不阻塞的!
  • 总结:
    • 同步和阻塞似乎相同?=》不相同,同步是指在第二阶段:内核空间数据拷贝到用户进程空间中都需要等待的,但是阻塞是指第一阶段是否需要等待!
    • 异步和非阻塞似乎相同?=》不相同,异步是指两个阶段用户线程都可以执行其他操作,但是非阻塞是指第一阶段用户线程可以处理其他操作!
    • 同步异步:第二阶段的差别
    • 阻塞非阻塞:第一阶段的差别

8.直接与非直接IO

  • 前置知识:

    • 众所周知:磁盘IO是非常慢的,所以计算机一般都是尽可能的减少磁盘IO次数来减少IO时间,在系统调用后,会把用户数据拷贝到内核中缓存起来,这个内核缓存空间也就是【页缓存:PageCache】,只有当缓存满足某些条件的时候,才发起磁盘 I/O 的请求。
    • 想想:局部性原理=》内存管理机制=》虚拟内存!!!=》页面置换算法!
  • 根据【是否使用操作系统的页缓存】,将IO分为直接 I/O 与非直接 I/O

    • 直接IO:不会发生内核缓存和用户程序之间数据复制,跳过操作系统的页缓存,直接经过文件系统访问磁盘。
    • 非直接IO:读操作时,数据从内核缓存中拷贝给用户程序,写操作时,数据从用户程序拷贝给内核缓存,再由内核决定什么时候写入数据到磁盘。
  • 想要实现直接I/O,需要你在系统调用中,指定 O_DIRECT 标志。如果没有设置过,默认的是非直接I/O。

  • 在进行写操作的时候以下几种场景会触发内核缓存的数据写入磁盘:

    以下摘自—《深入linux内核架构》

    1、可能因不同原因、在不同的时机触发不同的刷出数据的机制。

    • 周期性的内核线程,将扫描脏页的链表,并根据页变脏的时间,来选择一些页写回。如果系统不是太忙于写操作,那么在脏页的数目,以及刷出页所需的硬盘访问操作对系统造成的负荷之间,有一个可接受的比例。
    • 如果系统中的脏页过多(例如,一个大型的写操作可能造成这种情况),内核将触发进一步的机制对脏页与后备存储器进行同步,直至脏页的数目降低到一个可接受的程度。而“脏页过多”和“可接受的程度”到底意味着什么,此时尚是一个不确定的问题,将在下文讨论。
    • 内核的各个组件可能要求数据必须在特定事件发生时同步,例如在重新装载文件系统时。
    • 前两种机制由内核线程pdflush实现,该线程执行同步代码,而第三种机制可能由内核中的多处代码触发。

    2、可以从用户空间通过各种系统调用来启用内核同步机制,以确保内存和块设备之间(完全或部分)的数据一致完整性。有如下3个基本选项可用。

    • 使用sync系统调用刷出整个缓存内容。在某些情况下,这可能非常耗时。
    • 各个文件的内容(以及相关inode的元数据)可以被传输到底层的块设备。内核为此提供了fsync和fdatasync系统调用。尽管sync通常与上文提到的系统工具sync联合使用,但fsync和fdatasync则专用于特定的应用程序,因为刷出的文件是通过特定于进程的文件描述符(在第8章介绍)来选择的。因而,没有一个通用的用户空间工具可以回写特定的文件。
    • msync用于同步内存映射
  • 什么是脏页?

    • 脏页-linux内核中的概念,
    • 因为硬盘的读写速度远赶不上内存的速度,系统就把读写比较频繁的数据事先放到内存中,以提高读写速度,这就叫高速缓存,linux是以页作为高速缓存的单位,
    • 当进程修改了高速缓存里的数据时,该页就被内核标记为脏页,内核将会在合适的时间把脏页的数据写到磁盘中去,以保持高速缓存中的数据和磁盘中的数据是一致的。
  • 总结:何时将缓存数据写回磁盘?

    • 周期性的扫描脏页
      • 发现脏页存在的时间过了某个时间点,就会把脏页数据刷回磁盘
    • 当脏页太多时,内核会把一定数量的脏页数据写回到磁盘上
    • 用户主动调用sync、fsync、fdatasync,内核缓存会刷到磁盘上

9.缓冲与非缓冲IO

  • 在操作系统内核之上的程序也可以实现对数据的缓存,文件操作的标准库是可以实现数据的缓存,那么根据「是否利用标准库缓冲」,可以把文件 I/O 分为缓冲 I/O 和非缓冲 I/O

    • 缓冲 I/O,利用的是标准库的缓存实现文件的加速访问,而标准库再通过系统调用访问文件。也叫标准IO,符合ANSIC 的标准IO处理,不依赖系统内核,所以移植性强,我们使用标准IO操作很多时候是为了减少对read()和write()的系统调用次数,带缓存IO其实就是在用户层再建立一个缓存区,这个缓存区的分配和优化长度等细节都是标准IO库代你处理好了,不用去操心。
    • 非缓冲 I/O,直接通过系统调用访问文件,不经过标准库缓存。因为没有标准库提供的缓冲,只能用操作系统的缓存区,会造成很多次的系统调用,降低效率
  • 「缓冲」特指标准库内部实现的缓冲。比方说,很多程序遇到换行时才真正输出,而换行前的内容,其实就是被标准库暂时缓存了起来,这样做的目的是,减少系统调用的次数。

  • 标准 I/O提供了以下3 种类型的缓冲。—《深入linux内核架构》

    • 全缓冲。在这种情况下,在填满标准 I/O 缓冲区后才进行实际 I/O 操作。对于驻留在磁盘上的文件通常是由标准 IO库实施全缓冲的。在一个流上执行第一次 I/O 操作时,相关标准 I/O函数通常调用 malloc 获得需使用的缓冲区。
术语冲洗(fush)说明标准 IO 缓冲区的写操作。缓冲区可由标准 I/O 例程自动地冲洗(例如,当填满一个缓冲区时),或者可以调用函数 fflush 冲洗一个流。值得注意的是,在 UNTX环境中,fush有两种意思。在标准 I/O库方面,flush(冲洗)意味着将缓冲区中的内容写到磁盘上(该缓冲区可能只是部分填满的)。在终端驱动程序方面,flush(刷清)表示丢弃已存储在缓冲区中的数据。
    • 行缓冲。在这种情况下,当在输入和输出中遇到换行符时,标准 I/O 库执行 I/O 操作。这允许我们一次输出一个字符(用标准 I/O 函数fputc),但只有在写了一行之后才进行实际 I/O操作。当流涉及一个终端时(如标准输入和标准输出),通常使用行缓冲。
对于行缓冲有两个限制。第一,因为标准 I/O 库用来收集每一行的缓冲区的长度是固定的。所以只要填满了缓冲区,那么即使还没有写一个换行符,也进行 I/O 操作。第二,任何时候只要通过标准 I/O 库要求从(a)一个不带缓冲的流,或者(b)一个行缓冲的流(它从内核请求需要 数据)得到输入数据,那么就会冲洗所有行缓冲输出流。在(b)中带了一个在括号中的说明,其理由是,所需的数据可能已在该缓冲区中,它并不要求一定从内核读数据。很明显,从一个不带缓冲的流中输入(即(a)项)需要从内核获得数据。
    • 不带缓冲。标准 I/O 库不对字符进行缓冲存储,例如,若用标准 I/O 函数 fputs 写 15个字符到不带缓冲的流中,我们就期望这 15 个字符能立即输出,很可能使用write 函数将这些字符写到相关联的打开文件中。