首页 > 其他分享 >CSAPP学习笔记——chapter8 异常控制流

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

时间:2024-01-29 16:23:57浏览次数:32  
标签:ECF CSAPP 调用 chapter8 控制流 程序 信号 进程 操作系统

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

异常可以分为以下四类:

image-20240129094833896

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

  • 在同步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可以恢复之前的任务继续执行。

image-20240129095929629

陷阱(Trap)

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

image-20240129100010442

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

image-20240129100503052

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

故障(Fault)

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

image-20240129100809621

中止(Abort)

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

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

信号

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

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

image-20240129102721248

image-20240129105103106

发送信号

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

kill -9 <进程号>

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

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

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

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

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

image-20240129110327701

接受信号

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

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

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

image-20240129111021388

展示一个重新定义 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 */

这段代码的输出是:

image-20240129144753322

  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 */

image-20240129145307096

总结

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

标签:ECF,CSAPP,调用,chapter8,控制流,程序,信号,进程,操作系统
From: https://www.cnblogs.com/curiositywang/p/17994774

相关文章

  • CSAPP 第二章 信息的表示与处理(1)信息存储与整数表示
    1信息存储机器级程序将内存视为一个非常大的字节数组,成为虚拟内存(virtualmemory)。内存的每个字节都由唯一的数字来标识,称为它的地址(address),所有可能的地址集合就称为虚拟地址空间(virtualaddressspace)。每个程序对象可以简单地视为一个字节块,而程序本身就是一个字节序列。......
  • CSAPP学习笔记——Chapter12 并行编程
    CSAPP学习笔记——Chapter12并行编程并发编程有着其独特的魅力,之前接触cuda编程的时候,感受到一些,没想到书里还有相关的内容。今天我们主要围绕进程,I/O多路复用,线程三种并发的方式,介绍并发编程的相关概念。并最终拓展chapter11讲中的echo服务器,使其能够处理多个客户端的连接请求......
  • CSAPP学习笔记——Chapter10,11 系统级I/O与网络编程
    CSAPP学习笔记——Chapter10,11系统级I/O与网络编程Chapter10系统级I/O系统级I/O这一章的内容,主要可以通过这张图概括:UnixI/O模型是在操作系统内核中实现的。应用程序可以通过诸如open、close、lseek、read、write和stat这样的函数来访UnixI/O。较高级别的RIO和标......
  • CSAPP-C3
    0.警告不要试图通过这篇意识流笔记自学。右转睿站九曲阑干,可以帮你快速建立基本概念。1.基本的汇编语法I.数据格式三种数据类型:立即数:常数,一般用十进制表示,如果要使用十六进制表示,在前面加上$寄存器:寄存器内存:把内存抽象成一个大数组,使用M[i]的形式来理解i地址指......
  • Visual Studio 2019 SSIS工具控制流增加约束以及数据流增加数据匹配达到增量抽取
    情况1:在配置控制流时,想在数据流前面进行数据的过滤或者是前置的数据记录数的判断,那可以在数据流前面增加SQL执行任务用来放置判断SQL语句,随后得在SQL执行任务编辑界面找到ResultSet(结果集),在右侧下拉选择项中选择单行(这里选择单行是因为写的SQL判断语句只输出一行值)随后在左侧菜......
  • 5. 控制流
    控制流if语句:用于分支选择条件部分:用于判断是否执行语句部分:要执行的操作==与=操作=操作:用于赋值,将数值保存在变量所对应的内存中==操作:用于判断两个值是否相等可以将常量放在==左边以防止误用猜数字的游戏代码:#include<iostream>intmain(void){......
  • Java控制流
    Java流程控制Scanner对象基本语法:Scannerscn=newScanner(System.in); //Scanner类来获取用户的输入​ 通过Scanner类的next()与nextLine()方法获取输入的字符串,在读取前我们一般需要使用hasNext()与hasNextLine()判断是否还有输入的数据。1、next()一定要读取到有效......
  • Swift 笔记-1 基本类型,集合类型,控制流与基本函数
    目录基本类型变量与常量字符串单行多行整型浮点布尔值集合类型数组字典Dictionaries集合Sets枚举Enums控制流条件判断循环代码块抽象结构函数声明函数返回类型声明返回多个值自定义参数标签函数参数默认值函数与错误最近对iOS开发有兴趣,学习SwiftUI,主要跟的是hackingwiths......
  • 干货分享 | TSMaster小程序启动和停止的自动化控制流程
    在实际应用场景中,用户常常需要按一定逻辑和时序来控制TSMaster内置功能模块的启动和停止,TSMaster软件内置有C/Python小程序和图形程序,开发者可以通过编程对这些模块的运行进行精确控制。本文将重点和大家分享一下如何通过C代码来控制TSMaster内置模块的启动与停止。本文关键字:run_f......
  • CSAPP 第三章 笔记
    历史观点程序编码机器级代码x86-64可见的处理器状态:程序计数器PC:%rip,给出下一条指令的地址寄存器文件:16个,储存64位的值条形码寄存器:保存最近执行的算术或逻辑指令的状态信息,用来控制条件变化向量寄存器:存放多个整数或浮点数值函数调用保存策略调用者保存被......