第八章 异常控制流

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();
}

在下面三种情况时,进程会被终止

  1. 接收到一个终止信号
  2. 返回到 main
  3. 调用了 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)的函数
    • 诸如 printfsprintfmalloc 和 exit 都是不安全的!
  • 规则 3:在进入和退出的时候保存和恢复 errno
    • 这样信号处理器就不会覆盖原有的 errno 值
  • 规则 4:临时阻塞所有的信号以保证对于共享数据结构的访问
    • 防止可能出现的数据损坏
  • 规则 5:用 volatile 关键字声明全局变量
    • 这样编译器就不会把它们保存在寄存器中,保证一致性
  • 规则 6:用 volatile sig_atomic_t 来声明全局标识符(flag)
    • 这样可以防止出现访问异常

这里提到的异步信号安全(async-signal-safety)指的是如下两类函数:

  1. 所有的变量都保存在栈帧中的函数
  2. 不会被信号中断的函数

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读书笔记--第八章 异常控制流的更多相关文章

  1. [CSAPP笔记][第八章异常控制流][呕心沥血千行笔记]

    异常控制流 控制转移 控制流 系统必须能对系统状态的变化做出反应,这些系统状态不是被内部程序变量捕获,也不一定和程序的执行相关. 现代系统通过使控制流 发生突变对这些情况做出反应.我们称这种突变为异常 ...

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

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

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

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

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

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

  5. 【CSAPP笔记】14. 异常控制流和进程

    从给处理器加电,到断电为止,处理器做的工作其实就是不断地读取并执行一条条指令.这些指令的序列就叫做 CPU 的控制流(control flow).最简单的控制流是"平滑的",也就是 ...

  6. 深入理解计算机系统 第八章 异常控制流 Part1 第二遍

    第二遍读这本书,每周花两到三小时时间,能读多少读多少(这次看了第 500~507 页,共 8 页) 第一遍对应笔记链接 https://www.cnblogs.com/stone94/p/101651 ...

  7. 深入理解计算机系统 第八章 异常控制流 part1

    本章主旨 第八章的目的是阐述清楚应用程序是如何与操作系统交互的(之前章节的学习是阐述应用程序是如何与硬件交互的) 异常控制流 异常控制流,即 ECF(exceptional contril flow) ...

  8. 深入理解计算机系统 第八章 异常控制流 Part2 第二遍

    第二遍读这本书,每周花两到三小时时间,能读多少读多少(这次看了第 508~530 页,共 23 页) 第一遍对应笔记链接 https://www.cnblogs.com/stone94/p/10206 ...

  9. 《Effective Java》读书笔记八(异常)

    No57 只针对异常的情况才使用异常 异常应该只用于异常的情况下,它们永远不应该用于正常的控制流. No58 对可恢复的情况使用受检异常,对编程错误使用运行时异常 Java程序设计语言提供了三种可抛出 ...

随机推荐

  1. spark实验(一)--linux系统常见命令及其文件互传(2)

    2.使用 Linux 系统的常用命令 启动 Linux 虚拟机,进入 Linux 系统,通过查阅相关 Linux 书籍和网络资料,或者参考 本教程官网的“实验指南”的“Linux 系统常用命令”,完成 ...

  2. robot framework 如何自己写模块下的方法或者库

    一.写模块(RF能识别的模块) 例如:F:\Python3.4\Lib\site-packages\robot\libraries这个库(包)下面的模块(.py),我们可以看下源码 注意:这种是以方法 ...

  3. PTA的Python练习题(二)

    继续在PTA上练习Python (从 第2章-5 求奇数分之一序列前N项和  开始) 1. x=int(input()) a=i=1 s=0 while(i<=x): s=s+1/a a=a+2 ...

  4. 吴裕雄--天生自然PythonDjangoWeb企业开发:学员管理系统- 前台

    开发首页 做一个简单的用户提交申请的表单页面. 首先在student/views.py文件中编写下面的代码: # -*- coding: utf-8 -*- from __future__ impor ...

  5. 吴裕雄--天生自然ORACLE数据库学习笔记:表分区与索引分区

    create table ware_retail_part --创建一个描述商品零售的数据表 ( id integer primary key,--销售编号 retail_date date,--销售 ...

  6. python学习 第一章(说不定会有第零章呢)one day

    ------------恢复内容开始------------ 一.啥是python python是吉尔·范罗苏姆于1989年开发的一个新的脚本解释程序,是ABC语言的一种继承. 二.python的特点 ...

  7. 二、linux基础-路径和目录_用户管理_组_权限

    2.1路径和目录1.相对路径:参照当前目录进行查找.   如:[root@localhost ~]# cd ../opt/hosts/备注:相对路径是从你的当前目录开始为基点,去寻找另外一个目录(或者 ...

  8. How to recover if NMC cound not connect

    Some times we suddently find that the NMC can not login,. You would see the sybase database error if ...

  9. windows下pycharm连接vagrant的python环境

  10. Design and History FAQ for Python3

    Source : Design and History FAQ for Python3 Why is there no goto? 你可以通过异常来获得一个可以跨函数调用的 "goto 结构 ...