# OS面试问题

os

# 操作系统主要作用

操作系统本质上是一个运行在计算机上的软件程序 ,管理着计算机硬件和软件资源,为计算机硬件和软件提供了一种中间层,使应用软件和硬件进行分离, 屏蔽了硬件层的复杂性,让我们把关注点更多放在软件应用上。操作系统的主要功能有:

  1. 进程管理:进程管理的主要作用就是任务调度,以及进程的创建销毁、阻塞唤醒、进程同步、进程通信、死锁处理等功能。
  2. 内存管理:内存分配与回收、地址映射、虚拟内存以及页面的置换
  3. 文件管理:有效地管理文件的存储空间,合理地组织和管理文件系统,为文件访问和文件保护提供更有效的方法及手段。
  4. 设备管理:根据确定的设备分配原则对设备进行分配,使设备与主机能够并行工作,为用户提供良好的设备使用界面。

# 用户态和内核态

用户态和系统态是操作系统的两种运行状态:

  1. 内核态:内核态运行的程序可以访问计算机的任何数据和资源,不受限制,包括外围设备,比如网卡、硬盘等。处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况。
  2. 用户态:用户态运行的程序只能受限地访问内存,只能直接读取用户程序的数据,并且不允许访问外围设备,用户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程序获取。将操作系统的运行状态分为用户态和内核态,主要是为了对访问能力进行限制,防止随意进行一些比较危险的操作导致系统的崩溃,比如设置时钟、内存清理,这些都需要在内核态下完成 。

# 进程与线程

  1. 一个程序至少有一个进程,一个进程至少有一个线程,线程是依赖于进程存在的,线程是一个进程中代码的不同的执行路线;
  2. 进程是对运行时程序的封装,是操作系统进行资源调度和分配的最小单位,实现了操作系统的并发;线程是程序执行的最小单位,是CPU调度和分派的基本的单位,实现进程内部的并发。
  3. 资源与内存空间:进程是资源分配的基本单位,线程不拥有资源;进程之间拥有相互独立的内存单位,但是同一个进程下的各个线程之间共享程序的内存空间,(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
  4. 系统开销:创建或销毁进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等;而线程只需要堆栈指针以及程序计数器就可以了,开销远小于创建或撤销进程时的开销。在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
  5. 通信:线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助IPC。

# 进程的五种状态

thread-status
  1. 创建状态:进程刚被创建,尚未进入就绪队列。创建进程需要两个步骤:即为新进程分配所需要的资源和空间,设置进程为就绪态,并等待调度执行。
  2. 就绪状态:进程已获得除CPU以外的所需的一切资源,一旦得到CPU资源即可运行。
  3. 运行状态:进程正在处理器上面运行。
  4. 阻塞状态:进程正在等待某一事件而暂停运行,如等待某资源或者等待 IO 操作完成,即使CPU 空闲,该进程也不能运行。
  5. 终止状态:进程达到正常结束点或因其他原因被终止,下一步将被撤销。 终止一个进程需要两个步骤:先等待操作系统或相关的进程进行善后处理;然后回收占用的资源并被系统删除。

只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程, 在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。阻塞状态是缺少需要的资源从而由运行状态转换而来, 但是该资源不包括 CPU 时间,缺少 CPU 时间会从运行态转换为就绪态。

# 进程的调度算法

  1. 先来先服务:按照请求的顺序进行调度,使用队列实现。有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。
  2. 最短作业优先:按照估计运行时间最短的顺序进行调度。有利于短作业,但长作业有可能饿死,处于一直等待短作业执行完毕的状态,因为可能一直有短作业到来,那么长作业永远得不到调度。
  3. 最短剩余时间优先:按估计剩余时间最短的顺序进行调度。
  4. 时间片轮转:将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。
  5. 优先级调度:为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。
  6. 多级反馈队列调度算法:可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。它设置了多个队列,每个队列时间片大小都不同,进程在第一个队列没执行完,就会被移到下一个队列。

# 进程的五种通信方式

  1. 匿名管道pipe/有名管道:用于具有亲缘关系的进程间的通信; named pipe:可以用于无亲缘关系的进程间的通信,可以实现本机任意两个进程间的通信。
  2. 信号量 semophore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
  3. 消息队列Message Queuing:消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。消息队列克服了信号传递信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。管道和消息队列的通信数据都是先进先出的原则,但消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取,比 FIFO 更有优势。
  4. 共享内存 Shared memory:多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。它是针对其他进程间通信方式运行效率低而专门设计的。
  5. 套接字 socket:与其他通信机制不同的是,socket 可用于不同机器间的进程通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点。

# 线程间同步的方式

  1. 临界区 CriticalSection:在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。
  2. 互斥量 Mutex:采用互斥对象机制。只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。当前拥有互斥对象的线程处理完任务后必须将线程交出,以便其他线程访问该资源。互斥对象和临界区对象非常相似,但是互斥量允许在进程间使用,而临界区只限制于同一进程的各个线程之间使用,但是更节省资源,更有效率。
  3. 信号量 semophore:信号量其实就是一个计数器,限制了同一时刻访问同一资源的最大线程数。如果这个计数达到了零,则所有对这个Semaphore类对象所控制的资源的访问尝试都被放入到一个队列中等待,直到超时或计数值不为零为止。
  4. 事件 Event,wait/notify:事件机制,允许一个线程在处理完一个任务后,主动唤醒另一个线程执行任务,通过通知操作的方式来保持线程的同步。

# 上下文切换

对于单核单线程CPU而言,在某一时刻只能执行一条CPU指令。上下文切换(Context Switch)是一种将CPU资源从一个进程分配给另一个进程的机制。 从用户角度看,计算机能够并行运行多个进程,这恰恰是操作系统通过快速上下文切换造成的结果。在切换的过程中, 操作系统需要先存储当前进程的状态(包括内存空间的指针,当前执行完的指令等等),再读入下一个进程的状态,然后执行此进程。

# 死锁的四个必要条件

  1. 互斥条件:一个资源每次只能被一个进程使用。此时若有其他进程请求该资源,则请求进程只能等待。
  2. 请求与保持条件:进程已经获得了至少一个资源,但是又提出新的资源请求,而该资源已经被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。即当一个进程等待其他进程是,继续占有已经分配的资源。
  3. 不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,只能由获得该资源的进程释放。
  4. 循环等待条件:若干进程间形成首尾相接的循环等待资源的关系。

# 死锁预防

  1. 破除资源互斥条件
  2. 破除“请求与保持”条件:实行资源预分配策略,进程在运行之前,必须一次性获取所有的资源。缺点:在很多情况下,无法预知进程执行前所需的全部资源,因为进程是动态执行的,同时也会降低资源利用率,导致降低了进程的并发性。
  3. 破除“不可剥夺”条件:允许进程强行从占有者那里夺取某些资源。当一个已经保持了某些不可被抢占资源的进程,提出新的资源请求而不能得到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请。这意味着进程已经占有的资源会被暂时被释放,或者说被抢占了。
  4. 破除“循环等待”条件:实行资源有序分配策略,对所有资源排序编号,按照顺序获取资源,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。

# 死锁避免

死锁预防通过约束资源请求,防止4个必要条件中至少一个的发生,可以通过直接或间接预防方法,但是都会导致低效的资源使用和低效的进程执行。 而死锁避免则允许前三个必要条件,但是通过动态地检测资源分配状态,以确保循环等待条件不成立,从而确保系统处于安全状态。 所谓安全状态是指:如果系统能按某个顺序为每个进程分配资源(不超过其最大值),那么系统状态是安全的,换句话说就是, 如果存在一个安全序列,那么系统处于安全状态。银行家算法是经典的死锁避免的算法。

# 死锁检测

死锁预防策略是非常保守的,他们通过限制访问资源和在进程上强加约束来解决死锁的问题。死锁检测则是完全相反,它不限制资源访问或约束进程行为, 只要有可能,被请求的资源就被授权给进程。但是操作系统会周期性地执行一个算法检测前面的循环等待的条件。死锁检测算法是通过资源分配图来检测是否存在环来实现, 从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有存在环,也就是检测到死锁的发生

  1. 如果进程-资源分配图中无环路,此时系统没有死锁。
  2. 如果进程-资源分配图中有环路,且每个资源类中只有一个资源,则系统发生死锁。
  3. 如果进程-资源分配图中有环路,且所涉及的资源类有多个资源,则不一定会发生死锁。

# 死锁解除

死锁解除的常用方法就是终止进程和资源抢占,回滚。所谓进程终止就是简单地终止一个或多个进程以打破循环等待, 包括两种方式:终止所有死锁进程和一次只终止一个进程直到取消死锁循环为止;所谓资源抢占就是从一个或者多个死锁进程那里抢占一个或多个资源。

# 内存连续分配算法

为了能将用户程序装入内存,必须为它分配一定大小的内存空间。连续分配算法是最早出现的分配方式,该分配方式为用户程序在内存中分配一个连续的内存空间。连续分配方式可以分为四类:单一连续分配、固定分区分配、动态分区分配 和 动态可重定位分区分配

  1. 单一连续分配:内存在此方式下分为系统区和用户区,系统区仅提供给操作系统使用,通常在低地址部分;用户区是为用户提供的、除系统区之外的内存空间。这种方式无需进行内存保护。这种方式的优点是简单、无外部碎片,可以釆用覆盖技术,不需要额外的技术支持。缺点是只能用于单用户、单任务的操作系统中,有内部碎片,存储器的利用率极低。
  2. 固定分区分配:将用户内存空间划分为若干个固定大小的区域(分区大小可以相等也可以不等),每个分区只装入一道作业。 当有空闲分区时,便可以再从外存的后备作业队列中,选择适当大小的作业装入该分区,如此循环。这种分区方式存在两个问题:一是程序可能太大而放不进任何一个分区中,这时用户不得不使用覆盖技术来使用内存空间;二是主存利用率低,当程序小于固定分区大小时,也占用了一个完整的内存分区空间,存在称为内部碎片。
  3. 动态分区分配:该分区方法不预先划分内存划,而是在进程装入内存时,根据进程的大小动态地建立分区,并使分区的大小正好适合进程的需要,因此分区的大小和数目是可变的。在进程装入主存时,如果内存中有多个足够大的空闲块,操作系统必须确定分配哪个内存块给进程使用,这就是动态分区的分配策略,常见的分配策略有:
  4. 首次适应算法:从空闲分区链首开始查找,直至找到一个能满足其大小需求的空闲分区为止。然后再按照作业的大小,从该分区中划出一块内存分配给请求者。该算法倾向于使用内存中低地址部分的空闲分区,在高地址部分的空闲分区非常少被利用,从而保留了高地址部分的大空闲区。为以后到达的大作业分配大的内存空间创造了条件。缺点在于低址部分不断被划分,留下许多内存碎片,并且每次查找都从低址部分开始,会增加查找的开销。
  5. 循环首次适应算法:在为进程分配内存空间时,不再每次从链首开始查找,而是从上次找到的空闲分区开始查找,直至找到一个能满足需求的空闲分区,并从中划出一块来分给作业。该算法能使空闲中的内存分区分布得更加均匀,缺点是将会缺乏大的空闲分区。
  6. 最佳适应算法:把既能满足需求,又是最小的空闲分区分配给作业。为了加速查找,需要将所有的空闲区按照大小排序后,以递增顺序形成一个空白链。这样每次找到的第一个满足需求的空闲区,必然是最优的,因为每次分配后剩余的空间一定是最小的,缺点是在于内存中将留下许多难以利用的小空闲区。同时每次分配后必须重新排序,这也带来了一定的开销。
  7. 最差适应算法:按分区大小递减的顺序形成空闲区链,分配时直接从空闲区链的第一个空闲分区中分配,如果第一个空闲分区不能满足,那么再没有空闲分区能满足需要。在大空闲区中放入程式后,剩下的空闲区常常也非常大,于是还能装下一个较大的新程式。该算法克服了最佳适应算法留下的许多小碎片的不足,但缺点是保留大的空闲区的可能性减小了,而且空闲区回收也和最佳适应算法相同复杂。

# 虚拟内存

连续分配方式会形成许多“碎片”,虽然可以通过“紧凑”方法将碎片拼接成可用的大块空间,但开销很大,如果允许将一个进程分散地装入到许多不相邻的分区中, 便可以充分利用内存,而无须再进行“紧凑”。基于这一思想而产生了离散分配方式:分页存储管理、分段存储管理、段页式存储管理 而实现这种离散式分配的基础就是虚拟内存。

虚拟内存是为了让物理内存扩充成更大的逻辑内存,让程序获得更多的可用内存。实现的前提是应用程序在运行之前没有必要将页面或段全部装入内存,仅需将那些当前运行所需的少数页面或段先装入内存,其余部分暂留在磁盘上。

虚拟内存的基本思想是:每个程序拥有自己的地址空间,这个空间被分为大小相等的多个块,称为页,每个页都是一段连续的地址。这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。 当程序引用到一部分在物理内存中的地址空间时,由硬件立刻进行必要的映射;当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的命令

而虚拟内存是如何映射到物理内存上的呢?CPU 里有一个内存管理单元 MMU(Memory Management Unit) ,虚拟内存不是直接送到内存总线,而是先给到 MMU, 由 MMU 来把虚拟地址映射到物理地址,程序只需要管理虚拟内存就好 这样,对于进程而言,逻辑上似乎有很大的内存空间, 实际上其中一部分对应物理内存上的一块(称为帧,通常页和帧大小相等),还有一些没加载在内存中的对应在硬盘上。

虚拟内存的好处在于

  1. 在内存中更多的进程,提高系统并发度。由于对任何特定的进程都仅仅装入它的某些块,因此就有足够的空间来放置更多的进程。
  2. 进程可以比物理内存的全部空间还大。

# 页面置换算法

在进程运行的过程中,如果所要访问的页面不在内存,则需把他们调入内存,但是如果内存已无空闲空间时,系统必须从内存中调出一页程序或者数据送到磁盘的对换区中。这时把选择换出页面的算法称为页面置换算法。

  1. 最佳页面替换算法(OPT):被换出的页面将是最长时间内不再被访问的,通常可以保证获得最低的缺页率。是一种理论上的算法,因为无法知道一个页面多长时间不再被访问。
  2. 最近最少使用(LRU):淘汰最近一段时间内最久未被使用的页面。
  3. 最近未使用(NRU):
  4. 先进先出(FIFO):淘汰在内存中驻留时间最长的页。
  5. 时钟页面替换算法(Clock)

# 磁盘调度算法

  1. 先来先服务(FCFS):按照磁盘请求的顺序进行调度。优点是公平和简单,缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长。
  2. 最短寻道时间优先(SSTF):优先调度与当前磁头所在磁道距离最近的磁道。虽然平均寻道时间比较低,但是不够公平。如果新到达的磁道请求总是比一个在等待的磁道请求近,那么在等待的磁道请求会一直等待下去,也就是出现饥饿现象。具体来说,两端的磁道请求更容易出现饥饿现象。
  3. 电梯算法(SCAN):电梯算法和电梯的运行过程类似,总是按一个方向来进行磁盘调度,直到该方向上没有未完成的磁盘请求,然后改变方向。因为考虑了移动方向,因此所有的磁盘请求都会被满足,解决了 SSTF 的饥饿问题。

# 颠簸/抖动

颠簸本质上是指频繁的页调度行为,具体来讲,进程发生缺页中断,这时,必须置换某一页。然而,其他所有的页都在使用,它置换一个页,但又立刻再次需要这个页。 因此,会不断产生缺页中断,导致整个系统的效率急剧下降,这种现象称为颠簸(抖动)。内存颠簸的解决策略包括:

  1. 如果是因为页面替换策略失误,可以修改替换算法来解决这个问题;
  2. 如果是因为运行的程序太多,造成程序无法同时将所有频繁访问的页面调入内存,则要降低多道程序的数量;
  3. 否则,还剩下两个办法:终止该进程或增加物理内存容量。

# select、poll 和 epoll 之间的区别

  1. select:时间复杂度 O(n),select 仅仅知道有 I/O 事件发生,但并不知道是哪几个流,所以只能无差别轮询所有流,找出能读出数据或者写入数据的流,并对其进行操作。所以 select 具有 O(n) 的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
  2. poll:时间复杂度 O(n),poll 本质上和 select 没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个 fd 对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的。
  3. epoll:时间复杂度 O(1),epoll 可以理解为 event poll,不同于忙轮询和无差别轮询,epoll 会把哪个流发生了怎样的 I/O 事件通知我们。 所以说 epoll 实际上是事件驱动(每个事件关联上 fd)的。

# IO模型

# 阻塞式IO模型

假设应用程序的进程发起IO调用,但是如果内核的数据还没准备好的话,那应用程序进程就一直在阻塞等待,一直等到内核数据准备好了,从内核拷贝到用户空间,才返回成功提示,此次IO操作,称之为阻塞IO。

blocking
  1. 阻塞IO比较经典的应用就是阻塞socket、Java BIO。
  2. 阻塞IO的缺点就是:如果内核数据一直没准备好,用户进程将一直阻塞浪费性能,可以使用非阻塞IO优化。

# 非阻塞式IO模型

如果内核数据还没准备好,可以先返回错误信息给用户进程,让它不需要等待,而是通过轮询的方式再来请求。这就是非阻塞IO,流程图如下:

noblocking

非阻塞IO的流程如下:

  1. 应用进程向操作系统内核,发起recvfrom读取数据。
  2. 操作系统内核数据没有准备好,立即返回EWOULDBLOCK错误码。
  3. 应用程序进程轮询调用,继续向操作系统内核发起recvfrom读取数据。
  4. 操作系统内核数据准备好了,从内核缓冲区拷贝到用户空间。
  5. 完成调用,返回成功提示。 非阻塞IO模型,简称NIO,Non-Blocking IO。它相对于阻塞IO,虽然大幅提升了性能,但是它依然存在性能问题,即频繁的轮询,导致频繁的系统调用,同样会消耗大量的CPU资源。可以考虑IO复用模型。

# IO复用式IO模型

既然NIO无效的轮询会导致CPU资源消耗,我们等到内核数据准备好了,主动通知应用进程再去进行系统调用,那不就好了嘛?在这之前,我们先来复习下, 什么是文件描述符fd(File Descriptor),它是计算机科学中的一个术语,形式上是一个非负整数。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。 IO复用模型核心思路:系统给我们提供一类函数(如我们耳濡目染的select、poll、epoll函数),它们可同时监控多个fd的操作,任何一个返回内核数据就绪,应用进程再发起recvfrom系统调用。 应用进程通过调用select函数,可以同时监控多个fd,在select函数监控的fd中,只要有任何一个数据状态准备就绪了,select函数就会返回可读状态,这时应用进程再发起recvfrom请求去读取数据。

multiplexing

非阻塞IO模型(NIO)中,需要N(N>=1)次轮询系统调用,然而借助select的IO多路复用模型,只需要发起一次询问就够了,大大优化了性能。 但是呢,select有几个缺点:

  1. 监听的IO最大连接数有限,在Linux系统上一般为1024。
  2. select函数返回后,是通过遍历fdset,找到就绪的描述符fd。(仅知道有I/O事件发生,却不知是哪几个流,所以遍历所有流)。 因为存在连接数限制,所以后来又提出了poll。与select相比,poll解决了连接数限制问题。但是呢,select和poll一样,还是需要通过遍历文件描述符来获取已经就绪的socket。
  3. 如果同时连接的大量客户端,在一时刻可能只有极少处于就绪状态,伴随着监视的描述符数量的增长,效率也会线性下降。因此经典的多路复用模型epoll诞生。
  4. 为了解决select/poll存在的问题,多路复用模型epoll诞生,它采用事件驱动来实现,流程图如下:
epoll

epoll先通过epoll_ctl()来注册一个fd(文件描述符),一旦基于某个fd就绪时,内核会采用回调机制,迅速激活这个fd,当进程调用epoll_wait()时便得到通知。 这里去掉了遍历文件描述符的坑爹操作,而是采用监听事件回调的机制。这就是epoll的亮点。

select

epoll明显优化了IO的执行效率,但在进程调用epoll_wait()时,仍然可能被阻塞。能不能酱紫:不用我老是去问你数据是否准备就绪,等我发出请求后,你数据准备好了通知我就行了,这就诞生了信号驱动IO模型。

# 信号驱动式IO模型

信号驱动IO不再用主动询问的方式去确认数据是否就绪,而是向内核发送一个信号(调用sigaction的时候建立一个SIGIO的信号),然后应用用户进程可以去做别的事, 不用阻塞。当内核数据准备好后,再通过SIGIO信号通知应用进程,数据准备好后的可读状态。应用用户进程收到信号之后,立即调用recvfrom,去读取数据。

signal-driven

信号驱动IO模型,在应用进程发出信号后,是立即返回的,不会阻塞进程。它已经有异步操作的感觉了。但是你细看上面的流程图,发现数据复制到应用缓冲的时候, 应用进程还是阻塞的。回过头来看下,不管是BIO,还是NIO,还是信号驱动,在数据从内核复制到应用缓冲的时候,都是阻塞的。还有没有优化方案呢?AIO(真正的异步IO)!

# 异步IO式IO模型

前面讲的BIO,NIO和信号驱动,在数据从内核复制到应用缓冲的时候,都是阻塞的,因此都不算是真正的异步。AIO实现了IO全流程的非阻塞,就是应用进程发出系统调用后, 是立即返回的,但是立即返回的不是处理结果,而是表示提交成功类似的意思。等内核数据准备好,将数据拷贝到用户进程缓冲区,发送信号通知用户进程IO操作执行完毕。

asynchronous

异步IO的优化思路很简单,只需要向内核发送一次请求,就可以完成数据状态询问和数据拷贝的所有操作,并且不用阻塞等待结果。 日常开发中,有类似思想的业务场景:比如发起一笔批量转账,但是批量转账处理比较耗时,这时候后端可以先告知前端转账提交成功,等到结果处理完,再通知前端结果即可。

# DMA(直接内存访问)

在没有 DMA 技术前,I/O 的过程是这样的: CPU 发出对应的指令给磁盘控制器,然后返回; 磁盘控制器收到指令后,于是就开始准备数据, 会把数据放⼊到磁盘控制器的内部缓冲区中,然后产⽣⼀个中断; CPU收到中断信号后,停下⼿头的⼯作,接着把磁盘控制器的缓冲区的数据⼀次⼀个字节地读进⾃⼰的寄存器, 然后再把寄存器⾥的数据写⼊到内存,⽽在数据传输的期间 CPU 是⽆法执⾏其他任务的。 可以看到,整个数据的传输过程,都要需要 CPU 亲⾃参与搬运数据的过程,而且这个过程,CPU 是不能做其他事情的。 简单的搬运字符数据那没问题, 但是如果我们⽤千兆⽹卡或者硬盘传输⼤量数据的时候,都⽤CPU来搬运的话,肯定忙不过来。 计算机科学家们发现了事情的严重性后,于是就发明了DMA技术,也就是直接内存访问。

img.png

DMA技术就是在进⾏I/O 设备和内存的数据传输的时候,数据搬运的⼯作全部交给DMA 控制器,而CPU不再参与任何与数据搬运相关的事情,这样CPU 就可以去处理别的事务。

img.png

可以看到, 整个数据传输的过程,CPU不再参与数据搬运的⼯作,全程由DMA完成,但是CPU在这个过程中也是必不可少的, 因为传输什么数据,从哪⾥传输到哪⾥,都需要CPU来告诉 DMA 控制器。 早期DMA 只存在在主板上,如今由于I/O 设备越来越多, 数据传输的需求也不尽相同,所以每个 I/O 设备⾥⾯都有⾃⼰的 DMA 控制器。