0%

《操作系统》操作系统原理

CPU 都提供了多个特权级别,比如 Intel x86 架构的 CPU 一共有 0~3 四个特权级,0 级最高(可以访问所有资源),3 级最低(只能访问受限资源),操作系统内核代码运行在 0 级,用户进程运行在 3 级

CPU 刚启动时处于 0 级,开始执行操作系统内核程序(会启用内存保护机制,每个用户态进程启动时会分配虚拟内存,进程修改 CS 寄存器也只能指向自己的虚拟内存地址,无法达到执行其他代码的目的,错误指向会直接导致进程奔溃),内核启动完后主动将 CPU 级别降到 3 级,后面的进程就全部处于 3 级权限

谁能首先获得 CPU 的控制权,谁就可以变成操作系统,因为它可以把该堵的门都堵了,让后来者进不来

用户态进程只能通过系统调用中断进入到内核态执行有限操作(这些系统调用只是操作系统给的有限的一些系统调用)

操作系统掌管 CPU、内存等硬件资源,抽象出进程、线程、中断、用户态、内核态、进程上下文切换、系统调用等等概念

可以认为全部 CPU 核都一直是为操作系统服务的,操作系统负责线程的调度。好比 Golang 中某单一线程都是为 g0 服务的,g0 负责 g 的调度

Linux 架构图

进程

进程是由操作系统内核管理的,进程则是资源拥有的基本单位

线程

进程是由操作系统内核调度的,是操作系统进行资源调度的最小单元

一个进程下的所有线程共享这个进程的所有资源(虚拟内存、全局变量等等)

每个线程都有自己的栈空间

中断

int 中断指令是汇编中的指令,由用户进程调用,比如 int n(n 是中断号) 指令,编译后变成二进制码,或者硬件直接给 CPU 中断引脚发信号(比如键盘按键)

都会让 CPU 执行以下操作

  1. 标志寄存器入栈(PUSH 指令,CPU 通过总线直接操作内存),IF=0,TF=0
  2. CS、IP 入栈
  3. (IP)=(n*4),(CS)=(n*4+2), 从而执行此处的指令。此处的指令则是操作系统启动后放入此处的中断处理函数,每个地址会放入 n 对应的处理函数

中断指令是由 CPU 直接支持的,用户进程可以直接触发中断,CPU 收到中断指令就会现保存寄存器上下文,然后立马跑去执行中断处理函数

而中断处理函数是操作系统安装的,所以任何中断都能被操作系统拦截并处理,操作系统可以通过进程发起中断时传递的参数(就是中断号)来决定如何处理,操作系统根据中断号将中断分成了软件中断和硬件中断,中断号 80 被当成系统调用中断给用户态进程提供函数调用

下面是中断号对应的用途表:

向量范围 用途
019(0x00x13) 非屏蔽中断和异常
2131(0x140x1f) Intel 保留
32127(0x200x7f) 外部中断(IRQ)
128(0x80) 系统调用
129238(0x810xee) 外部中断(IRQ)
239(0xef) 本地 APIC 时钟中断(重点)
240250(0xf00xfa) 由 Linux 留做将来使用
251255(0xfb0xff) 处理器间中断(必须是SMP机器)

软件中断

软件中断又称内部中断,是指用户进程调用中断指令所引起的中断

硬件中断

硬件中断又称外部中断,是指外设调用中断指令所引起的中断,操作系统可以根据中断号区分是哪种外设,比如网卡收到数据就会调用中断指令通知内核

用户态、内核态

CPU 处于 0 级执行(或者说 CPU 在执行内核代码)时处于内核态,处于 3 级执行(或者说 CPU 在执行非内核代码)时处于用户态

进程上下文切换

内核会负责进程的上下文切换,进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。

内核将进程 A 切换到进程 B 运行前,需要先保存 A 的所有上下文,然后加载进程 B 的所有上下文,进而执行进程 B(整个过程由内核在内核态进行)

进程上下文切换是非常耗时的操作,资源的保存与恢复还有每个线程的上下文保存与恢复

发生进程上下文切换的场景:

  1. 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待 CPU 的进程运行。
  2. 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行。
  3. 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度。
  4. 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行
  5. 发生硬件中断时,进程会被操作系统内核无条件挂起,转而执行内核中的中断处理函数。

线程上下文切换

进程上下文切换只是资源的切换,而任务调度则是针对线程的,进程只是给线程提供了虚拟内存、全局变量等资源

相比进程上下文切换,线程上下文切换无需进行资源的切换,所以相对更加高效

系统调用

用户进程虽然不能直接操作硬件资源,但是操作系统内核提供了一些中断指令(例如 Linux 的 int 80h 中断,专门给用户进程进行系统调用)

可以通过中断指令让 CPU 切换到内核态执行操作系统内核的中断处理函数(每个类型的中断对应一个处理函数,对应关系称为中断向量表,由内核维护),从而达到操作硬件资源的目的,这里的 80h 中断指令是软件中断指令

一次系统调用会触发两次 CPU 上下文切换(用户态-内核态-用户态)

系统调用只会在进程中发生,进程、线程切换过程不会发生

除了 int 80h 可以触发系统调用以外,还可以使用 SYSENTERSYSCALL 指令

他们是专门为系统调用设计的汇编指令,它们不需要在中断描述表中查找系统调用对应的执行过程,也不需要保存堆栈和返回地址等信息,所以能够减少所需要的额外开销。

系统调用是用户态调用内核态的函数,相对用户态调用用户态、内核态调用内核态,速度慢了几十倍,因为涉及到了用户态与内核态之间的切换

所以出现了 vDSO

int 80h 中断过程

下面一段是用户进程的汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.section .data

msg:
.ascii "Hello, World!\n"

.section .text
.globl _start

_start:
# write 的第 3个参数 count: 14
movl $14, %edx
# write 的第 2 个参数 buffer: "Hello, World!\n"
movl $msg, %ecx
# write 的第 1 个参数 fd: 1
movl $1, %ebx
# write 系统调用本身的数字标识也就是系统调用号:4
movl $4, %eax
# 执行系统调用: write(fd, buffer, count)
int $0x80

# status: 0
movl $0, %ebx
# 函数: exit
movl $1, %eax
# system call: exit(status)
int $0x80
  1. 用户进程执行到 int $0x80 时,转入内核态执行操作系统设定的 80 号中断对应的中断处理函数
  2. 中断处理函数取出 eax 寄存器数值,得到系统调用号
  3. 接着执行系统调用号对应的系统调用函数
  4. 执行完后返回中断处理函数
  5. 接着返回用户态执行用户进程

vDSO

vDSO 全称是虚拟动态链接对象(Virtual Dynamically Shared Object、vDSO)

是 Linux 内核对用户空间暴露内核空间部分函数的一种机制

简单来说,我们将 Linux 内核中不涉及安全的系统调用直接映射到用户空间

这样用户空间中的应用程序在调用这些函数时就不需要切换到内核态以减少性能上的损失

vDSO 使用了标准的链接和加载技术,作为一个动态链接库,它由 Linux 内核提供并映射到每一个正在执行的进程中

我们可以使用 ldd /bin/catcat /proc/self/maps 命令查看该动态链接库在进程中的位置

vDSO 可以为用户程序提供虚拟的系统调用,它会使用内核提供的数据在用户态模拟系统调用

好比两个微服务合并成一个微服务,是他们之间的调用更快




微信关注我,及时接收最新技术文章