目录
- -
-
概述
用户态和内核态
- 用户态(User Mode) : 用户态的进程拥有较低的权限,可以读取用户程序的数据。
- 内核态(Kernel Mode):内核态的进程拥有更高的权限,几乎可以访问计算机的任何资源(包括内存、设备、驱动程序等),能够执行更底层、更敏感的操作。当应用程序需要执行某些需要特殊权限的操作(如读写磁盘、网络通信等),就需要向操作系统发起系统调用请求,进入内核态。进入内核态的开销较高(上下文切换和权限检查),因此应尽量减少进入内核态的次数,以提高系统性能。
如果只有内核态会怎么样?
- 恶意程序可能会覆盖操作系统的关键代码或数据,或终止其他进程,导致系统崩溃。
- 所有进程都可以无限占用内存、CPU、磁盘等资源,导致资源枯竭。
用户态切换到内核态的 3 种方式
- 系统调用:用户态进程主动要求切换到内核态。
- 中断:当外围设备完成用户请求的操作后(如硬盘读写操作完成),会向 CPU 发送中断信号,CPU 立即保存当前执行上下文并转入内核态执行中断处理程序。如果中断发生时 CPU 运行的是用户态代码,则会发生用户态到内核态的切换。
- 异常:当 CPU 在执行用户态进程时发生异常(如缺页异常、除零错误),会触发 CPU 进入内核态,执行相应的异常处理程序,以恢复或终止进程。
系统调用
程序基本都是运行在用户态,当我们要调用操作系统提供的内核态级别的子功能(设备管理、文件管理、进程控制、内存管理等)时,就需要系统调用,由操作系统代为完成。
系统调用的过程
- 用户态程序通过 glibc(与系统调用一一对应的函数库)调用
syscall
,触发 Trap 进入内核态。 - CPU 保存用户态上下文,并根据系统调用号查找相应的系统调用处理函数。
- 内核执行完系统调用后,使用
sysret
等恢复用户态上下文,CPU 切换回用户态。
中断
为了避免中断处理程序执行时间过长,Linux 将中断处理程序分为上半部和下半部:
- 上半部:硬中断,由硬件触发,用来快速处理中断。
- 下半部:软中断,由内核触发,用来异步处理上半部未完成的工作,通常都是耗时比较长的事情,特点是延迟执行。
进程和线程
- 进程(Process) 指正在运行的一个程序实例。举例:你打开的微信就是一个进程。
- 线程(Thread) 也称轻量级进程。多个线程可以在同一个进程中同时执行,并且共享进程的资源(如内存空间、文件句柄、网络连接等)。举例:你打开的微信里就有一个线程专门用来拉取别人发你的最新的消息。
进程和线程的区别
- 线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。
- 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈。
- 各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
- 线程执行开销小,但不利于资源的管理和保护;而进程正相反。
有了进程为什么还需要线程?
- 线程切换和调度的成本远低于进程(共享虚拟内存无需切换页表)。
- 多个线程可以并发处理不同的任务,更有效地利用了多核计算机资源。而进程只能在一个时间干一件事,如果在执行过程中阻塞,就会挂起直到结果返回。
- 同一进程内的线程共享内存和文件,相互通信无须调用内核。
进程状态
- 创建状态:进程正在被创建,尚未到就绪状态。
- 就绪状态:进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。
- 运行状态:进程正在 CPU 上运行(单核 CPU 任意时刻只有一个进程在运行状态)。
- 阻塞状态:又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。
- 结束状态:进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。
进程控制块 PCB
PCB(Process Control Block)数据结构是进程存在的唯一标识,每个进程都对应着一个独立的 PCB。当进程执行时,PCB 中的信息会不断变化,相同状态进程的 PCB 通过链表链在一起(形成就绪队列、阻塞队列等),操作系统会根据这些信息来管理和调度进程:
- 进程描述信息,进程名称、进程标识符、用户标识符等
- 进程调度信息,进程状态、阻塞原因、进程优先级等。
- 资源需求情况,CPU 时间、内存空间、I/O 设备等。
- 打开的文件信息,文件描述符、文件类型、打开模式等。
- 处理机状态信息,通用寄存器、指令计数器、程序状态字 PSW、用户栈指针。
进程间通信方式
管道/匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程间的通信,只存在于内存中。
$ ps -aux | grep xxx
有名管道(Named Pipes) : 可以实现本机上任意两个进程间通信。有名管道遵循 FIFO,以磁盘文件的方式存在。
==================== 终端一 ==================== $ mkfifo myPipe $ ls -l prw-r--r--. 1 root root 0 Jul 17 02:45 myPipe $ echo "hello" > myPipe // 将数据写进管道 // 停住了 $ // 终端二将管道里的数据读出后,才可以退出 ==================== 终端二 ==================== $ cat < myPipe // 读取管道里的数据 hello
- 消息队列(Message Queuing) :无需等待消息被读出再返回;并且每个消息体都是固定大小的存储块,发送方和接收方能够约定好其数据类型;此外,消息不一定要以 FIFO 的次序读取,也可以按消息的类型读取。消息队列存放在内核中,只有在被显式地删除或内核重启时,消息队列才会被真正的删除。缺点是消息体大小有限制,并且存在用户态与内核态之间的数据拷贝开销。
- 共享内存(Shared memory) :多个进程的一块虚拟地址空间映射到同一块物理内存,避免了用户态与内核态之间的消息拷贝,并且不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式往往需要依靠互斥锁或信号量来完成同步操作。
信号(Signal) :用于通知接收进程某个事件已经发生,是进程间通信机制中唯一的异步通信机制。除了
SIGKILL
和SEGSTOP
信号外,其它信号的处理方式都包括执行默认操作、捕获信号以及忽略信号。kill -l
命令可以查看所有的信号。优雅关闭进程的方法:
kill -15 (SIGTERM)
相较于kill -9 (SIGKILL)
,能够给目标进程一个清理善后工作的机会。例如 Java 中可以使用Runtime.getRuntime#addShutdownHook
注册一个关闭钩子,用于关闭一些连接等。- 信号量(Semaphores) :一个整型的计数器,用于多进程对共享数据的访问,实现进程间的互斥与同步。
- 套接字(Sockets) : 用于跨网络的不同主机上进程间通信。
进程调度算法
- 非抢占式:先来先服务、短作业优先、最高优先级、最高响应比优先。
- 抢占式:时间片轮转、抢占式优先级调度、最短剩余时间优先、多级队列调度、多级反馈队列调度。
线程间同步方式
线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。常见的同步方式:
- 互斥锁(Mutex) :采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的
synchronized
关键词和各种Lock
都是这种机制。 - 读写锁(Read-Write Lock) :多个线程能够同时读取共享资源,但只有一个线程可以进行写操作。
- 信号量(Semaphore) :允许同一时刻多个线程访问同一资源,需要控制同一时刻访问此资源的最大线程数量。
- 屏障(Barrier) :屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。当一个线程到达屏障时,它会停止执行并等待其他线程到达屏障,直到所有线程都到达屏障后,它们才会一起继续执行。比如 Java 中的
CyclicBarrier
是这种机制。 - 事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
线程的实现
- 用户线程(User Thread):在用户空间实现的线程,由用户态的线程库管理。由于操作系统只看到一个进程,而用户线程是由进程管理的,因此即使一个进程内有多个用户线程,它们也无法真正并行执行;并且如果一个线程阻塞,整个进程都会阻塞。
- 内核线程(Kernel Thread):在内核中实现的线程,由内核管理。其创建、终止和切换都是通过系统调用的方式来进行,开销较大。
- 轻量级进程(LightWeight Process,LWP):由内核管理,是用户线程与内核线程之间的桥梁。一个进程可以有一个或多个 LWP,每个 LWP 跟内核线程一对一映射,跟用户线程一对多映射(现代 Linux 为一对一)。LWP 本质上就是内核线程,但多个 LWP 共享一个进程的资源。每有一个用户线程,就创建一个内核线程,开销较大。
僵尸进程和孤儿进程
在 Unix/Linux 系统中,子进程通常是通过 fork()
系统调用创建的,该调用会创建一个新的进程,该进程是原有进程的一个副本。子进程和父进程的运行是相互独立的,它们各自拥有自己的 PCB,即使父进程结束了,子进程仍然可以继续运行。
当一个进程调用 exit()
系统调用结束自己的生命时,内核会释放该进程的所有资源,包括打开的文件、占用的内存等,但是该进程的 PCB 仍存在于系统中。这些信息只有在父进程调用 wait()
或 waitpid()
系统调用时才会被释放,以便让父进程得到子进程的状态信息。这样的设计可以让父进程在子进程结束时得到子进程的状态信息,并且可以防止出现“僵尸进程”(即子进程结束后 PCB 仍然存在但父进程无法得到状态信息的情况)。
僵尸进程:子进程已经终止,但是其父进程仍在运行,且父进程没有及时通过 wait()
或 waitpid()
系统调用来释放子进程占用的资源,导致子进程的 PCB 依然存在于系统中,但无法被进一步使用。Linux中 Top
命令中的 zombie
值表示僵尸进程的数量。
孤儿进程:一个进程的父进程已经终止或者不存在,但该进程仍在运行。孤儿进程通常是由于父进程意外终止或未及时通过系统调用回收子进程导致的。为了避免孤儿进程占用系统资源,操作系统会将孤儿进程的父进程设置为 init
进程(进程号为 1),由 init 进程来回收孤儿进程的资源。
死锁
死锁(Deadlock):多个进程/线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于进程/线程被无限期地阻塞,因此程序不可能正常终止。
死锁的必要条件
- 互斥条件:某些资源只能被一个进程独占使用。
- 不剥夺条件:资源不能被强制剥夺,只能由进程主动释放。
- 请求和保持条件:一个进程已经持有某些资源,同时又在请求其他资源。
- 环路等待条件:存在一个进程链,使每个进程都在等待链中下一个进程持有的资源。
死锁的预防
破坏四个必要条件中的任意一个就能够预防死锁的发生:
- 破坏互斥条件:使资源能够被同时访问(如磁盘),但很多资源往往不能同时访问。
- 破坏不剥夺条件:采用剥夺式调度算法,仅适用于主存资源和处理器资源的分配,会导致资源利用率下降。
- 破坏请求与保持条件:采用静态分配策略,一个进程必须在执行前就申请到它所需要的全部资源。
- 破坏环路等待条件:采用层次分配策略,将资源分成了多个层次,一个进程得到某一层的资源后,只能再申请较高层的资源。
死锁的避免(银行家算法)
假设资源分配情况如下:
Process | Allocation | Need | Available |
---|---|---|---|
P0 | 0 0 3 2 | 0 0 1 2 | 1 6 2 2 |
P1 | 1 0 0 0 | 1 7 5 0 | |
P2 | 1 3 5 4 | 2 3 5 6 | |
P3 | 0 3 3 2 | 0 6 5 2 | |
P4 | 0 0 1 4 | 0 6 5 6 |
则存在安全序列 {P0,P3,P4,P1,P2}
,如下表所示:
Process | Work | Need | Allocation | Work + Allocation | Finish |
---|---|---|---|---|---|
P0 | 1 6 2 2 | 0 0 1 2 | 0 0 3 2 | 1 6 5 4 | true |
P3 | 1 6 5 4 | 0 6 5 2 | 0 3 3 2 | 1 9 8 6 | true |
P4 | 1 9 8 6 | 0 6 5 6 | 0 0 1 4 | 1 9 9 10 | true |
P1 | 1 9 9 10 | 1 7 5 0 | 1 0 0 0 | 2 9 9 10 | true |
P2 | 2 9 9 10 | 2 3 5 6 | 1 3 5 4 | 3 12 14 14 | true |
假设 P2
请求 Request2(1,2,2,2)
:满足小于等于 Need2(2,3,5,6)
和 Available(1,6,2,2)
。但分配后 Available = (0,4,0,0)
,无法满足任何进程,故拒绝分配。
内存管理
操作系统的内存管理主要负责:
- 内存分配与回收:对进程申请的内存进行分配(malloc)和释放(free)。
- 地址转换:将程序中的虚拟地址转换成内存中的物理地址。
- 内存扩充:当系统内存不够时,利用虚拟内存或自动覆盖技术,从逻辑上扩充内存。
- 内存映射:将一个文件直接映射到进程的进程空间中,这样可以通过内存指针用读写内存的办法直接存取文件内容,速度更快。
- 内存优化:通过调整内存分配策略和回收算法来优化内存使用效率。
- 内存安全:保证进程间使用内存互不干扰,避免恶意程序修改内存,破坏系统安全。
- ……
虚拟内存
虚拟内存(Virtual Memory) 是逻辑存在的、一个假想出来的内存空间,主要作用是作为进程访问主存(物理内存)的桥梁并简化内存管理,虚拟内存主要提供了以下能力:
- 隔离进程:物理内存通过虚拟地址空间访问,虚拟地址空间与进程一一对应。每个进程都认为自己拥有了整个物理内存,进程之间彼此隔离,一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。
- 提升物理内存利用率:有了虚拟地址空间后,操作系统只需要将进程当前正在使用的部分数据或指令加载入物理内存。
- 简化内存管理:进程都有一个一致且私有的虚拟地址空间,程序员不用和真正的物理内存打交道,而是借助虚拟地址空间访问物理内存,从而简化了内存管理。
- 多个进程共享物理内存:进程在运行过程中,会加载许多操作系统的动态库。这些库对于每个进程而言都是公用的,它们在内存中实际只会加载一份,这部分称为共享内存。
- 提高内存使用安全性:控制进程对物理内存的访问,隔离不同进程的访问权限。
- 提供更大的可使用内存空间:可以让程序拥有超过系统物理内存大小的可用内存空间。当物理内存不够用时,将物理内存页保存到磁盘文件(会影响读写速度),根据需要在物理内存与磁盘之间移动。
没有虚拟内存有什么问题?
- 用户程序可以访问任意物理内存,可能会不小心操作到系统运行必需的内存,进而造成操作系统崩溃,严重影响系统的安全。
- 可能出现多个程序给同一内存地址赋值造成覆盖,导致应用程序会崩溃。
- 程序运行过程中使用的所有数据或指令都要载入物理内存,而其中很大一部分可能都不会用到,白白占用了宝贵的物理内存资源。
虚拟地址和物理地址
- 物理地址(Physical Address):内存地址寄存器中的真正物理内存的地址。
- 虚拟地址(Virtual Address):程序中访问的内存地址。
- 物理地址空间:物理内存的范围。
- 虚拟地址空间:虚拟内存的范围,每个进程都有一个一致且私有的虚拟地址空间。
- 地址翻译/地址转换(Address Translation):操作系统一般通过 CPU 中的 MMU(Memory Management Unit,内存管理单元)将虚拟地址转换为物理地址。
内存碎片
内存碎片会导致内存利用率下降,它是由内存的申请和释放产生的,通常分为:
- 内部内存碎片(Internal Memory Fragmentation):已经分配给进程使用但未被使用的内存。例如一个进程只需要 65 字节的内存,但为其分配了 128(2 的幂次方) 大小的内存,剩余的 63 字节就成为了内部碎片。
- 外部内存碎片(External Memory Fragmentation):由于未分配的连续内存区域太小,不能满足任意进程的内存分配请求,这些小且不连续的内存空间被称为外部碎片。
DMA / RDMA
连续内存管理
早期计算机操作系统采用块式管理的连续内存管理方式,存在严重的内存碎片问题。它将内存分为几个固定大小的块,每个块中只包含一个进程。如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了,造成大量内部碎片,两个内存块之间还可能会有外部碎片。
Linux 系统中,连续内存管理采用的是伙伴系统(Buddy System)算法,可以有效解决外部碎片的问题。伙伴系统中每一块内存大小都是 2 的幂次大小,相邻的内存块组合成一对伙伴。当进行内存分配时,伙伴系统会尝试找到大小最合适的内存块。如果找到的内存块过大,就一直将其分成两个大小相等的伙伴块,直到切分到合适的大小为止。假设两块相邻的内存块都被释放,系统会将其合并成一个更大的内存块,以便后续的内存分配。此外,对于内部碎片的问题,Linux 采用 SLAB 进行解决。(略)
非连续内存管理
非连续内存管理存在下面 3 种方式:
- 段式管理:将应用程序的虚拟地址空间被分为大小不等的段(一段连续的物理内存)。
- 页式管理:把物理内存分为连续等长的物理页,应用程序的虚拟地址空间也被划分为连续等长的虚拟页,是现代操作系统广泛使用的一种内存管理方式。
- 段页式管理机制:结合了段式管理和页式管理的一种内存管理机制,把物理内存先分成若干段,每个段又继续分成若干大小相等的页。
分段机制
分段机制(Segmentation)以段(一段连续的物理内存)的形式分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。分段机制容易出现外部内存碎片。
分段管理通过段表(Segment Table)映射虚拟地址和物理地址,段表中还存有段长(可用于检查虚拟地址是否超出合法范围)、段类型(例如代码段、数据段等)等。分段机制下的虚拟地址由段号和段内偏移量组成。
- MMU 首先解析得到虚拟地址中的段号;
- 通过段号去该应用程序的段表中取出对应的段信息(找到对应的段表项);
- 从段信息中取出该段的起始地址(物理地址)加上虚拟地址中的段内偏移量得到最终的物理地址。
段表项不存在的原因
- 段表项被删除:软件错误、软件恶意行为等情况可能会导致段表项被删除。
- 段表项还未创建:如果系统内存不足或者无法分配到连续的物理内存块就会导致段表项无法被创建。
分页机制
分页机制(Paging) 把主存(物理内存)分为连续等长的物理页,应用程序的虚拟地址空间划也被分为连续等长的虚拟页,能够有效避免外部内存碎片的问题。
分页管理通过页表(Page Table)映射虚拟地址和物理地址,页表中还存有访问标志(是否被访问过)、脏数据标识位等。分页机制下的虚拟地址由页号和页内偏移量组成。
- MMU 首先解析得到虚拟地址中的虚拟页号;
- 通过虚拟页号去该应用程序的页表中取出对应的物理页号(找到对应的页表项);
- 用该物理页号对应的物理页起始地址(物理地址)加上虚拟地址中的页内偏移量得到最终的物理地址。
页缺失(Page Fault)
也称为称硬错误、硬中断、分页错误、寻页缺失、缺页中断、页故障等,指的是当软件试图访问已映射在虚拟地址空间中,但是目前并未被加载在物理内存中的一个分页时,由 MMU 所发出的中断。常见的页缺失有:
- 硬性页缺失(Hard Page Fault):物理内存中没有对应的物理页。Page Fault Handler 会指示 CPU 从磁盘中读取相应内容到物理内存,而后交由 MMU 建立相应虚拟页和物理页的映射关系。
- 软性页缺失(Soft Page Fault):物理内存中有对应的物理页,但虚拟页还未与其建立映射。Page Fault Handler 会指示 MMU 建立相应虚拟页和物理页的映射关系。
- 无效缺页错误(Invalid Page Fault):应用程序访问了无效的物理内存。上述两种错误虽然出现了物理页缺失或映射关系未建立的问题,但访问的还是有效的物理内存。
多级页表
以 32 位的环境为例,虚拟地址空间范围共有 2^32 (4G)
。假设一个页的大小是 4KB
,那页表项就有 4G / 4K = 2^20
个。每个页表项为一个地址,占用 4B
。一个程序页表大小至少就得占用 2^20 * 4B = 4MB
,而实际用到的可能只是其中的几项。
为了解决这个问题,操作系统引入了多级页表(时间换空间),多级页表对应多个页表,每个页表与前一个页表相关联。32 位系统一般为二级页表,64 位系统一般为四级页表。
以二级页表为例,一级页表共有 1024 个页表项,一级页表又关联二级页表,二级页表同样共有 1024 个页表项。二级页表按需加载,进而节省空间占用。假设只需要 2 个二级页表,那么内存占用情况为: 4KB(一级页表)+ 4KB * 2(二级页表)= 12 KB
。
页面置换
当物理内存不足的时候,操作系统将一些物理页放到磁盘上去,等要用到的时候再将它们读取到物理内存中。这就是为什么操作系统中即使所有进程运行所需物理内存大于实际物理内存时,也是可以正常运行的,只是运行速度会变慢。
当发生硬性页缺失时,如果物理内存中没有空闲的物理页面可用的话。操作系统就必须将物理内存中的一个物理页淘汰出去,腾出空间来加载新的页面。页缺失太频繁会非常影响性能,一个好的页面置换算法需要减少页缺失出现的次数。常见的页面置换算法有:
- 最佳页面置换算法(OPT,Optimal):优先选择淘汰的页面是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。但由于人们目前无法预知进程在内存下的若干页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现,只是理论最优的页面置换算法,可以作为衡量其他置换算法优劣的标准。
- 先进先出(FIFO,First In First Out): 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。该算法易于实现和理解,一般只需要通过一个 FIFO 队列即可满足需求。不过,它的性能并不是很好,较早调入的页往往是经常被访问或者需要长期存在的页,这些页会被反复调入和调出。
- 最近最久未使用(LRU ,Least Recently Used):每次淘汰最近最久未使用的页面。LRU 算法赋予每个页面一个访问字段,记录自上次被访问以来所经历的时间 T,当需淘汰一个页面时,选择现有页面中 T 值最大的。
- 最少使用(LFU,Least Frequently Used) : 和 LRU 算法相似,每次淘汰前一段时间内使用最少的页面。
- 时钟页面置换算法(Clock):一种最近未使用算法,使用一个使用位和一个修改位。
LRU 算法在实际使用中比较多,被认为是最接近 OPT 的页面置换算法。实际应用中这些算法会被做一些改进,比如 InnoDB Buffer Pool 就改进了传统的 LRU,使用了一种称为 Adaptive LRU 的算法(同时结合了 LRU 和 LFU)。
局部性原理(Locality Principle)
数据和指令的访问存在一定的空间和时间上的局部性特点,分页机制利用局部性原理,采用缓存和预取技术,提高内存访问效率:
- 时间局部性:程序中存在一定的循环或者重复操作,因此会反复访问同一个页或一些特定的页。为利用时间局部性,分页机制中通常采用缓存机制来提高页面的命中率,即将最近访问过的一些页放入缓存中。
- 空间局部性:程序中数据和指令的访问通常是具有一定的空间连续性的,因此当访问某个页时,往往会顺带访问其相邻的一些页。为利用空间局部性,分页机制中通常采用预取技术来预先将相邻的一些页读入内存缓存中。
转址旁路缓存(Translation Lookaside Buffer,TLB)
也称为快表,通过减少主存的访问次数,提高虚拟地址到物理地址的转换速度。在主流的 AArch64 和 x86-64 体系结构下,TLB 属于 MMU 内部的单元,本质上就是一块高速缓存,缓存了虚拟页号到物理页号的映射关系,你可以将其简单看作是存储着键(虚拟页号)值(物理页号)对的哈希表。使用 TLB 之后的地址翻译流程如下:
- 用虚拟地址中的虚拟页号作为 key 去 TLB 中查询;
- TLB hit:能查到对应的物理页,无需再查询页表。
- TLB miss:查不到对应的物理页,需要查询页表,同时将该映射表项添加到 TLB 中。
- 当 TLB 填满后,需要登记新页时,按照一定的淘汰策略淘汰掉其中一个页。
分页 vs 分段
共同点:
- 都是非连续内存管理的方式。
- 都采用了地址映射的方法,将虚拟地址映射到物理地址,以实现对内存的管理和保护。
区别:
- 页的大小是固定的,由操作系统决定;段的大小不固定,由程序决定。
- 页是物理单位,操作系统将物理内存划分成 2 的幂次方大小的页面;段是逻辑单位,根据程序对内存空间的逻辑需求来划分。
- 分页机制容易出现内部内存碎片,分段机制容易出现外部内存碎片。
- 分页机制采用了页表来完成虚拟地址到物理地址的映射,页表通过一级页表和二级页表来实现多级映射;而分段机制则采用了段表来完成虚拟地址到物理地址的映射,每个段表项中记录了该段的起始地址和长度信息。
- 分页机制对程序没有任何要求,程序只需要按照虚拟地址进行访问即可;而分段机制需要程序员将程序分为多个段,并且显式地使用段寄存器来访问不同的段。
文件系统
文件系统主要负责管理和组织计算机存储设备上的文件和目录,其功能包括:
- 存储管理:将文件数据存储到物理存储介质中,并且管理空间分配,以确保每个文件都有足够的空间存储,并避免文件之间发生冲突。
- 文件管理:文件的创建、删除、移动、重命名、压缩、加密、共享等等。
- 目录管理:目录的创建、删除、移动、重命名等等。
- 文件访问控制:管理不同用户或进程对文件的访问权限,以确保用户只能访问其被授权访问的文件,以保证文件的安全性和保密性。
硬链接和软链接
1、硬链接(Hard Link)
- 每个文件和目录都有一个唯一的索引节点(inode)号,用来标识该文件或目录。硬链接和源文件的 inode 节点号相同,两者对文件系统来说是完全平等的(两者互为硬链接,源头是同一份文件),删除一个对另一个没有影响,可以通过给文件设置硬链接文件来防止重要文件被误删。
- 只有删除了源文件和所有对应的硬链接文件,该文件才会被真正删除。
- 不能对目录(防止循环)或不存在的文件创建硬链接,并且硬链接不能跨越文件系统(每个文件系统的 inode 表是独立的,硬链接可能会导致 inode 节点号冲突)。
ln
命令用于创建硬链接。
2、软链接(Symbolic Link 或 Symlink)
- 软链接和源文件的 inode 节点号不同,而是指向一个文件路径,类似于快捷方式。
- 源文件删除后,软链接依然存在,但指向的是一个无效的文件路径。
- 可以对目录防止文件系统循环或不存在的文件创建软链接,并且软链接可以跨越文件系统。
ln -s
命令用于创建软链接。
磁盘调度算法
- 先来先服务算法(First-Come First-Served,FCFS):按照请求到达磁盘调度器的顺序进行处理,先到达的请求的先被服务。FCFS 算法实现起来比较简单,不存在算法开销。不过,由于没有考虑磁头移动的路径和方向,平均寻道时间较长。同时,该算法容易出现饥饿问题,即一些后到的磁盘请求可能需要等待很长时间才能得到服务。
- 最短寻道时间优先算法(Shortest Seek Time First,SSTF):也被称为最佳服务优先(Shortest Service Time First,SSTF)算法,优先选择距离当前磁头位置最近的请求进行服务。SSTF 算法能够最小化磁头的寻道时间,但容易出现饥饿问题,即磁头附近的请求不断被服务,远离磁头的请求长时间得不到响应。实际应用中,需要优化一下该算法的实现,避免出现饥饿问题。
- 扫描算法(SCAN):也被称为电梯(Elevator)算法,基本思想和电梯非常类似。磁头沿着一个方向扫描磁盘,如果经过的磁道有请求就处理,直到到达磁盘的边界,然后改变移动方向,依此往复。SCAN 算法能够保证所有的请求得到服务,解决了饥饿问题。但是,如果磁头从一个方向刚扫描完,请求才到的话。这个请求就需要等到磁头从相反方向过来之后才能得到处理。
- 循环扫描算法(Circular Scan,C-SCAN):SCAN 算法的变体,只在磁盘的一侧进行扫描,并且只按照一个方向扫描,直到到达磁盘边界,然后回到磁盘起点,重新开始循环。
- 边扫描边观察算法(LOOK):SCAN 算法中磁头到了磁盘的边界才改变移动方向,这样可能会做很多无用功,因为磁头移动方向上可能已经没有请求需要处理了。LOOK 算法对 SCAN 算法进行了改进,如果磁头移动方向上已经没有别的请求,就可以立即改变磁头移动方向,依此往复。也就是边扫描边观察指定方向上还有无请求,因此叫 LOOK。
- 均衡循环扫描算法(C-LOOK):C-SCAN 只有到达磁盘边界时才能改变磁头移动方向,并且磁头返回时也需要返回到磁盘起点,这样可能会做很多无用功。C-LOOK 算法对 C-SCAN 算法进行了改进,如果磁头移动的方向上已经没有磁道访问请求了,就可以立即让磁头返回,并且磁头只需要返回到有磁道访问请求的位置即可。
Docker
Docker 是基于 Linux 内核提供的 CGroup 功能和 namespace 来实现的,以及 AUFS 类的 UnionFS 等技术,对进程进行封装隔离,属于操作系统层面的虚拟化技术。
- namespace:Linux 内核用来隔离内核资源的方式。通过把一个或多个进程的相关资源指定在同一个 namespace 中,可以让进程只能看到与自己相关的一部分资源,进程之间根本就感觉不到对方的存在。
- CGroup(Control Groups):Linux 内核提供的一种可以限制、记录、隔离进程组所使用的物理资源 (如 CPU、内存、I/O 等) 的机制。
- 联合文件系统(UnionFS):一种分层、轻量级且高性能的文件系统,支持对文件系统的修改作为一次提交来一层层的叠加,同时将不同目录挂载到同一个虚拟文件系统下。
文章标题:【八股】操作系统
文章作者:nek0peko
文章链接:https://nek0peko.com/index.php/archives/137/
商业转载请联系站长获得授权,非商业转载请注明本文出处及文章链接,未经站长允许不得对文章文字内容进行修改演绎。
本文采用创作共用保留署名-非商业-禁止演绎4.0国际许可证