异常

由硬件和操作系统共同实现。 当处理器检测到有事件发生时,就会通过一张异常表进行间接过程调用,将控制转移到操作系统。 事件:虚拟内存缺页、除以 0、I/O 请求完成等 异常处理程序处理后,根据事件类型,会发生以下情况中的一种:

  • 将控制返回当前指令
  • 将控制返回如果没有发生异常的下一条指令
  • 终止被中断的程序

异常处理

每种类型的异常被分配了一个唯一的异常号,处理器检测到事件后,根据异常号查询异常表并跳转到相应的处理程序。

异常的类别

可以被分为四类。

中断

异步发生,来自处理器外部的 I/O 设备的信号导致。 通过设置处理器的中断引脚来指示。 例:I/O 中断(Ctrl+C、网络数据包到达、硬盘数据到达等)、按下重启按钮、Ctrl+Alt+Delete 等 将控制返回给下一条指令

陷阱

程序故意引起。 例:系统调用、程序断点 syscall 指令:导致一个到异常处理程序的陷阱,这个处理程序解析参数并调用适当的内核程序。 将控制返回给下一条指令

故障

由错误情况引起,可能可以被故障处理程序修正。 如果能修正错误情况,则将控制返回给当前指令,否则终止程序。 例:

  • 缺页异常,缺页处理程序将磁盘中的页数据加载到物理内存中,将控制返回给引起故障的指令。
  • 无效内存访问

终止

不可恢复的致命错误,通常是硬件错误。 将程序终止。

进程

定义:一个执行中的程序的实例 提供了两个关键抽象:

  • 独立的逻辑控制流:每个程序似乎都独占 CPU
  • 私有的地址空间:每个程序似乎都独占主存 系统中的每个程序都运行在某个进程的上下文中。上下文包括内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

逻辑控制流

处理器的一个物理控制流被分成了多个逻辑控制流,每个进程一个,交错执行。

并发流

一个逻辑流的执行在时间上与另一个流重叠,称为并发流。

私有地址空间

一个进程的私有地址空间不能被其他进程读写。

用户模式和内核模式

用处理器中的某个控制寄存器的模式位来指示。 内核模式可以执行指令集中的任何指令,并且访问系统中的任何内存位置。 用户模式不允许执行特权指令,也不允许访问内核去的代码和数据。

进程从用户名是变位内核模式的唯一方法时通过异常,异常发生时,进程从用户模式进入内核模式,异常处理程序结束后,进程变回用户模式。

上下文切换

内核为每个进程维持一个上下文。 在进程执行的某些时刻,内核可以决定抢占当前进程并重新开始一个先前被抢占的进程,这是由内核中的调度器处理的。

上下文切换:

  • 保存当前进程的上下文
  • 恢复先前被抢占的进程的上下文
  • 将控制传递给这个新恢复的进程

进程控制

获取进程 ID

每个进程有一个唯一的整数进程 ID(PID)。 getpid 函数返回调用进程的 PID,getppid 函数返回父进程的 PID。

创建和终止进程

进程总是处于以下三种状态之一:

  • 运行:正在执行或等待被执行
  • 停止:被挂起并且不会被调度
  • 终止:永远停止。
    • 收到一个默认行为是终止进程的信号
    • 从主程序返回
    • 调用 exit 函数

fork 函数

父进程通过 fork 函数创建一个子进程,子进程会克隆父进程(除了 PID) fork 函数被调用一次,但返回两次:

  • 父进程中,返回子进程的 PID
  • 子进程中,返回 0 由于进程调度的不确定性,不能假定父子进程的运行顺序。

僵尸进程

一个进程终止时,内核并不会立即将它从系统中清除,它仍会消耗系统资源。 只有当它被父进程回收后才会抛弃已终止的进程。 如果父进程在没有回收子进程的情况下终止,那么子进程会被 init 进程获取并回收。 因此只需在长时间运行的进程内显式地进行子进程的回收。

wait 函数

int wait(int *child_status)
  • 挂起当前进程知道它的一个子进程终止。
  • 返回值为终止的子进程的进程号。
  • child_status 不为空,它将被设置为指示子进程终止原因的状态。
  • 如果多个子进程退出,wait 返回的顺序可以是任意的。
  • 使用宏 WIFEXITED 和 WEXITSTATUS 来获取退出状态的具体信息

waitpid 函数

waitpid(pid, &status, options)

挂起当前进程直到特定进程终止。

execve 函数

在当前进程的上下文中加载并运行一个新程序。

int execve(const char* filename, const char* argv[], const char* envp[])
  • filename:可执行文件名
  • argv 参数列表
  • envp:环境变量列表
  • ! 该函数不会返回!(除非出现错误)
  • 覆盖当前进程的代码、数据和堆栈

壳程序

Shell,命令行解释器。 后台任务:运行时间长,不想等待结束的进程 怎么回收后台子进程?

信号

一个信号是一条小消息,它通知目标进程系统中发生了一个某种类型的事件。

  • 发送信号:内核通过修改目标进程上下文中的某些状态向目标进程发送(传递)信号
    • 内核检测到一个系统事件
    • 一个进程调用了 kill 函数
  • 接收信号:当目标进程被OS内核强制以某种方式对信号的到来作出反应时,这被称为“接收信号
    • 忽略、终止进程或执行信号处理程序

一个发出而没有被接收的信号叫挂起信号。

  • 任何时刻一种类型至多只有一个挂起的信号,后续的同类型信号将被丢弃
  • 一个被挂起的信号最多被接收一次

进程可以主动阻塞某些信号。 内核在每个进程的上下文中维护挂起和阻塞的位向量。 传送了一个类型为 的信号,内核会设置 pending 中的第 位,而质押接收了一个类型为 的信号,内核就会清除 pending 中的第 位。

发送信号

用/bin/kill 程序发送信号

/bin/kill 程序可以向另外的进程发送任意的信号。 kill -<信号ID> <目标进程(组)>

从键盘发送信号

Unix 用作业这个抽象概念表示为对一条命令行求值而创建的进程。 任意时刻,至多有一个前台作业和 0 个或多个后台作业。 shell 为每个作业创建一个独立的进程组。

输入 Ctrl+C 会导致内核发送一个 SIGINT 信号到前台进程组中的每个进程,默认是终止前台作业。 输入 Ctrl+Z 会发送一个 SIGTSTP 信号到前台进程组中的每个进程,默认会停止(挂起)前台作业。

用 kill 函数发送信号

int kill(pid_t pid, int sig);

如果 pid 大于 0,发送信号号码 sig 给进程 pid 如果 pid 等于 0,发送信号 sig 给调用进程所在进程组中的每个进程,包括调用进程自己 如果 pid 小于 0,发送信号 sig 给进程组 -pid 中的每个进程

接收信号

内核吧进程 从内核模式切换到用户模式时,它会检查进程 的未被阻塞的待处理信号集合(pnb = pending & ~blocked),若 pnb == 0,恢复执行进程 ,否则在 pnb 中选择最低非零位 并强制 接收信号 ,依次处理 pnb 中所有非零位,然后恢复执行

每种信号有一个预定义的默认行为:

  • 进程终止
  • 进程终止并转储内存
  • 进程停止(挂起)直到被 SIGCONT 信号重启
  • 进程忽略该信号

signal 函数可以修改接收信号时的默认行为。

sighandler_t signal(int signum, sighandler_t handler);
  • handler 是 SIG_IGN,忽略类型为 signum 的信号

  • handler 是 SIG_DFL,将类型为 signum 的信号行为恢复到默认行为

  • handler 是用户定义的函数地址,这个函数就被称为信号处理程序,进程接收到类型为 signum 的信号时,就会调用这个程序。当该处理程序返回时,控制(通常)返回给信号接收中断位置处的指令。

  • ! 信号处理程序是一个与主进程并发运行的独立逻辑流,但不是独立进程。它具有独立的栈和处理器上下文。

  • 信号处理程序可以被其他信号处理程序中断。

异步信号安全

如果一个函数是可重入(仅有局部变量)的或是不可被信号中断,则该函数是异步信号安全的。

非本地跳转

将控制从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用和返回。 通过 setjmplongjmp 实现

int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int retval);

setjmp 函数在 env 缓冲区中保存当前调用环境,包括 PC、栈指针和通用目的寄存器。

  • 一次调用,可能有多次返回
  • 正常调用时返回 0 longjmp 函数从 env 缓冲区中恢复调用环境,并将返回值(%eax)设置为 retval
  • 一次调用,永不返回

局限性:必须符合栈的运行规律,只能跳转到已经调用但是尚未返回的函数中