跳转到内容

1 万字 51 张手绘图,详解 34 道操作系统面试高频题(让天下没有难背的八股),面渣背会这些 OS 八股文,这次吊打面试官,我觉得稳了(手动 dog)。整理:沉默王二,戳转载链接,作者:三分恶,戳原文链接

引论

01、什么是操作系统?

操作系统(Operating System, OS)是计算机系统中管理硬件和软件资源的中间层系统,屏蔽了硬件的复杂性,并且为用户提供了便捷的交互方式,比如说 Windows、Linux、MacOS 等。

三分恶面渣逆袭:操作系统是什么

02、操作系统主要有哪些功能?

 三分恶面渣逆袭:操作系统主要功能

①、负责创建和终止进程。进程是正在运行的程序实例,每个进程都有自己的地址空间和资源。

②、负责为进程分配资源,比如说内存,并在进程终止时回收内存。

③、提供创建、删除、读写文件的功能,并组织文件的存储结构,比如说目录。

④、通过设备驱动程序控制和管理计算机的硬件设备,如键盘、鼠标、打印机等。

  1. Java 面试指南(付费)收录的比亚迪面经同学 1 面试原题:操作系统的主要功能,以及上下文切换

操作系统结构

03、什么是内核?

可以这么说,内核是一个计算机程序,它是操作系统的核心,提供了操作系统最核心的能力,可以控制操作系统中所有的内容。

04、什么是用户态和内核态?

在计算机系统中,内存可以分为两大区域:内核空间(Kernel Space)和用户空间(User Space)。这种划分主要用于保护系统稳定性和安全性。

  • 内核空间,是操作系统内核代码及其运行时数据结构所在的内存区域,拥有对系统所有资源的完全访问权限,如进程管理、内存管理、文件系统、网络堆栈等。
  • ⽤户空间,是操作系统为应用程序(如用户运行的进程)分配的内存区域,用户空间中的进程不能直接访问硬件或内核数据结构,只能通过系统调用与内核通信。

二哥的 Java 进阶之路:用户空间和内核空间

当程序使⽤⽤户空间时,我们常说该程序在 ⽤户态 执⾏,⽽当程序使内核空间时,程序则在 内核态 执⾏。

  1. Java 面试指南(付费)收录的得物面经同学 8 一面面试原题:内核空间和用户空间是什么,什么时候会进入内核空间

05、用户态和内核态是如何切换的?

当应用程序执行系统调用时,CPU 将从用户态切换到内核态,进入内核空间执行相应的内核代码,然后再切换回用户态。

三分恶面渣逆袭:用户态&内核态切换

系统调用是应用程序请求操作系统内核提供服务的接口,如文件操作(如 open、read、write)、进程控制(如 fork、exec)、内存管理(如 mmap)等。

  1. Java 面试指南(付费)收录的得物面经同学 8 一面面试原题:内核空间和用户空间是什么,什么时候会进入内核空间

进程和线程

06、并行和并发有什么区别?

并发就是在一段时间内,多个任务都会被处理;但在某一时刻,只有一个任务在执行。单核处理器做到的并发,其实是利用时间片的轮转,例如有两个进程 A 和 B,A 运行一个时间片之后,切换到 B,B 运行一个时间片之后又切换到 A。因为切换速度足够快,所以宏观上表现为在一段时间内能同时运行多个程序。

并行就是在同一时刻,有多个任务在执行。这个需要多核处理器才能完成,在微观上就能同时执行多条指令,不同的程序被放到不同的处理器上运行,这个是物理上的多个进程同时进行。

并发和并行

07、什么是进程上下文切换?

三分恶面渣逆袭:进程上下文切换

上下文切换是操作系统在多任务处理环境中,将 CPU 从一个进程切换到另一个进程的过程。通过让多个进程共享 CPU 资源,使系统能够并发执行多个任务。

进程上下文切换通畅包含以下几个步骤:

  • 保存当前进程的上下文:操作系统保存当前进程的 CPU 寄存器,程序状态等关键信息。
  • 选择下一个进程:调度程序选择下一个要执行的进程。
  • 恢复上一个进程的上下文。
  • 切换到下一个进程。

08、进程有哪些状态?

当一个进程开始运行时,它可能会经历下面这几种状态:

上图中各个状态的意义:

  • 运⾏状态(Runing):该时刻进程占⽤ CPU;
  • 就绪状态(Ready):可运⾏,由于其他进程处于运⾏状态⽽暂时停⽌运⾏;
  • 阻塞状态(Blocked):该进程正在等待某⼀事件发⽣(如等待输⼊/输出操作的完成)⽽暂时停⽌运⾏,这时,即使给它 CPU 控制权,它也⽆法运⾏;

进程3种状态

当然,进程还有另外两个基本状态:

  • 创建状态(new):进程正在被创建时的状态;
  • 结束状态(Exit):进程正在从系统中消失时的状态;

进程5种状态

09、什么是僵尸进程?

僵尸进程是已完成且处于终止状态,但在进程表中却仍然存在的进程。

僵尸进程一般发生有父子关系的进程中,一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait() 或 waitpid() 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait() 或 waitpid(),那么子进程的进程描述符仍然保存在系统中。

10、什么是孤儿进程?

一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被 init 进程 (进程 ID 为 1 的进程) 所收养,并由 init 进程对它们完成状态收集工作。因为孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害。

11、进程有哪些调度算法?

进程调度是操作系统中的核心功能之一,它负责决定哪些进程在何时使用 CPU。这一决定基于系统中的进程调度算法。

DIDA-lJ-进程调度算法

①、先来先服务

这是最简单的调度算法,也称为先进先出(FIFO)。进程按照请求 CPU 的顺序进行调度。这种方式易于实现,但可能会导致较短的进程等待较长进程执行完成,从而产生“饥饿”现象。

三分恶面渣逆袭:先来先服务

②、短作业优先

选择预计运行时间最短的进程优先执行。这种方式可以减少平均等待时间和响应时间,但缺点是很难准确预知进程的执行时间,并且可能因为短作业一直在执行,导致长作业持续被推迟执行。

三分恶面渣逆袭:短作业优先

③、优先级调度

在这种调度方式中,每个进程都被分配一个优先级。CPU 首先分配给优先级最高的进程。优先级调度可以是非抢占式的或抢占式的。在非抢占式优先级调度中,进程一旦开始执行将一直运行直到完成;在抢占式优先级调度中,更高优先级的进程可以中断正在执行的低优先级进程。

三分恶面渣逆袭:优先级调度

④、时间片轮转

时间片轮转调度为每个进程分配一个固定的时间段,称为时间片,进程可以在这个时间片内运行。如果进程在时间片结束时还没有完成,它将被放回队列的末尾。时间片轮转是公平的调度方式,可以保证所有进程得到公平的 CPU 时间,适用于共享系统。

三分恶面渣逆袭:时间片轮转

⑤、最短剩余时间优先

这是短作业优先的一种改进形式,它是抢占式的。即如果一个新进程的预计执行时间比当前运行进程的剩余时间短,调度器将暂停当前的进程,并切换到新进程。这种方法也可以最小化平均等待时间,但同样面临预测执行时间的困难。

多级反馈队列

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

多级队列就是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列的时间片大小不同,比如 2,4,6,8······。进程在第一个队列没执行完,就会被移到下一个队列。

这种方式下,之前的进程只需要交换 7 次就可以了。每个队列优先权不一样,最上面的队列优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。

可以将这种调度算法看成是时间片轮转调度算法与优先级调度算法的结合。

DIDA-lJ-多级反馈队列

  1. Java 面试指南(付费)收录的华为面经同学 9 Java 通用软件开发一面面试原题:进程的调度方式

12、进程间通信有哪些方式?

推荐阅读:编程十万问:进程间通信的方式有哪些?

进程间通信的方式有 6 种,管道、信号、消息队列、共享内存、信号量和套接字。

编程十万问:进程间通信

简单说说管道:

管道可以理解成不同进程之间的传话筒,一方发声,一方接收,声音的介质可以是空气或者电缆。

进程间的管道就是内核中的一串缓存,从管道的一端写入数据,另一端读取。数据只能单向流动,遵循先进先出(FIFO)的原则。

编程十万问:管道

①、匿名管道:允许具有亲缘关系的进程(如父子进程)进行通信。

三分恶面渣逆袭:“奉先我儿”

使用 C 语言在 Unix/Linux 环境下通过匿名管道实现两个进程(通常是父子进程)之间通信的示例:

c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main() {
    int pipefd[2];
    pid_t cpid;
    char buf;

    // 创建管道
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    // 创建子进程
    cpid = fork();
    if (cpid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (cpid == 0) {    /* 子进程 */
        close(pipefd[1]);  // 关闭写端

        // 从管道读取数据
        while (read(pipefd[0], &buf, 1) > 0)
            write(STDOUT_FILENO, &buf, 1);

        write(STDOUT_FILENO, "\n", 1);
        close(pipefd[0]);
        exit(EXIT_SUCCESS);
    } else {            /* 父进程 */
        close(pipefd[0]);  // 关闭读端

        // 向管道写入数据
        write(pipefd[1], "Hello, Child!", 13);
        close(pipefd[1]);  // 关闭写端,触发EOF
        wait(NULL);        // 等待子进程退出
        exit(EXIT_SUCCESS);
    }
}

②、命名管道:允许无亲缘关系的进程通信,通过在文件系统中创建一个特殊类型的文件来实现。

缺点:管道的效率低,不适合进程间频繁地交换数据。

简单说说信号:

信号可以理解成以前的 BB 机,用于通知接收进程某件事情发生了,是一种较为简单的通信方式,主要用于处理异步事件。

比如kill -9 1050就表示给 PID 为 1050 的进程发送SIGKIL信号。

这里顺带普及一下 Linux 中常用的信号:

  • SIGHUP:当我们退出终端(Terminal)时,由该终端启动的所有进程都会接收到这个信号,默认动作为终止进程。
  • SIGINT:程序终止(interrupt)信号。按 Ctrl+C 时发出,大家应该在操作终端时有过这种操作。
  • SIGQUIT:和 SIGINT 类似,按 Ctrl+\ 键将发出该信号。它会产生核心转储文件,将内存映像和程序运行时的状态记录下来。
  • SIGKILL:强制杀死进程,本信号不能被阻塞和忽略。
  • SIGTERM:与 SIGKILL 不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出。

简单说说消息队列:

消息队列是保存在内核中的消息链表,按照消息的类型进行消息传递,具有较高的可靠性和稳定性。

编程十万问:消息队列

缺点:消息体有一个最大长度的限制,不适合比较大的数据传输;存在用户态与内核态之间的数据拷贝开销。

编程十万问:消息队列

简单说说共享内存:

允许两个或多个进程共享一个给定的内存区,一个进程写⼊的东西,其他进程⻢上就能看到。

共享内存是最快的进程间通信方式,它是针对其他进程间通信方式运行效率低而专门设计的。

三分恶面渣逆袭:共享内存

缺点:当多进程竞争同一个共享资源时,会造成数据错乱的问题。

简单说说信号量:

信号量可以理解成红绿灯,红灯停(信号量为零),绿灯行(信号量非零)。它本质上是一个计数器,用来控制对共享资源的访问数量。

三分恶面渣逆袭:信号量

它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。Java 中的 java.util.concurrent.Semaphore 类就实现了类似的功能。

控制信号量的⽅式有两种原⼦操作:

  • ⼀个是 P 操作(wait,减操作),当进程希望获取资源时,它会执行 P 操作。如果信号量的值大于 0,表示有资源可用,信号量的值减 1,进程继续执行。如果信号量的值为 0,表示没有可用资源,进程进入等待状态,直到信号量的值变为大于 0。
  • 另⼀个是 V 操作(signal,加操作),当进程释放资源时,它会执行 V 操作,信号量的值加 1。如果有其他进程因为等待该资源而被阻塞,这时会唤醒其中一个进程。

编程十万问:信号量

简单说说套接字 Socket:

这个和 Java 中的 Socket 很相似,提供网络通信的端点,可以让不同机器上运行的进程之间进行双向通信。

  1. Java 面试指南(付费)收录的华为一面原题:说一下进程的通信机制
  2. Java 面试指南(付费)收录的字节跳动商业化一面的原题:进程和线程区别,线程共享内存和进程共享内存的区别
  3. Java 面试指南(付费)收录的字节跳动商业化一面的原题:进程间如何通信
  4. Java 面试指南(付费)收录的华为面经同学 6 Java 通用软件开发一面面试原题:说说你对 JVM 调优的了解
  5. Java 面试指南(付费)收录的美团同学 2 优选物流调度技术 2 面面试原题:进程间的通信方式,代码使用匿名管道使两个进程通信
  6. Java 面试指南(付费)收录的同学 30 腾讯音乐面试原题:linux 下进程的通信方式有哪几种?

13、进程和线程的联系和区别?

进程是一个正在执行的程序实例。每个进程都有自己独立的地址空间、全局变量、堆栈、和文件描述符等资源。

线程是进程中的一个执行单元。一个进程可以包含多个线程,它们共享进程的地址空间和资源。

多线程-图片来源于网络

每个进程在独立的地址空间中运行,不会直接影响其他进程。线程共享同一个进程的内存空间、全局变量和文件描述符。

进程切换需要保存和恢复大量的上下文信息,代价较高。线程切换相对较轻量,因为线程共享进程的地址空间,只需要保存和恢复线程私有的数据。

线程的生命周期由进程控制,进程终止时,其所有线程也会终止。

特性进程线程
地址空间独立共享
内存开销
上下文切换慢,开销大快,开销小
通信需要 IPC 机制,开销较大共享内存,直接通信
创建销毁开销大,较慢开销小,较快
并发性
崩溃影响一个进程崩溃不会影响其他进程一个线程崩溃可能导致整个进程崩溃
  1. Java 面试指南(付费)收录的得物面经同学 8 一面面试原题:Linux 中进程和线程的区别
  2. Java 面试指南(付费)收录的同学 30 腾讯音乐面试原题:进程和线程的主要区别是什么样子?

14、线程上下文切换了解吗?

这还得看线程是不是属于同⼀个进程:

  • 当两个线程不是属于同⼀个进程,则切换的过程就跟进程上下⽂切换⼀样;

  • 当两个线程是属于同⼀个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据

所以,线程的上下⽂切换相⽐进程,开销要⼩很多。

15、线程有哪些实现方式?

主要有三种线程的实现⽅式:

  • 内核态线程实现:在内核空间实现的线程,由内核直接管理直接管理线程。

内核态线程实现

  • ⽤户态线程实现:在⽤户空间实现线程,不需要内核的参与,内核对线程无感知。

用户态线程

  • 混合线程实现:现代操作系统基本都是将两种方式结合起来使用。用户态的执行系统负责进程内部线程在非阻塞时的切换;内核态的操作系统负责阻塞线程的切换。即我们同时实现内核态和用户态线程管理。其中内核态线程数量较少,而用户态线程数量较多。每个内核态线程可以服务一个或多个用户态线程。

混合线程实现

16、线程间如何同步?

同步解决的是多线程操作共享资源的问题,不管线程之间是如何穿插执行的,最后的结果都是正确的。

在操作系统层面,保证线程同步的方式有很多,比如锁、信号量等。那在此之前,需要先了解什么是临界区。

cxuan:使用临界区的互斥

临界区:对共享资源访问的程序片段,我们希望这段代码是互斥的,可以保证在某个时刻只能被一个线程执行,也就是说一个线程在临界区执行时,其它线程应该被阻止进入临界区。

临界区不仅针对线程,同样针对进程。同步的实现方式有:

①、互斥锁

使⽤加锁操作和解锁操作可以解决并发线程/进程的互斥问题。

任何想进⼊临界区的线程,必须先执⾏加锁操作。若加锁操作顺利通过,则线程可进⼊临界区;在完成对临界资源的访问后再执⾏解锁操作,以释放该临界资源。

加锁和解锁锁住的是什么呢?可以是临界区对象,也可以只是一个简单的互斥量,例如互斥量是0无锁,1表示加锁。

根据锁的实现不同,可以分为忙等待锁⽆忙等待锁

  • 忙等待锁(也称为自旋锁,Spinlock)是指当一个线程试图获取锁时,如果该锁已经被其他线程持有,当前线程不会立即进入休眠或阻塞,而是不断地检查锁的状态,直到该锁可用为止。这个过程被称为忙等待(busy waiting),因为线程在等待锁时仍然占用 CPU 资源,处于活跃状态。优点是避免了线程的上下文切换。
  • 无忙等待锁是指当一个线程尝试获取锁时,如果锁已经被其他线程持有,当前线程不会忙等待,而是主动让出 CPU,进入阻塞状态或休眠状态,等待锁释放。当锁被释放时,线程被唤醒并重新尝试获取锁。这类锁的主要目的是避免忙等待带来的 CPU 资源浪费。

②、信号量

信号量是操作系统提供的⼀种协调共享资源访问的⽅法。通常表示资源的数量,对应的变量是⼀个整型(sem)变量。

另外,还有两个原⼦操作的系统调⽤函数来控制信号量,分别是:

  • P 操作:当线程想要进入临界区时,会尝试执行 P 操作。如果信号量的值大于 0,信号量值减 1,线程可以进入临界区;否则,线程会被阻塞,直到信号量大于 0。
  • V 操作:当线程退出临界区时,执行 V 操作,信号量的值加 1,释放一个被阻塞的线程。
  1. Java 面试指南(付费)收录的拼多多面经同学 4 技术一面面试原题:操作系统内核对象实现同步与互斥

17、什么是死锁?

在两个或者多个并发线程中,如果每个线程持有某种资源,而又等待其它线程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组线程产生了死锁。通俗的讲就是两个或多个线程无限期的阻塞、相互等待的一种状态。

死锁

18、死锁产生有哪些条件?

产生死锁需要同时满足四个必要条件:

  • 互斥条件(Mutual Exclusion):资源不能被多个进程共享,即资源一次只能被一个进程使用。如果一个资源已经被分配给了一个进程,其他进程必须等待,直到该资源被释放。
  • 持有并等待条件(Hold and Wait):一个进程已经持有了至少一个资源,同时还在等待获取其他被占用的资源。在此期间,该进程不会释放已经持有的资源。
  • 不可剥夺条件(No Preemption):已分配给进程的资源不能被强制剥夺,只有持有该资源的进程可以主动释放资源。
  • 循环等待条件(Circular Wait):存在一个进程集合 P1,P2,...,Pn,其中 P1 等待 P2 持有的资源,P2 等待 P3 持有的资源,依此类推,直到 Pn 等待 P1 持有的资源,形成一个进程等待环。

假设有两个进程 P1P2,以及两个资源 R1R2,一个简单的死锁场景是这样的:

  1. P1 持有资源 R1,并请求资源 R2
  2. P2 持有资源 R2,并请求资源 R1

在这种情况下,发生死锁的步骤如下:

  1. 互斥条件R1R2 都只能被一个进程占用。
  2. 持有并等待条件P1 持有 R1 并等待 R2,同时 P2 持有 R2 并等待 R1
  3. 不可剥夺条件R1R2 都不能被强制从 P1P2 中剥夺。
  4. 循环等待条件P1 等待 P2 持有的 R2,而 P2 等待 P1 持有的 R1,形成一个循环。
  1. Java 面试指南(付费)收录的字节跳动面经同学 8 Java 后端实习一面面试原题:死锁条件
  2. Java 面试指南(付费)收录的拼多多面经同学 4 技术一面面试原题:死锁的条件

19、如何避免死锁呢?

产⽣死锁的有四个必要条件:互斥条件、持有并等待条件、不可剥夺条件、环路等待条件。

避免死锁,破坏其中的一个就可以。

消除互斥条件

这个是没法实现,因为很多资源就是只能被一个线程占用,例如锁。

消除请求并持有条件

消除这个条件的办法很简单,就是一个线程一次请求其所需要的所有资源。

消除不可剥夺条件

占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可剥夺这个条件就破坏掉了。

消除环路等待条件

可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后就不存在环路了。

20、活锁和饥饿锁了解吗?

饥饿锁:

饥饿锁,这个饥饿指的是资源饥饿,某个线程一直等不到它所需要的资源,从而无法向前推进,就像一个人因为饥饿无法成长。

活锁:

在活锁状态下,处于活锁线程组里的线程状态可以改变,但是整个活锁组的线程无法推进。

活锁可以用两个人过一条很窄的小桥来比喻:为了让对方先过,两个人都往旁边让,但两个人总是让到同一边。这样,虽然两个人的状态一直在变化,但却都无法往前推进。

内存管理

21、物理内存和虚拟内存有什么区别?

物理内存指的是计算机中实际存在的硬件内存。物理内存是计算机用于存储运行中程序和数据的实际内存资源,操作系统和应用程序最终都必须使用物理内存来执行。

也就是我们常说的那个 8G、16G、64G 的内存条。

虚拟内存是操作系统提供的一种内存管理技术,它使得应用程序认为自己有连续的、独立的内存空间,而实际上,这个虚拟内存可能部分存储在物理内存上,部分存储在 磁盘(如硬盘的交换分区或页面文件) 中。

三分恶面渣逆袭:虚拟内存

虚拟内存的核心思想是通过硬件和操作系统的配合,为每个进程提供一个独立的、完整的虚拟地址空间,解决物理内存不足的问题。

①、每个进程都有自己的虚拟地址空间,虚拟内存使用的是逻辑地址,它与实际的物理内存地址不同,必须经过地址转换才能映射到物理内存。

②、操作系统通过 页表(Page Table) 将虚拟地址映射到物理地址。当程序访问某个虚拟地址时,CPU 会通过页表找到对应的物理地址。

③、操作系统将虚拟内存划分为若干个页(Pages),每个页可以被映射到物理内存中的一个页面。如果物理内存不够,操作系统会将不常用的页暂时存储到磁盘的交换区(Swap)中,这个过程叫做页交换(Paging)。

  1. Java 面试指南(付费)收录的深信服面经同学 3 Java 后端线下一面面试原题:指针是存在虚拟内存中,问物理内存和虚拟内存的区别

22、什么是内存分段?

程序是由若⼲个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的,所以就⽤分段(Segmentation)的形式把这些段分离出来。

分段机制下的虚拟地址由两部分组成,段号段内偏移量

虚拟地址和物理地址通过段表映射,段表主要包括段号段的界限

虚拟地址、段表、物理地址

我们来看一个映射,虚拟地址:段 3、段偏移量 500 ----> 段基地址 7000+段偏移量 500 ----> 物理地址:8700+。

段虚拟地址映射

23、什么是内存分页?

分⻚是把整个虚拟和物理内存空间切成⼀段段固定尺⼨的⼤⼩。这样⼀个连续并且尺⼨固定的内存空间,我们叫**⻚**(Page)。在 Linux 下,每⼀⻚的⼤⼩为 4KB 。

访问分页系统中内存数据需要两次的内存访问 :一次是从内存中访问页表,从中找到指定的物理页号,加上页内偏移得到实际物理地址,第二次就是根据第一次得到的物理地址访问内存取出数据。

内存分页

24、多级页表知道吗?

推荐阅读:操作系统导论:多级页表

多级页表(Multilevel Page Table)是一种内存管理技术,用于在虚拟内存系统中高效地管理和转换虚拟地址到物理地址。它通过分层结构减少页表所需的内存开销,以解决单级页表在大地址空间中的效率问题。

三分恶面渣逆袭:多级页表示意图

在虚拟内存系统中,虚拟地址需要转换为物理地址。页表是实现这种转换的关键数据结构。对于 32 位系统,一个进程的地址空间可以达到 4 GB,如果使用单级页表,每个页表条目(PTE)占用 4 字节,则需要 4 MB 的内存来存储页表。然而,许多进程只使用其中的一小部分地址空间,导致单级页表的内存浪费。

多级页表通过将单级页表拆分为多个层级,减少了内存浪费。以两级页表为例:

  • 一级页表(页目录):存储二级页表的地址。每个页目录条目(PDE)指向一个二级页表。
  • 二级页表(页表):存储实际的页框地址。每个页表条目(PTE)指向一个物理页框。

虚拟地址分为多个部分,每一部分用于索引相应层级的页表。例如,对于一个 32 位地址和 4 KB 页大小的两级页表:

  • 高 10 位:一级页表索引(页目录索引)。
  • 中 10 位:二级页表索引(页表索引)。
  • 低 12 位:页内偏移。
  1. Java 面试指南(付费)收录的得物面经同学 1 面试原题:多级页表

25、什么是快表?

同样利用了局部性原理,即在⼀段时间内,整个程序的执⾏仅限于程序中的某⼀部分。相应地,执⾏所访问的存储空间也局限于某个内存区域。

利⽤这⼀特性,把最常访问的⼏个⻚表项存储到访问速度更快的硬件,于是计算机科学家们,就在 CPU 芯⽚中,加⼊了⼀个专⻔存放程序最常访问的⻚表项的 Cache,这个 Cache 就是 TLB(Translation Lookaside Buffer) ,通常称为⻚表缓存、转址旁路缓存、快表等。

TLB示意图-来源参考[3]

26、分页和分段有什么区别?

  • 段是信息的逻辑单位,它是根据用户的需要划分的,因此段对用户是可见的 ;页是信息的物理单位,是为了管理主存的方便而划分的,对用户是透明的。
  • 段的大小不固定,有它所完成的功能决定;页的大小固定,由系统决定
  • 段向用户提供二维地址空间;页向用户提供的是一维地址空间
  • 段是信息的逻辑单位,便于存储保护和信息的共享,页的保护和共享受到限制。

27、什么是交换空间?

操作系统把物理内存(Physical RAM)分成一块一块的小内存,每一块内存被称为页(page)。当内存资源不足时,Linux 把某些页的内容转移至磁盘上的一块空间上,以释放内存空间。磁盘上的那块空间叫做交换空间(swap space),而这一过程被称为交换(swapping)。物理内存和交换空间的总容量就是虚拟内存的可用容量。

用途:

  • 物理内存不足时一些不常用的页可以被交换出去,腾给系统。
  • 程序启动时很多内存页被用来初始化,之后便不再需要,可以交换出去。

33、什么是缺页中断?(补充)

2024 年 03 月 29 日增补

缺页中断(Page Fault)是虚拟内存管理的一个重要概念。当一个程序访问的页(页面)不在物理内存中时,就会发生缺页中断。操作系统需要从磁盘上的交换区(或页面文件)中将缺失的页调入内存。

举个例子,你正在一间图书馆(内存)里查找一本特定的书(数据/程序页),图书馆的书架(内存空间)能放的书是有限的。现在,如果你找的那本书正好在书架上,那太好了,直接拿来阅读(内存命中)。

但如果书架上没有(缺页),你需要先去找图书管理员。

图书管理员(操作系统)注意到书架上缺了这本书,然后去仓库里帮你找(缺页中断)。找到书之后,管理员发现书架已经满了,需要先从书架上拿掉一本书(页面置换算法决定哪本书被拿掉),然后把新找到的书放上去,最后把书递给你。

这个过程中,“去仓库找书并换回来”的这一过程就像是发生了缺页中断,而决定哪本书被移出书架以腾出位置放新书的规则,就是页面置换算法在做的事情。

这么做的目的是尽量确保你常读的书都能在书架(内存)上直接找到,避免每次都要去仓库(硬盘)搜寻,因为去仓库找书的过程比较耗时。

  1. Java 面试指南(付费)收录的字节跳动面经同学 9 飞书后端技术一面面试原题:操作系统缺页中断,页面置换算法

28、页面置换算法有哪些?

推荐阅读:页面置换算法详解

页面置换算法的目标是最小化缺页中断的次数,常见的页面置换算法有最佳⻚⾯置换算法(OPT)、先进先出置换算法(FIFO)、最近最久未使⽤的置换算法(LRU)和时钟页面置换算法等。

三分恶面渣逆袭:常见页面置换算法

①、最佳⻚⾯置换算法

基本思路是,淘汰以后不会使用的页面。这是理论上的最佳算法,因为它可以保证最低的缺页率。但在实际应用中,由于无法预知未来的访问模式,OPT 通常无法实现。

Leophen:OPT

②、先进先出置换算法

基本思路是,优先淘汰最早进入内存的页面。FIFO 算法维护一个队列,新来的页面加入队尾,当发生页面置换时,队头的页面(即最早进入内存的页面)被移出。

三分恶面渣逆袭:按照进入内存早晚构建的页面链表

③、最近最久未使⽤的置换算法

基本思路是,淘汰最近没有使用的页面。LRU 算法根据页面的访问历史来进行置换,最长时间未被访问的页面将被置换出去。

相对更接近最优算法的效果,因为最近未使用的页面可能在将来也不会被使用。但 LRU 算法的实现需要跟踪页面的访问历史,可能会增加系统的开销。

三分恶面渣逆袭:LRU实现

④、时钟页面置换算法

时钟算法是 LRU 的一种近似和实现简单的形式。它通过一个循环列表(类似时钟的指针)遍历页面,每个页面有一个使用位,当页面被访问时,使用位设置为 1。

当需要页面置换时,时钟指针会顺时针移动,直到找到使用位为 0 的页面进行置换。这个过程类似于给每个页面一个二次机会。算法执行时,会先将使用位从 1 清零,如果该页面再次被访问,它的使用位再次被设置为 1。

三分恶面渣逆袭:时钟页面置换算法

⑤、最不常⽤置换算法

根据页面被访问的频率进行置换,访问次数最少的页面最先被置换。实现较为复杂,需要记录每个页面的访问频率。

  1. Java 面试指南(付费)收录的字节跳动面经同学 9 飞书后端技术一面面试原题:操作系统缺页中断,页面置换算法

文件

29、硬链接和软链接有什么区别?

  • 硬链接就是在目录下创建一个条目,记录着文件名与 inode 编号,这个 inode 就是源文件的 inode。删除任意一个条目,文件还是存在,只要引用数量不为 0。但是硬链接有限制,它不能跨越文件系统,也不能对目录进行链接。

硬链接-来源参考[3]

  • 软链接相当于重新创建⼀个⽂件,这个⽂件有独⽴的 inode,但是这个**⽂件的内容是另外⼀个⽂件的路径**,所以访问软链接的时候,实际上相当于访问到了另外⼀个⽂件,所以软链接是可以跨⽂件系统的,甚⾄⽬标⽂件被删除了,链接⽂件还是在的,只不过打不开指向的文件了而已。

    软链接-来源参考[3]

IO

30、零拷贝了解吗?

假如需要文件传输,使用传统 I/O,数据读取和写入是用户空间到内核空间来回赋值,而内核空间的数据是通过操作系统的 I/O 接口从磁盘读取或者写入,这期间发生了多次用户态和内核态的上下文切换,以及多次数据拷贝。

传统文件传输示意图-来源参考[3]

为了提升 I/O 性能,就需要减少用户态与内核态的上下文切换内存拷贝的次数

这就用到了我们零拷贝的技术,零拷贝技术实现主要有两种:

  • mmap + write

mmap() 系统调⽤函数会直接把内核缓冲区⾥的数据「映射」到⽤户空间,这样,操作系统内核与⽤户空间就不需要再进⾏任何的数据拷⻉操作。

mmap示意图-来源参考[3]

  • sendfile

在 Linux 内核版本 2.1 中,提供了⼀个专⻔发送⽂件的系统调⽤函数 sendfile() 。

⾸先,它可以替代前⾯的 read() 和 write() 这两个系统调⽤,这样就可以减少⼀次系统调⽤,也就减少了 2 次上下⽂切换的开销。

其次,该系统调⽤,可以直接把内核缓冲区⾥的数据拷⻉到 socket 缓冲区⾥,不再拷⻉到⽤户态,这样就只有 2 次上下⽂切换,和 3 次数据拷⻉。

sendfile示意图-来源参考[3]

很多开源项目如 Kafka、RocketMQ 都采用了零拷贝技术来提升 IO 效率。

31、聊聊阻塞与⾮阻塞 IO、 同步与异步 IO?

  • 阻塞 I/O

先来看看阻塞 I/O,当⽤户程序执⾏ read ,线程会被阻塞,⼀直等到内核数据准备好,并把数据从内核缓冲区拷⻉到应⽤程序的缓冲区中,当拷⻉过程完成, read 才会返回。

注意,阻塞等待的是内核数据准备好数据从内核态拷⻉到⽤户态这两个过程

阻塞I/O

  • 非阻塞 I/O

⾮阻塞的 read 请求在数据未准备好的情况下⽴即返回,可以继续往下执⾏,此时应⽤程序不断轮询内核,直到数据准备好,内核将数据拷⻉到应⽤程序缓冲区, read 调⽤才可以获取到结果。

非阻塞I/O

  • 基于非阻塞的 I/O 多路复用

我们上面的非阻塞 I/O 有一个问题,什么问题呢?应用程序要一直轮询,这个过程没法干其它事情,所以引入了I/O **多路复⽤**技术。

当内核数据准备好时,以事件通知应⽤程序进⾏操作。

基于非阻塞的I/O多路复用

注意:⽆论是阻塞 I/O、还是⾮阻塞 I/O、非阻塞 I/O 多路复用,都是同步调⽤。因为它们在 read 调⽤时,内核将数据从内核空间拷⻉到应⽤程序空间,过程都是需要等待的,也就是说这个过程是同步的,如果内核实现的拷⻉效率不⾼,read 调⽤就会在这个同步过程中等待⽐较⻓的时间。

  • 异步 I/O

真正的异步 I/O内核数据准备好数据从内核态拷⻉到⽤户态这两个过程都不⽤等待。

发起 aio_read 之后,就⽴即返回,内核⾃动将数据从内核空间拷⻉到应⽤程序空间,这个拷⻉过程同样是异步的,内核⾃动完成的,和前⾯的同步操作不⼀样,应⽤程序并不需要主动发起拷⻉动作。

异步/IO

拿例子理解几种 I/O 模型

老三关注了很多 UP 主,有些 UP 主是老鸽子,到了更新的时间:

阻塞 I/O 就是,老三不干别的,就干等着,盯着 UP 的更新。

非阻塞 I/O 就是,老三发现 UP 没更,就去喝个茶什么的,过一会儿来盯一次,一直等到 UP 更新。

基于⾮阻塞的 I/O 多路复⽤好⽐,老三发现 UP 没更,就去干别的,过了一会儿 B 站推送消息了,老三一看,有很多条,就去翻动态,看看等的 UP 是不是更新了。

异步 I/O 就是,老三说 UP 你该更了,UP 赶紧爆肝把视频做出来,然后把视频亲自呈到老三面前,这个过程不用等待。

鸽宗

32、详细讲一讲 I/O 多路复用?

我们先了解什么是 I/O 多路复用?

我们在传统的 I/O 模型中,如果服务端需要支持多个客户端,我们可能要为每个客户端分配一个进程/线程。

不管是基于重一点的进程模型,还是轻一点的线程模型,假如连接多了,操作系统是扛不住的。

所以就引入了I/O 多路复用 技术。

简单说,就是一个进程/线程维护多个 Socket,这个多路复用就是多个连接复用一个进程/线程。

I/O多路复用

我们来看看 I/O 多路复用三种实现机制:

  • select

select 实现多路复⽤的⽅式是:

将已连接的 Socket 都放到⼀个**⽂件描述符集合**fd_set,然后调⽤ select 函数将 fd_set 集合拷⻉到内核⾥,让内核来检查是否有⽹络事件产⽣,检查的⽅式很粗暴,就是通过遍历 fd_set 的⽅式,当检查到有事件产⽣后,将此 Socket 标记为可读或可写, 接着再把整个 fd_set 拷⻉回⽤户态⾥,然后⽤户态还需要再通过遍历的⽅法找到可读或可写的 Socket,再对其处理。

select 使⽤固定⻓度的 BitsMap,表示⽂件描述符集合,⽽且所⽀持的⽂件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最⼤值为 1024 ,只能监听 0~1023 的⽂件描述符。

select 机制的缺点:

(1)每次调用 select,都需要把 fd_set 集合从用户态拷贝到内核态,如果 fd_set 集合很大时,那这个开销也很大,比如百万连接却只有少数活跃连接时这样做就太没有效率。

(2)每次调用 select 都需要在内核遍历传递进来的所有 fd_set,如果 fd_set 集合很大时,那这个开销也很大。

(3)为了减少数据拷贝带来的性能损坏,内核对被监控的 fd_set 集合大小做了限制,一般为 1024,如果想要修改会比较麻烦,可能还需要编译内核。

(4)每次调用 select 之前都需要遍历设置监听集合,重复工作。

  • poll

poll 不再⽤ BitsMap 来存储所关注的⽂件描述符,取⽽代之⽤动态数组,以链表形式来组织,突破了 select 的⽂件描述符个数限制,当然还会受到系统⽂件描述符限制。

但是 poll 和 select 并没有太⼤的本质区别,都是使⽤线性结构存储进程关注的 Socket 集合,因此都需要遍历⽂件描述符集合来找到可读或可写的 Socke,时间复杂度为 O(n),⽽且也需要在⽤户态与内核态之间拷⻉⽂件描述符集合,这种⽅式随着并发数上来,性能的损耗会呈指数级增⻓。

  • epoll

epoll 通过两个⽅⾯,很好解决了 select/poll 的问题。

第⼀点,epoll 在内核⾥使⽤红⿊树来跟踪进程所有待检测的⽂件描述字,把需要监控的 socket 通过 epoll_ctl() 函数加⼊内核中的红⿊树⾥,红⿊树是个⾼效的数据结构,增删查⼀般时间复杂度是 O(logn) ,通过对这棵⿊红树进⾏操作,这样就不需要像 select/poll 每次操作时都传⼊整个 socket 集合,只需要传⼊⼀个待检测的 socket,减少了内核和⽤户空间⼤量的数据拷⻉和内存分配

第⼆点, epoll 使⽤事件驱动的机制,内核⾥维护了⼀个链表来记录就绪事件,当某个 socket 有事件发⽣时,通过回调函数,内核会将其加⼊到这个就绪事件列表中,当⽤户调⽤ epoll_wait() 函数时,只会返回有事件发⽣的⽂件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,⼤⼤提⾼了检测的效率。

epoll接口作用-来源参考[3]

epoll 的⽅式即使监听的 Socket 数量越多的时候,效率不会⼤幅度降低,能够同时监听的 Socket 的数⽬也⾮常的多了,上限就为系统定义的进程打开的最⼤⽂件描述符个数。因⽽,epoll 被称为解决 C10K 问题的利器

34.普通内存比一般的机械硬盘快多少?(补充)

2024 年 04 月 10 日增补

机械硬盘,也叫 HDD(Hard Disk Drive),是一种通过磁盘旋转和磁头移动来存储数据的设备,读写速度比较慢,通常比内存的速度慢 10 万倍左右。

  • HDD 的访问时间大约在 5-10ms,数据传输速率约为 100 到 200 MB/s。
  • 内存,也就是 RAM(Random Access Memory),访问时间大约在 10-100ns,数据传输速率约为数十 GB/s。

固态硬盘(Solid State Drive,SSD),SSD 的读写速度比 HDD 快 200 倍左右,价格也在逐渐下降,已经逐渐取代了 HDD。

图片来源于网络

  1. Java 面试指南(付费)收录的奇安信面经同学 1 Java 技术一面面试原题:普通内存比一般的机械硬盘快多少?

图文详解 34 道操作系统面试高频题,这次吊打面试官,我觉得稳了(手动 dog)。整理:沉默王二,戳转载链接,作者:三分恶,戳原文链接

make it come true