CS162操作系统课程第二课-4个核心OS概念
熟肉视频地址:
我们讨论了操作系统如何扮演裁判,魔术师和粘合剂的角色,裁判是指对于资源保护的管理;魔术师是指我们要让它看起来像我们有一套非常干净易用的资源的抽象,而不是使用实际的没有统一接口的物理资源。粘合剂是一组通用服务,它们使在操作系统上编写程序变得更容易,例如文件系统服务、网络服务等等。
今天我们要讲操作系统四个基本概念: 



- 包括一个程序计数器,当前正在执行的指令,程序计数器指向内存中的下一条指令,所有的指令都存储在内存中
- 包括用于进行计算的中间值
- 有一个栈指针,它有一个指向内存中栈的顶部的指针,线程的其余部分都在内存中。 当线程的状态不在寄存器中即不是常驻的时候,线程就被挂起(suspended)或不再执行。被挂起的线程实际上是在内存中,还没有执行或者根本没有执行,有其他线程正在执行。
这是另一个关于执行过程的视图(冯·诺依曼体系) 
那么我们是如何产生多处理器的错觉的呢?我们上次讲过在你的笔记本电脑上做一个ps -aux或者其他的操作唤起任务管理器,如果你仔细看,你会发现有数百个进程在运行,大部分在休眠状态。但它们都可以在你当前的处理器上使用,那么它是如何工作的呢? 
我们举一个例子: 
一些 Q&A:
- 每个线程是否都有自己独占的 CPU 缓存?答案是否定的,一般每个核有一个高速缓存,线程都共享同一个缓存,你可以想到如果切换太快,就没有线程能利用好缓存了。原始处理器中的缓存或 TLB 必须在切换时被刷新,更高级的处理器则不会。缓存本身通常在物理空间中,你从一个线程切换到另一个,你只是改变了页表,不需要清空缓存。
- 线程上下文切换这需要多长时间?这大概需要几微秒的时间,需要确保转换的时间不要太长,不要把大部分时间都花在转换上。
然后是什么触发了上下文切换?全局倒计时器时间到了,或自愿让出CPU等这样的事情触发的。比如某个线程要做一些 I/O,那么操作系统就会调度其他线程。
我们可能有一堆内存(蓝色的代表内存),我们可以想象这些虚拟进程中的每一线程都有自己的栈、堆、数据和代码,它们都以某种方式分布在内存中,我们要做的就是以某种方式记录所有东西的位置。线程控制块是所有东西的所在,当我们从绿色切换到黄色,我们要做的第一件事是将所有的绿色线程的寄存器保存到它的线程控制块中,顺便说一下,那是内核内存的一部分。
我们今天要讲的第二个操作系统概念是地址空间: 
- 程序计数器(PC,Program Counter)指向某个地址,意味着处理器能执行在那个地址的指令。
- 栈指针(SP,Stack Pointer)指向某个地址,通常是栈的底部(图里面栈向下增长,所以最后一个元素是栈底)
- 栈(stack):栈就是当你递归调用一个函数时,前一个函数的变量会被推到栈上,然后栈指针向下移动,当函数返回时,你把它们从栈中取出,栈指针向上移动。
- 堆(heap):你用 malloc 分配东西等的时候,通常就会放在堆上。堆的初始物理内存也比程序最终需要的要少,随着程序的增长,这会在堆上分配东西,你会遇到页面错误,然后分配实际的物理内存。
- 代码段(Code Segment):保存着要执行的代码。
- 静态数据(Static Data):静态变量,全局变量等等

- 可靠性:危及操作系统通常会导致系统崩溃
- 安全性:你想要限制恶意软件的作用范围
- 隐私:限制每个线程访问它应该访问的数据,不希望我的密码或者秘密被泄露
- 公平性:我不希望这样一个线程,例如它计算 PI 的最后一位,突然就能占用所有的cpu,以牺牲其他所有的线程为代价。
操作系统必须保护用户程序之间的安全,防止一个用户拥有的线程影响另一个用户拥有的线程。那么硬件能做些什么来帮助操作系统保护自己免受程序的侵害呢,这里有一个非常简单的想法,事实上非常简单,以至于微小的物联网设备可以用很少的晶体管做到这一点,这个概念我称之为基础和界限(B&B,base and bound):
我们要做的是我们有两个寄存器,一个base寄存器和一个bound寄存器,这两个寄存器记录黄色线程允许访问内存的哪一部分。程序运行的时候,磁盘上的文件被加载并移动到内存的这一部分。所以现在当程序开始执行的时候,它与程序计数器一起工作,比如在1010范围内,这就是代码所在的地方。硬件会做一个快速比较看看这个程序的计数器是否大于 base,以及它是否小于 bound。
这种方式实现很简单,但是访问里面的每一块内容,都要记录一个长地址。 但是对于这个进程分配的内存,在这个模型下是很难改变的。因为有多个进程在共享这个内存,所以当你想扩展内存的时候,可能需要将当前黄色的部分复制到内存中其他剩余空间更大的一块地方。 为了优化这些问题,我们一般不直接访问物理内存,而是加上地址空间翻译机制(Address Space Translation):
其中一种翻译设计是加入一个硬件加法器: 
另一种方法是利用分段,在x86硬件中,我们有代码段,堆栈段等等各种段,每段有不同的基址与长度,即不同的 base 和 bound,即硬件寄存器,有 base 和 bound 硬编码在该段。代码段是有物理起点(即 base)和长度(即 bound)的,而实际运行的指令指针是段内的偏移量。
最后一种是我们实际中更常用的,我们要做的是,我们要把地址空间,也就是所有的DRAM,分成一堆大小相等的页(Page)。硬件会使用页表(Page Table)进行从虚拟内存地址到硬件 DRAM 内存地址的转换。
我们今天要讲的第三个操作系统概念是进程: 
进程提供了内存保护抽象,在保护和效率之间有一个基本的权衡,如果你在同一个进程中有一堆线程,它们之间可以很容易地通信,因为它们共享相同的内存,它们可以通过一个写入内存,另一个读取内存来通信,但是它们之间可能会互相覆盖导致并发安全问题。有时候你想要高性能,提高并行性,你会想要在一个进程中有很多线程。但是当你想要保护时,你想要限制进程之间的通信,所以进程之间的通信故意变得更加困难,这就是我们得到保护的方式。

线程封装了并发性,为什么进程要用多线程?
- 一个是并行性(Parallelism)。如果你有多个核,通过在同一个进程中有多个线程,可以让许多任务同时处理。
- 另一个是为了并发(Concurrency)。并发性就是大多数线程大部分时间都在休眠的情况,比如某个线程需要做一些 I/O,开始 I/O 时进入睡眠,然后在 I/O 完成时醒来,那么 CPU 不必等待 I/O 完成,而是可以做其他的事情。这就是多线程的好处。
那么为什么我们需要进程来保证可靠性、安全性和隐私性呢?
- 对于可靠性:Bug 只会覆盖一个进程的内存,恶意的或者被破坏的进程不能干扰其他进程。
- 对于安全还有隐私性:进程不能修改其他进程的内存
- 公平性:共享磁盘,CPU 等资源。
这个保护主要通过翻译机制实现的,每个进程地址空间通过翻译机制映射到物理内存,这个翻译是进程本身不可控的。那么操作系统是如何保证进程无法修改页表从而影响翻译呢?这就引入了下一个话题:双模式操作(Dual-mode Operation)

硬件至少提供了两种模式:内核模式(Kernel mode,或管理模式 Supervisor mode)和用户模式(User mode)。当你在用户模式下运行时某些操作会被禁止,比如当你在用户模式时你不能改变你使用的页表,这只有在内核模式下的操作系统才能做到。在用户模式下还不能禁止中断,这样,一个如果想计算PI最后一位的进程就不能阻止其他进程在计时器结束时获得CPU时间。在用户模式下你也被阻止直接与硬件交互等等,因此不能破坏磁盘上的文件。
我们小心控制的用户模式和内核模式之间的转换的是什么?包括系统调用(System call),中断,异常等等。如上图中的流程所示,我们有用户进程,它们对内核进行系统调用,从用户模式切换到内核模式执行系统调用中对应的操作,完成后退出内核模式,系统调用返回。

- 用户模式包含你所有的程序和库等等。
- 系统调用:表示可以安全访问各种资源的代码。
- 内核模式包含:信号处理,I/O 系统,文件系统,块交换 I/O 系统,磁盘驱动,CPU 调度,页交换,虚拟内存管理等等。内核通过接口访问并控制硬件。

总共有三种可能触发用户模式到内核模式转换的操作类型:
- 系统调用:
- 调用一个系统服务,例如 exit 退出进程
- 函数调用,但是涉及访问进程外的一些资源
- 目前没有系统函数需要的内存地址
- RPC 远程函数调用
- Marshall 寄存器中的系统调用id和参数并执行系统调用
- 中断:
- 外部异步事件触发上下文切换,例如,定时器,I/O设备
- Trap 或者异常:
- 内部同步事件触发上下文切换
- 例如,违反保护(segmentation fault),除以零,…
如果你注意到这里有两个进程,一个绿色的,一个黄色的。灰色的代表的是操作系统的内存。
微信搜索“干货满满张哈希”关注公众号,加作者微信,每日一刷,轻松提升技术,斩获各种offer:
我会经常发一些很好的各种框架的官方社区的新闻视频资料并加上个人翻译字幕到如下地址(也包括上面的公众号),欢迎关注:










我会经常发一些很好的各种框架的官方社区的新闻视频资料并加上个人翻译字幕到如下地址(也包括上面的公众号),欢迎关注: