CSAPP读书笔记--第八章 异常控制流
第八章 异常控制流
2017-11-14
概述
控制转移序列叫做控制流。目前为止,我们学过两种改变控制流的方式:
1)跳转和分支;
2)调用和返回。
但是上面的方法只能控制程序本身,发生以下系统状态的变化复杂问题时就没法使用上面的方法控制:
- 数据从磁盘或者网络适配器到达
- 指令除以了零
- 用户按下 ctrl+c
- 系统的计时器到时间
现代系统通过使控制流发生突变来对系统状态的变化做出反应,这些突变称为异常控制流。
异常控制流有四种实现机制:
1)异常(低层级);2)进程上下文切换;3)信号;4)非本地跳转。(2-4高层级)
8.1 异常
异常(exception)就是控制流中的突变,用来响应处理器状态中的某些变化。这里的异常是指将控制交给系统内核来处理某些事情。
内核是操作系统常驻内存的部分。
任何情况下,当处理器检测到有事件发生时,它会通过一张就做异常表的跳转表,进行一个间接过程的调用,使用异常处理程序(运行在内核模式下)来处理这类事件。这类事件包括被零除、缺页、算术溢出、I/O请求完成。异常处理程序完成处理后,会发生以下三种情况:
1)处理程序将控制放回给当前指令Icurr,即事件发生时正在执行的指令。
2)将控制返回给Inext;
3)终止被中断的程序。
系统中为每种类型的异常都分配了一个唯一的非负整数的异常号,系统会通过异常表来确定跳转的位置,每个事件都有对应的异常号,发生对应事件就调用对应的异常处理代码。
异常的类别
异常分为异步异常和同步异常。异步异常是硬件中断(称为中断处理程序),是有外部IO设备造成的,同步异常是执行当前指令的结果(称为故障指令)。
陷阱属于系统调用,运行在内核模式中,而普通的程序调用运行在用户模式中,限制了函数可以执行的指令的类型。
类别 | 原因 | 异步/同步 | 返回行为 |
中断 | 来自I/O设备的信号 | 异步 | 总是返回到下一条指令 |
陷阱 | 有意的异常(read,exit) | 同步 | 总是返回到下一条指令 |
故障 | 潜在可恢复的错误 | 同步 | 可能返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
这里重点分析一下故障异常,以缺页异常故障为例:
缺页异常发生的条件是:指令引用一个虚拟地址,但是与该地址相对应的物理页面不在内存中,因此必须从磁盘中读取,就会发生故障。
- 用户写入内存位置
- 但该位置目前还不在内存中
int a[];
main ()
{
a[] = ;
}
那么系统会通过 Page Fault 把对应的部分载入到内存中,然后重新执行赋值语句:
8.2 进程
通俗的定义是:占用内存空间的正在运行的程序。经典的定义是:一个执行中的程序的实例。进程提供给应用程序的关键抽象:1)一个独立的逻辑控制流;2)一个私有的地址空间。
并发流的概念:流X和Y互相并发,当且仅当X在Y开始之后和Y结束之前进行。或者Y在X开始之后和X结束之前开始。
上下文切换:1)保存当前进程的上下文;2)恢复某个先前被抢占的进程被保存的上下文;3)将控制传递给这个新恢复的进程。
8.3 进程控制
获取进程ID:
#include<sys/types.h>
#include<unistd.h> pid_t getpid(void);
pid_t getppid(void);
getpid返回调用进程的PID,getppid返回它的父进程的PID。
我们可以认为,进程有三个主要状态:
- 运行 Running
- 正在被执行、正在等待执行或者最终将会被执行
- 停止 Stopped
- 执行被挂起,在进一步通知前不会计划执行
- 终止 Terminated
- 进程被永久停止
另外的两个状态称为新建(new)和就绪(ready),这里不再赘述。
fork()函数创建进程。
#include<sys/types.h>
#include<unistd.h> pid_t fork(void); //子进程返回0,父进程返回子进程的PID,如果出错,则返回-1
需要注意:
- 调用一次,返回两次;
- 并发执行;
- 相同但是独立的地址空间;
- 共享文件。
进程图
通过画进程图来理解fork函数:
- 每个节点代表一条执行的语句
- a -> b 表示 a 在 b 前面执行
- 边可以用当前变量的值来标记
printf
节点可以用输出来进行标记- 每个图由一个入度为 0 的节点作为起始
int main()
{
pid_t pid;
int x = ; pid = Fork();
if (pid == )
{ // Child
printf("child! x = %d\n", --x);
exit();
} // Parent
printf("parent! x = %d\n", x);
exit();
}
在下面三种情况时,进程会被终止:
- 接收到一个终止信号
- 返回到
main
- 调用了
exit
函数
exit
函数会被调用一次,但从不返回,具体的函数原型是
// 以 status 状态终止进程,0 表示正常结束,非零则是出现了错误
void exit(int status)
8.4 回收子进程
一个终止了但是没有被回收的进程称为僵尸进程。如果父进程已经终止了,那么对应的没终止的子进程就称为孤儿进程。对于孤儿进程,内核会安排init进程称为他的孤儿进程的养父。init进程的PID是1,是在操作系统启动后由内核创建的,init进程会回收孤儿进程。
一个进程可以调用waitpid函数来等待它的子进程终止或者停止。
#include<sys/types.h>
#include<sys.wait.h> pid_t waitpid(pid_t pid,int *statusp,int options);
//pid 等待终止的目标子进程的ID,如果传递-1,则与wait函数相同,可以等待任意子进程终止
//statusp 传入变量的地址值
//如果设置为WNOHANG,即使没有终止的子进程也不会进入阻塞状态,而是返回0并且退出
//如果成功则返回子进程的ID,如果是WNOHANG,则为0,出错则为-1
- WIFEXITED子进程正常终止时则返回真;
- WEXITSTATUS返回子进程的返回值。
if(WIFEXITED(statusp)){//是正常终止吗?
puts("Normal termination!");
printf("Child pass num:%d",WEXITSTATUS(statusp));//那么返回值是多少?
}
如果想在子进程载入其他的程序,就需要使用 execve
函数,具体可以查看对应的 man page,这里不再深入。
8.5 信号
Linux 的进程树,可以通过 pstree
命令查看。
对于前台进程来说,我们可以在其执行完成后进行回收,而对于后台进程来说,因为不能确定具体执行完成的时间,所以终止之后就成为了僵尸进程,无法被回收并因此造成内存泄露。
编号 | 名称 | 默认动作 | 对应事件 |
---|---|---|---|
2 | SIGINT | 终止 | 用户输入 ctrl+c |
9 | SIGKILL | 终止 | 终止程序(不能重写或忽略) |
11 | SIGSEGV | 终止且 Dump | 段冲突 Segmentation violation |
14 | SIGALRM | 终止 | 时间信号 |
17 | SIGCHLD | 忽略 | 子进程停止或终止 |
一个发出而没有被接收的信号叫做待处理信号,一种类型的待处理信号只能有一个,待处理信号不会排队,它们只能简单的被丢弃,一个进程可以有选择的阻塞接收某种信号。当一种信号被阻塞时,它仍可以被发送,但是产生的待处理信号不会被接收,直到进程取消对这种信号的阻塞。blocked位向量中维护着被阻塞的信号集合。
发送信号:
每个进程都属于一个进程组:getpgrp函数返回当前进程的进程组ID:
#include<unistd.h>
pid_t getpgrp(void);
默认情况下父子进程属于同一个进程组,可以通过setpgid改变进程组信息:
#include<unistd.h>
int setpgid(pid_t pid,pid_t pgid);//将进程pid的进程组改为pgid
//如果成功返回0,失败返回-1
使用/bin/kill发送信号(因为有些unix有自己的KILL函数)
bin/kill - -//发送信号9(SIGKILL)给进程15213
使用kill函数发送信号给其他进程
#include<sys/types.h>
#include<signal.h> int kill(pid_t pid,int sig);
//kill发送信号sig给进程pid
接收信号
sigaction好处是跨平台,可移植。
#include<signal.h>
int sigaction(int signo,const struct sigaction *act,stuct sigaction* oldact);
/*
*signo 传递信号信息
*act 对应第一个参数的信号处理函数信息
*oldact 通过该参数获取之前注册的信号处理函数指针,不需要则传递0
*/
sigaction结构体定义:
struct sigaction{
void (*sa_handler)(int);//保存信号处理函数地址
sigset_t sa_mask; //以下初始化为0
int sa_flags;
}
阻塞和解除阻塞信号
Linux提供阻塞信号的隐式和显式的机制:
- 隐式阻塞机制:内核默认阻塞当前正在处理信号类型的待处理信号。(如果有一个待处理信号,内核在父进程处理当前信号之后处理这个待处理信号)
- 显式阻塞机制:使用sigpromask函数和它的辅助函数,明确的阻塞和解除阻塞的信号。
常用的几个辅助函数意义:
sigemptyset
- 创建空集sigfillset
- 把所有的信号都添加到集合中(因为信号数目不多)sigaddset
- 添加指定信号到集合中sigdelset
- 删除集合中的指定信号
sigset_t mask, prev_mask;
Sigemptyset(&mask); // 创建空集
Sigaddset(&mask, SIGINT); // 把 SIGINT 信号加入屏蔽列表中
// 阻塞对应信号,并保存之前的集合作为备份
Sigprocmask(SIG_BLOCK, &mask, &prev_mask);
...
... // 这部分代码不会被 SIGINT 中断
...
// 取消阻塞信号,恢复原来的状态
Sigprocmask(SIG_SETMASK, &prev_mask, NULL);
注意how的取值:
SIG_BLOCK:将set中的信号添加到blocked中;
SIG_UNBLOCKED:从blocked中删除set中的信号;
SIG_SETMASK:block = set;
如果oldset非空,那么blocked位向量之前的值都保存在oldset中。
安全处理信号
信号处理器的设计并不简单,因为它们和主程序并行且共享相同的全局数据结构,尤其要注意因为并行访问可能导致的数据损坏的问题,这里提供一些基本的指南(后面的课程会详细介绍)
- 规则 1:信号处理器越简单越好
- 例如:设置一个全局的标记,并返回
- 规则 2:信号处理器中只调用异步且信号安全(async-signal-safe)的函数
- 诸如
printf
,sprintf
,malloc
和exit
都是不安全的!
- 诸如
- 规则 3:在进入和退出的时候保存和恢复
errno
- 这样信号处理器就不会覆盖原有的
errno
值
- 这样信号处理器就不会覆盖原有的
- 规则 4:临时阻塞所有的信号以保证对于共享数据结构的访问
- 防止可能出现的数据损坏
- 规则 5:用
volatile
关键字声明全局变量- 这样编译器就不会把它们保存在寄存器中,保证一致性
- 规则 6:用
volatile sig_atomic_t
来声明全局标识符(flag)- 这样可以防止出现访问异常
这里提到的异步信号安全(async-signal-safety)指的是如下两类函数:
- 所有的变量都保存在栈帧中的函数
- 不会被信号中断的函数
Posix 标准指定了 117 个异步信号安全(async-signal-safe)的函数(可以通过 man 7 signal
查看)
非本地跳转 Non local Jump
所谓的本地跳转,指的是在一个程序中通过 goto 语句进行流程跳转,尽管不推荐使用goto语句,但在嵌入式系统中为了提高程序的效率,goto语句还是可以使用的。本地跳转的限制在于,我们不能从一个函数跳转到另一个函数中。如果想突破函数的限制,就要使用 setjmp
或 longjmp
来进行非本地跳转了。
setjmp
保存当前程序的堆栈上下文环境(stack context),注意,这个保存的堆栈上下文环境仅在调用 setjmp
的函数内有效,如果调用 setjmp
的函数返回了,这个保存的堆栈上下文环境就失效了。调用 setjmp
的直接返回值为 0。
longjmp
将会恢复由 setjmp
保存的程序堆栈上下文,即程序从调用 setjmp
处重新开始执行,不过此时的 setjmp
的返回值将是由 longjmp
指定的值。注意longjmp
不能指定0为返回值,即使指定了 0,longjmp
也会使 setjmp
返回 1。
我们可以利用这种方式,来跳转到其他的栈帧中,比方说在嵌套函数中,我们可以利用这个快速返回栈底的函数,我们来看如下代码
对应的跳转过程为:
jmp_buf env; P1()
{
if (setjmp(env))
{
// 跳转到这里
}
else
{
P2();
} } P2()
{
...
P2();
...
P3();
} P3()
{
longjmp(env, );
}
也就是说,我们直接从 P3 跳转回了 P1,但是也有限制,函数必须在栈中(也就是还没完成)才可以进行跳转,下面的例子中,因为 P2 已经返回,所以不能跳转了
因为 P2 在跳转的时候已经返回,对应的栈帧在内存中已经被清理,所以 P3 中的 longjmp
并不能实现期望的操作。
操作进程的Linux指令
PS | 列出当前系统的进程 |
TOP | 打印出当前进程资源使用的信息 |
PMAP | 显式进程的内存映射 |
/proc | 虚拟文件系统,以ASCII文本格式输出内核数据结构的内容 |
STRACE | 每个系统调用的轨迹 |
CSAPP读书笔记--第八章 异常控制流的更多相关文章
- [CSAPP笔记][第八章异常控制流][呕心沥血千行笔记]
异常控制流 控制转移 控制流 系统必须能对系统状态的变化做出反应,这些系统状态不是被内部程序变量捕获,也不一定和程序的执行相关. 现代系统通过使控制流 发生突变对这些情况做出反应.我们称这种突变为异常 ...
- CSAPP:第八章 异常控制流2
CSAPP:第八章 异常控制流2 关键点:进程控制.信号 8.4 进程控制8.5 信号 8.4 进程控制 Unix提供了大量从C程序中操作进程的系统调用.8.4.1 获取进程ID 每个进程都有一 ...
- CSAPP:第八章 异常控制流1
CSAPP:第八章 异常控制流1 关键点:异常 8.1 异常8.2 进程 现代系统通过使控制流发生突变来对这些情况做出反应,一般而言,我们把这些突变称为异常控制流(Exceptional Cont ...
- csapp:第八章 异常控制流ECF
第八章 异常控制流ECF 8.1 异常 Exception graph LR E[异常Exception]-->E2[中断:异步异常] E-->E3[同步异常] E3-->陷阱 E3 ...
- 【CSAPP笔记】14. 异常控制流和进程
从给处理器加电,到断电为止,处理器做的工作其实就是不断地读取并执行一条条指令.这些指令的序列就叫做 CPU 的控制流(control flow).最简单的控制流是"平滑的",也就是 ...
- 深入理解计算机系统 第八章 异常控制流 Part1 第二遍
第二遍读这本书,每周花两到三小时时间,能读多少读多少(这次看了第 500~507 页,共 8 页) 第一遍对应笔记链接 https://www.cnblogs.com/stone94/p/101651 ...
- 深入理解计算机系统 第八章 异常控制流 part1
本章主旨 第八章的目的是阐述清楚应用程序是如何与操作系统交互的(之前章节的学习是阐述应用程序是如何与硬件交互的) 异常控制流 异常控制流,即 ECF(exceptional contril flow) ...
- 深入理解计算机系统 第八章 异常控制流 Part2 第二遍
第二遍读这本书,每周花两到三小时时间,能读多少读多少(这次看了第 508~530 页,共 23 页) 第一遍对应笔记链接 https://www.cnblogs.com/stone94/p/10206 ...
- 《Effective Java》读书笔记八(异常)
No57 只针对异常的情况才使用异常 异常应该只用于异常的情况下,它们永远不应该用于正常的控制流. No58 对可恢复的情况使用受检异常,对编程错误使用运行时异常 Java程序设计语言提供了三种可抛出 ...
随机推荐
- vb.net与vb的区别
本文链接:https://blog.csdn.net/dfshsdr/article/details/63255645最近接触了vb.net,它增加了vb的很多特性,而且演化成为完全面向对象的编程语言 ...
- 第三周之Hadoop学习(三)
从上周的这篇教程中继续hadoop的安装过程:http://dblab.xmu.edu.cn/blog/install-hadoop-in-centos/ 上节课安装到对hadoop中的输出的文件夹的 ...
- tensorflow文本分类实战——卷积神经网络CNN
首先说明使用的工具和环境:python3.6.8 tensorflow1.14.0 centos7.0(最好用Ubuntu) 关于环境的搭建只做简单说明,我这边是使用pip搭建了python的 ...
- scala的trait执行报错: 错误: 找不到或无法加载主类 cn.itcast.scala.`trait`
scala的trait执行报错: 错误: 找不到或无法加载主类 cn.itcast.scala.`trait`.Children 原因:包名写成了trait,与trait关键字重名了: package ...
- C++11常用特性介绍——auto类型修饰符
1.C++11常用特性介绍 从本篇开始介绍C++11常用特性,大致分:关键字及新语法.STL容器.多线程.智能指针内存管理,最后讲一下std::bind和std::function 二.关键字和新语法 ...
- 使用IDEA查看数据库
emm...今天捣鼓了一下IDEA意外发现(原谅我后知后觉)不用Navicat也能查看数据库中的表,虽然可视化不如Navicat,但毕竟Navicat要钱,一些朋友也没有资源 = =,所以就记录并分享 ...
- HackerOne去年发放超过8200万美元的赏金,联邦政府参与度大幅上涨
2019年,由黑客驱动的漏洞赏金平台HackerOne支付的漏洞奖金几乎是前几年总和的两倍,达到8200万美元. HackerOne平台在2019年也将注册黑客数量翻了一番,超过了60万,同时全年收到 ...
- A easy and simple way to establish Oracle ADG
Yes, thanks to Then, I can give simple and reasy way to make it. Suppose hosts and IPs like that: 15 ...
- web前端面试第一次[定时器]
BOM中定时器--计时器 定时器参数两个:(函数,时间(单位ms(1000ms=1s))) 时间设置1s,每过1s执行一次函数 //设置定时器 setInterval(funtion(){ alert ...
- Oracle常用命令复习(备考资料)
Oracle期末考试复习资料,大概的总结了常用的命令,不包括基础理论知识,有的不太考的东西没有整理.资料整理是在有道云笔记里完成的,在这里重新编辑太麻烦了,就附个链接了. 文档:Oracle命令复习2 ...