熟肉视频地址:CS162操作系统课程2-4个核心操作系统概念(上)CS162操作系统课程2-4个核心操作系统概念(下)我们讨论了如何操作系统扮演着裁判、魔术师和胶水的角色。Referee指的是资源保护的管理;魔术师意味着我们想让它看起来像我们有一个非常干净和易于使用的资源抽象,而不是使用没有统一接口的实际物理资源。Glue是一组通用服务,可以更轻松地在操作系统上编写程序,例如文件系统服务、网络服务等。今天我们来聊聊操作系统的四个基本概念:我们先从什么是线程(Thread)说起,一个线程会完整的描述一个程序状态或者一个线性的执行上下文,它会有程序计数器,寄存器,执行标志,线程堆栈等等。然后是地址空间(AddressSpace),它是一组程序可以访问的内存地址。然后我们会介绍什么是进程(Process),它包括一个地址空间和一个或多个线程。最后,我们将在课程早期讨论一种特别重要的硬件机制,称为双模式操作,即一个典型的处理器至少有两种不同的模式,我们可以粗略地称之为内核模式(Kernelmode)和用户模式(Usermode).我们要学习如何将程序编写和编译成可执行文件,然后将这些可执行文件从文件系统中取出来创建一个运行的进程,并将文件加载到内存中,我们将详细讨论堆栈和堆该过程需要提供运行。然后是TransferControl,意思是处理器的程序计数器会指向进程用户代码中的指令,然后程序就会开始执行。操作系统需要为这些进程提供各种系统服务,操作系统需要保护操作系统和进程不受其他进程和其他用户的影响。我们来回忆一下CS61B+CS61C课程的内容:一开始,处理器有一个程序计数器(PC,ProgramCounter),还有一个可以读取的内存(Memory),在里面有一组指令(Instruction)记忆。程序计数器指向内存中的指令,允许处理器获取下一条指令。我们从内存中提取指令,对其进行解码,然后将其送入执行管道。我们常说的CS61C是风险风格的处理器,有五个执行流水线。解码后,它们将输入一组寄存器和ALU以执行实际操作,然后递增程序计数器并继续执行下一条指令。我们的第一个操作系统概念是控制线程(ThreadofControl),一个线程其实就是一个唯一的执行上下文,它有程序计数器、寄存器、执行标志、线程栈、内存状态。线程就像您在上一张幻灯片中看到的处理器的虚拟版本。一个线程是在一个处理器或者一个核心上执行的,顺便说一句,我现在把处理器和核心这两个概念混为一谈,稍后我们会更清楚地理解这两个概念的区别。但只有在处理器寄存器中常驻(Resident)时才会执行。在寄存器中有一个线程的上下文(Context)或根状态(RootState),有些东西在寄存器中,其余的在内存中:包括一个程序计数器,当前正在执行的指令,程序计数器指向下一条Instructioninmemory,所有的指令都存储在内存中包括用来做计算的中间值都有一个栈指针,它有一个指针指向内存中的栈顶,线程的其余部分都在内存中。当线程的状态不在寄存器中,即不常驻时,线程被挂起(suspended)或不再执行。被挂起的线程其实在内存中,还没有执行或者根本没有执行,而其他线程正在执行。这是执行过程的另一种观点(冯·诺依曼系统)这是一组地址,后面我们称它为地址空间,从0到2^32-1。里面是一组将要执行的指令,粉色的是你的处理器。进程有与之相关的保护状态,处理器包括寄存器集合和流水线,我们的执行序列(ExecutionSequence)包括:在程序计数器上取一条指令,译码,执行,写回一个寄存器,取出下一条指令,并重复这个执行周期。那么我们是如何创造多处理器的幻觉的呢?上次我们谈到在您的笔记本电脑上执行ps-aux或其他操作以调出任务管理器,如果您仔细观察,您会看到数百个进程正在运行,大部分处于休眠状态。但是它们都可以在您当前的处理器上使用,那么它是如何工作的呢?假设一个物理处理器上只有一个核心,并且在任何给定时间硬件上只有一个执行线程。但是现在我们想要的是有多个CPU,或者多个CPU同时运行的错觉,这样我们就可以有多个线程同时运行。从程序员的角度来看,我有一堆东西在运行,它们都共享内存。每个线程都是一个虚拟核心,我们这里有三个线程,洋红色线程,青色线程和黄色线程,它们创造同时运行的错觉的方式是我们多路复用硬件,我们运行洋红色线程一会儿,再把青色的线跑一会儿,再把黄色的线跑一会儿,如此循环。线程的内容是什么?显然,每个线程都需要一个程序计数器和一个堆指针,以及所有寄存器状态。如果线程正在运行,则寄存器状态在处理器的寄存器中;如果没有运行,这些状态都在内存中,称为线程控制块(TCB,ThreadControlBlock)。举个例子:在T1时vCPU1在运行,而在T2时vCPU2在运行。在T1和T2,发生上下文切换(ContextSwitch)。底部是一个事件。我们将所有程序计数器、堆栈指针和所有其他寄存器保存在内存中。在相应的线程控制块中。我们从vcpu2中加载程序计数器、栈指针等,然后运行vcpu2。一些问答:每个线程都有自己独占的CPU缓存吗?答案是不。一般每个核心都有一个缓存,线程共享同一个缓存。你可以想象,如果切换太快,没有线程可以很好地利用缓存。原始处理器中的缓存或TLB必须在切换时刷新,更高级的处理器则不需要。缓存本身通常在物理空间中,你从一个线程切换到另一个线程,你只是改变了页表,你不需要清空缓存。线程上下文切换需要多长时间?这可能需要几微秒,因此请确保转换不会花费太长时间并且不会花费大部分时间进行转换。那么是什么触发了上下文切换呢?全局倒数计时器到时,或者主动让出CPU等事情触发。例如,如果一个线程需要做一些I/O,操作系统会调度其他线程。我们可能有一堆内存(蓝色的代表内存),我们可以想象这些虚拟进程中的每个线程都有自己的栈、堆、数据和代码,它们都以某种方式分布在内存中,我们有要做的事情就是以某种方式记录所有内容的位置。线程控制块是一切所在,当我们从绿色切换到黄色时,我们做的第一件事就是将所有绿色线程的寄存器保存到它的线程控制块中,顺便说一下,这是内核内存的一部分。我们今天要介绍的第二个操作系统概念是地址空间:它是一组可访问地址和与它们关联的状态,这只是处理器对可用地址的看法。对于32位处理器来说,可以访问的地址空间是2的32次方,也就是40亿左右,对于64位处理器来说,2的64次方是18万亿。但这并不意味着所有空间都有对应的物理DRAM内存可用,只是其中一些空间有对应的物理DRAM。当您在地址空间中读取或写入地址时,它可能表现得像普通内存,或者完全忽略写入操作;或者可能是系统导致了一个I/O操作发生,这被称为内存映射I/O;或者它可能会导致异常,如果您尝试读取或写入堆栈和堆之间的某处,就像我在此处显示的那样没问题,但如果没有分配给进程的物理内存,则会出现页面错误;或者写入内存的行为是为了与另一个程序通信。程序计数器(PC,ProgramCounter)指向一个地址,这意味着处理器可以执行该地址处的指令。栈指针(SP,StackPointer)指向一个地址,通常是栈底(图中栈是向下增长的,所以最后一个元素就是栈底)栈(stack):调用函数时递归地,将前一个A函数的变量压入栈中,然后栈指针向下移动,当函数返回时,你将它们弹出栈,栈指针向上移动。堆:当您使用malloc分配东西等时,它通常会在堆上进行。堆最初的物理内存也比程序最终需要的少,随着程序的增长,这会在堆上分配东西,出现页面错误,然后分配实际的物理内存。代码段:存放要执行的代码。StaticData:静态变量,全局变量等。操作系统必须保护自己不受用户程序的影响,这样做的原因有很多:可靠性:破坏操作系统通常会导致系统崩溃安全性:你想限制范围恶意软件隐私:限制每个线程访问它应该访问的数据,不希望我的密码或秘密被泄露公平:我不希望一个线程,例如计算PI的最后一位,突然接管所有CPU,以所有其他线程为代价。操作系统必须在用户程序之间提供安全性,防止一个用户拥有的线程影响另一个用户拥有的线程。那么硬件可以做些什么来帮助操作系统保护自己不受程序的影响,这是一个非常简单的想法,事实上非常简单,微型物联网设备可以用很少的晶体管来做到这一点,这个概念我称之为基础和界限(B&B,基础和界限):我们做的是我们有两个寄存器,一个基址寄存器和一个绑定寄存器,这两个寄存器记录了黄色线程允许访问内存的哪一部分。当程序运行时,磁盘上的文件被加载并移动到这部分内存中。所以现在当程序开始执行时,它与程序计数器一起工作,比如在1010范围内,这就是代码所在的位置。硬件将进行快速比较,以查看程序的计数器是否大于base,以及是否小于bound。这个方法实现起来很简单,但是要访问里面的每一条内容,都要记录一个长长的地址。但是对于这个进程分配的内存,在这个模型下是很难改变的。因为有多个进程共享这块内存,所以当你想扩展内存时,可能需要将当前黄色部分复制到内存中另一个剩余空间更大的地方。为了优化这些问题,我们一般不直接访问物理内存,而是加入地址空间翻译机制(AddressSpaceTranslation):其中一种翻译设计是加入硬件加法器:地址实际上是动态转换的,并且all地址其实就是一个偏移量。操作系统记录每个进程的内存基地址(BaseAddress),然后将这个偏移量加上实际的物理内存地址。另一种方法是使用分段。在x86硬件中,我们有代码段、堆栈段等各种段,每个段都有不同的基地址和长度,也就是不同的base和bound,也就是硬件寄存器,还有base和bound是硬编码的在这个部分。代码段有一个物理起点(即base)和一个长度(即bound),实际运行的指令指针是段内的偏移量。最后一种是我们实践中比较常用的。我们要做的是把地址空间,也就是所有的DRAM,分成一堆大小相等的页(Page)。硬件会使用页表(PageTable)将虚拟内存地址转换为硬件DRAM内存地址。今天要讲的第三个操作系统概念就是进程:进程其实就是一个受限的执行环境。我们说过,简单的虚拟线程存在每个线程都可以访问每个线程的内存的问题,内存翻译机制保护了我们可以访问的内存,也就是一个受保护的内存块。它由操作系统中称为进程的实体专有。它由一个受限的地址空间和一个或多个线程组成,线程拥有一些文件描述符和文件系统上下文。进程提供了一个内存保护抽象,保护和效率之间存在一个基本的权衡,如果你在同一个进程中有一堆线程,它们可以很容易地相互通信,因为它们共享相同的内存,它们可以被访问通过一个写内存,一个读内存进行通信,但是它们可能会互相覆盖,造成并发安全问题。有时你想要高性能,为了增加并行性,你会想要在一个进程中有很多线程。但是当你想要保护的时候,你想要限制进程之间的通信,所以故意让进程之间的通信变得更加困难,这就是我们获得保护的方式。这是一个单线程进程和一个多线程进程。对于单线程进程,只有一组寄存器和栈内存。对于多线程进程,代码段、数据和文件是共享的,但每个线程都有独立的寄存器和堆栈。当我们从一个线程切换到另一个线程时,为了产生多处理的错觉,我们需要从第一个线程切换出寄存器,以便我们可以从第二个线程将它们加载回来。线程封装并发,进程为什么要用多线程?一种是并行性(Parallelism)。如果你有多个核心,通过在同一个进程中有多个线程,你可以同时处理许多任务。另一个是为了并发。并发是大多数线程大部分时间都在休眠的情况,比如一个线程需要做一些I/O,开始I/O就进入休眠状态,I/O完成后再醒来,那么CPU不必等待I/OODone,而是可以做其他事情。这就是多线程的好处。那么,为什么我们需要可靠性、安全性和隐私的流程呢?对于可靠性:错误只会覆盖一个进程的内存,恶意或损坏的进程不能干扰其他进程。为了安全和隐私:进程不能修改其他进程的内存。公平性:共享磁盘、CPU等资源。这种保护主要是通过翻译机制来实现的。每个进程的地址空间通过翻译机制映射到物理内存。这种翻译不受过程本身的控制。那么操作系统如何保证进程不能修改页表从而影响翻译呢?这就引出了下一个话题:双模式操作(Dual-modeOperation)硬件至少提供了两种模式:内核模式(Kernelmode,或管理模式Supervisormode)和用户模式(Usermode)。当您在用户模式下运行时,某些操作是被禁止的,例如,您不能更改您在用户模式下使用的页表,这只有在内核模式下的操作系统才有可能。在用户模式下还不能禁用中断,因此想要计算PI最后一位的进程无法阻止其他进程在计时器到期时获得CPU时间。在用户模式下,您也无法直接与硬件等进行交互,因此不会损坏磁盘上的文件。我们精心控制的用户模式和内核模式之间的转换是什么?包括系统调用(Systemcall)、中断、异常等。如上图的流程所示,我们有用户进程对内核进行系统调用,从用户态切换到内核态执行系统调用中的相应操作,完成后退出内核态,系统调用返回。下表列出了典型的Unix系统结构中每个模式包含的内容:用户模式包含你所有的程序和库等。系统调用:代表可以安全访问各种资源的代码。内核模式包括:信号处理、I/O系统、文件系统、块交换I/O系统、磁盘驱动程序、CPU调度、分页、虚拟内存管理等。内核通过接口访问和控制硬件。例如,我们有具有内核模式和用户模式的硬件。硬件可能会执行创建一个新进程。用户态的系统调用会进入内核态,执行完操作后返回用户态。中断可能会导致用户模式进入内核,然后内核可能会检查硬件,例如I/O就绪,最后从中断中返回。当你除以零或发生页面错误时,就会发生异常,导致进入内核模式,并最终返回。一共有三种类型的操作可能会触发用户态到内核态的转换:系统调用:调用一个系统服务,比如exit退出进程函数调用,但是涉及访问当前正在做的进程之外的一些资源没有系统函数所需的内存地址RPC远程函数调用Marshall中的系统调用id和参数注册并执行系统调用中断:外部异步事件触发上下文切换,例如定时器,I/O设备Trap或异常:内部同步事件触发上下文切换,例如,违反保护(分段错误),除以零,...如果您注意到这里有两个进程,一个绿色和一个黄色。灰色代表操作系统的内存。微信搜索“满满张哈希的干货”,关注公众号,微信添加作者,天天刷,轻松提升技术,赢各种优惠:我会经常发一些官方社区各种框架的好消息视频资料及个人翻译字幕到以下地址(含上公众号),欢迎关注:知乎:https://www.zhihu.com/people/...B站:https://space.bilibili.com/31…
