这部分内容相当重要,这部分我们可以学习到应用是如何与操作系统交互的。

控制流是什么

从给处理器加电开始,直到断电为止,程序计数器(寄存器)假设一个序列的值a1–an,其中每个ak是某个相应指令Ik的地址,每次从ak到ak+1的过渡称为控制转移(control transfer),这样的控制转移序列叫做处理器的控制流(control flow)。

最简单的控制流是一个平滑的序列,每个Ik和Ik+1在存储器中都是相邻的,当这种平滑控制流发生突变Ik+1和Ik不相邻(通常由跳转、调用和返回这样一些程序指令造成),对于这种突变系统需要能够有效做出反应。

这些突变控制流(exceptional control flow,ECF)也叫做异常控制流可能发生在系统的各个层次:

  • 硬件层:硬件检测到的事件会触发控制流突变,将控制流转移到异常处理程序;
  • 操作系统层:内核通过上下文切换将控制流从一个用户进程转移到另一个用户进程;
  • 应用层:一个进程可以发送信号给到另一个进程,而接收进程也会将控制流转移到它的信号处理程序

对于整个系统来说,ECF的作用主要包括:

  • ECF是操作系统来实现I/O、进程和虚拟存储器的基本机制;
  • ECF帮助理解应用如何与操作系统交互:应用程序可以通过陷阱(trap)或者系统调用(system call)的ECF向操作系统请求服务,如磁盘读写、网络数据读取、创建或终止进程等
  • ECF帮助理解软件异常如何工作:像java,C++这种高级语言能够通过try,catch以及throw来提供软件异常机制。

异常控制流

系统中的各个层次有各种形式的ECF:

  • 异常:位于硬件与操作系统交界部分
  • 系统调用:应用程序到操作系统的交界部分
  • 非本地跳转:ECF的应用层形式

异常

异常一部分由硬件实现(一部分由硬件实现是因为具体细节可能随操作系统的不同而不同),一部分由操作系统实现。

处理器状态的突变,触发控制流从应用程序到异常处理程序的控制转移,在异常处理程序完成处理后,它将控制返回给应用程序或者终止。这里说到的突变被称为事件,它可能和当前执行的指令直接相关,例如发生虚拟内存缺页、算术移除或者除零操作,当然它也可能和当前指令没有关系,比如系统定时器产生信号或者I/O请求完成。

在处理器检测到有事件发生,它就会通过一张叫做异常表(exception table)的跳转表(异常表是在系统启动时由操作系统分配和初初始化的跳转表,它的起始地址放在一个叫做异常表基寄存器的特殊CPU寄存器里),进行一个间接过程调用,把控制流专项给专门设计用来处理这类事件的异常处理程序

在异常处理程序完成后,可能发生以下情况中的一种:

  • 处理程序将控制返回给当前的指令(当事件发生时正在执行的指令);
  • 处理程序将控制返回给下一条指令;
  • 处理程序终止被中断的程序。

异常的类别

异常可以分为四种:中断、陷阱、故障和终止。

  1. 中断:中断时异步发生的,它是来自处理器外部I/O设备的信号。硬件中断不是由任何一条专门的指令造成的,硬件中断的异常处理程序通常被称为中断处理程序。常见的中断,比如说网络适配器、磁盘控制器或者定时器芯片,通过向处理器芯片上的一个管脚发信号,并将异常号发到系统总线上来触发中断,这个异常号能够标识引起中断的设备。
  2. 陷阱:陷阱是有意的异常,是执行一条指令的结果。陷阱最重要的用途是用户程序和内核之间提供一个向过程调用的接口,叫做系统调用。它给应用程序向操作系统申请服务提供了途径。
  3. 故障:故障由错误引起,它可能被故障处理程序修正。故障发生时,处理器将控制转移给故障处理程序,如果处理程序能够修正这个错误它就将控制返回给故障指令重新执行,否则处理程序返回到内核中的abort例程,终止引起故障的应用程序。
  4. 终止:终止是不可恢复的致命错误造成的结果。典型的是一些硬件错误。终止处理程序从不将控制返回给应用程序而是将控制转移给abort例程。

进程

异常提供基本的构造块,它允许操作系统提供进程的概念。

系统中的每个程序都是运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成,包括存放在存储器中的程序的代码和数据、用户栈、它的通用目的寄存器的内容、环境变量、程序计数器和打开文件描述符集合。

进程能够给应用程序提供两个关键的抽象:

  • 一个独立的逻辑控制流:它提供一个假象——我们的程序独占的使用处理器;
  • 一个私有地址空间:它提供一个假象——我们的程序独占地使用存储器系统。

逻辑控制流

我们程序运行过程中一系列的PC(程序计数器)值构成的序列,称为逻辑控制流。这些指令包含我们程序的可执行文件中的指令和在运行时动态链接到我们程序的共享对象的指令。

一般而言,和不同进程相关的逻辑流不会影响其他进程的状态,每个逻辑流和其他逻辑流都是独立的。但是在使用进程间通信(IPC)比如管道、套接字、共享内存和信号量显示交互的时候,这个规则就会有例外。

私有地址空间

在一台n位地址的机器上,地址空间是2n个可能的地址集合,一个进程为每个程序提供它的私有地址空间。一般而言,这个空间中某个地址相关的那个存储器字节是不能被其他进程读写的,从这个意义上讲,这个地址空间是私有的。

虽然每个私有地址空间相关联的存储器内容一般是不同的,但是每个这样的地址空间内部结构都是相同的。

地址空间底部是保留给用户程序的,包括常用的代码、数据、堆和栈。代码段总是从地址0x400000开始。地址空间顶部保留给内核(操作系统常驻内存的部分)。地址空间的这个部分包含内核在代表进程执行指令时使用的代码、数据和栈。

linux私有地址空间

用户模式和内核模式

处理器通常是用某个控制寄存器中的一个模式位来限制一个应用程序可执行的指令以及它可访问的地址空间范围。该寄存器描述了进程当前享有的特权。当设置了模式位时,进程就运行在内核模式中(超级用户模式)。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置

没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令(privileged instruction),比如停止处理器、改变模式位,或者引发一个I/O操作,也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。用户程序必须通过系统调用接口间接地访问内核代码和数据。

通过诸如中断、故障或者陷入系统调用这样的异常,进程可以从用户模式变为内核模式。在异常发生时,控制传递到异常处理程序,处理器将模式从用户模式转变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式再改回到用户模式。

Linux提供一种叫做**/proc文件系统**的机制,它允许用户模式进程访问内核数据结构的内容,/proc下存储的是当前内核运行状态的一系列特殊文件,用户可以通过这些文件查看有关系统硬件及当前正在运行进程的信息,甚至可以通过更改其中某些文件来改变内核的运行状态。

上下文切换

操作系统内核是通过一种称为**上下文切换(context switch)**这种较高层次形式的异常控制流来实现多任务。上下文切换机制建立在较低层次的异常机制(中断、陷阱、故障和终止)之上。

内核为每个进程维持一个上下文(context),它是内核重新启动一个被强占的进程所需的状态,它由一些对象的值组成,这些值包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构(比如描述地址空间的页表、包含有关当前进程的进程表,以及包含进程已打开文件的信息的文件表)

在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策叫做调度(scheduling)。调度是由内核中被称为调度器的代码处理。它使用一种称为上下文切换的机制来将控制转移到一个新的进程。

上下文切换的过程

  1. 保存当前进程的上下文
  2. 恢复某个先前被抢占的进程被保存的上下文
  3. 将控制传递给这个新恢复的进程。

内核代表用户执行系统调用时,可能会发生上下文切换。如果系统调用因为等待某个事件而发生阻塞,那么内核可以让当前进程休眠,切换到另一个进程,另外,即使系统调用没有阻塞,内核也可以决定执行上下文切换,而不是将控制返回给调用进程。

系统调用错误

当Unix系统级函数遇到错误时,它们通常会返回-1,并且设置全局整数变量errno来表示出了什么错,利用错误处理包装函数,可以保持代码简洁,并且输出必要的错误信息。

进程控制

unix提供了大量从C程序中操作进程的系统调用。例如:

  • 获取进程PID
  • 创建和终止进程
  • 回收子进程
  • 让进程休眠
  • 加载并运行程序
  • 利用fork和execve运行程序

信号

一个信号就是一个消息,它通知进程一个某种类型的事件已经在系统中发生,linux支持的信号如下: image-20220119150140817

每种信号类型都对应于某个类型的系统事件。底层的硬件异常是由内核异常处理程序处理的,对于用户进程来说通常是不可见的。信号提供一种机制向用户进程通知这些异常的发生。例如一个进程如果试图除以0,内核就会发送一个SIGFPE信号,或者进程有非法存储器引用,内核就发送SIGSEGV信号,再或者进程在前台运行时,键入CRTL+C,那么内核会发送一个SIGINT信号等等。

非本地跳转