- 当前位置:
- 首页
- VIP_八股文
- aaaaVIP_面试突击专题
- 正文
操作系统突击八股文
操作系统学习指南
需要掌握的知识概括:
参考学习文章以及资料
我会在对应的面试题那里,补充对应的文章,专栏,视频和书籍,是一个持续补充的过程,大家有看到好的文章也可以发我,然后我也会给大家推荐对应的书籍 + 咱们训练营的专栏,作为一个进阶补充,有时间你就都看。
系统资料推荐:操作系统的学习方式,一般是通过学校教学或者极简视频入门,之后再基于面试题去学习,重点需要深入理解进程和线程,入门看 操作系统极简入门,之后配合文章即可,或者把你们手里的硬核书籍,看一下进程相关章节。
正文
【操作系统专题】进程与线程通信/区别基本问题(超高频)🌟🌟🌟🌟🌟
PS:操作系统的面试,基本就是问这个问题,烂大街问题,所以最好可以说出自己的理解,而不是只会背诵
1、说一说进程间的通信方式有哪些?
2、那线程之间的通信方式又有哪些呢?
3、那你觉得有了进程,为啥还需要线程呢?他们有啥区别?
4、刚才说到了线程切换保存上下文,那你觉得上下文都有哪些内容需要保持呢?
5、进程里面有哪些内容?简单说一些
6、说一下进程(任务)调度算法
【参考文章以及资料补充】:
参考回答:
1、说一说进程间的通信方式有哪些?
大致有五种:两种管道,消息队列,共享内存,信号和 Socket。
首先第一个管道,管道在 Linux 中分为匿名管道和命名管道,Linux 命令中的 | ,> 等都是匿名管道,作用是将前一个命令的输出作为后一个命令的输入。命名管道可以用 mkfifo 命令创建,可以用在一个进程往里写,另一个进程从里读的场景。
管道的优点是使用简单,但只能用于有亲缘关系的进程间通信,且为单向通信。
然后是消息队列,这个消息队列不是中间件那些 MQ,它是保存在操作系统内核的消息链表,A 进程要给 B 进程发消息,只需将数据写入这个消息链表,B 进程需要时读取即可。
消息队列的优点是支持独立于进程的消息传递,缺点是传递的数据要在用户和内核空间来回复制,效率低,同时数据大小受操作系统限制。
共享内存的出现解决了消息队列中的数据要在用户和内核空间复制的问题,它是让所有进程的某块虚拟地址,都映射到一块相同的物理内存中。这样某个进程对这块地址的写入,其它进程都能立马看到。
这种方式是进程间通信速度最快的,但需要考虑共享资源的同步问题。
还有就是信号 signal,信号是进程通信中唯一的异步通信机制,任何时候都可以对某个进程发送某种信号,比如常见的 kill 命令,Cltr + C 发送终止信号,Cltr + Z 发送暂停信号等。
除了同一台主机上不同进程的通信,还可以使用 Socket 实现不同主机上进程间通信,期间需要建立连接,然后使用 write 和 read 来传递数据。
2、那线程之间的通信方式又有哪些呢?
线程间的通信模型有两种:共享内存和消息传递
基于共享内存,可以使用 volatile,JUC 下的 CountDownLatch,ReentrantLock,LockSupport。基于消息传递,可以使用 Object 类提供的 wait,notify 方法。
3、那你觉得有了进程,为啥还需要线程呢?他们有啥区别?
术语:进程是系统资源分配的基本单位,线程是处理器调度的基本单位。
引入进程,是为了使多个程序并发执行,以改善资源利用率、提高系统吞吐量。引入线程,则是为了减少程序并发执行时的所付出的时空开销。
进程相当于一个容器,线程只是里面的一个东西。并且程序的本质是线程在运行。从内存结构的角度出发,进程的内存结构包括代码段,数据段,堆栈等,统称为 PCB。而线程之间共享同一进程的代码段和数据段资源,但也有自己的栈空间等等,这些统称为 TCB;从包含关系出发,一个进程至少存在一个线程,也可以拥有多个线程,但一个线程只属于一个进程。
4、刚才说到了线程切换保存上下文,那你觉得上下文都有哪些内容需要保持呢?
由于线程是由进程创建的,同一个进程下的所有线程共享进程的代码段和数据段等资源,除此之外,线程还有自己的栈空间,以及自己的线程存储空间。
所以当线程切换时,只需要保存线程私有的栈空间以及私有数据等等。
5、进程里面有哪些内容?简单说一些
在操作系统中,使用进程控制块,也就是 PCB 来描述进程的。PCB 中主要包含进程描述信息,如进程标识符和用户标识符,前者用于唯一标识一个进程,后者用于标识进程所归属的用户;还有进程控制和管理信息,包括进程当前的状态和进程优先级等等;还有 CPU 相关信息,包括 CPU 中各个寄存器的值,当进程切换时,CPU 相关信息都保存在相应的 PCB 中,以便后续断点恢复。
6、说一下进程(任务)调度算法
常见的大约有六种:先来先服务,最短作业优先,高响应比优先,时间片轮转,最高优先级,多级反馈队列算法。
“首先先来先服务” 调度算法是最简单的。顾名思义,就是最先达到就绪队列的进程,优先运行。这对长作业有利,而短作业可能需要很长时间的等待时间。
“最短作业优先” 调度也是顾名思义,优先选择就绪队列中运行时间最短的运行。这对短作业有利,但对长作业不利,可能会造成多作业很多,长作业不断被延后的极端情况。
这两种算法都没有很好的权衡长短作业,而 “高响应比优先调度” 解决了这个问题,它给每个作业都定义了一个响应比:等待时间 + 要求服务时间➗要求服务时间,谁的响应比高就优先运行谁。当两个进程等待时间相等时,要求服务时间越小,响应比就越高,短作业就更容易运行;当两个进程要求服务时间相同,等待时间越长,响应比越高,这也兼顾了长作业进程。
还有就是使用最广,最公平的 “时间片轮转” 调度算法,它轮流为每个进程都分配一个时间片,进程只有获得时间片时才能运行,运行的时间取决于时间片的大小。如果时间片太大,会导致短作业的响应时间边长。如果时间片太小,又会带来过多的进程上下文切换,降低 CPU 效率。所以通常时间片设置为 20ms – 50ms 是一个合理的折中值。
由于 “时间片轮转” 算法是基于所有的进程同等重要,对于多用户计算机系统来说,它希望进程的调度具有优先级,每次从都从就绪队列中挑选出具有最高优先级的进行优先运行,这种算法叫做 “最高优先级” 调度算法。具体的优先级可以分为静态优先级和动态优先级。前者是指进程创建时就确定了优先级,后者是进程可以根据等待时间等等因素动态提高或降低优先级。
但 “时间片轮转” 也有缺点,比如低优先级的进程可能永远不会运行。而 “多级反馈队列” 调度算法综合了前面 “时间片轮转” 算法和 “最高优先级” 算法,它有多个就绪队列,每个队列代表的优先级从高到低,优先级越高时间片越短。它的工作流程为:新创建的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待被调度,如果在第一级队列规定的时间片没运行完成,则将其转入到第二级队列的末尾,以此类推,直至完成;这样的话,对于短作业来说,会在前几个锁分配时间片较小的队列就能优先完成。对于长作业来说,虽然经过的队列多,等待时间长,但同时分配的时间片也变长了。这个算法很好的兼顾了长短作业,同时也有较好的相应时间。
【操作系统专题】进程与线程进阶问题🌟🌟
PS:下面这些问题,相对有难度一些,一般都是通过追问的方式来问的
1、AB共享内存通信,A new一块内存与B通信,A异常退出,B还能访问那块内存吗 为什么?
2、线程切换的时机?给线程分配时间片,操作系统怎么知道时间片用完了?线程切换 一定会引起内核态和用户态的切换吗?
3、单核情况下,为啥多线程能够比单线程效率好?
4、多线程效率一定比单线程要好吗?
5、居然多线程或者单线程各有优劣势,那你可以举几个适合多线程或者单线程的例子吗?
【参考文章以及资料补充】:
参考回答:
1、AB共享内存通信,A new一块内存与B通信,A异常退出,B还能访问那块内存吗 为什么?
是可以的,因为共享内存本身独立于 A,B 进程,是由操作系统来分配和管理的。并且 A,B 在使用共享内存通信时,都需要先分别 ”连接“ 上这块共享内存。如果 A 中途异常退出,但没有销毁这块共享内存,是不会影响 B 对共享内存继续读或者写的。
2、线程切换的时机?给线程分配时间片,操作系统怎么知道时间片用完了?线程切换 一定会引起内核态和用户态的切换吗?
大多数情况下会在时间片耗尽时进行线程切换。
操作系统主要通过一些硬件的支持来实现时间片。具体的,操作系统会设置一个定时器,以固定的时间间隔产生时钟中断。时间片的大小就可以用时钟中断的间隔来体现,每次时钟中断发生时,操作系统会中断当前正在执行的线程,将控制权交还给操作系统内核,从而实线程切换。
不一定,首先线程切换和调度只有拥有最高权限的内核才可以完成。从这个角度来说,如果线程当前处于用户态,如果要发生线程切换,确实是要先进入内核态,整个过程有用户态和内核态之间的切换。但如果线程当前已经处于内核态了,也一样可能遇到中断等等发生线程切换,此时就直接在内核态进行切换,不需要在切换到用户态了。总之,线程切换和状态切换没有直接关系,只不过线程切换一般都交给内核完成,是否发生状态切换应于线程此时所在的状态有关。
3、单核情况下,为啥多线程能够比单线程效率好?
这个需要看情况,如果是 CPU 密集型作业,在单核情况下,多线程的效率是不如单线程的。因为多线程会带来额外的线程上下文切换开销,单线程只需一直运行即可。
但如果是 IO 密集型作业,线程在进行 IO 操作时就不得不阻塞,此时 CPU 是闲着的,也就是 CPU 没有充分得到利用。此时就可以引入多线程,将任务拆分给多个线程去完成,当某个线程因进行 IO 而阻塞时,CPU 还可以处理其它线程任务,这个情况下多线程是比单线程效率高的。进一步,作业中 IO 操作的时间越长,多线程带来的效率提升就越高。
4、多线程效率一定比单线程要好吗?
不一定,除了上边说的情况,还有就是如果作业量很小,使用多线程带来的线程创建开销和上下文切换开销是大于多线程本身的效率提高的。我做过一个实验,对两个变量进行累加和累减。并发情况将累加和累减交给两个线程完成,串行情况则在一个线程内完成两个操作,实验结果为在数据量在十万以下串行执行时间是小于并行的,数据量在百万以上并行的效率才与串行拉开差距。
5、既然多线程或者单线程各有优劣势,那你可以举几个适合多线程或者单线程的例子吗?
在对视频或者图像进行处理,压缩时,由于每个像素可以独立处理,可以使用多线程来加快处理速度。还有对于高并发网络服务器而言,引入多线程可以同时处理多个客户端请求,能很大程度提高响应速度和吞吐量。
对于一些简单的,执行时间短的脚本就没必要进入多线程。也可以通过一些机制与单线程配合,最大程度发挥单线程的优点,比如 Redis 等等(感觉单线程的运用能扯到 Redis 上)。
【操作系统专题】与Linux系统相关的几个问题🌟🌟🌟
PS:这块问的也不多,但是一旦问,可能几个问题就一起问了
1、什么是守护进程?守护进程和普通进程有啥区别?
2、什么是是僵尸进程?僵尸进程会导致哪些问题?
3、如何避免僵尸进程呢?
4、孤儿进程呢?
【参考文章以及资料补充】:
6. 案例篇:系统中出现大量不可中断进程和僵尸进程怎么办?(上)
7. 案例篇:系统中出现大量不可中断进程和僵尸进程怎么办?(下)
参考回答:
1、什么是守护进程?守护进程和普通进程有啥区别?
守护进程是在操作系统中以后台服务的形式运行的进程,主要执行一些在后台持续运行的任务,如监控硬件状态,网络服务等等。
它与普通进程的区别在于几个方面,首先是守护进程不与终端或者用户界面相关联,通常伴随着系统的启动而启动,直到系统关闭而退出,相比之下,普通进程通常是由用户来决定启动和退出的。并且守护进程几乎不与用户进行交互,而普通进程通常需要依赖于用户的输入和输出。
2、什么是是僵尸进程?僵尸进程会导致哪些问题?
僵尸进程是指,一个子进程执行完毕后,它的父进程没有及时回收该子进程的相关资源,如进程描述符,内存等等,导致子进程的状态信息还存留在进程表中未被清理,但它本身已经不再执行任何操作了。
因为僵尸进程本身不会在运行,只是会存留一些状态信息。但如果僵尸进程多的话,他们会占用一定的进程表空间,导致进程表耗尽,系统不能再创建新的进程,从而影响系统的正常运行。
3、如何避免僵尸进程呢?
僵尸进程产生的根本原因就是父进程未等待子进程结束,解决就是父进程通过一定方式,获取到子进程的退出状态,然后完成子进程状态的清理。
具体的方式可以使用 wait( ) 或者 waitpid( ) 等系统调用,或者设置一个信号处理函数,当子进程结束时会给父进程发送信号。再有就是比较复杂的「双重 fork 法」,具体的,首先由父进程创建子进程,子进程再创建孙子进程,由孙子进程来处理具体的任务,而子进程在创建完孙子进程后就退出。由于孙子进程的父进程,也就是子进程退出了,那孙子进程变为了一个孤儿进程,Linux 中处理孤儿的进程的方式就是,由 init 进程接管孤儿进程,当孙子进程完成任务后,会直接被 init 进程回收,不会成为僵尸进程。
4、孤儿进程呢?
孤儿进程是指子进程还没结束呢,父进程先挂了,导致没有父进程来等待它的退出状态。这种情况下,孤儿进程会被系统的 init 进程接管,由它来确保孤儿进程的资源释放。孤儿进程本身没啥危害。
【操作系统专题】内核态与用户态问题🌟🌟
PS:这块相对问的比较少,如果问了,这块设计的内容比较深哦
1、介绍一下内核态与用户态?
2、为啥要区分内核态和用户态呢?
3、系统调用与库函数又有啥区别呢?
【参考文章以及资料补充】:
待补充
参考回答:
1、介绍一下内核态与用户态?
首先,操作系统的任务是管理计算机硬件,并件并提供服务给应用程序,为了完成这个任务,操作系统需要将软件运行时的状态分为两种模式:用户态和内核态。
用户态是指应用程序运行时的模式,此时应用程序只能访问自己的资源,无法直接访问操作系统的资源,比如访问内存或者打开文件等操作。也就是说,它的所有操作都会被操作系统限制在它自己的地址空间之内,以保证系统的稳定性和安全性。
内核态是指操作系统运行时的模式,此时操作系统拥有完全的控制权,可以访问系统的所有资源,包括内存、设备和外围硬件等。当应用程序需要进行一些特权操作,比如读取系统时间或访问网络等时,那么这个时候就必须进行从用户态到内核态的切换,进而有权限进行这些操作。
2、为啥要区分内核态和用户态呢?
主要还是考虑到安全性和稳定性,清内存,设置时钟等等都是比较危险的操作,都必须在内核态下进行,如果随便执行这些操作,系统就很容易崩溃。
3、系统调用与库函数又有啥区别呢?
https://haicoder.net/operation/system-library-differ.html
系统调用是面向底层硬件的,通过系统调用,可以使处在用户态的进程与计算机的硬件设备如 CPU,打印机等等进行交互。例如 open,close :打开,关闭文件或设;,fork,exit :创建和终止进程等等。由于系统调用需要调用内核相关的函数执行,如 sys_read( ) 等,用户程序在调用时通常需要通过软中断切换到内核态执行这些函数。
系统调用是指,向应用程序提供运行在系统内核中,需要更高执行权限的服务的接口。比如 open,close :打开,关闭文件,。由于系统调用需要在内核态执行,所以应用程序在调用时通常有从用户态到内核态的切换。
库函数是把一些函数放在一个库里,方便别人使用的一种方式。这些函数是基于系统调用之上的一层包装,当然也可能不包含系统调用。就比如 glibc,它是 Linux 下使用的开源标准 C 库,里面的 printf( ) 的实现最终还是调用了 write 这样的系统调用,而像 strlen( ), strcat( ) 等等就只涉及到字符串的长度,比较等等,不涉及系统调用。
总的来说,系统调用是与操作系统本身进行交互的接口,而库函数则是为了人们编程的方便。
【操作系统专题】内存相关问题🌟🌟🌟🌟🌟
PS:这块问的也比较简单
1、虚拟内存了解吗?为啥要有虚拟内存呢?
2、虚拟内存和物理内存有啥区别?
3、讲一讲内存分页、分段?页表呢?🌟🌟
【参考文章以及资料补充】:
参考回答:
1、虚拟内存了解吗?为啥要有虚拟内存呢?
虚拟内存其实是处在进程和物理内存的中间层,它为每个进程提供了一个连续,完整的地址空间,实际上这块空间是由多个物理内存碎片组成的,并且还有一部分暂时存储在外部磁盘上,将内存扩展到了硬盘空间。这样以来,应用程序就可以访问比实际物理内存容量更大的地址空间,从而使更大的程序可以运行在计算机上。
除此之外,虚拟内存可以控制进程对物理内存的访问,隔离不同进程的访问权限,提高系统安全性。
2、虚拟内存和物理内存有啥区别?
物理内存就是计算机真实的内存容量,而虚拟内存是在物理内存之上做了一层抽象,将磁盘上的一部分空间加上整个物理内存构成虚拟内存空间。物理内存的空间是所有进程都共享的,而虚拟内存的空间是每个进程都独有的,但进程访问的虚拟内存地址最终还带映射到具体的物理内存地址上,这个操作交由操作系统的 MMU 来管理。也就是说,进程只管访问自己的虚拟内存空间,MMU 会进行从虚拟地址到物理地址的翻译工作,如果物理地址已经在内存就直接访问,如果是在磁盘,那就会发生缺页中断,将所缺的页先加载进内存,再进行访问。
这其中还需要考虑如果内存已经满了,但现在需要加载新的页到内存,此时应该踢出哪些已经在内存的页呢?这就涉及到页面置换算法的设计了。
3、讲一讲内存分页、分段?页表呢?
内存分页和内存分段是操作系统管理虚拟地址与物理地址之间的关系提出的。最早提出的是内存分段,之后由于内存分段存在一定缺陷,又提出了内存分页这种方式,现在大多数操作系统都采用内存分页的方式管理内存。
内存分段就是将操作系统中的程序分成若干个逻辑分段,例如代码段,数据段,栈堆段等等,不同的段带有不同的属性。一个虚拟地址只需保存程序的段选择因子和段内偏移量,就能确定该程序对应的段所在的具体位置。它虽然解决了从虚拟地址到物理地址的具体该如何映射,但分段机制本身存在内存碎片,进而还会导致内存交换效率低的问题。
由于分段机制下的各个段可以根据实际需求分配内存,有多大需求就分配多大的段,不会存在内部碎片问题。但由于每个段的大小不固定,多个段加起来未必能恰好使用完所有的内存空间,段与段之间对存在小的 “内存缝隙”,这些不连续的小物理内存加起来可能是比较大的,会导致新的程序无法装载。解决的方法就是通过内存交换,重新整理物理内存,将这些不连续的小物理内存给凑一起。具体就需要用到内存交换空间,在 Linux 中就是常见的 Swap 空间,在整理时先把物理空间中的一个段移到 Swap 空间中,然后再从 Swap 空间紧挨着上一个段移到物理空间中,这样就解决了内存碎片问题,但由于 Swap 空间在硬盘中,不可避免的需要访问磁盘,这个过程相比于内存来说是很慢的。而在分段机制下,内存碎片的发生频率又比较高,需要内存交换的频率就搞,但访问磁盘又慢,就会导致机器卡顿。
为了解决分段机制带来的问题,就推出了内存分页机制。具体来说,就是将整个虚拟空间和物理空间分成一个个小的页,每个页的大小为 4KB,然后再将这些页都排排号放在页表中,页表就交给 MMU 来管理。由于分页机制下的内存空间都是提前分配好的,页与页直接不存在 “内存缝隙”,也就不会有外部内存碎片问题。但分页机制下,分配内存的最小单位是一页,也就是 4KB,即使程序不足 4KB,我们也只能分配一个页来装载,也就是分页机制存在内部碎片问题。但这个碎片大小比起分段机制下动辄几百 MB 的碎片要好得多。
但简单的分页还有问题,在 32 位环境下,每个进程需要存储的页表大小为 4GB / 4KB * 4B = 4MB,如果有 100 个进程,光存储页表就需要占用 400MB 内存,这是非常大的数了,更别说 64 位环境了。这个问题的解决就是引入 ”多级页表“,将所有的页表每 1024 个凑成一个大的页表项,这样 4GB 的内存可以刚好凑成 1024 个大页表项,这些大页表项刚好可以只用一个页表存储,这个页表就称为一级页表,每个一级页表项就对应一个二级页表,二级页表中存储的就是具体的 4KB 的页。然后再根据程序的局部性原理,假设大部分程序只会使用到 20% 的虚拟内存,那么每个进程此时只需存储一个一级页表 4KB,外加 20% 的二级页表,总共就是 4KB + 20% * 4MB = 0.8 MB,相比原先的一级分页需要存储 4MB,是一种巨大的节约。那么把二级分页再推广到多级分页,就可以发现页表占用的空间更少了,这主要归功于对局部性原理的充分利用。现在常见的 64 位系统一般是四级分页。
【操作系统专题】中断问题:什么是中断?什么情况下会发生中断?🌟🌟🌟
1、什么是中断?
2、什么情况下会发生中断?
3、中断和异常有啥区别?
【参考文章以及资料补充】:
参考回答:
1、什么是中断?
中断就是计算机在运行时,突然发生了某个事件,需要 CPU 停止当前的任务,转而去处理这个事件,并在这个事件结束后接着进行原来的任务。
中断机制引入的原因是让 CPU 化主动为被动,避免 CPU 去轮询查看某条件是否成立。现在 CPU 只需要不停的执行任务,有事件到达事件会主动中断 CPU,避免 CPU 轮询带来的开销。
2、什么情况下会发生中断?
中断一般分为硬中断和软中断,硬中断是指由计算机的外部设备比如磁盘,网卡,键盘等产生的,用来通知操作系统外设的状态变化。软中断是指由正在运行的进程产生的一条 CPU 指令进行中断,比如触发了系统调用等等。
3、中断和异常有啥区别?
中断主要由外部的 IO 设备和一些指令触发,异常则是 CPU 执行时发生了意想不到的错误,此时会根据情况跳到对应的异常处理程序中进行处理。异常处理程序在执行时也需要保存现场,然后决定是终止程序还是重新执行,最后恢复现场。
【操作系统专题】锁相关问题🌟🌟🌟🌟
1、什么是死锁?怎么产生的?
2、如何预防死活?
3、如何避免死锁?
4、如何解除死锁?
【参考文章以及资料补充】:
待补充
参考回答:
1,4 合并在一起回答:
死锁就是:一组互相竞争资源的线程因互相等待,导致永久阻塞的现象。
死锁出现时一定会满足四个条件:第一个是互斥,即共享资源只能被一个线程占用;第二个是占有且等待,即线程在持有一个共享资源,并等待另一个共享资源时,不会释放当前的共享资源;第三个是不可抢占,即任何线程不能抢占其他线程的共享资源;第四个是环路等待,最终形成死锁时,各个线程会形成一个进程到资源的环形链。
因为锁一定是互斥的,所以解决死锁只需破坏以上后三个条件任意一个即可。对于占有且等待,可以让线程加锁时一次性申请所有的资源,这样就不存在线程等待了。对于不可抢占,可以让线程获取不到资源时,主动释放当前已经占有的资源。对于环路等待,可以对资源进行按序申请。也就是说,线程在申请资源时优先申请序号小的,再申请序号大的,这样就不会存在等待环路了
2,3、实践中一般通过“资源有序分配法”来避免死锁,也就是破坏环路等待条件。具体来说,就是当多个线程同时需要占用多个资源时,规定每个线程获取资源的顺序一样,比如都必须先获得资源 A,再获得资源 B 等等,这样进程间就不可能出现相互等待对方未释放资源的情况,也就不会发生死锁了。
【操作系统专题】其他问题:调度算法,文件系统等🌟🌟🌟
1、页面调度算法介绍一下
2、操作系统读写磁盘,影响磁盘读写时间的因素有哪些?
3、操作系统的原子操作是怎么实现的?🌟🌟
【参考文章以及资料补充】:
待补充
参考回答:
1、页面调度算法介绍一下
由于虚拟内存的存在,如果某个进程所需要访问的页面不在物理内存中,而是在磁盘空间上,此时 CPU 就会产生一个缺页中断,请求操作系统将所缺页调到物理内存中,如果此时刚好内存里装的页面满了,操作系统就带根据一定的算法来选取将哪些页换出,将缺页换入。具体的页面置换算法大约有五种,基本目标都是尽可能的在一段时间内减少页面的换入换出次数。
理想情况下效率最高的的置换算法是“最佳页面置换算法”,思想是,置换在「未来」时间内最长时间不访问的页面。这个算法在实际系统中无法实现,因为程序访问页面是动态的,我们无法预测在以后的时间需要访问哪个页面。但它像是一个标准,我们可以用它来跟我们自己的算法效率作比较,越接近就代表我们的算法越好。
既然我们无法预知页面在下一次访问时的时间,那我们可以通过选择在内存中驻留时间最长的页面进行置换,这是“先进先出算法 FIFO”的思路,在实践中效率不是很高,因为某些页面可能需要持续使用,自然驻留的时间就长,一不小心换出就还带花时间换入。
再改进一下就是使用最近最久未使用 LRU 算法,思路是发生缺页时,选择到目前为止,最长时间没有被访问的页面,由此推断它以后被访问的概率也很小。这种算法的效率还不错,但代价有点高,因为需要在内存中维护一个所有页面的链表,最近最多使用的在表头,最近最少使用的在表尾。如果某个时刻需要访问的页面在链表中的某个位置,就需要遍历整个链表然后放到表头,遍历这个过程是�(�)O(N) 很耗费时间。实践中也很少使用。
那有没有同时结合 FIFO 和 LRU 优点的算法,有,就是“时钟页面置换算法”,它将所有的页面都保存在一个类似钟表的环形链表中,并将每一个页面都附加一个标志位 0 或 1,用来表示最近是否有使用,同时用一个指针指向最老的页面。当发生缺页中断时,看指针所指向的页面,如果它的标志位为 0,则就将它置换出去,再将新页插到现在这个位置。如果标志位为 1,就将它改为 0 但不置换,继续看下一个页面。
也可以单独对 LRU 算法进行改进,使用 LFU 最不常使用算法。它的思路是将每个页面设置一个「访问计数器」,每当一个页面被访问时,它的计数器就 + 1,发生缺页中断时,淘汰计数器值最小的那个。思路还是比较好理解的,但其中有很多问题。首先就是每个页面都要加一个计数器,这个硬件成本比较高。其次是如果在所有页面中快速找到计数值最小那个?以及怎么只根据计数值来判断某个页面在最近一段时间内的访问频率?还有计数值超了咋办?(参考 Redis 淘汰键值对的 LFU 算法实现)。
2、操作系统读写磁盘,影响磁盘读写时间的因素有哪些?
首先是“读写方式”,传统的机械硬盘需要磁头和磁头臂与磁盘配合,先找到对应的磁道才能读取数据。而固态硬盘则是通过电学信号的传输完成对闪存芯片的读写,主要依靠的是电学信号,没有寻道时间,速度很快。
对于机械硬盘来说,影响读写时间的因素主要有硬盘段转速,是连续 IO 还是随机 IO,以及硬盘中的内置缓存大小。硬盘的转速越快,磁头寻道的速度就越快,读写就越快。连续 IO 的话,磁盘只需寻一次道,接着顺序读写即可,随机 IO 则需要多次寻道。磁盘缓存的话就是利用时间局部性原理,一旦某个数据被访问,它在不久的将来可能再次被访问。将它放在缓存中可以方便以后快速读取。
3、操作系统的原子操作是怎么实现的?