[csapp] 第八章 异常控制流

程序计数器假设是a的序列。每个ak是某个相应指令Ik的地址。从ak到ak+1的过渡称为控制转移。这种控制转移序列叫做处理器的控制流。

线代系统通过使控制流发生突变来对某些情况做出反应。我们把这些突变称为异常控制流(ECF)。

8.1 异常

当处理器检测到有事件发生时,他就会通过一张叫做异常表的跳转表,进行一个间接过程调用,到一个专门处理这类事件的操作系统子程序(异常处理程序)。

系统启动时,操作系统分配和初始化异常表,使得表目包含处理程序的地址。

在运行时,处理器检测到发生一个时间,并确定异常号k。随后,处理器触发异常,方法是执行间接过程调用,通过异常表表目转到相应的处理程序。

异常表的起始地址放在一个叫做异常表基址寄存器的特殊CPU寄存器里。

异常类似于过程调用,但有不同之处:

1. 过程调用时,在跳转到处理程序之前,处理器将返回地址压入栈中。然而,根据异常类型,返回地址要么是当前指令,要么是下一条指令。

2.处理器也把一些额外的处理器状态压到栈里,处理程序返回时,重新开始执行被中断的程序会需要这些状态。比如X86里的EFLAGS寄存器。

3. 如果控制从用户程序转到内核,所有这些项目都被压到内核栈中,而不是用户栈。

4.异常处理程序运行在内核模式下,这意味着它们对所有的系统都有完全的访问权限。

8.1.2 异常类别

1.中断

中断是异步发生的,是来自处理器外部IO设备的信号的结果。硬件中断不是由任何一条专门的指令造成的,从这个意义上它是异步的。硬件中断的异常处理程序常常称为中断处理程序。

IO设备通过向处理器芯片上的一个引脚发信号,并将异常号放到系统总线上,来触发中断。在当前指令完成执行之后,处理器注意到中断引脚的电压变高,就从系统总线读取异常号,然后调用中断处理程序。处理程序返回时他就将控制返回给下一条指令。

2. 陷阱和系统调用

陷阱是有意的异常。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。

执行syscall指令会导致一个到异常处理程序的陷阱,这个处理程序解析参数并调用适当的内核程序。系统调用运行在内核模式中,内核模式允许系统调用执行特权指令,并访问定义在内核中的栈。

3. 故障

故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则返回到内核中的abort例程,该例程会终止引起故障的程序。

一个经典的故障示例是缺页异常。

4. 终止

终止是不可恢复的致命错误造成的结果。通常是硬件错误,终止处理程序将控制返回给abort例程。

8.1.3 Linux 系统中的异常

除法错误。当应用试图除以零时,或者一个除法指令的结果对于目标操作数来说太大。Linux通常把除法错误报告为“浮点异常”。

一般保护故障。通常是因为一个程序引用了未定义的虚拟内存区域,或者因为程序试图写一个只读的文本段。Linux会把这种一般保护故障报告为“段故障”。

每个系统调用都有唯一的整数号,对应于一个到内核中跳转表的偏移量。(不是异常表)

所有到Linux系统调用的参数都是通过通用寄存器而不是栈传递的。

%rax包含系统调用号,%rdi第一个参数,%rsi第二个,%rdx,%r10,%r8,%r9。

从系统调用返回时,%rcx和%r11都会被破坏,%rax包含返回值。-4095到-1之间的负数返回值表明发生了错误,对应于负的errno.

8.2 进程

异常是允许操作系统内核提供进程概念的基本构造块。

进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中程序的代码和数据,栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。

关键抽象:

1.一个独立的逻辑控制流,好像在独占处理器。

2.一个私有的地址空间,好像在独占内存。

8.2.1 逻辑控制流

每个竖直的条表示一个进程的逻辑流的一部分。

图的关键点在于进程是轮流使用处理器的。

8.2.2 并发流

一个逻辑流的执行在时间上与另一个流重叠,称为并发。一个进程与其他进程轮流运行的概念称为多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。

如果两个流并发地运行在不同的处理器核或者计算机上,我们称之为并行流。

8.2.3 私有地址空间

进程为每个程序提供它自己的私有地址空间。一般而言,其他进程不可读写。从这个意义上说,这个地址空间是不可读写的。

每个这样的空间都有相同的通用结构。

8.2.4 用户模式和内核模式

处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。

处理器通常是用某个控制寄存器的一个模式位来提供这种功能的。当设置了模式位时,进程就运行在内核模式中。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。

进程从用户模式变为内核模式的唯一方法是通过诸如中断,故障或者陷入系统调用这样的异常。

Linux提供了一种机制,叫做/proc文件系统,它允许用户模式进程访问内核数据结构的内容。/proc文件系统将许多内核数据结构的内容输出位一个用户程序可以读的文本文件的层次结构。比如可以找出一般的系统属性,比如CPU类型(/proc/cpuinfo),或者某个特殊的进程使用的内存段(/proc/<process-id>/maps)。

/sys文件系统输出关于系统总线和设备的额外的低层信息。

8.2.5 上下文切换

上下文就是内核重新启动一个被抢占的进程所需的状态。包括通用目的寄存器,浮点寄存器,程序计数器,用户栈,状态寄存器,内核栈和各种内核数据结构,比如描述地址空间的页表,包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。

内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策叫做调度,是由内核中称为调度器的代码处理的。

上下文切换:

1.保存当前进程的上下文。

2.恢复某个先前被抢占的进程被保存的上下文。

3.将控制传递给这个新恢复的进程。

当内核代表用户执行系统调用时,可能会发生上下文切换。如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程。比如访问磁盘。另一个示例是sleep系统调用。

中断也可能引发上下文切换。时间片。

8.3 系统调用错误处理

通过使用错误处理包装函数,可以简化代码。

8.4 进程控制

8.4.1 获取进程ID

每个进程都有唯一的正数PID。getpid返回调用进程的pid。getpid返回它的父进程的PID。

8.4.2 创建和终止进程

进程总是处于以下三种状态之一:

运行。要么在CPU上执行,要么在等待被执行。

停止。进程的执行被挂起,且不会被调度。直到收到SIGCONT信号。

终止。进程永远停止。三种原因:1.收到终止进程信号。2.从主程序返回。3.调用exit函数。

父进程通过fork创建新进程。

父进程与子进程之间最大的区别是他们有不同的PID。

1.调用一次,返回两次。

2.并发执行。父进程和子进程是并发运行的独立进程。内核能够以任意方式交替执行。

3.相同但是独立的地址空间。

4.共享文件。父进程和子进程都把输出显示在屏幕上。原因是父进程调用fork时stdout文件是打开的并指向屏幕,子进程继承了这个文件。

进程图是刻画程序语句的偏序的前趋图。每个顶点对应一条程序语句的执行。

8.4.3 回收子进程

当进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被父进程回收。当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程。一个终止了但还未被回收的进程称为僵死进程(zombie)。

如果一个父进程终止了,内核会安排Init进程成为它孤儿进程的养父。Init的PID是1,系统启动时由内核创建。它不会终止,是所有进程的祖先。

即使僵死子进程没有运行,仍然消耗系统的内存资源。

pid_t waitpid(pid_t pid, int *statusup, int options);

如果成功则为子进程的PID,如果为WNOHANG,则为0。错误则为1。

默认情况下,waitpid挂起调用进程的执行,直到它的等待集合中的一个子进程终止。如果等待集合中的一个进程在刚调用的时候就已经终止了,那么waitpid立即返回。此时,已终止的子进程被回收。

1.判定等待集合的成员

如果pid>0, 那么等待集合就是一个单独的子进程。

如果pid=-1,那么等待集合就是由父进程的所有子进程组成的。

2.修改默认行为

可以通过将option设置为常量的各种组合来修改默认行为:

WHOHANG:如果等待集合中的任何子进程都没有终止,就立即返回0。默认的行为是挂起进程直到有终止。

WUNTRACED: 挂起调用进程的执行,直到等待集合中的一个进程变成已终止或者被停止。返回PID。默认的行为是只返回已终止的进程。

WCONTINUED:挂起调用进程的执行,直到等待集合中一个正在运行的进程终止或等待集合中一个被停止的进程收到SIGCONT信号重新开始执行。

WNOHANG| WUNTRACED:立即返回,如果等待集合都没有被停止或终止,则返回0;如果有一个停止或终止,则为PID。

3.检查已回收子进程的退出状态

如果statusp参数是非空的,那么waitpid就会在*statusp中放上关于导致返回的子进程的状态信息。

WIFEXITED:子进程通过调用exit或return正常终止,就返回真。

WEXITSTATUS:返回一个正常终止的子进程的退出状态。

WIFSIGNALED:子进程是因为一个未被捕获的信号终止的。

WTERMSIG:返回导致子进程终止的信号的编号。

WIFSTOPPED:引起返回的子进程当前是停止的。

WSTOPSIG:返回引起子进程停止的信号的编号。

WIFCONTINUED:如果子进程收到SIGCONT信号重新启动,则返回真。

4. 错误条件

如果调用进程没有子进程,那么waitpid返回-1,并设置errno为ECHILD。如果waitpid函数被一个信号中断,那么返回-1,设置errno为EINTR。

5. wait函数

pid_t wait(int *statusp);

如果成功,则为子进程PID。出错为-1。

调用wait(&status)等价于调用waitpid(-1,&status,0);

6.使用waitpid的示例

展示一个简单的改变,它消除了顺序的不确定性。

8.4.4 让进程休眠

sleep函数将一个进程挂起一个指定时间。

unsigned int sleep(unsigned int secs);

如果请求的时间量已经到了,sleep返回0,否则返回还剩下的要休眠的秒数。

pause函数让调用函数休眠,直到该进程收到一个信号。

int pause(void);

8.4.5 加载并运行程序

int execve(const char* filename,const char *argv[],const char *envp[]);

如果成功则不返回,如果错误则返回-1。

execve函数加载并运行可执行目标文件filename,只有出现错误时,例如找不到file,execve才会返回到调用程序。execve调用一次并从不返回。

argv变量指向一个以null结尾的指针数组,其中每个指针都指向一个参数字符串。按照惯例,argv[0]是可执行目标文件的名字。

envp变量指向一个以null结尾的指针数组,其中每个指针指向一个环境变量字符串,每个串都是形如“name=value”。

在execve加载了filename后,它调用7.9节中描述的启动代码。启动代码设置栈,并将控制传递给新程序的主函数。

当main开始执行时,用户栈的结构如图所示。

int main(int argc, char*argv[], char *envp[]);

argc,给出argv[]数组中非空指针的数量;

argv, 指向argv[]数组中的第一个条目;

envp, 指向envp[]数组中的第一个条目。

#include<stdlib.h>

char *getenv(const char*name);

getenv函数在环境数组中搜索匹配name的字符串,返回一个指向value的指针。

#include<stdlib.h>

int setenv(const char* name,const char* newvalue, int overwrite);
//若成功则为0,错误为-1

void unsetenv(const char* name);

如果环境数组包含一个形如“name=oldvalue”的字符串,那么unsetenv会删除它,用newvalue代替oldvalue,但是只有在overwrite非零时才会这样。如果name不存在,那么setenv就把“name=newvalue”添加到数组中。

程序和进程

程序可以作为目标文件存在于磁盘,或者作为段存在于地址空间。

进程是执行中程序的一个具体实例。

fork函数在新的子进程运行相同的程序。

execve函数在当前进程的上下文中加载并运行一个新的程序。它会覆盖当前进程的地址空间。

8.4.6 利用fork和execve运行程序

shell是一个交互性应用级程序,它代表用户运行其他程序。

展示一个简单shell的main:

parseline函数,这个函数解析了以空格分隔的命令行参数,并构造最终会传递给execve的argv向量。如果最后一个参数是“&”,表示应该在后台执行(shell不会等他完成)。

eval函数调用builtin_command函数,该函数检查第一个命令行参数是否是一个内置的shell命令。如果是就立即解释这个命令,这里只列出了exit。如果不是,那么shell创建一个子进程,并在子进程中执行所请求的程序。

如果用户要求在后台运行该程序,那么shell返回循环顶部,等待下一个命令行。否则shell指令使用waitpid函数等待作业终止,作业终止再开始下一轮循环。

这个shell的缺陷是不回收后台子进程。修改这个缺陷要求使用信号。

8.5 信号

我们了解了硬件和软件是如何合作以提高基本的底层异常机制的,我们也看到了操作系统如何利用异常来支持进程上下文切换的异常控制流形式。在本节中,我们研究一种更高层的软件形式的异常,称为linux信号,它允许进程和内核中断其他进程。

一个信号就是一条小消息,他通知进程系统中发生了一个某种类型的事件。

如果一个进程除以0,SIGFPE(浮点异常)

执行非法指令,SIGILL。

非法内存引用,SIGSEGV(段故障)

CTRL+C,键盘中断,SIGINT给这个前台进程组的每个进程。

一个进程可以通过向另一个进程发送SIGKILL信号强制终止。

一个子进程终止或停止时,内核会发送一个SIGCHLD给父进程。

8.5.1 信号术语

传送一个信号到目的进程两个步骤:

1.发送信号。内核通过更新目的进程上下文中的某个状态,发送一个信号给目的进程。

发送信号的原因:

1.内核检测到一个系统事件,比如除零错误或子进程终止。

2.一个进程调用了Kill函数,显示的要求内核发送一个信号给目的进程。一个进程可以发送信号给他自己。

2.接收信号。当目的进程被内核强迫以某种方式对信号的发送做出反应时,就接受了信号。进程可以忽略、终止或者通过执行信号处理程序的用户层函数捕获这个信号。

一个发出而没有被接收的信号叫做待处理信号。在任何时刻,一种类型至多只会有一个待处理信号。如果一个进程有一个类型为k的待处理信号,那么任何接下来发送到这个进程的类型为K的信号都被简单的丢弃。

一个进程可以有选择性的阻塞接收某种信号。当一种信号被阻塞时,它仍可以被发送,但是产生的待处理信号不会被接收,直到进程取消对这种信号的阻塞。

内核为每个进程在pending位向量中维护着待处理信号的集合,blocked位向量维护着被阻塞的信号集合。只要传送了一个类型为K的信号,内核就会设置pending中的第K位,而只要接收了一个类型为k的信号,内核就会清除pending中的第k位。

8.5.2 发送信号

大量向进程发送信号的机制都是基于进程组这个概念的。

1.进程组

#include<unistd.h>

int getpgrp(void);

//返回进程组id

默认一个子进程和他的父进程属于一个进程组。一个进程可以通过使用setpgid函数来改变自己或者其他进程的进程组。

int setpgid(pid_t pid,pid_t pgid);

如果Pid是0,那么就使用当前进程的pid。如果pgid是0,那么就用pid指定的进程的pid作为进程组id。

例如,如果进程15213是调用进程,那么setpgid(0,0); 会创建一个新的进程组,并且把进程15213加入到这个新的进程组中。

2. 用/bin/kill 程序发送信号

/bin/kill 程序可以向另外的进程发送任意的信号。

linux> /bin/kill -9 15213

发送信号9(SIGKILL)给进程15213。

linux> /bin/kill -9 -15213

发送一个SIGKILL信号给进程组15213中的每个进程。一个负的PID会导致信号被发送到进程组PID中的每个进程。

3.从键盘发送信号

shell使用作业(job)这个抽象概念来表示为对一条命令行求值而创建的进程。在任何时候,至多只有一个前台作业和若干后台作业。shell为每个作业创建一个独立的进程组。

linux> ls | sort

会创建一个由两个进程组成的前台作业,这两个进程是通过unix管道连接起来的。

在键盘上输入ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认结果是终止前台作业。

输入ctrl+z会发送一个SIGTSTP信号到前台进程组的每个进程,默认结果是停止前台作业。

4. 用kill函数发送信号

#include<sys/types.h>
#include<signal.h>

int kill(pid_t pid,int sig);
//成功则返回0,失败返回-1

pid>0,发送给进程pid

pid=0,发送给调用进程所在进程组的每个进程,包括调用进程自己。

pid<0, 发送给进程组|pid|的每个进程。

5. 用alarm函数发送信号

#include<unistd.h>

unsigned int alarm(unsigned int secs);
//返回前一次闹钟剩余的描述,如果没有则为0

alarm函数安排内核在secs秒后发送一个AIGALRM信号给调用进程。在任何情况下,对alarm的调用都将取消任何待处理的闹钟,并返回待处理闹钟在被发送前还剩下的秒数。

8.5.3 接收信号 

当内核把进程p从内核模式切换到用户模式时,它会检查进程p的未被阻塞的待处理信号的集合。如果集合为空,那么内核将控制传递到p的逻辑控制流的下一条指令。如果集合非空,那么内核通常选择集合中最小的k并强制p接收信号k。信号触发进程采取某种行为,一旦进程完成了那么控制就传递给p的下一条指令。

进程可以通过使用signal函数修改和信号相关联的默认行为。唯一的例外是SIGSTOP和SIGKILL不能修改。

#include<signal.h>
typedef void (*sighandler_t)(int); 
sighandler_t signal(int signum,sighandler_t handler);

定义一个函数指针,参数为int,返回类型void。

若成功则返回指向前次处理程序的指针(没改之前),出错则为SIG_ERR。

如果handler是SIG_IGN,那么忽略类型为sigsum的信号。

如果是SIG_DFL,那么signum的信号行为将恢复为默认行为。

如果handler是用户定义的函数地址,这个函数被称为信号处理程序。调用信号处理程序被称为捕获信号,执行信号处理程序被称为处理信号。

信号处理程序可以被其他信号处理程序中断。

8.5.4 阻塞和解除阻塞信号

隐式阻塞机制。内核默认阻塞任何当前处理程序正在处理信号类型的待处理的信号。

显式阻塞机制。应用程序可以使用sigprocmask函数和它的辅助函数明确的阻塞和解除阻塞。

#include<signal.h>
int sigprocmask(int how, const sigset_t *set,sigset_t *oldset);

sigprocmash函数改变当前阻塞的信号集合。具体的行为依赖how的值:

SIG_BLOCK: 把set的信号添加到blocked中。

SIG_UNBLOCK: 从blocked中删除set中的信号。

SIG_SETMASK:block=set。

如果oldset非空,那么blocked位向量之前的值保存在oldset中。

使用下述函数对set信号集合进行操作:sigemptyset初始化set为空集合。sigfillset函数把每个信号都添加到set中。sigaddset函数把signum添加到set,sigdelset从set中删除signum。 如果signum是set的成员,那么sigismember返回1,否则返回0。

8.5.5 编写信号处理程序

处理程序有几个属性使得他们很难推理分析:

1.处理程序与主程序并发运行,共享同样的全局变量,因此可能与主程序和其他处理程序互相干扰。

2.如何以及何时接收信号的规则有违人的直觉。

3.不同的系统有不同的信号处理语义。

1.安全的信号处理

一些保守的编写处理程序的原则使得这些处理程序能安全的并发运行。

1.处理程序要尽可能简单。

2.在处理程序中只调用异步信号安全的函数。要么是可重入的(如只访问局部变量),要么不能被信号处理程序中断。

许多常见的函数都不在此列。(printf,sprintf,malloc和exit)。

信号处理程序中产生输出唯一安全的方法是使用write函数。

write(STDOUT_FILENO,s,strlen(s));

3. 保存和恢复errno。许多异步信号安全的函数都会在出错返回时设置errno。在处理程序中调用这样的函数可能会干扰主程序中其他依赖errno的部分。解决方法是在进入处理程序时把errno保存在一个局部变量中,在处理程序返回前恢复它。

4.阻塞所有的信号,保护对共享全局数据结构的访问。如果处理程序和主程序或者其他处理程序共享一个全局数据结构,那么在访问该数据结构时,处理程序和主程序应该暂时阻塞所有的信号。

5. 用volatile声明全局变量。可以用volatile来定义一个变量,高速编译器不要缓存这个变量。volatile强迫编译器每次都要从内存读取g的值。

6. 用sig_atomic_t 声明标志。对于共享的标志,C提供一种整型数据类型,对他的读写保证会是原子的,因为可以用一条指令来实现他们:

volatile sig_stomic_t flag;

因为他们不可中断,所以可以安全的读和写sig_atomic_t变量,而不需要暂时阻塞信号。

注意这里对原子性的保证只适用于单个的读和写,不适合flag++这样的更新,因为可能需要多条指令。

2.正确的信号处理

每种类型最多只能有一个未处理的信号,这样会如何影响正确性。

由此得到的教训是不可以用信号来对其他进程中发生的事件计数。

为了修正这个问题,我们回想一下,存在一个待处理的信号只是暗示自进程最后一次收到一个信号以来,至少已经有一个这种类型的信号被发送了。因此我们必须使得SIGCHLD处理程序被调用时,回收尽可能多的僵死子进程。

父进程只回收了两个进程。在处理第一个信号时,第二个信号加入待处理,第三个信号被丢弃。

处理程序一被调用就回收现今所有僵死子进程。

3. 可移植的信号处理

unix信号处理的另一个缺陷是不同的系统有不同的信号处理语义。要解决这些问题,posix提出了sigaction函数,他要求用户设置一个复杂结构的条目。Signal是它的包装函数,调用方式和signal一样。其信号处理语义如下:

1. 只有这个程序当前正在处理的那种类型的信号被阻塞。

2.信号不会排队等待。

3.被中断的系统调用会自动重启

4.一旦设置了信号处理程序就会一直保持。

8.5.6 同步流以避免讨厌的并发错误

某个shell结构。当父进程创建新的子进程后就把子进程添加到作业列表中,当父进程在SIGCHLD处理程序中回收一个僵死子进程时,他就从作业列表中删除这个子进程。

考虑这种情况: 子进程比父进程的addjob先执行exit,导致还没加进作业列表就终止了,此时SIGCHLD因为作业列表为空,deljob什么也不做。

这是一个称为竞争的经典同步错误的示例。

下图展示了一种消除竞争的方法,在调用fork前,阻塞SIGCHLD信号,在调用addjob后再取消阻塞。注意子进程继承了父进程的被阻塞集合,所以必须在子进程调用execve前小心的解除阻塞信号。

8.5.7 显式地等待信号

有些主程序需要显式地等待某个信号处理程序运行。例如shell创建前台作业时,在接收下一条用户命令前,它必须等待作业终止,即等待SIGCHLD。

使用while(1)太浪费资源。

while(!pid)

pause();

如果在while后和pause之前收到SIGCHLD,pause会永远睡眠。

while(!pid)

sleep(1);

无法确定休眠间隔。

合适的解决方法是使用sigsuspend

int sigsuspend(const sigset_t *mask);

sigsuspend暂时用mask替换当前的阻塞集合,然后挂起该进程,直到收到一个信号。

在本例中,sigsuspend会暂时取消阻塞SIGCHLD,然后休眠,直到父进程捕获信号。在返回之前,他会恢复原始的阻塞集合,再次阻塞SIGCHLD。

如果父进程捕获一个SIGINT信号,那么会继续循环。如果父进程捕获一个SIGCHLD,会退出循环。

8.6 非本地跳转

C语言提供了一种用户级异常控制流形式,它将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用返回序列。

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

setjmp函数在env缓冲区中保存当前环境以供后面的longjmp使用。调用环境包括程序计数器,栈指针和通用目的寄存器。setjmp返回的值不能赋值给变量,不过可以用在switch或条件语句。

#include<setjmp.h>
void longjmp(jmp_buf env,int retval);
void siglongjmp(sigjmp_buf env,int retval

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

非本地跳转的一个重要应用就是允许从一个深层嵌套的函数调用中立即返回,通常是由检测到某个错误引起的。

jmp_buf buf;

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

非本地跳转的另一个重要应用是使一个信号处理程序分支到特殊代码位置,而不是返回到被信号到达中断了的指令位置。

这个程序用信号和非本地跳转实现软重启。

sigsetjmp和siglongjmp是可以被信号处理程序使用的版本。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值