CSAPP学习笔记——chapter8 异常控制流

简介

异常控制流(Exceptional Control Flow,ECF)是在计算机系统中处理不寻常或异常情况的一种机制。它允许系统跳出正常的顺序控制流,响应那些并不直接由程序的控制流逻辑触发的事件。ECF在硬件、操作系统和应用程序层面都有体现,并且是现代计算机系统功能的一个重要组成部分。下面是ECF在不同层次上的体现及其重要性:

硬件层面和操作系统层面的ECF

在硬件层面,ECF通常表现为中断和异常。当硬件设备(如定时器、网络接口或磁盘)需要CPU注意时,它会发送信号导致中断。CPU响应中断请求,暂停当前执行的任务,转而执行一个中断处理程序,处理完中断后再返回到被中断的地方继续执行。这种机制允许系统以异步方式处理外部事件。

操作系统使用上下文切换来在用户进程间切换控制流,实现多任务处理。此外,系统调用也是一种ECF形式,它允许用户程序请求操作系统服务,如文件操作、进程控制等。

应用程序层面的ECF

应用程序可以通过信号处理来响应外部或系统生成的事件。当特定事件发生时(如除零错误、非法访问内存等),操作系统会向引起该事件的进程发送信号,进程可以定义信号处理函数来响应这些信号。

ECF的重要性

  1. 系统概念理解:ECF是实现I/O、进程和虚拟内存等操作系统概念的基础。要深入理解这些概念,必须先理解ECF。
  2. 与操作系统的交互:应用程序通过系统调用(一种ECF形式)来请求操作系统服务。理解系统调用机制有助于理解如何向磁盘写数据、创建进程等。
  3. 新应用程序开发:操作系统提供了强大的ECF机制供应用程序使用,如进程控制、事件通知等。理解这些机制可以帮助开发出如Unix shell和Web服务器等有趣的程序。
  4. 理解并发:ECF是实现系统并发的基础机制,包括异常处理程序、并发执行的进程和线程,以及信号处理程序。理解ECF是理解并发概念的起点。
  5. 理解ECF帮助理解软件异常是如何工作的。比如C++中的try,catch,throw;软件异常运行程序进行非本地跳转(违反通常的调用/返回栈规则的跳转)来响应错误情况。

硬件和操作系统层面的ECF

异常可以分为以下四类:

这里简单提一下同步和异步的概念:

  • 在同步I/O操作中,进程或线程发起I/O请求后必须等待操作完成才能继续执行。在这种情况下,执行流程是线性的,控制流在等待I/O操作完成期间被阻塞。例如,在同步I/O中,程序发起一个读取磁盘文件的操作,并且直到文件读取完成并且数据被送入程序的缓冲区后,程序才会继续执行下一步操作。在这期间,程序不会执行其他任务。
  • 在异步I/O操作中,进程或线程发起I/O请求后可以立即继续执行其他任务,当I/O操作完成时,会通过回调函数、事件、信号或其他机制通知发起者。例如,在异步I/O中,程序可能发起一个读取磁盘文件的操作,然后立即执行其他逻辑。当文件读取操作完成后,操作系统会通知程序(例如,通过一个中断或在程序的某个事件循环中设置一个标志),程序随后可以处理读取到的数据。

中断(Interrupt)

中断是由硬件设备或条件触发的异步事件。它们通常发生在任意时刻,与CPU的主控制流程无关。中断使得CPU可以响应外部事件,如输入/输出设备请求数据传输、硬件计时器超时等。当中断发生时,CPU会暂停当前任务,保存其状态,并跳转到中断处理程序(Interrupt Service Routine, ISR)来处理该事件。处理完毕后,CPU可以恢复之前的任务继续执行。

陷阱(Trap)

陷阱是由程序执行中的特定条件或指令(如系统调用)触发的同步事件。它是一种受控的异常控制流,允许用户程序向操作系统请求服务或通知操作系统发生了某个事件。例如,当程序执行系统调用时,会产生一个陷阱,导致控制权转移给操作系统以执行请求的服务。

系统调用虽然也是一个函数,但是是运行在内核模式下的,普通的函数则是运行在用户模式下。当内核代表用户执行系统调用时,可能会发生上下文切换。如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程。比如,如果一个read系统调用需要访问磁盘,内核可以选择执行上下文切换,运行另外一个进程,而不是等待数据从磁盘到达,这样可以提高处理器的吞吐量。

同时我们也应该注意到,频繁的上下文切换是很拖慢系统的,如果一个进程有很多的小的I/O操作,我们可以可以利用一个缓冲区,将多次小的I/O操作合成一个大的I/O操作,从而减少上下文切换所造成的效率变低。

故障(Fault)

故障是由程序错误导致的同步事件,通常指示可能可恢复的错误条件。当发生故障时,系统将尝试纠正这个错误,例如,当一个程序试图访问未分配的内存时就会发生页故障(Page Fault)。如果故障可以被纠正,程序可以继续执行;否则,可能会升级为中止。

中止(Abort)

中止指示了一个严重的错误,通常是不可恢复的。当中止事件发生时,程序不会继续执行。例如,当一个硬件故障发生或多个故障无法被纠正时,系统可能会中止执行当前的应用程序或操作。

之后书里介绍了一些进程相关的概念,包括获取进程ID,创建和终止进程,回收子进程,让进程休眠,加载并运行程序,就不一一展开介绍了,这些内容会在下面的信号章节进行一个统一的运用。

信号

传送一个信号到目的进程是由两个不同步骤组成的:

  1. 发送信号。内核通过更新目的进程上下文中的某个状态,发送(递送)一个信号给目的进程。发送信号可以有如下两种原因:1)内核检测到一个系统事件,比如除零错误或者子进程终止。2)一个进程调用了 kill函数,显式地要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。
  2. 接收信号。当目的进程被内核强迫以某种方式对信号的发送做出反应时,它就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理程序(signal handler)的用户层函数捕获这个信号。图8-27 给出了信号处理程序捕获信号的基本思想。

发送信号

不知道大家是否注意到终止一个进程的这个命令:

kill -9 <进程号>

kill是Linux定义的一个可以向其他进程发送信号的程序,其中的9就是上图的终止信号。

这里其实就有一个细节:就是我们应该如何合理地关闭shell控制台中卡死的进程

https://www.cnblogs.com/curiositywang/p/17994756

根据上面信号的定义,Ctrl + Z只是停止这个进程,但是没有被杀死,意味着进程所占有的资源还没有被释放;所以我们应该使用的是 Ctrl + C,这会终止这个进程,并且释放资源。

前面其实就涉及了两种发送信号的方式了,一个是使用kill,另一个则是键盘输入,其他的还有在应用程序内调用kill函数以及alarm函数等。

接受信号

进程接受信号后的行为主要有:

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

前面的图8-26介绍了进程收到信号的默认行为;有意思的地方是,我们可以通过signal函数修改进程对信号相关联的默认行为。唯一的例外是SIGSTOP和SIGKILL,这两个是不能被修改的。

展示一个重新定义 Ctrl+C发送的SIGINT信号处理逻辑的程序:

/* $begin sigint */
#include "csapp.h" void sigint_handler(int sig) /* SIGINT handler */ //line:ecf:sigint:beginhandler
{
printf("Caught SIGINT!\n"); //line:ecf:sigint:printhandler //line:ecf:sigint:exithandler
exit(0);
} int main()
{
/* Install the SIGINT handler */
if (signal(SIGINT, sigint_handler) == SIG_ERR) //line:ecf:sigint:begininstall
unix_error("signal error"); //line:ecf:sigint:endinstall int pid = getpid();
printf("pid is %d \n", pid);
while(1){
sleep(2);
} return 0;
}
/* $end sigint */

信号安全

这一小节作者介绍了编写信号的安全的处理程序的一些准则,包括使用异步信号安全的函数等(还提供了输入输出函数SIO包),再往后还介绍了非本地跳转,它的一个重要应用就是允许从一个深层嵌套的函数调用中立即返回,通常是由检测到某个错误情况引起的。如果在一个深层嵌套的函数调用中发现了一个错误情况,我们可以使用非本地跳转直接返回到一个普通的本地化的错误处理程序,而不是费力地解开调用栈,这里等真正落实到具体的项目的时候再回过头来看吧;

还分析了信号的一个特性是如何影响正确性的:

信号的一个与直觉不符的方面是未处理的信号是不排队的。因为 pending 位向量中每种类型的信号只对应有一位,所以每种类型最多只能有一个未处理的信号。因此,如果两个类型飞的信号发送给一个目的进程,而因为目的进程当前正在执行信号 k的处理程序,所以信号k 被阻塞了,那么第二个信号就简单地被丢弃了;它不会排队。关键思想是如果存在一个未处理的信号就表明至少有一个信号到达了。

#include "csapp.h"
/* $begin signal1 */
/* WARNING: This code is buggy! */ void handler1(int sig)
{
int olderrno = errno; if ((waitpid(-1, NULL, 0)) < 0)
sio_error("waitpid error");
Sio_puts("Handler reaped child\n");
Sleep(1);
errno = olderrno;
} int main()
{
int i, n;
char buf[MAXBUF]; if (signal(SIGCHLD, handler1) == SIG_ERR)
unix_error("signal error"); /* Parent creates children */
for (i = 0; i < 3; i++) {
if (Fork() == 0) {
printf("Hello from child %d\n", (int)getpid());
exit(0);
}
} /* Parent waits for terminal input and then processes it */
if ((n = read(STDIN_FILENO, buf, sizeof(buf))) < 0)
unix_error("read"); printf("Parent processing input\n");
while (1)
; exit(0);
}
/* $end signal1 */

这段代码的输出是:

  1. 当一个子进程终止时,内核会发送SIGCHLD信号给父进程。
  2. 如果父进程正在执行信号处理器,并且另一个子进程在此时终止,第二个SIGCHLD信号会被加入到待处理信号集合,因为UNIX信号默认不排队。这意味着,当第三个信号到达相同的信号到达时,它会被丢弃。
  3. 在这个特定的例子中,handler1中的Sleep(1);调用使得信号处理器执行时间较长,增加了在处理第一个SIGCHLD信号时丢失后续SIGCHLD信号的风险。

但是我们可以通过使用一个while循环,使其正确运行:

/* $begin signal2 */
void handler2(int sig)
{
int olderrno = errno; while (waitpid(-1, NULL, 0) > 0) {
Sio_puts("Handler reaped child\n");
}
if (errno != ECHILD)
Sio_error("waitpid error");
Sleep(1);
errno = olderrno;
}
/* $end signal2 */

总结

本篇博文介绍了现代操作系统中异常的一些概念,我们常见的系统调用其实也是异常的一种,内核会先保存调用者的上下文,进入内核模式,执行系统调用,当执行完毕之后,再去恢复调用者的上下文,继续执行,另外还有中断,陷阱等,这些是操作系统和硬件层面的异常;而对于进程层面的异常,则主要围绕信号这一抽象概念,包括接受信号和处理信号,最后介绍了有关信号安全的知识,还引出了一个如何有效释放进程资源的例子。

CSAPP学习笔记——chapter8 异常控制流的更多相关文章

  1. CSAPP学习笔记—虚拟内存

    CSAPP学习笔记—虚拟内存 符号说明 虚拟内存地址寻址 图9-12展示了MMU如何利用页表来实现这种映射.CPU中的一个控制寄存器,页表基址寄存器(Page Table Base Register, ...

  2. python学习笔记5_异常

    python学习笔记5_异常 1.什么事异常 Python使用异常对象(exception object) 来表示异常情况.遇到错误会发生异常. 如果异常对象未被处理或被捕捉,程序就会用所谓的回溯(t ...

  3. CSAPP:第八章 异常控制流2

    CSAPP:第八章 异常控制流2 关键点:进程控制.信号 8.4 进程控制8.5 信号 8.4 进程控制   Unix提供了大量从C程序中操作进程的系统调用.8.4.1 获取进程ID  每个进程都有一 ...

  4. CSAPP:第八章 异常控制流1

    CSAPP:第八章 异常控制流1 关键点:异常 8.1 异常8.2 进程   现代系统通过使控制流发生突变来对这些情况做出反应,一般而言,我们把这些突变称为异常控制流(Exceptional Cont ...

  5. CSAPP学习笔记(异常控制流1)

    1:诸如子进程结束之后父进程需要被告知,有时候应用程序需要系统调用,内核通过上下文切换将控制从一个进程切换到另一个进程,还有一个进程发送信号到另一个进程时接收者转而到它的信号处理函数去执行等等,我们的 ...

  6. 《深入理解计算机系统》学习笔记整理(CSAPP 学习笔记)

    简介 本笔记目前已包含 CSAPP 中除第四章(处理器部分)外的其他各章节,但部分章节的笔记尚未整理完全.未整理完成的部分包括:ch3.ch11.ch12 的后面几小节:ch5 的大部分. 我在整理笔 ...

  7. [Java学习笔记] Java异常机制(也许是全网最独特视角)

    Java 异常机制(也许是全网最独特视角) 一.Java中的"异常"指什么 什么是异常 一句话简单理解:异常是程序运行中的一些异常或者错误. (纯字面意思) Error类 和 Ex ...

  8. csapp:第八章 异常控制流ECF

    第八章 异常控制流ECF 8.1 异常 Exception graph LR E[异常Exception]-->E2[中断:异步异常] E-->E3[同步异常] E3-->陷阱 E3 ...

  9. Java编程思想学习笔记_4(异常机制,容器)

    一.finally语句注意的细节: 当涉及到break和continue语句的时候,finally字句也会得到执行. public class Test7 { public static void m ...

  10. Python学习笔记006_异常_else_with

    >>> # try-except语句 >>> >>> # try : >>> # 检测范围 >>> # exc ...

随机推荐

  1. asp.net core 3.x 通用主机是如何承载asp.net core的-中

    便于理解直接录制视频了 必备知识: 依赖注入.配置系统.选项模式.推荐参考:A大博客 通用主机(参考:https://www.cnblogs.com/jionsoft/p/12154519.html) ...

  2. C# 给当前程序创建桌面快捷方式

    C# 给当前程序创建桌面快捷方式 //by wgscd //date 2024-10-22 using System; using System.Reflection; using System.IO ...

  3. java技术架构图

    架构图有哪几种 业务架构:需求初期业务的结果和过程描述一般比较模糊,可能来自于某个老板.运营或用户的反馈.客户说海尔洗衣机洗土豆会堵,海尔立马设计专门的土豆洗衣机 业务方向往往是定方向和结果的叫战略, ...

  4. redis-cluster 集群增加节点

    分布式存储机制-槽 [1]Redis Cluster 在设计中没有使用一致性哈希(Consistency Hashing),而是使用数据分片(Sharding)引入哈希槽[2]Redis Cluste ...

  5. VueJs(2)---操作指南

    VueJs(9)---组件(父子通讯) 组件(父子通讯) 一.概括 在一个组件内定义另一个组件,称之为父子组件. 但是要注意的是:1.子组件只能在父组件内部使用(写在父组件tempalte中); 2. ...

  6. ReentrantLock实现机制

    掌握Reentrantlock 具体结构 下文Reentrantlock简称RL,阅读之前强烈建议读一下AQS源码解析: https://www.cnblogs.com/seamount3/p/186 ...

  7. uniapp-中picker-view用户不触发channge事件也知道用户选择的值

    我们都知道,只用用户触发change事件的时候,我们才知道,用户选择的是哪一个值: 如何用户没有触发change事件,我们压根就不知道用户选择的是哪一个值: 那么什么时候,用户不会触发change事件 ...

  8. Windows中使用http-server搭建一个本地服务

    我们在开发中,经常会需要搭建一个本地服务去浏览开发的静态html文件,如果当静态文件中存在一些http.https或者访问文件之类的请求时,直接双击打开html文件是会报错预览不成功的,这时候就需要将 ...

  9. 德承GP-3100 x DeepSeek:边缘运算工控机在Windows系统下私有化部署DeepSeek-R1 AI模型教程

    2025年春节前夕,中国人工智能企业深度求索(DeepSeek)发布其开源AI模型DeepSeek-R1,性能对标OpenAI开发的GPT-o1正式版,一时之间各类相关的话题引爆国内外.除了可以在手机 ...

  10. MAC消息认证码介绍

    此MAC是密码学概念,与计算机网络不同 为什么有了摘要算法还要有MAC 摘要算法保障的是消息的完整性 归根到底就是由H(x)来保证x的完整 那么问题来了,如果我知道你所使用的摘要算法(例如中间人攻击) ...