Post

操作系统八股文

操作系统八股文

前言

由于笔者在准备实习面试、本篇将记录我在准备操作系统八股文所学知识。我会将有关操作系统的八股文划分为几个部分:内存管理、进程调度、文件系统、网络系统,常规。

内存管理相关

q1:为什么需要虚拟内存?

虚拟内存的设计可以提供很多的功能,比如用户地址空间的隔离,逻辑内存的扩大。首先我们来讲讲虚拟内存是如何能够使得不同进程的内存地址不会相互干扰。在Linux中,所有被新创建的进程都会维护一张多级页表,负责虚拟内存向实际内存的映射。所以每个进程的内存地址的范围都是一样的,在64位系统中,就是0x0到0x00007FFFFFFFF000。这里提到两个概念:虚拟内存和物理内存。我们平时的程序中内存的地址是虚拟地址,当向物理内存中写入数据的时候,会进行地址翻译。

那么虚拟内存如何映射到物理内存呢?这里先说明内存管理的两种模式,一种是segment管理,另一种是page 管理。

segment管理,将用户地址空间划分会几个确定的segment

在只用段管理的系统中,虚拟地址由段选择因子和段内偏移量组成。段选择因子由段号和标志位组成。翻译过程如下:由段号向段表查询到段基地址以及段界限,段界限是段的总偏移量,再根据段偏移量就能定位到实际内存位置。

segment会出现外部内存碎片,而不会出现内部内存碎片。因为在内部地址空间,由于段的大小是不确定的,所以需要多少内存就可以申请多少的内存,不会出现内部内存碎片。但是在外部地址空间,假设现在有三个进程,每个进程都向系统申请了一段,但是大小不一样,夹在中间的进程的内存较小且很快结束释放,但是新来的进程的内存大小不够,就不会分配这块小的内存,所以会出现内存碎片。

alt text

解决方法也很简单,就是定期对内存进行整理,方式是内存交换,但是会影响性能。

page管理:外部内存被预先划分好成诺干页,每个页具有其页号,页表可以查询到页号。Linux中每个进程创建时,会初始化好一级页表的内容并将其地址写到寄存器中。页表本质上下一级页表的基地址的集合,虚拟地址只用储存各级页表的页号。页号与对应基地址相加即是页表项的物理地址

alt text

如果页表项中没有物理地址,会触发page fault。缺页会向内存申请页表并将页表集地址填入页表项中。如果内存紧张则会触发一些内存回收机制,这里后面再讲。

在64位Linux中存在四级页表分别是:PGD PUD PMD PTE。每个页表项的大小是8字节,所以一个页表的大小是4KB。这样的设计是为了减少页表的大小,因为页表是在内存中的,如果页表太大,会导致内存的浪费。

alt text

TLB页表缓存

段页式管理

段页式管理是将段管理和页管理结合在一起,段管理是为了解决内存的外部碎片问题,页管理是为了解决内存的内部碎片问题。先将程序划分为多个有逻辑意义的段,然后将这些段划分为多个固定大小的页。

这样地址就由三部分组成,段号、多级页号、页内偏移组成。所以进程会维护一张段表,每张段表对应一系列多级页表。

alt text

未被段映射的地址称为逻辑地址,被段管理单元映射后的地址称为线性地址,也就是虚拟地址,被页管理单元映射后的地址称为物理地址。

Linux内存布局

Linux内存采用页式内存管理,同时不可避免地涉及了段机制。 但是Linux又通过一种巧妙的方式避开了段式管理,就是Linux内每个段就是从0地址开始的整个256Td地址空间,这样的逻辑地址就等于虚拟地址。这样就避免了段式管理的复杂性。

进程的地址空间分为两个部分:内核空间和用户空间。内核空间是0xC0000000到0xFFFFFFFF,用户空间是0x00000000到0xBFFFFFFF。内核空间地址是线性的,且被所有用户空间所共用的。用户空间是每个进程独有的,每个进程的用户空间都是从0开始的。

alt text

alt text

q2:内存满了、会发生什么?

当内存满了的时候,会直接杀死某个进程来获得内存吗,虽然这是最直观有效的,但也是最危险的。

进程调度相关

进程

线程

文件系统

q1:如何管理空闲磁盘空间?

首先空闲的磁盘空间分为两种:inode空闲和数据块空闲。管理它们的方式是使用bitmap的方法,也叫位图法。每个位代表一个inode或者数据块,如果为1代表已经被占用,为0代表空闲。

存储系统IO软件分层

alt text

q1 键盘敲入字母时,期间发生了什么?

在回答这个问题之前,先说明下CPU和硬件之间是如何通信的。CPU的内存接口,和系统总线连接在一起,系统总线连接着IO桥接器。这个IO桥接器一边接入内存总线,使得CPU可以和内存相互通信。另一边,接入一个IO总线,用来连接IO设备。

alt text

当用户输入键盘字符的时候,键盘的设备控制器会捕捉并产生数据,将其缓冲在设备控制器内的寄存器中,然后发送中断信号到CPU。CPU捕捉到中断信号,保持上下文,执行键盘设备注册的中断处理程序。中断处理程序会在键盘的控制器的寄存器的缓冲区读取扫描码,然后再根据扫描码转换成用户输入的字符。如果输入字符为显示字符,就会把扫描码翻译成对应的显示字符的ASCII码,然后将其存储到读缓冲区队列。接下来就是需要显示字符显示屏幕了,显示设备的驱动程序会定时得从读缓冲区队列读取数据放到写缓冲区队列,最后把写缓冲区队列的数据写到显示设备的块设备控制器的寄存器的数据缓冲区中,显示设备的块设备控制器会将数据显示到屏幕上。

恢复中断进程的上下文。

q1:零拷贝技术

两种零拷贝技术:

  • mmap + write
  • sendfile

mmap + write

代码演示:

1
2
char *buf = mmap(file, len);
write(sockfd, buf, len);

mmap系统调用函数的底层是把内核缓冲区的数据映射到用户空间,用另一句话来说,用户态和内核态共享一块内存。这样就能够减少内核缓冲区和用户缓冲区相互拷贝这个过程。而是直接将内核缓冲区的数据写到socket缓冲区中。

alt text

sendfile

1
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

该调用可以直接将文件内容拷贝到内核缓冲区,然后再把内核缓冲区的数据拷贝到socket缓冲区里,不用再拷贝到用户态。

当然,如果你的Linux版本高,支持SG_DMA技术,可以直接将内核缓冲区的数据直接读到网卡的设备缓冲区中。

SG_DMA实际上才是真正的零拷贝技术,设备间缓冲区的数据传输不需要经过内存的缓存,而是直接点到点的传输。

经典问题:电脑从开机到桌面显示的过程中发生了什么?

  1. bios电自检(POST:power on system test):确认硬件是可用的,没有被损坏(代表CPU可用)
  2. cpu去实行bios的指令,根据内部的Boot Sequence(引导顺序),找到引导设备,如果找不到可引导设备就启动失败!
  3. 找到可引导设备后,读取该设备的第一个扇区,即MBR,里面前446 Bytes 包含引导程序1,然后跳转到引导程序2
  4. 内核初始化进程等各种init,比如initrd initramfs,已经把控制权给os了
  • 相关名词解释:
    1. BIOS(Basic input output system):是在存储在主板ROM里面的一个固件,并为操作系统提供基本的硬件抽象层。里面记录了主机板的芯片集与相关设置,启动设备的搜索顺序和硬盘大小与类型等。
    2. Boot Sequence(引导顺序):决定了计算机在启动时依次检查哪些设备以寻找可引导的操作系统。引导顺序通常可以在BIOS/UEFI设置界面中配置,常见的引导设备包括:
      1. 硬盘(HDD/SSD)
      2. 光盘驱动器(CD/DVD)
      3. USB设备
      4. 网络启动(PXE)
    3. MBR:Master Boot Record(主引导程序):MBR是硬盘的第一个扇区,大小为512字节。MBR包含以下几个部分:
      1. 引导程序1(这个就被称作是bootloader):前446字节,包含引导加载程序的第一阶段代码。(负责查找并装载引导加载程序的下一阶段(通常位于硬盘的特定位置)) (引导程序2就是所谓的GRUB的核心映像
      2. 分区表:接下来的64字节,包含四个分区表项,每个分区表项16字节。
      3. 签名:最后2字节,通常是0x55AA,用于标识这是一个有效的MBR。
  • Boot Sequence决定启动设备:当计算机启动时,BIOS/UEFI根据引导顺序依次检查各个设备,一旦找到可引导设备(如硬盘),BIOS/UEFI会读取该设备的第一个扇区,即MBR。
  • 读取和执行MBR:BIOS/UEFI将MBR加载到内存中,并执行其中的引导程序1代码。MBR中的引导程序1代码负责查找并加载引导加载程序的下一阶段(我称之为引导程序2)
  • 引导程序2:这是GRUB的核心映像,由于它比较大,所以无法被直接写入MBR里面。此阶段被加载并执行后,通常会提供一个菜单(就是我们开机按F8看到的所谓bios界面),允许用户选择要启动的操作系统或内核。最终,引导加载程序会加载操作系统内核,并将控制权交给内核,完成启动过程。

  • 补充:CPU复位(又爱又恨的cpu reset): 电源开启后,CPU会进行复位操作,复位后CPU会从一个预定义的地址开始执行指令。 对于x86架构的CPU,这个地址通常是0xFFFF0,这是BIOS固件的入口地址。

内存管理相关

看这里

  • 在计算机启动过程中,实模式和保护模式是两种不同的CPU工作模式。以下是各个阶段对应的模式:

    实模式(Real Mode)

  • 只能访问20bits(1M)的内存,由两个16bits的寄存器组成

    什么是实模式?

  • 实模式:其中形如0x1000:0x0010这样的寻址形式,对应的目标位置物理地址为(0x1000 « 4) + 0x0010 = 0x10010。前一部分称为段地址,后一部分称为段内偏移。两个地址数值上必须限定在16位宽度。两部分合起来计算出的物理地址位宽限定在20位宽度。
  • 举例子:0xFFFF:0xFFFF = 0xFFFF « 4 + 0xFFFF = 0x10FFEF,这样的一个物理地址至少需要21个比特位来表达,就超过了20个比特位的限制。所以,0xFFFF:0xFFFF实际的物理地址为0xFFEF。超出20个比特位部分被舍弃掉了。

启动的哪些阶段是实模式?

  1. BIOS初始化和POST:在电源开启后,CPU reset并进入实模式。BIOS固件开始执行,进行硬件自检(POST)和初始化。在实模式下,CPU可以直接访问内存地址,且只能访问1MB的内存空间。
  2. 引导加载程序的第一阶段:BIOS根据引导顺序查找启动设备,并读取设备的第一个扇区(MBR)。MBR中的引导程序(前446字节)在实模式下执行,负责加载引导加载程序的下一阶段。

    实模式的缺点:

  3. 实模式下操作系统和用户程序属于同一特权级,这哥俩平起平坐,没有区别对待。
  4. 用户程序所引用的地址都是指向真实的物理地址,也就是说逻辑地址等于物理地址,实实在在地指哪打哪。
  5. 用户程序可以自由修改段基址,可以不亦乐乎地访问所有内存,没人拦得住。
    以上 3 个原因属于安全缺陷,没有安全可言的 CPU 注定是不可依赖的,这从基因上决定了用户程序乃至操作系统的数据都可以被随意地删改,一旦出事往往都是灾难性的,而且不容易排查。

  6. 访问超过 64KB 的内存区域时要切换段基址,转来转去容易晕乎。
  7. 一次只能运行一个程序,无法充分利用计算机资源。
  8. 共 20 条地址线,最大可用内存为 1MB,这即使在 20 年前也不够用

保护模式(Protected Mode)

  • 保护模式需开启分页机制下,开启分页机制前,需要准备好页目录表,页表。并将页目录表的物理地址加载到CR3控制寄存器。进入保护模式后,再置位CR0.PG可开启分页机制。

    启动的哪些阶段是保护模式?

  • 引导加载程序的第二阶段:引导加载程序的第二阶段代码(如GRUB)通常会切换到保护模式。在保护模式下,CPU可以访问更多的内存(超过1MB),并且支持内存保护、多任务等高级功能。
  • 加载操作系统内核:引导加载程序在保护模式下加载操作系统内核。操作系统内核启动后,继续在保护模式下运行,进行进一步的初始化和配置。

长模式(Long Mode)又称作 IA-32e Mode

什么是长模式?

  • 进入64位的x64处理器时代后,产生了一种新的运行模式,叫Long Mode(intel手册里还把它叫做IA-32e Mode),传统的三种模式则被统称为Legacy Mode。Long Mode又分为2种子模式,分别是64位长模式(64-Bit Mode)和64位兼容模式(Compatibility Mode)。
  • 因为Long Mode使用64位虚拟地址,所以不管是64-Bit Mode还是Compatibility Mode的,都要求操作系统和工具链必须是64位的,其中64-Bit Mode又要求应用程序也得是64位的(纯纯的64位啊)。
  • 因此,现存的32位应用程序可以不经重新编译就在处于Compatibility Mode的64位操作系统上运行,但要在处于Long Mode的64位操作系统上运行,就必须重新编译了。

    什么时候是长模式?

  • 如果是64位操作系统,引导加载程序会在保护模式下加载内核,然后内核会切换到长模式。

兼容模式(Compatibility mode)

  • CPU兼容模式:是长模式(Long Mode)的一部分,在x86-64架构的处理器中,兼容模式允许64位处理器运行32位或16位的操作系统和应用程序。不细说了

    总结

    实模式:BIOS初始化和POST、引导加载程序的第一阶段。 保护模式:引导加载程序的第二阶段、加载操作系统内核及其后的操作。 长模式:如果是64位操作系统(前提是硬件支持64bits),引导加载程序会在保护模式下加载内核,然后内核会切换到长模式。 兼容模式:是长模式(Long Mode)的一部分,但它限制了处理器的功能,使其能够运行旧的32位或16位代码。

什么是内核态和用户态?

用户态向内核态切换需要完成什么样的工作?

用户态向内核态的切换的触发有三种事件:系统调用、硬中断、异常处理。用户态向内核态切换的前置准备工作,保存用户态上下文,也就是保存当前用户态的CPU寄存器状态,包括PC、CR3、SP等等。然后修改CPU的位模式,由用户态切换到内核态,这个过程是通过修改CPU的特权级别来实现的。同时切换到内核态堆栈,使用内核堆栈完成内核态的操作。还需要切换页表基地址寄存器CR3,将用户态的页表切换为内核态的页表。根据触发事件的类型,选择相对应的代码跳转位置,执行内核态的代码。最后,执行完内核态的代码后,恢复用户态的上下文,也就是恢复用户态的CPU寄存器状态,将CPU的特权级别切换回用户态,恢复用户态的堆栈,恢复页表基地址寄存器CR3,最后返回用户态继续执行用户态的代码。

中断

中断指的是某个地方发送中断信号给cpu,打断当前正在运行的进程,让CPU空闲出来去执行中断处理程序的过程。然后我们来接着深挖这句话。

首先是中断,中断分为硬中断和软中断。为什么要分为硬中断和软中断呢?首先中断处理程序是分有上下文的,那为什么要分上下文呢?首先我们要考虑到硬中断的特点是响应频繁,中断处理程序需要很快完成才能够cpu才能够处理的过来,不然就会出现阻塞的情况。所以我们人为将中断处理程序分为上下文,硬中断的中断处理程序是上文,只做与硬件处理相关的事情,软中断的处理程序是下文,处理复杂的耗时的。同时软中断处理程序可以被硬中断打断,保证能够及时响应硬中断。

那系统中有哪些硬中断和软中断呢?

硬中断:

通常与系统的硬件设备相关;

  • 系统时钟:时钟中断;
  • IO设备:IO中断完成任务后产生的,通知CPU处理程序;
  • 网络设备:网络设备在收到数据包、完成传输时产生的
  • 错误:错误中断,硬件在检测到错误时触发的
  • 电源:电源管理中断

软中断:软中断一共有10个类型

  • HI /
  • TIMER / 定时中断
  • NET_TX / 网络发送中断
  • NET_RX / 网络接收中断
  • BLOCK /
  • BLOCK /
  • TASKLET
  • SCHED / 调度中断
  • HRTIMER
  • RCU / RCU锁中断

你知道哪些POSIX系统调用?系统调用的开销?

可以介绍一下网络的系统调用。

系统调用的开销:

这个问题本质上是问你用户是如何进入内核态的?因为系统调用会触发用户态向内核态的切换。下面详细介绍一下用户态和内核态。

在讲解用户态和内核态之前,来讲讲用户空间和内核空间。在32位系统中,寻址空间一般为4G,分为内核地址空间和用户地址空间。内核地址空间一般是1G,用户地址空间一般是3G。为什么需要划分内核和用户的概念呢,因为计算机的资源不能全部暴露给用户,需要内核这个拥有全部权限的角色去负责管理和分配资源。同时CPU指令中有些指令是很危险的,如果错用会导致系统崩溃,所以也需要内核去负责使用,用户只能使用非特权指令。划分地址空间保证两个不会相互干扰。

分好地址空间后,自然就形成了内核态和用户态。当程序运行在内核地址空间时,就是处于内核态;当程序运行在用户地址空间时,就是处于用户态。

那么用户态如何进入内核空间呢?

首先当用户调用系统调用时,会向CPU发送系统调用中断信号,CPU首先在中断向量表中查找对应的中断处理程序的入口地址; 切换CPU模式修改CPU特权级为0。保存用户态上下文,保存用户态的当前指令地址进入内核栈,CPU寄存器(通用寄存器、标志寄存器等)到内核栈,切换栈指针到内核栈。每个进程在内核中都有独立的内核栈,用于保存上下文和执行内核代码。到此切换完成,可以开始执行内核代码。

LINUX的内存管理模型?

fork!

介绍一下fork?

fork后父子进程的内存大小是两倍吗?

父子进程复用什么资源?

This post is licensed under CC BY 4.0 by the author.