深入理解计算机系统(第三版)/ CSAPP 杂谈,第8章:异常控制流

  • 异常控制流(Exceptional Control Flow,ECF)是操作系统为应用提供的一种访问处理器资源之外的能力,对应于嵌入式和CPU等硬件的中断概念。

  • 系统调用,进程管理,并发,IO 访问都属于异常控制流。

  • 异常(exception)是控制流的突变,用来处理处理器状态中的某些变化。异常通过事件(event)触发,有专门的异常表(exception table)用于事件的跳转。

  • 每种类型的异常都有唯一的异常号(exception number),有可能是处理器设计时分配的零除,缺页,内存访问违例,断点和算术运算溢出;也可能是操作系统分配的系统调用,外部IO设备信号。操作系统的异常号是在系统启动时初始化的。

  • 异常表的格式是:异常号->地址。

  • 异常的处理是在内核空间内,拥有访问所有资源的权限。

  • 异常分为,中断(interrupt,异步),陷阱(trap),故障(fault),终止(abort)。

  • 系统调用属于陷阱异常,用户程序想要调用服务n时,就执行处理器提供的特殊指令 syscall n。这会触发异常处理程序,程序会解析参数,并调用合适的内核程序。普通函数调用则无法进入内核空间,也就无法访问特殊的资源。

  • 终止异常是硬件问题,如 RAM 校验等不可恢复的错误,只能直接终止程序

  • 段故障(segment fault)通常因为程序访问了受限的内存块导致的,如读未定义的虚拟内存,写只读内存等。

  • 并发流(concurrent flow)与处理器核心数无关,只要时间上重叠即可,多个进程以并发流形式运行成为多任务(multitasking);并行流(parallel flow)必须是在不同的处理器或计算机上同时运行。也就是说,并行流一定是并发流,反之不一定。

  • 模式位(mode bit)在处理器层上提供内核模式和用户模式的内存空间访问范围控制。没有设置模式位时,就处于用户模式,此时不允许使用特权指令(privileged instruction),如停止处理器,改变模式位,或发起一个 I/O 操作。

  • 上下文切换(context switch)是操作系统基于异常来实现的异常控制流,用于实现多任务。内核为每一个进程(process)维持一个上下文,具体内容包括寄存器,用户栈,内核栈,和各种内核数据结构如页表,当前进程信息的进程表,当前进程打开的文件的文件表。

  • 内核使用调度器(scheduler)对进程做上下文切换,从而切换当前运行的进程。切换时机由调度器决定,比如时间片用完,sleep,请求磁盘数据后。

  • init 进程是所有进程的祖先,如果父进程在回收它的僵尸子进程前就终止了,则由 init 进程回收它们。waitpid 可以用来等待自己的子进程终止或停止。

——- 20190101 ——–

  • fork 创建一个新的进程,execve 在原有进程上执行新的程序的 main 函数。

  • Linux 信号允许进程和内核中断其他进程。信号可以理解为一条消息,一个事件。

  • 发送信号的程序可以是内核,另一个程序,自己

  • 接收信号可以用 signal handler 来捕获这个信号(相当于嵌入式的中断函数),或者忽略。SIGKILL 和 SIGSTOP 信号无法被捕获和忽略。发出但没有被接收的信号叫 pending signal。一个类型最多只能有一个待处理信号,后面的同类型信号将会被丢弃。这个特性可以让程序有选择性的阻塞接收特定信号,做到不重复处理的功能。

  • 可以大量向 process group 进程组发送信号,父进程和子进程属于同一个进程组。使用 setpgid 设置自己的 pid 进程组

  • 阻塞信号分为两种:隐式和显式。隐式是指处理信号 s 的程序正在运行且未返回时新的信号 s 会变成待处理而没有被接收(注意最多只能有一个 s 待处理信号)。显式是使用 sigprocmask 明确的阻塞和接触阻塞选定的信号。

  • 信号处理程序需要:

    1. 尽可能简单,如设置一个flag。否则会被阻塞;
    2. 只调用异步信号安全的函数,他们有可重入(例如只访问局部变量),或不能被中断的特性
    3. 保存和回复 errno,在进入处理程序时吧 errno 保存在局部变量中,并在返回时恢复它(如果你的处理程序会调用会修改 errno 的方法的话)
    4. 访问全局变量时,阻塞所有信号。否则会出现竞争导致不可预知的结果
    5. 使用 volatile 声明全局变量。优化编译器有可能会优化掉周期读取全局变量的代码,进而使用缓存值。vollatile 会让编译器不要缓存这个变量;并强迫代码每次引用全局变量时,强制从内存中读取。
    6. 使用 sig_atomic_t 来声明标志,它是原子读写的(不可中断的)。这样就不需要暂时阻塞信号。原子性只能保证单个读写,不能保证 falg++ flag+=10 这样的操作。
  • signal 函数不同系统的语义可能不一样。比如每次触发信号处理之后可能需要再次调用 signal 函数,否则不会再次触发。

  • setjmp 和 longjmp 函数可以实现非本地跳转 nonlocal jump,即从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用-返回序列。

  • setjmp 在 env 缓冲区保存当前调用环境,以供后面的 longjmp 使用,返回 0。longjmp 调用后 setjmp 再次返回,此时返回值非 0(类似 fork)。该功能可用于捕获异常信号

  • C++ 和 Java 的异常机制是 setjmp 和 longjmp 的更加结构化的版本。try catch 类似 setjmp,throw 类似 longjmp。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据