187 lines
9.0 KiB
Markdown
187 lines
9.0 KiB
Markdown
# Four Fundamental OS Concepts
|
||
|
||
## Complexity 复杂性
|
||
|
||
操作系统的硬件非常复杂。如果没有操作系统,用户必须直接与硬件交互,这将非常困难。操作系统通过提供抽象来隐藏硬件的复杂性,使用户能够更容易地使用计算机。
|
||
|
||
这也就意味着,如果处理不当:
|
||
|
||
- 复杂性会“泄露”出来。例如一些第三方驱动程序导致系统崩溃,或者某些功能无法正常工作。
|
||
- 安全漏洞。例如2017年,Intel的CPU存在一个名为Meltdown的漏洞。攻击者甚至可以访问kernel模式的内存!
|
||
- 不同版本的库会导致应用程序可能出问题。(现在我们有了Docker)
|
||
|
||
一般来说,操作系统进行的"抽象"包括:
|
||
|
||
- Processor -> Thread
|
||
- Memory -> Address Space
|
||
- Disks, SSDs, ... -> Files
|
||
- Networks -> Sockets
|
||
- Machines -> Processes
|
||
|
||
## Thread of Control
|
||
|
||
Thread 是一个独一无二的context, 其包括程序计数器、寄存器、堆栈、CPU标志位、内存等等。
|
||
|
||
### Resident / Running 驻留(运行态)
|
||
|
||
当一个线程处于Resident状态时,表示该线程正在被CPU执行。
|
||
|
||
Resident指: 寄存器当前保存了该线程的state (content)。
|
||
|
||
- 包括程序计数器(PC, program counter)、当前执行的指令的地址
|
||
- PC指向内存中下一条指令的地址
|
||
- 包括程序当前正在计算的数据
|
||
- 栈指针
|
||
- 剩下的、内存里的数据
|
||
|
||
### Suspended / Ready 挂起(就绪态)
|
||
|
||
当一个线程处于Suspended状态时,表示该线程已经准备好运行,但由于某些原因(如等待资源、等待I/O操作完成等)而暂时未执行。
|
||
|
||
与Resident状态具有很多相反的状态:
|
||
|
||
- CPU的state不再是该线程的content,而是另一个线程的content
|
||
- PC指向另一个线程的指令地址
|
||
- 大多数情况下,线程的content被保存在内存中,而不是寄存器中
|
||
|
||
### Content Switch 上下文切换
|
||
|
||
#### 使用场景
|
||
|
||
现代的操作系统通常支持多线程。电脑上可以同时运行很多线程,但单个CPU核心一次只能执行一个线程。当操作系统需要切换正在运行的线程时,就会发生上下文切换。
|
||
|
||
以图中为例:
|
||

|
||
|
||
- T1时刻,vCPU1在CPU核心上,vCPU2在内存中
|
||
- T2时刻,vCPU1在内存中,vCPU2在CPU核心上
|
||
|
||
在T1和T2之间,操作系统进行了一次**context switch**,也就是上下文切换:
|
||
|
||
- 将vCPU1的状态保存到**Thread Control Block** (TCB, 位于内存) 中。
|
||
- 将vCPU2的状态从内存加载到CPU核心上
|
||
- 其他操作.....
|
||
|
||
上下文切换不是毫无开销的。一般来讲,它会耗费几微秒的时间。因此,操作系统会尽量减少上下文切换的次数,并使用各种手段减少上下文切换的开销,以提高系统性能。
|
||
|
||
有多种条件可以出发一次Content Switch: 计时器、I/O事件、系统调用、线程优先级变化等等。
|
||
|
||
#### Thread Control Block (TCB)
|
||
|
||
TCB是一些数据,保存了线程的状态信息,包括寄存器值、堆栈指针、程序计数器、PC等。
|
||
|
||
通常来讲,TCB被存储在内存的kernel空间中。
|
||
|
||
#### Address Space 地址空间
|
||
|
||

|
||
|
||
## Reliability 可靠性
|
||
|
||
简单的上下文切换不提供操作系统保护,而我们显然不会希望一个user program崩掉整个系统!因此,操作系统需要提供一些保护机制。
|
||
|
||
- 可靠性:破坏操作系统通常会导致其崩溃
|
||
- 安全性:限制线程的操作范围
|
||
- 隐私性:限制每个线程仅能访问其被允许访问的数据
|
||
- 公平性:每个线程应限制在其应得的系统资源份额内(CPU时间、内存、I/O等)
|
||
|
||
仅靠软件是不足以更好的保护系统的,因此我们有了一些硬件层面的保护机制:
|
||
|
||
### Base and Bound (B&B) 基址寄存器、边界寄存器
|
||
|
||

|
||
如图所示,程序地址“看起来”位于 0 ~ 100 之间;但,当它被加载到内存内时,它被重新定位到 1000 ~ 1100 之间。
|
||
|
||
这是一种基于编译器、加载器的保护机制。它把操作系统和用户程序隔离,保护操作系统。
|
||
|
||
我们也可以通过添加一个加法器来实现更高效的B&B机制:
|
||

|
||
这是一种硬件辅助的内存重定向。
|
||
|
||
B&B机制有很多缺点。最显著的缺点之一是,它无法简单地处理用户程序allocate或者free内存的情况。由此,我们有了一个更复杂的机制:
|
||
|
||
### Address Space Translation 地址空间转换
|
||
|
||
我们可以通过增添一个特定的“小盒子”,来实现更高效的地址空间转换:
|
||

|
||
这个“小盒子”被称为**Memory Management Unit** (MMU),它可以将虚拟地址转换为物理地址。
|
||
|
||
这个解决方案很好地解决了B&B机制的缺点。用户程序可以allocate或者free内存,而不需要担心地址空间的重叠问题。其中,有一个至今仍然在使用的机制: **Paging** (分页机制)。
|
||
|
||
### Paging 分页机制
|
||
|
||
分页机制将内存划分为固定大小的页(通常是4KB)。每个页都有一个对应的页表项,记录了该页在物理内存中的位置。当程序访问一个虚拟地址时,MMU会查找对应的页表项,将虚拟地址转换为物理地址。
|
||

|
||
|
||
我们还可以实现一点更“酷”的东西。为了节约内存,我们甚至可以让一些Page实际上不在内存中!对这些page的访问会触发一个**page fault**,程序中断后,操作系统将所需的page从磁盘加载到内存中,然后继续执行程序。
|
||
|
||
## Processes 进程
|
||
|
||
进程是具有受限权限的执行环境。
|
||
|
||
- (受保护的)具有一个或多个线程的地址空间
|
||
- 拥有内存(地址空间)
|
||
- 拥有文件描述符、文件系统上下文...
|
||
- 封装一个或多个共享进程资源的线程
|
||
|
||
复杂的应用程序可以 fork/exec(创建/执行)子进程。
|
||
|
||
某种意义上来说,进程是一些“城市”,而操作系统是管理他们的“政府”。
|
||
|
||
- 每个城市都有自己的居民(线程),有自己的资源(内存、文件描述符等)。程序假设它们是独立的,不会相互干扰。
|
||
- 政府负责管理这些城市,确保城市A出问题不会波及到城市B、审查城市AB之间的通信、分配硬件资源给城市, etc.
|
||
|
||
### 为什么需要进程?
|
||
|
||
- 它让不同程序、组件之间相互隔离,不会一个崩溃连带其他崩溃。
|
||
- 操作系统免受它们的影响
|
||
- 提供内存保护
|
||
|
||
### 保护与效率之间的基本权衡
|
||
|
||
- 进程**内**通信更容易。
|
||
- 线程之间可以直接访问共享内存
|
||
- 进程**间**通信更难
|
||
- 需要使用IPC机制(如管道、消息队列、共享内存等)来进行通信
|
||
|
||
### 单线程与多线程
|
||
|
||

|
||
可以从途中看到,各个线程**共享资源**(同一个地址空间、文件描述符等),但每个线程都有**自己的程序计数器、寄存器、堆栈**等。
|
||
|
||
## privilege levels 权限级别
|
||
|
||
上边我们提到了页表。那如果,应用A想要把页的映射地址改到一个它不应该访问的地址上怎么办?我们有了权限级别来解决这个问题。
|
||
|
||
### Dual Mode Operation 双模式操作
|
||
|
||
CPU至少会提供以下两个模式:
|
||
|
||
- User mode (Ring 3) 用户模式:应用程序运行在这个模式下,权限受限,无法直接访问硬件资源。
|
||
- Kernel mode (Ring 0) 内核模式:操作系统运行在这个模式下,拥有完全的权限。
|
||
|
||
由此,我们解决了问题:当操作系统让其他应用程序运行时,它会将CPU切换到User mode。这样子,应用A就不能直接修改页表了。
|
||
|
||
如果应用程序需要从硬盘读取数据,它会发出一个系统调用(system call),请求操作系统执行这个操作。操作系统会将CPU切换到Kernel mode,执行相应的操作,然后再切换回User mode。
|
||
|
||
由此,我们得出了经典的UNIX系统架构:
|
||

|
||
|
||
### How to switch? 如何切换当前模式?
|
||
|
||
从用户态到内核态:
|
||
|
||
- ```System call``` 系统调用
|
||
- 定义: 进程请求系统服务,例如exit
|
||
- 系统调用类似于函数调用
|
||
- 没有要调用的系统函数的地址
|
||
- 类似于远程过程调用 (RPC)
|
||
- 将系统调用 ID 和参数整理到寄存器中,并执行系统调用
|
||
- ```Interrupt``` 中断
|
||
- 某些原因触发上下文切换
|
||
- 例如:Timer 、I/O 设备
|
||
- ```Trap``` 陷阱或```Exception``` 异常
|
||
- 进程内部同步事件触发上下文切换
|
||
- 例如:```Protection Violation - Segmentation Fault```(段错误)、除以零, etc.
|
||
|
||
在一次,例如```Interrupt```发生后,CPU会把当前正在运行的程序保存在```Thread Control Block (TCB)```中。TCB中保存了程序计数器、寄存器值、堆栈指针等,并存储在Kernal memory中。 |