深入理解计算机系统(CSAPP)
  • 本电子书信息
  • 出版信息
    • 出版者的话
    • 中文版序一
    • 中文版序二
    • 译者序
    • 前言
    • 关于作者
  • 第 1 章:计算机系统漫游
    • 1.1 信息就是位 + 上下文
    • 1.2 程序被其他程序翻译成不同的格式
    • 1.3 了解编译系统如何工作是大有益处的
    • 1.4 处理器读并解释储存在内存中的指令
    • 1.5 高速缓存至关重要
    • 1.6 存储设备形成层次结构
    • 1.7 操作系统管理硬件
    • 1.8 系统之间利用网络通信
    • 1.9 重要主题
    • 1.10 小结
  • 第一部分:程序结构和执行
    • 第 2 章:信息的表示和处理
      • 2.1 信息存储
      • 2.2 整数表示
      • 2.3 整数运算
      • 2.4 浮点数
      • 2.5 小结
      • 家庭作业
    • 第 3 章:程序的机器级表示
      • 3.1 历史观点
      • 3.2 程序编码
      • 3.3 数据格式
      • 3.4 访问信息
    • 第 4 章:处理器体系结构
    • 第 5 章:优化程序性能
    • 第 6 章:存储器层次结构
  • 第二部分:在系统上运行程序
    • 第 7 章:链接
      • 7.1 编译器驱动程序
      • 7.2 静态链接
      • 7.3 目标文件
      • 7.4 可重定位目标文件
      • 7.5 符号和符号表
      • 7.6 符号解析
      • 7.7 重定位
      • 7.8 可执行目标文件
      • 7.9 加载可执行目标文件
      • 7.10 动态链接共享库
      • 7.11 从应用程序中加载和链接共享库
      • 7.12 位置无关代码
      • 7.13 库打桩机制
      • 7.14 处理目标文件的工具
      • 7.15 小结
      • 家庭作业
    • 第 8 章:异常控制流
      • 8.1 异常
      • 8.2 进程
      • 8.3 系统调用错误处理
      • 8.4 进程控制
      • 8.5 信号
      • 8.6 非本地跳转
      • 8.7 操作进程的工具
      • 8.8 小结
      • 家庭作业
    • 第 9 章:虚拟内存
      • 9.1 物理和虚拟寻址
      • 9.2 地址空间
      • 9.3 虚拟内存作为缓存的工具
      • 9.4 虚拟内存作为内存管理的工具
      • 9.5 虚拟内存作为内存保护的工具
      • 9.6 地址翻译
      • 9.7 案例研究:Intel Core i7 / Linux 内存系统
      • 9.8 内存映射
      • 9.9 动态内存分配
      • 9.10 垃圾收集
      • 9.11 C 程序中常见的与内存有关的错误
      • 9.12 小结
      • 家庭作业
  • 第三部分:程序间的交互和通信
    • 第 10 章:系统级 I/O
      • 10.1 Unix I/O
      • 10.2 文件
      • 10.3 打开和关闭文件
      • 10.4 读和写文件
      • 10.5 用 RIO 包健壮地读写
      • 10.6 读取文件元数据
      • 10.7 读取目录内容
      • 10.8 共享文件
      • 10.9 I/O 重定向
      • 10.10 标准 I/O
      • 10.11 综合:我该使用哪些 I/O 函数?
      • 10.12 小结
      • 家庭作业
    • 第 11 章:网络编程
      • 11.1 客户端—服务器编程模型
      • 11.2 网络
      • 11.3 全球 IP 因特网
      • 11.4 套接字接口
      • 11.5 Web 服务器
      • 11.6 综合:TINY Web 服务器
      • 11.7 小结
      • 家庭作业
    • 第 12 章:并发编程
      • 12.1 基于进程的并发编程
      • 12.2 基于 I/O 多路复用的并发编程
      • 12.3 基于线程的并发编程
      • 12.4 多线程程序中的共享变量
      • 12.5 用信号量同步线程
      • 12.6 使用线程提高并行性
      • 12.7 其他并发问题
      • 12.8 小结
      • 家庭作业
  • 附录 A:错误处理
  • 参考文献
  • 实验
    • 实验总览
      • 常见问题
    • 实验 1:Data Lab
      • README(讲师版)
      • README(学生版)
      • Writeup
    • 实验 2:Bomb Lab
      • README(讲师版)
      • Writeup
    • 实验 3:Attack Lab
    • 实验 4:Architechture Lab
    • 实验 5:Cache Lab
    • 实验 6:Performance Lab
    • 实验 7:Shell Lab
    • 实验 8:Malloc Lab
    • 实验 9:Proxy Lab
由 GitBook 提供支持
在本页
  1. 第二部分:在系统上运行程序
  2. 第 8 章:异常控制流

8.6 非本地跳转

C 语言提供了一种用户级异常控制流形式,称为非本地跳转(non local jump),它将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用—返回序列。非本地跳转是通过 setjmp 和 longjmp 函数来提供的。

#include <setjmp.h>
int setjmp(jmp_buf env);
int sigsetjmp(sigjmp_buf env, int savesigs);

// 返回:setjmp 返回 0,longjmp 返回非零。

setjmp 函数在 env 缓冲区中保存当前调用环境,以供后面的 longjmp 使用,并返回 0。调用环境包括程序计数器、栈指针和通用目的寄存器。岀于某种超出本书描述范围的原因,setjmp 返回的值不能被赋值给变量:

rc = setjmp(env);  /* Wrong! */

不过它可以安全地用在 switch 或条件语句的测试中【62】。

#include <setjmp.h>

void longjmp(jmp_buf env, int retval);
void siglongjmp(sigjmp_buf env, int retval);

// 从不返回。

longjmp 函数从 env 缓冲区中恢复调用环境,然后触发一个从最近一次初始化 env 的 setjmp 调用的返回。然后 setjmp 返回,并带有非零的返回值 retval。

第一眼看过去,setjmp 和 longjmp 之间的相互关系令人迷惑。setjmp 函数只被调用一次,但返回多次:一次是当第一次调用 setjmp,而调用环境保存在缓冲区 env 中时,一次是为每个相应的 longjmp 调用。另一方面,longjmp 函数被调用一次,但从不返回。

非本地跳转的一个重要应用就是允许从一个深层嵌套的函数调用中立即返回,通常是由检测到某个错误情况引起的。如果在一个深层嵌套的函数调用中发现了一个错误情况,我们可以使用非本地跳转直接返回到一个普通的本地化的错误处理程序,而不是费力地解开调用栈。

图 8-43 展示了一个示例,说明这可能是如何工作的。main 函数首先调用 setjmp 以保存当前的调用环境,然后调用函数 foo,foo 依次调用函数 bar。如果 foo 或者 bar 遇到一个错误,它们立即通过一次 longjmp 调用从 setjmp 返回。setjmp 的非零返回值指明了错误类型,随后可以被解码,且在代码中的某个位置进行处理。

#include "csapp.h"

jmp_buf buf;

int error1 = 0;
int error2 = 1;

void foo(void), bar(void);

int main()
{
    switch (setjmp(buf)) {
    case 0:
        foo();
        break;
    case 1:
        printf("Detected an error1 condition in foo\n");
        break;
    case 2:
        printf("Detected an error2 condition in foo\n");
        break;
    default:
        printf("Unknown error condition in foo\n");
    }
    exit(0);
}

/* Deeply nested function foo */
void foo(void)
{
    if (error1)
        longjmp(buf, 1);
    bar();
}

void bar(void)
{
    if (error2)
        longjmp(buf, 2);
}

图 8-43 非本地跳转的示例。本示例表明了使用非本地跳转来从深层嵌套的函数调用中的错误情况恢复,而不需要解开整个栈的基本框架

longjmp 允许它跳过所有中间调用的特性可能产生意外的后果。例如,如果中间函数调用中分配了某些数据结构,本来预期在函数结尾处释放它们,那么这些释放代码会被跳过,因而会产生内存泄漏。

非本地跳转的另一个重要应用是使一个信号处理程序分支到一个特殊的代码位置,而不是返回到被信号到达中断了的指令的位置。图 8-44 展示了一个简单的程序,说明了这种基本技术。当用户在键盘上键入 Ctrl+C 时,这个程序用信号和非本地跳转来实现软重启。sigsetjmp 和 siglongjmp 函数是 setjmp 和 longjmp 的可以被信号处理程序使用的版本。

#include "csapp.h"

sigjmp_buf buf;

void handler(int sig)
{
    siglongjmp(buf, 1);
}

int main()
{
    if (!sigsetjmp(buf, 1)) {
        Signal(SIGINT, handler);
        Sio_puts("starting\n");
    }
    else
        Sio_puts("restarting\n");

    while (1) {
        Sleep(1);
        Sio_puts("processing...\n");
    }
    exit(0); /* Control never reaches here */
}

图 8-44 当用户键入 Ctrl+C 时,使用非本地跳转来重启动它自身的程序

在程序第一次启动时,对 sigsetjmp 函数的初始调用保存调用环境和信号的上下文(包括待处理的和被阻塞的信号向量)。随后,主函数进入一个无限处理循环。当用户键入 Ctrl+C 时,内核发送一个 SIGINT 信号给这个进程,该进程捕获这个信号。不是从信号处理程序返回,如果是这样那么信号处理程序会将控制返回给被中断的处理循环,反之,处理程序完成一个非本地跳转,回到 main 函数的开始处。当我们在系统上运行这个程序时,得到以下输出:

linux> ./restart
starting
processing...
processing...
Ctrl+C
restarting
processing...
Ctrl+C
restarting
processing...

关于这个程序有两件很有趣的事情。首先,为了避免竞争,必须在调用了 sigsetjmp 之后再设置处理程序。否则,就会冒在初始调用 sigsetjmp 为 siglongjmp 设置调用环境之前這行处理程序的风险。其次,你可能已经注意到了,sigsetjmp 和 siglongjmp 函数不在图 8-33 中异步信号安全的函数之列。原因是一般来说 siglongjmp 可以跳到任意代码,所以我们必须小心,只在 siglongjmp 可达的代码中调用安全的函数。在本例中,我们调用安全的 sio_puts 和 sleep 函数。不安全的 exit 函数是不可达的。

旁注 - C++ 和 Java 中的软件异常

C++ 和 Java 提供的异常机制是较高层次的,是 C 语言的 setjmp 和 longjmp 函数的更加结构化的版本。你可以把 try 语句中的 catch 子句看做类似于 setjmp 函数。相似地,throw 语句就类似于 longjmp 函数。

上一页8.5 信号下一页8.7 操作进程的工具

最后更新于8个月前