操作系统面经

用户态 内核态

根据进程访问资源的特点,我们可以把进程在系统上的运行分为两个级别:

  • 用户态(user mode) : 用户态运行的进程可以直接读取用户程序的数据。
  • 系统态(kernel mode):可以简单的理解系统态运行的进程或程序几乎可以访问计算机的任何资源,不受限制。

##系统调用

  • 我们运行的程序基本都是运行在用户态,如果我们调用操作系统提供的系统态级别的子功能咋办呢?那就需要系统调用了!
  • 也就是说在我们运行的用户程序中,凡是与系统态级别的资源有关的操作(如文件管理、进程控制、内存管理等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。
    这些系统调用按功能大致可分为如下几类:
  • 设备管理。完成设备的请求或释放,以及设备启动等功能。
  • 文件管理。完成文件的读、写、创建及删除等功能。
  • 进程控制。完成进程的创建、撤销、阻塞及唤醒等功能。
  • 进程通信。完成进程之间的消息传递或信号传递等功能。
  • 内存管理。完成内存的分配、回收以及获取作业占用内存区大小及地址等功能。

如何从用户态切换到内核态

  • 系统调用
    这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如 read 操作,比如前例中 fork() 实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。
  • 异常
    当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
  • 外围设备的中断
    当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

这3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。

其他必会知识

  • 并行与并发
    并发:在操作系统中,某一时间段,几个程序在同一个CPU上运行,但在任意一个时间点上,只有一个程序在CPU上运行。
    并行:两个程序在某一时刻同时运行,强调同时发生。

  • 阻塞与非阻塞
    阻塞是指调用线程或者进程被操作系统挂起。
    非阻塞是指调用线程或者进程不会被操作系统挂起。

  • 同步与异步
    同步与异步同步是阻塞模式,异步是非阻塞模式。
    同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,知道收到返回信息才继续执行下去;
    异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回式系统会通知进程进行处理,这样可以提高执行的效率。

PCB是什么

进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对PCB 的操作, PCB 是进程存在的唯一标识。

  • PCB主要包含下面几部分的内容:
    • 进程的描述信息,比如进程的名称,标识符,
    • 处理机的状态信息,当程序中断是保留此时的信息,以便 CPU 返回时能从断点执行
    • 进程调度信息,比如阻塞原因,状态,优先级等等
    • 进程控制和资源占用,同步通信机制,链接指针(指向队列中下一个进程的 PCB 地址)
  • PCB 的作用
    • PCB是进程实体的一部分,是操作系统中最重要的数据结构
    • 由于它的存在,使得多道程序环境下,不能独立运行的程序成为一个能独立运行的基本单位,使得程序可以并发执行
    • 系统通过 PCB 来感知进程的存在。(换句话说,PCB 是进程存在的唯一标识)
    • 进程的组成可以用下图来表示,PCB 就是他唯一标识符。
      在这里插入图片描述

线程、进程、协程的区别

答:

  • 进程是资源分配的最基本的单位,运行一个程序会创建一个或多个进程,进程就是运行起来的可执行程序。
  • 线程是程序执行的最基本的单位,是轻量级的进程,每个进程里都有一个主线程,且只能有一个,和进程是相互依存的关系,生命周期和进程一样。
  • 协程是用户态的轻量级线程,是线程内部的基本单位。无需线程上下文切换的开销、无需原子操作锁定及同步的开销、方便切换控制流,简化编程模型。

进程和线程的区别 关系

  • 首先从资源来说,进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。
  • 然后从调度来说,线程是独立调度的基本单位,在同一进程中线程切换的话不会引起进程的切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程的切换。
  • 从系统开销来讲,由于创建或撤销进程,系统都要分配回收资源,所付出的开销远大于创建或撤销线程时的开销。类似的,在进行进程切换的时候,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境设置,而线程切换只需保存和设置少量寄存器的内容,开销很小。
  • 通信方面来说,线程间可以通过直接读写同一进程的数据进行通信,但是进程通信需要借助一些复杂的方法。

另一个版本

  • 进程 :
    • 进程是资源分配的基本单位。
    • 进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对PCB 的操作。
  • 线程 :
    • 线程是独立调度的基本单位。
    • 一个进程中可以有多个线程,它们共享进程资源。
  • 区别 :
    • 拥有资源 :
      • 进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。
    • 调度 :
      • 线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
    • 系统开销 :
      • 由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小
      • 由于不同进程的资源相互独立,因此在创建和销毁进程的时候,系统需要进行资源分配和回收,因此进程的创建和销毁的开销要大于线程的创建和销毁。
      • 在进行进程切换的时候,需要对CPU环境和主存进行切换,而线程切换只需要对少部分的寄存器内容进行切换,开销较小
    • 通信方面 :
      • 线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。

进程间通信的方式

管道/匿名管道:用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。只存在于内存中。
有名管道:遵循先进先出,以磁盘文件的形式存在,实现本机任意两个进程通信。
信号:用于通知进程某个事件已经发生。
消息队列:消息的链表,存放在内核中,由消息队列标识符标识。要先进先出的消费,但可以随机查询。只有内核重启(即操作系统重启)或者显式删除一个消息队列时才会被删除。和消息队列相比,信号承载的信息量少,管道只能承载无格式字节流且缓冲区大小受限。
信号量:是个计数器,用于多进程对共享数据的访问,意图在于进程间同步。
内存共享:使多进程同时访问同一块内存空间,需要依靠某种同步操作,例如互斥锁或信号量。
套接字:用于在客户端和服务器之间通过网络通信,套接字是支持tcp/IP的网络通信的基本操作单元。

另一个版本:

  • 管道

    • 匿名管道:举个例子:linux 里的竖线,就是管道的意思,比如 ps -aux|grep mysql 这句话的意思是把前一个进程查询的结果作为 grep mysql 的输入,如果两个进程要进行通信的话,就可以用这种管道来进行通信。
      这种通信的方式是半双工通信的,只能单向交替传输
      并且只能在具有亲属关系的进程之间通信使用。
      可以看成是一种特殊的文件,但是这种文件只能存在于内存之中。
    • 命名管道:可以用 mkfifo 命令创建一个命名管道,可以用一个进程向管道里写数据,然后可以让另一个进程把里面的数据读出来。命名管道的优点是去除了只能在父子进程中使用的限制,并且命名管道有路径名和它相关联,是以一种特殊设备文件形式存在于文件系统中的。
      在这里插入图片描述
  • 消息队列

    • 消息队列的通信模式是这样的:a 进程要给 b 进程发消息,只需要把消息挂在消息队列(可以是中介邮局,也可以是进程自己的信箱)里就行了,b 进程需要的时候再去取消息队列里的消息。
    • 消息队列可以独立于读写进程存在,就算进程终止时,消息队列的内容也不会被删除。
    • 读进程可以根据消息类型有选择的接收消息,而不像 FIFO 那样只能默认接收。
      在这里插入图片描述如果进程发送的数据较大,并且两个进程通信非常频繁的话,消息队列模型就不太合适了,因为如果发送的数据很大的话,意味着发送消息(拷贝)这个过程就需要很多时间来读写内存。
  • 共享内存

    • 共享内存的方式就可以解决拷贝耗时很长的问题了。
    • 共享内存是最快的一种进程通信的方式,因为进程是直接对内存进行存取的。因为可以多个进程对共享内存同时操作,所以对共享空间的访问必须要求进程对共享内存的访问是互斥的。所以我们经常把信号量和共享内存一起使用来实现进程通信。
    • (这里补个知识!!!系统加载一个进程的时候,分配给进程的内存并不是实际的物理内存,而是虚拟内存空间。那么我们可以让两个进程各自拿出一块儿虚拟地址空间来,映射到同一个物理内存中。这样两个进程虽然有独立的虚拟内存空间,但有一部分是映射到相同的物理内存,这样就完成共享机制了。)
  • 信号量

    • 共享内存最大的问题就是多进程竞争内存的问题,就像平时所说的线程安全的问题,那么就需要靠信号量来保证进程间的操作的同步与互斥。
    • 信号量其实就是个计数器,例如信号量的初始值是 1,然后 a 进程访问临界资源的时候,把信号量设置为 0,然后进程 b 也要访问临界资源的时候,发现信号量是 0,就知道已有进程在访问临界资源了,这时进程 b 就访问不了了,所以说信号量也是进程间的一种通信方式。
  • 套接字
    套接字可以实现两个不同的机器之间的进程通信,比如 socket 使用。

不同通信方式的优缺点?

  • 管道:速度慢,容量有限;
  • Socket:任何进程间都能通讯,但速度慢;
  • 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题;
  • 信号量:不能传递复杂消息,只能用来同步;
  • 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。

线程间同步的方式

互斥量:持有“互斥量(就是锁)”的线程才有访问公共资源的权限。
信号量:允许同一时刻多个线程访问统一资源,但是需要控制最大线程数量。
事件:wait/notify 通过通知保证多线程同步

进程同步的方式

记住这个版本

  • 临界区
    首先对临界资源的访问那段代码被称为临界区,为了互斥的访问临界区,每个进程在进入临界区时,都需要先进行检查,也就是查看锁。

  • 同步与互斥
    同步:多个进程因为合作产生的直接制约关系,使得进程有一定的先后顺序。
    互斥:多个进程在同一时刻只有一个进程能进入临界区。

  • 信号量
    为控制一个具有有限数量用户资源而设计。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。
    信号量是一个整型变量,可以对其执行 P 和 V 操作。
    P:如果信号量大于零,就对其进行减 1 操作;如果信号量等于 0,进程进入 waiting 状态,等待信号量大于零。
    V:对信号量执行加 1 操作,并唤醒正在 waiting 的进程
    如果信号量只能取 0 或者 1,那么就变成了互斥量,其实也可以理解成加锁解锁操作,0 表示已经加锁,1 表示解锁。

  • 管程
    使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。
    管程有一个重要特性:在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程,否则其它进程永远不能使用管程。
    管程引入了 条件变量 以及相关的操作:wait() 和 signal() 来实现同步操作。对 条件变量 执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。

另一个版本 这个没有管程的 换乘事件

  • 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
    • 当多个线程访问一个独占性共享资源时,可以使用临界区对象。拥有临界区的线程可以访问被保护起来的资源或代码段,其他线程若想访问,则被挂起,直到拥有临界区的线程放弃临界区为止,以此达到用原子方式操 作共享资源的目的。
    • 优点:保证在某一时刻只有一个线程能访问数据的简便办法。
    • 缺点:虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。
  • 互斥量:为协调共同对一个共享资源的单独访问而设计的。互斥量跟临界区很相似,比临界区复杂,互斥对象只有一个,只有拥有互斥对象的线程才具有访问资源的权限
    • 优点:使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。
    • 缺点:
      • 互斥量是可以命名的,也就是说它可以跨越进程使用,所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。
      • 通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号量对象可以说是一种资源计数器。
  • 信号量:为控制一个具有有限数量用户资源而设计。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。互斥量是信号量的一种特殊情况,当信号量的最大资源数=1就是互斥量了。
    • 如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。
    • 优点:适用于对Socket(套接字)程序中线程的同步。
    • 缺点:
      • 信号量机制必须有公共内存,不能用于分布式操作系统,这是它最大的弱点;
      • 信号量机制功能强大,但使用时对信号量的操作分散, 而且难以控制,读写和维护都很困难,加重了程序员的编码负担;
      • 核心操作P-V分散在各用户程序的代码中,不易控制和管理,一旦错误,后果严重,且不易发现和纠正。
  • 事件: 用来通知线程有一些事件已发生,从而启动后继任务的开始。
    • 优点:事件对象通过通知操作的方式来保持线程的同步,并且可以实现不同进程中的线程同步操作。

进程的五种状态

面试在答的时候这么答:有创建状态、就绪状态、运行状态、阻塞状态、结束状态。

其中只有就绪状态和运行状态能互相转化,当进程为就绪态时,等待 CPU 分配时间片,得到时间片后就进入 运行状态
运行状态在使用完 CPU 时间片后,又重回就绪态。
阻塞状态是进程在运行状态时,需要等待某个资源比如打印机资源,而进入一个挂起的状态,等资源拿到后会回到就绪状态,等待 CPU 时间片。

进程的调度算法

为了实现最大cpu利用率

  1. 先来先服务 first-come first-serverd(FCFS)

非抢占式的调度算法,按照请求的顺序进行调度。

有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。

  1. 短作业优先 shortest job first(SJF)

非抢占式的调度算法,按估计运行时间最短的顺序进行调度。

长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。

  1. 最短剩余时间优先 shortest remaining time next(SRTN)

最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。

  1. 时间片轮转

将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。

时间片轮转算法的效率和时间片的大小有很大关系:

因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。
而如果时间片过长,那么实时性就不能得到保证。在这里插入图片描述
5. 优先级调度
为每个进程分配一个优先级,按优先级进行调度。

为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。

6.多级反馈队列

一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。

多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,…。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。

每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。

可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。
在这里插入图片描述

僵尸进程是什么?如何解决?

  • 子进程运行结束了而父进程还没有,而且父进程未对子进程进行回收,就会产生僵尸进程

  • 原因:

    • 子进程在完成工作后,会给父进程发送SIGCHILD信号,等待父进程进行处理
    • 如果父进程没有妥善处理,就会产生僵尸进程
  • 目的:

    • 维护子进程的信息,让父进程在以后的某个时间获取
    • 信息包括了进程ID,进程的终止状态,以及该进程使用的CPU时间
    • 父进程调用wait或waitpid时就可以得到这些信息
  • 解决方法:

    • 父进程调用wait方法

// 参数保存子进程退出通知码,返回 -1 表示没有子进程或者错误。否则返回子进程的进程 id 号。 pid_t wait(int *status); // 例子 pid = wait(NULL); // 忽略子进程通知码
如果僵尸进程一直不处理,就会导致系统的资源被耗尽(如果父进程不消亡的话)

如果父进程先消亡了,那么就有init进程来继承它们,并对这些进程进行清理操作

如何避免僵尸进程?

  • 通过signal(SIGCHLD, SIG_IGN)通知内核对子进程的结束不关心,由内核回收。==如果不想让父进程挂起,可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN);表示父进程忽略SIGCHLD信号,该信号是子进程退出的时候向父进程发送的。

  • 父进程调用wait/waitpid等函数等待子进程结束,如果尚无子进程退出wait会导致父进程阻塞。waitpid可以通过传递WNOHANG使父进程不阻塞立即返回。

  • 如果父进程很忙可以用signal注册信号处理函数,在信号处理函数调用wait/waitpid等待子进程退出。

  • 通过两次调用fork。父进程首先调用fork创建一个子进程然后waitpid等待子进程退出,子进程再fork一个孙进程后退出。这样子进程退出后会被父进程等待回收,而对于孙子进程其父进程已经退出所以孙进程成为一个孤儿进程,孤儿进程由init进程接管,孙进程结束后,init会等待回收。

孤儿进程是什么?有危害吗?

  • 父进程先于子进程结束,那么剩下的子进程就成为孤儿进程
  • 孤儿进程是无害的,不需要进行回收
  • 孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

死锁

讲讲死锁发生的条件是什么

  1. 互斥条件:是资源分配是互斥的,资源要么处于被分配给一个进程的状态,要么就是可用状态。
  2. 请求和保持条件:进程在请求资源得不到满足的时候,进入阻塞等待状态,且不释放已占有的资源。
  3. 不剥夺条件:已经分配给一个进程的资源不能强制性地被抢占,只能等待占有他的进程释放。
  4. 循环等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程释放所占有的资源。

如何避免死锁的发生 死锁预防

  • 死锁预防:从形成死锁的条件入手,基本思想就是打破形成死锁的四个条件中的一个或多个,保证系统不会进入死锁状态。

    • 破坏互斥条件:比如只读文件、磁盘等软硬件资源可采用这种办法处理。
    • 破坏占有和等待条件:在进程开始执行之前,就把其要申请的所有资源全部分配给他,直到所有资源都满足,才开始执行。
    • 破坏不剥夺条件:允许进程强行从资源占有者那里夺取某些资源
    • 破坏环路等待条件:给系统的所有资源编号,规定进程请求所需资源的顺序必须按照资源的编号依次执行。
  • 银行家算法 死锁避免

如果发生死锁了怎么办 死锁检测与恢复

  • 死锁检测:发生死锁之前总归需要先检测到死锁吧,不然怎么进行接下来的操作?可以通过检测有向图中是否存在环来检测,从一个节点出发进行 dfs,对访问过的节点进行标记,如果访问到了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。
  • 死锁恢复:从下到上逐渐变态。。。
    • 撤销进程法:
    1. 撤消陷于死锁的全部进程;
    2. 逐个撤消陷于死锁的进程,直到死锁不存在;
    • 资源剥夺法:
    1. 从陷于死锁的进程中逐个强迫放弃所占用的资源,直至死锁消失;
    2. 从另外的进程那里强行剥夺足够数量的资源分配给死锁进程,以解除死锁状态。
    • 鸵鸟算法,直接不管!

内存管理作用 是干什么的

  • 内存分配
  • 内存回收
  • 地址转换
  • 内存保护功能

讲讲内存管理的几种机制

  1. 分块管理
    是连续管理的一种,把内存分为几个大小相等且固定的块,每个进程占用其中一个,如果进程很小的话,会浪费大量的空间。已经淘汰。

  2. 分页管理
    把内存分为若干个很小的页面,相对比分块的划分力度更大一些。提高内存利用率。减少碎片,页式管理通过页表对应逻辑地址和物理地址。

  3. 分段管理
    把内存分为几个大小不定的有实际意义的段,比如 main 函数段,局部变量段,通过管理段表来把逻辑地址转为物理地址。

  4. 段页式管理
    结合了段式管理和页面管理的优点,把主存先分为若干个段,每个段又分为若干个页,也就是说段页式管理的段与段以及段的内部都是离散的。

详细版本:

  • 连续分配管理:为用户分配一个连续的内存空间
    • 块式管理
      • 将内存分成几个固定大小的块,每个块只包含一个进程。如果程序需要的内存较少,那么分配的这块内存很大一部分几乎被浪费了。在块中没有被使用到的空间即为碎片
  • 离散分配管理:允许一个程序使用的内存分布在离散或者不相邻的内存中
    • 页式管理
      • 将内存分为大小相等且固定的一页一页的形式,页较小,相比于块式管理划分得更细,提高内存利用率,减少碎片。页式管理通过页表对应逻辑地址和物理地址
        逻辑地址划分为固定大小的页,同样,物理地址划分为同样大小的页框,页与页框之间通过页表对应起来
        没有外碎片(页的大小是固定的),但是会产生内碎片(一个页可能会填不满)
    • 段式管理
      • 将程序的地址空间划分为若干段,如代码段、数据段、堆栈段等
      • 每一段具有实际意义
      • 段式管理通过段表对应逻辑地址和物理地址
      • 没有内碎片,但是会存在外碎片
  • 段页式管理
    • 先将主存分成若干段,每个段又分成若干页,段页式管理中段与段之间以及段的内部都是离散的。
    • 集合了段式管理和页式管理的优点,提高内存的利用效率

内存管理 需要总结

https://www.nowcoder.com/discuss/810741?channel=-1&source_id=profile_follow_post_nctrack

10 到 17

分页和分段有什么区别呢?

  • 共同点的话:
    • 首先都是离散内存分配的,但是每个页和每个段的内存是连续的。
    • 都是为了提高内存利用率,减少内存碎片。
  • 不同点:
    • 分页式管理的页面大小是固定的,由操作系统决定;分段式管理的页面是由用户程序所决定的。
    • 分页是为了满足操作系统内存管理的需求,每一页是没有实际的意义的;而段是有逻辑意义的,在程序中可认为是代码段、数据段。
    • 分页的内存利用率高,不会产生外部碎片;而分段如果单段长度过大,为其分配很大的连续空间不方便,会产生外部碎片。

讲讲页面置换算法

程序在运行时,在请求分页系统中,每当所要访问的页面不在内存时,便产生一个缺页中断,需要将所缺的页调入内存。
如果内存中有空闲块,则分配一个块,将要调入的页装入该块,并修改页表中的相应页表项。
但内存已无空闲空间时,就需要从内存中淘汰某页,而选择淘汰哪个页面的算法就是页面置换算法。

  • ①最佳置换算法(OPT) Optimal

最佳置换算法所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。

但是由于人们目前无法预知进程在内存下的若干页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现。

最佳置换算法可以用来评价其它算法。

  • ②先进先出页面置换算法(FIFO)First In First Out

优先淘汰最早进入内存的页面,即在内存中驻留时间最久的页面。

该算法实现:把调入内存的页面根据先后次序链接成队列,设置一个指针总指向最早的页面。

但该算法与进程实际运行时的规律不适应,因为在进程中,有的页面经常被访问。

  • ③最近最久未使用置换算法(LRU) Least Currently Used

选择最近最长时间未访问过的页面予以淘汰,

它认为过去一段时间未访问过的页面,在最近的将来可能也不会被访问。

该算法为每个页面设置一个访问字段,来记录页面自上次访问以来所经历的时间,淘汰页面时选择现有页面中值最大的予以淘汰。

  • ④时钟置换算法(CLOCK)

时间置换算法是给每一帧关联一个附加位,称为使用位。当某一页首次装入主存时,该帧的使用位设置位1;当该页随后再被访问到时,它的使用位也被置为1。

算法要循环扫描缓冲区,像时钟一样转动,所以叫时钟算法。

  • 5 最少使用页面置换算法(LFU ) Least Frequently Used
    把使用最少的页面淘汰掉。

逻辑(虚拟)地址与物理地址的含义?

  • 在编程中指针指向的内存地址就是逻辑地址,逻辑地址由操作系统决定
  • 物理地址是指真实内存单元的地址

逻辑地址与物理地址的转换过程?

  • 内存管理单元(MMU)管理逻辑地址和物理地址的转换,其中页表存储页(逻辑地址空间)和页框(物理内存空间)的映射表
  • 一个虚拟地址分为两个部分,一个部分存储页面号,一部分存储偏移量,两个部分结合,就可以在页表中得到页对应的页框地址

CPU寻址?为什么需要虚拟地址空间?

  • CPU寻址使用的是虚拟寻址的方式,将虚拟地址翻译成物理地址,以此访问到真实的物理内存
  • 一般完成该工作是CPU中的内存管理单元(MMU)的硬件
  • 如果没有虚拟地址空间的话,程序将直接操作真实的物理内存,此时:
    • 用户可以访问和修改任意内存,很容易有意或无意间破坏操作系统,造成操作系统崩溃
    • 同时运行多个程序将会非常困难,因为多个程序之间有可能会相互覆盖对方内存地址上的数据
  • 通过使用虚拟地址空间:
    • 程序可以使用一系列连续的虚拟地址空间来访问物理地址中不相邻的内存空间
    • 程序可以使用虚拟地址来访问大于可用物理内存的内存缓冲区间。当物理内存不足时, 可以将物理内存页保存到磁盘中。数据和代码页可以根据需要在物理内存和磁盘间移动
    • 不同进程间的虚拟地址彼此隔离。因此不同的进程无法干涉彼此正在使用的物理内存

什么是虚拟内存?

  • 虚拟内存是计算机系统提供的一种内存管理技术
  • 它给每个进程提供了一连续的虚拟地址空间,使得每一个进程具有独占主存的错觉,通过将内存扩展到硬盘空间,让程序可以拥有超过系统物理内存大小的可用内存空间==
  • 它定义了一个连续的虚拟地址空间,并且把内存扩展到了硬盘空间
  • 虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存,也就是说一个程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序成为可能
    • 对于没有映射到物理内存的页,可以在使用的时候将其装入物理内存
  • 虚拟内存是计算机系统提供的一种内存管理技术,它给每个进程提供了一个一致的、私有的、连续的地址空间,把内存扩展到了硬盘空间,让程序可以拥有超过物理内存大小的内存空间

另一个版本

  • why? 传统的内存管理必须把作业一次性的 load 到内存中,并且一直驻留到其作业运行结束,当作业很大时,是没有办法一次性装入内存的。
  • how? 而在一段时间内,只需要访问小部分数据就可以保证程序的正常运行。所以基于局部性原理,在程序加载的时候,把很快就会用到的部分放入内存中,暂时用不到的部分留在磁盘上。在程序执行的过程中,当信息不在内存时,再从外存把信息加载到内存里。当内存不够的时候,根据一些策略把用不到的内存换出到外存中,从而腾出空间给要调入内存的信息。而在 os 的管理下,让应用程序认为自己拥有一连续可用的内存,产生独享主存的错觉,这就是虚拟内存。

其实虚拟内存的基础是局部性原理,也正是因为有局部性原理,程序运行时才可以做到只装入部分到内存就可以运行。

局部性原理了解吗?

  • 在某个较短的时间内,程序执行局限于某一小部分,程序访问的存储空间也局限于某个区域
  • 两个方面:
    • 时间局部性:某条指令被执行后,不久后可能会再次执行;某个数据被访问后,不久后可能会再次被访问。该局部性的典型原因是程序中存在大量的循环操作。
    • 空间局部性:程序访问了某个存储单元后,不久后其附近的存储单元也将被访问到。因为程序指令往往是顺序存放、顺序执行的,数据一般也是聚簇存储的。
  • 时间局部性通过将近来使用的指令和数据存储在高速缓存中,并且使用高速缓存的层次结构来实现。空间局部性则使用较大的高速缓存并通过预取机制集成到高速缓存中实现。
  • 利用局部性原理来实现高速缓存

虚拟内存的三种技术实现方式

虚拟内存的实现需要基于离散分配的内存管理方式的基础上

  • 1 请求分页存储管理
    • 在分页管理基础上,添加了请求调页功能和页面置换功能
    • 在程序运行时,只装入当前要执行的部分页,如果在后续运行过程中发现访问的页不存在于内存中的话,则由处理器通知操作系统按照对应的页面置换算法将相应的页面置换到主存中,将暂时不用的页置换到外存中
  • 2 请求分段存储管理
    在分段管理基础上,添加了请求调段功能和分段置换功能
    过程类似上述
  • 3 请求段页存储管理

这和内存管理的机制有什么不同呢?
请求分页式存储管理建立在分页管理之上,他们的根本区别是用不用把程序所需的全部地址空间 load 到内存里。请求分页式不需要全部 load 到内存中,而分页式管理需要,前者能够提供虚拟内存,后者不可以!

请求分页和分页的主要区别在于是否在一开始就将所有的地址空间都装入内存
由于请求分页无需一次性装载,因此该方法可以提供虚拟内存

IO模型

记住这个版本

详细版
链接: 详细版.

还有一个版本
①BIO(blocking IO)

阻塞IO,即在读写数据的过程中会发生阻塞现象。

当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,操作系统就会将数据从内核空间拷贝到用户空间,并返回结果给用户线程,用户线程才解除阻塞状态。

(因为我们的用户程序只能获取用户空间的内存,无法直接获取内核空间的内存)

应该注意到,在阻塞的过程中,其它应用进程还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其它应用进程还可以执行,所以不消耗 CPU 时间,这种模型的 CPU 利用率会比较高。

典型的阻塞IO模型的例子为:

data = socket.read();

如果数据没有就绪,就会一直阻塞在read方法。

②NIO(nonblocking IO)

当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时(数据未准备就绪),它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。所以事实上,在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪(这种方式称为轮询 polling),也就说非阻塞IO不会交出CPU,而会一直占用CPU。

但是对于非阻塞IO就有一个非常严重的问题,在while循环中需要不断地去询问内核数据是否就绪,这样会导致CPU占用率非常高,因此一般情况下很少使用while循环这种方式来读取数据。

③AIO (异步IO Asynchronous IO)

【异步IO模型才是最理想的IO模型】

在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事。

而另一方面,从内核的角度,当它收到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何阻塞。

然后,内核会等待数据准备完成,再将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。

当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。

(也就说用户线程完全不需要关心实际的整个IO操作是如何进行的,只需要先发起一个请求。)

也就说在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用IO函数进行具体的读写。这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用IO函数进行实际的读写操作;而在异步IO模型中,收到信号表示IO操作已经完成,不需要再在用户线程中调用IO函数进行实际的读写操作。

④IO多路复用(IO multiplexing)

在多路复用IO模型中,使用 select 或者 poll 等待数据,会有一个线程不断去轮询多个socket的状态,只有当某一个socket真正有读写事件时,才真正调用实际的IO读写操作。

因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和 进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。

⑤信号驱动IO(signal driven IO)

在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个SIGIO信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。

IO 复用 select poll epoll比较

链接.

select 允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作。

poll 的功能与 select 类似,也是等待一组描述符中的一个成为就绪状态。

比较

  1. 功能
    select 和 poll 的功能基本相同,不过在一些实现细节上有所不同。
  • select 会修改描述符,而 poll 不会;
  • select 的描述符类型使用数组实现,FD_SETSIZE 大小默认为 1024,因此默认只能监听少于 1024 个描述符。如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译;而 poll 没有描述符数量的限制;
  • poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高。
  • 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定。
  1. 速度
    select 和 poll 速度都比较慢,每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区。

  2. 可移植性
    几乎所有的系统都支持 select,但是只有比较新的系统支持 poll。

epoll
从上面的描述可以看出,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符。

epoll 仅适用于 Linux OS。

epoll 比 select 和 poll 更加灵活而且没有描述符数量限制。

epoll 对多线程编程更有友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符也不会产生像 select 和 poll 的不确定情况。

工作模式

epoll 的描述符事件有两种触发模式:LT(level trigger)和 ET(edge trigger)。

  1. LT 模式
    当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。

  2. ET 模式
    和 LT 模式不同的是,通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。

很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

应用场景

很容易产生一种错觉认为只要用 epoll 就可以了,select 和 poll 都已经过时了,其实它们都有各自的使用场景。

  1. select 应用场景
    select 的 timeout 参数精度为微秒,而 poll 和 epoll 为毫秒,因此 select 更加适用于实时性要求比较高的场景,比如核反应堆的控制。
    select 可移植性更好,几乎被所有主流平台所支持。

  2. poll 应用场景
    poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。

  3. epoll 应用场景

    • 只需要运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是长连接。
    • 需要同时监控小于 1000 个描述符,就没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。
    • 需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且 epoll 的描述符存储在内核,不容易调试。

磁盘调度算法

  1. 先来先服务

FCFS, First Come First Served

按照磁盘请求的顺序进行调度。
优点是公平和简单。缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长。

  1. 最短寻道时间优先

SSTF, Shortest Seek Time First

优先调度与当前磁头所在磁道距离最近的磁道。

虽然平均寻道时间比较低,但是不够公平。如果新到达的磁道请求总是比一个在等待的磁道请求近,那么在等待的磁道请求会一直等待下去,也就是出现饥饿现象。具体来说,两端的磁道请求更容易出现饥饿现象。
在这里插入图片描述

  1. 电梯算法

SCAN

电梯总是保持一个方向运行,直到该方向没有请求为止,然后改变运行方向。

电梯算法(扫描算法)和电梯的运行过程类似,总是按一个方向来进行磁盘调度,直到该方向上没有未完成的磁盘请求,然后改变方向。

因为考虑了移动方向,因此所有的磁盘请求都会被满足,解决了 SSTF 的饥饿问题。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值