CSAPP-异常控制流
进程
进程是一个执行中的程序的实例,它是操作系统分配资源(如CPU时间、内存等)的基本单位。同时操作系统给我们的程序提供一种假象:我们独占所有CPU和内存,而进程是操作系统管理程序的方式。
操作系统为进程维护着一个上下文,包含了程序正确运行所需的状态。
父进程&子进程
父进程在创建子进程后,子进程和父进程拥有各自独立的内存空间。但操作系统使用写时复制(Copy-On-Write,COW)策略来优化内存使用。在子进程创建时,父进程的内存内容并不被立即复制,而是父进程的内存页被映射到子进程的地址空间。这意味着,父子进程初期共享相同的内存内容,只有当子进程修改某个内存页时,操作系统才会为子进程创建该内存页的副本,从而避免不必要的内存复制。
同时,父进程负责在子进程结束后进行回收工作。当子进程调用
exit()
或者收到终止信号时,它的资源(如内存、文件描述符等)会被释放,但子进程的进程表条目仍然保留在进程表中,直到父进程调用
wait()
或 waitpid()
获取子进程的退出状态并将其清理掉。父进程通过这些系统调用来回收子进程的状态信息,并从进程表中删除子进程条目。
僵死进程:如果进程在停止运行后没有被及时回收,这个子进程将会成为僵死进程。
孤儿进程:如果子进程还未被回收,而父进程先退出,子进程会成为孤儿进程。操作系统会将孤儿进程的父进程设置为
PID 为 1 的 init
进程(现在大部分 Linux 系统中为
systemd
)。init
会负责回收这些孤儿进程,避免它们变成僵死进程,由它们负责回收工作。
进程&线程
线程是进程中的执行单元。它是程序执行的最小单位。线程拥有自己独立的执行路径,但它们与同一进程中的其他线程共享进程的资源(如地址空间、文件描述符等)。
内核态与用户态
为了提高进程的稳定性,操作系统和处理器提供一种叫位模式的功能。同时将进程划分为用户模式和内核模式。
用户模式:此时的进程运行着用户编写的代码,同时不允许执行特权指令。即不允许直接访问 IO 设备、停止处理器等,这些需要通过系统调用向操作系统请求服务。
内核模式:此时进程运行着内核代码,允许执行任何指令,同时允许进程执行对硬件的直接操作,访问内存、I/O 设备等。
注意:即使以超级用户 (Root) 身份运行程序,进程依旧分为用户模式和内核模式。不过此时程序具有很高的权限,可以执行很多特殊的系统调用。只有编写内核代码并将模块载入内核运行,这些代码才运行在内核模式。
进程从用户模式切换为内核模式,通过中断、故障等异常进行。
系统调用:是程序主动请求内核服务的一种方式,例如
read()
,write()
,open()
等。这些调用会通过陷入(trap)机制从用户模式切换到内核模式。中断/异常:比如 I/O 完成、硬件故障等触发的中断也会导致用户模式切换到内核模式。
发生异常后,处理器从用户模式切换为内核模式,由内核执行相关操作并返回源程序,同时返回用户模式。注意,这并不意味着发生了进程切换,这些都是同一个进程在运行。尽管确实会影响性能。
上下文切换
前面提到:操作系统为进程维护着一个上下文,包含了程序正确运行所需的状态。比如:地址空间的页表、当前进程信息的进程表、进程已打开文件的文件表等。
当内核调度新进程时,内核会先抢占当前进程并进行以下操作:
保存当前进程上下文,以便日后恢复。
恢复被调度的进程的上下文。
将控制权转交给新进程。
值得一提的是,当发生异常导致进程从用户模式转向内核模式时,进程极有可能被执行上下文切换以运行其他进程。