CSAPP学习笔记:异常控制流

异常控制流

异常控制流定义

什么是控制流

从计算机启动到关闭,CPU做的仅仅是处理一系列顺序的指令,一次一条。这个顺序指令叫做CPU的控制流。

如何改变控制流

迄今为止已经学习了软件方面的控制流改变:1,branches/jumps 2,call/return。这两种改变都是针对“程序状态”

对于“系统级状态”的改变,控制流应该如何处理呢?比如磁盘数据上载到内存,或者程序除以0,或者键盘按下ctrl+c,或者系统计时器终止等等,控制流需要靠“异常控制流”的机制应对。

异常控制流

存在于计算机的各个层级,分别如下

  1. 异常:当发生了系统事件(system event)时,比如系统状态改变等,需要产生一个“异常”来处理,需要硬件和操作系统软件的配合
  2. 进程上下文转换:需要硬件计时器和操作系统软件配合
  3. 信号:由操作系统软件控制
  4. 非本地跳转:由C的runtime library控制

异常

定义

异常是指为了响应某一事件event,控制权由当前执行的程序转移到操作系统内核(OS kernel)

内核:操作系统内存常驻部分

常见事件event:除以0,计算溢出,页缺失(page faults),I/O请求完成,键入ctrl+c等等

事件处理过程如下

CSAPP学习笔记:异常控制流_第1张图片

可以发现:在kernel的异常处理器处理完成了,有三种情况。

CSAPP学习笔记:异常控制流_第2张图片
可以发现:内核中有一张异常表,每一种事件都对应着一个编号,供查到对应的处理程序。

异常分类

异步异常

发生在处理器外部的事件引起的异常,通过处理器的中断引脚(interrupt pin)传给处理器,并且异常处理器(exception handler)完成后,会接着处理原控制流的下一条指令(next)

举例:

CSAPP学习笔记:异常控制流_第3张图片

可以发现:timer interrupt可以帮助内核掌握控制权。

同步异常

由于执行某一条指令发生的异常

分类:

陷阱traps,错误faults,中止aborts。

CSAPP学习笔记:异常控制流_第4张图片

可以发现:traps可以恢复,且是当前程序主动请求的,比如请求system call系统调用,来调用系统函数,faults不是主动请求的而是不可能预测的,发生faults时,程序后能会在原处(不是next)接着重新运行,也可能中断

常见system call符号:

CSAPP学习笔记:异常控制流_第5张图片

CSAPP学习笔记:异常控制流_第6张图片

上图为系统调用举例

CSAPP学习笔记:异常控制流_第7张图片

上图为页缺失举例:当前内存没有所需要的地址a[500],从磁盘中调用

CSAPP学习笔记:异常控制流_第8张图片

上图为页缺失的另一种情况:当为错误地址时,发出signal

进程

进程定义

定义:进程是指运行程序的一个实例(因为运行的程序可能有多种存在形式,比如.c,.o,.text或者加载到了内存中)

两大关键抽象

  • 逻辑控制流:每个进程看上去都在单独的在使用CPU,通过OS的”上下文切换“机制产生
  • 私有地址空间:每个进程看上去都在单独的使用内存,通过”虚拟内存“的机制产生。

多进程的假象:看上去同时运行着多个进程,每个进程都在使用CPU和内存。

传统单核处理器处理多进程:交错处理,进行多进程转换时,先把当前寄存器中的值和地址空间存在内存中,然后转到另一个进程执行,当执行另一个进程时,把那个进程之前存好的寄存器值和地址空间读取出来即可

现代多核处理器处理多进程:依然是共享内存和一些缓存,但是每一个核都可以处理一个进程了,进程数超过核心数,也要进行上下文切换

多进程的并发性:如果在进程A的逻辑控制流核进程B的逻辑控制流时间交叠,则称这两个进程是并发的(实际在物理层面,它们并没有交叠)比如下图

A/B,A/C并发,B/C顺序

CSAPP学习笔记:异常控制流_第9张图片

CSAPP学习笔记:异常控制流_第10张图片

上下文切换:由操作系统内核控制着进程及上下文切换(注意内核不是单独运行的进程,而是作为某些进程的一部分在运行着)

CSAPP学习笔记:异常控制流_第11张图片

进程控制(操作进程)


系统函数调用错误

调用系统函数(system call)规则:一定要检查系统函数的返回状态!除了那些返回值为void的系统函数

处理系统函数调用错误:通常系统会会设置errno来表示错误原因,并返回-1。如下图调用fork()

CSAPP学习笔记:异常控制流_第12张图片

可以使用错误报告函数unix_error来简化错误报告。

CSAPP学习笔记:异常控制流_第13张图片

也可以使用错误处理包装来包装系统调用函数

CSAPP学习笔记:异常控制流_第14张图片

获得进程ID

CSAPP学习笔记:异常控制流_第15张图片

产生/终止进程

从程序员角度,我们可以把进程状态分为三种:

  1. 运行:正在运行或者准备运行(已经内核被安排上)
  2. 停止:进程被某个信号停止了(suspended)并且不会被安排运行,直到收到信号
  3. 终止:永久停止了

进程终止原因:

  1. 收到某个信号
  2. 在main函数中return了
  3. 调用exit()函数

exit()函数:特殊的函数,一旦调用,永远不会返回。(但是可以从main函数中return 一个状态值)

CSAPP学习笔记:异常控制流_第16张图片

产生进程:使用fork()函数产生新的子进程(child process)

fork()函数详解

  1. 子进程获得父进程的一模一样的虚拟内存地址的副本,但是PID和父进程不同
  2. 调用一次fork(),会return两次(子进程,父进程各return一次)
  3. fork()在子进程中return 0,在父进程中return子进程的PID
  4. 不能预测子进程和父进程哪个先执行

CSAPP学习笔记:异常控制流_第17张图片

使用进程图分析fork()

使用进程图,可以方便我们分析当前并发程序(假并发)中,语句的执行顺序

CSAPP学习笔记:异常控制流_第18张图片

可以发现语句执行顺序为拓扑结构,由于可以进行上下文切换,有多种可行的执行顺序。

CSAPP学习笔记:异常控制流_第19张图片

子进程回收

当子进程运行完后,依然占用着系统的资源(包括exit status退出状态,OS表等),称为僵尸(半死半活)

父进程需要对僵尸进行回收:使用wait/waitpid函数,父进程能够得到子进程的退出状态(exit status),然后内核清理僵尸子进程。

如果父进程没有回收,父进程终止后,那些僵尸进程会被init 进程(pid == 1)回收,但是我们需要显式的回收长期运行的进程(比如shell/servers)

CSAPP学习笔记:异常控制流_第20张图片

CSAPP学习笔记:异常控制流_第21张图片

上图为子进程长期运行的例子(可知我们必须显式的终止长期运行的进程)

我们可以使用wait来回收子进程(与子进程同步):

CSAPP学习笔记:异常控制流_第22张图片

当child_status非空时,会指向一个子进程的退出状态,根据此状态,可以确定有某个子进程终止,还能确定退出状态(终止状态?)

CSAPP学习笔记:异常控制流_第23张图片

加载并运行新程序

execve()允许当前进程的上下文中加载并允许新的程序

在这里插入图片描述

其中:filename:为可执行文件:如.o文件或者脚本文件(通过解释器如shell,bash等允许命令行,通常是以#! intepreter 比如#! bin/bash开头)

argument[]以null结尾,指向一系列参数字符串,通常第一个为需要指向的文件名,之后为命令行指令参数。

envp[]以null结尾,指向一系列环境变量,形式:“name = value”

重点:会覆盖原有进程的code/data/stack!!!但是保留了PID,打开的文件,信号上下文??

特点:此函数如果不报错(也就是只要filename指向的程序能够运行)就不会在执行进程中的原程序了,也就是call once and never returns

CSAPP学习笔记:异常控制流_第24张图片

在Main的stack frame的头顶,会存放着我们调用execve()是的参数,比如argu[]和envp[]等(与传统直接调用Main不同的点?)

CSAPP学习笔记:异常控制流_第25张图片

可以发现执行”bin/ls -lt /usr/include“这条命令,会分成三段放在argv[]里面,(注意调用系统函数还是要检查返回值。。)

Shell

linux系统的进程结构:

CSAPP学习笔记:异常控制流_第26张图片

其中:init进程的PID为1,所有其他进程是init的子进程

Daemon:系统中长期运行的其他进程(辅助进程)

login shell:用户登录后会有一个与身份对应的shell进程(shell本身是一个程序)

常见shell:

shell本身是一个应用程序,可以代表user来跑其他程序,常见的shell有sh/csh/bash,其中bash为linux默认shell

shell程序举例

CSAPP学习笔记:异常控制流_第27张图片

其中:从stdin把用户输入的命令行写入cmdline,首先判断是否输入的是否为ctrl+d,如果是,则退出shell,否则,执行输入的命令行(解释)(eval)

CSAPP学习笔记:异常控制流_第28张图片

其中:buf:保存着修改后的comdline,

argv[]保存着可执行文件的文件名以及参数

如果argv是内置的指令,则执行,不是内置指令,则fork一个子进程,然后在子进程中调用execve来执行目标文件。

bg:判断是否为后台进程,如果不是后台(是前台)进程,则得一直等待进程的结束,才能执行之后的命令。

可以发现:对于前台进程,shell可以回收,但是对于后台进程,当它们终止后会变成僵尸进程,并且一直不会回收(因为shell是一直运行的)从而占用内存资源。所以我们需要一种机制,来提醒shell回收进程———signal

信号

定义

信号是用来提醒进程:现在系统中发生了一些事情

信号的特点及常见ID

CSAPP学习笔记:异常控制流_第29张图片

可以发现:信号是内核与进程中的异常处理机制(软件级别)不包含其他信息。

发出信号

由内核发送给目标进程,做的仅仅是更新了进程上下文的几个state(bits)。

通常内核发出信号的原因有:

  1. 内核检测到了系统中的一些事件,比如除0,或者子进程终止
  2. 其他进程请求内核发出信号(通过调用系统函数system call中的kill(),这种是显式的发出信号。

接受信号

目标进程当它被内核要求对发出的信号做出反应时,称为信号被接受。

通常接受信号时目标进程有以下反应:

CSAPP学习笔记:异常控制流_第30张图片

可以发现:用户可以自己定义signal-handler来处理信号,与硬件级别发生中断很像,只是exception handler是内核处理,此处的handler是一个用户定义的函数。

信号待定和信号阻塞

信号待定是指:一个信号已经被发送,但是还没有被接受,最多只能由一个被待定的同类型信号,不存在”排队“的概念。如果一个进程已经存在了一个信号K的待定,则其他发送给此进程的信号K,都会被丢弃。

信号阻塞:给某进程的信号K如果被阻塞,则此信号K仍可以发送,只是在信号K的阻塞解除前,都不能接收到此信号。

内核在每一个进程都维护着待定和阻塞的位向量(bit vector)(其实就是一个32位int)。pending set:表示正在待定的信号们,blocked set:表示已经阻塞了的信号们(可以用sigprocmask来设置此进程哪些信号阻塞了)

发出信号

进程群

每一个进程都属于唯一的进程群

CSAPP学习笔记:异常控制流_第31张图片

发送信号的方式

  1. 使用/bin/kill程序对进程或进程群发出信号

CSAPP学习笔记:异常控制流_第32张图片

  1. 使用键盘发送信号

    CSAPP学习笔记:异常控制流_第33张图片

    CSAPP学习笔记:异常控制流_第34张图片

  2. 使用kill函数来发送信号

CSAPP学习笔记:异常控制流_第35张图片

kill():指定目标进程的PID,以及想要发送的信号。

接受信号

接受信号时间点

由于接受信号需要对上下文进行一定的修改(上下文转换),所以接受信号发生在内核在exception handler return的时候,准备把控制权转交给进程p的时候,如果发现signal set已经被修改(有信号send),此时会让进程p对信号进行响应。

CSAPP学习笔记:异常控制流_第36张图片

如上图:kernal如果发现进程B有信号pending,则会强制让进程B响应。

接受信号过程

CSAPP学习笔记:异常控制流_第37张图片
可以发现:内核会根据pending和blocked的情况,计算出pnb,然后处理所有非零位(从高到低)(即处理所有信号),处理完成后,再进行p的下一条指令。

响应信号

有四种默认的响应方式

  1. 终止该进程
  2. 终止该进程并dump core(?)
  3. 暂停程序直到收到了sigcont信号
  4. 忽略信号

除了默认响应外,允许用户自定义signal handler来响应信号,具体过程如下:

  1. 安装signal handler
  2. 进程接受到信号(signum)后,会调用相应的信号处理函数
  3. 处理信号(handling signal)
  4. return到原进程指令流。

CSAPP学习笔记:异常控制流_第38张图片

信号相应函数和main函数一样,也是独立的逻辑流,与main程序并发运行(不同于进程间的并发,这是进程内的并发)

CSAPP学习笔记:异常控制流_第39张图片

CSAPP学习笔记:异常控制流_第40张图片

可以发现:与普通的函数不同的是,当控制权交给handler函数后,需要先把控制权交给内核,再由内核把控制权交给main函数

嵌套信号处理函数:

CSAPP学习笔记:异常控制流_第41张图片

阻塞信号

隐式阻塞

当某种pending信号正在被处理时,其他所有同类型信号都会被阻塞

显式阻塞

使用sigpromask函数

CSAPP学习笔记:异常控制流_第42张图片

CSAPP学习笔记:异常控制流_第43张图片

可以调用sigpromask函数,显式的阻塞某一特定信号。

编写安全的信号处理函数

信号处理函数缺点:

  1. 不安全,由于可以访问main函数中的所有数据,如果处理不好,会破坏main函数功能
  2. 没有提示(no cued)
  3. 不能移植,signal handlers在不同的Linux系统不能兼容

信号处理函数编写准则

  1. 尽量简洁:比如就设置下全局标签global flag然后返回
  2. 使用异步信号安全函数(async-signal-safe functions)???
  3. 保存errorno信息
  4. 不开放一些数据结构的权限(是这个意思吗。。没懂)
  5. 定义全局变量为volatile(此时全局变量不会存在寄存器中)
  6. 定义全局标签为volatile(flag:只能读写,不能修改比如flag++)

CSAPP学习笔记:异常控制流_第44张图片

使用异步信号安全函数

异步信号安全函数:是可重入的或者是不能被信号打断的。

可重入是指:函数所有变量都存在于stack frame上,没有使用全局变量或全局函数,从而没有锁,不会互斥,可被多个进程同时调用。

种类:117个

典型错误:死锁,如调用printf,由于不是信号异步函数,handler调用printf可能会出现死锁(此时printf被main锁住了)

不能使用handler计数信号

因为只能由一个Pending的同种类信号,其余信号都会被丢弃。

错误计数:
CSAPP学习笔记:异常控制流_第45张图片
正确计数:(把if改成while)

CSAPP学习笔记:异常控制流_第46张图片

利用同步流防止竞争

对于一些公共数据的处理,需要防止handler和main程序的资源竞争,具体做法是在处理公共资源的前后,加上sigpromask来阻止信号中断。

CSAPP学习笔记:异常控制流_第47张图片

CSAPP学习笔记:异常控制流_第48张图片

上图的代码出现了并发进程的典型错误(虽然这两个是同一进程,但也算并发了),那就是无法确定是handler是先运行还是main先运行,如果handler先运行,则会删除joblist中不存在的PID。

解决办法:在fork子进程之前,屏蔽了SIGCHID,这样main函数运行时,就算子进程已经回收发出了SIGCHID信号,也会被pending,待将joblist更新后,在来reap child,这样就能保障顺序(同步)。

我们不能决定child和parent谁先运行,但是我们可以在main中parent中暂时屏蔽信号。

显式等待某种信号

具体做法:设置一个flag称为pid,当调用了某个信号处理函数,更改pid,从而可以确定某个信号已经处理完毕。

CSAPP学习笔记:异常控制流_第49张图片

CSAPP学习笔记:异常控制流_第50张图片

可以发现:1,首先利用sigpromask保证main先把pid设置成0

2,使用while(!pid)来判断是否接受到了特定信号

3,程序很浪费:一直在打印,占用资源

CSAPP学习笔记:异常控制流_第51张图片

想到了其他办法:

1,使用pause():可以暂停,一直到信号接受,但是有bug,因为信号可以在while和pause中间产生中断,从而忽略了此次pid。

2,使用sleep(),但时间不好控制,时间太短,会一直占用CPU,时间太长,会使Main程序变慢。

解决办法:使用sigsuspend(原子性,不能被中断)

CSAPP学习笔记:异常控制流_第52张图片

可以发现:sigsuspend函数,可以先解除对sigchild的屏蔽,然后在屏蔽

CSAPP学习笔记:异常控制流_第53张图片

使用sigsuspend函数显式的等待某项信号。

非本地跳转

C语言可以允许一个函数(callee)不返回caller,而返回其他并没有调用这个callee的函数,称为非本地跳转(longjump…)

具体见教材。

你可能感兴趣的:(CSAPP学习笔记)