从给处理器加电开始,直到断电为止,程序计数器假设一个值的序列:a0, a1, a2, …, an。其中每个 a(k) 都是某个相应的指令 I(k) 的地址。
每次从 a(k) 到 a(k+1) 的过渡称为控制转移。这样的控制转移序列叫做处理器的控制流(control flow)。
最简单的控制流是一个平滑的序列,其中每个 I(k) 和 I(k+1) 都是相邻的。
异常控制流 ECF 发生在计算机系统的各个层次:
ECF 的应用:
**异常(exception)**是异常控制流的一种形式,一部分由硬件实现,一部分由操作系统实现。异常位于硬件和操作系统交界的部分。
**注意:**这里的异常和 C++ 或 Java 中的应用级异常是不同的。
异常就是控制流中的突变,用来响应处理器状态中的某些变化。在处理器中,状态被编码为不同的位和信号,状态变化称为事件(event)。
**事件的例子:**发生虚拟内存缺页、算术溢出、一条指令试图除以 0、一个系统定时器产生的信号等。
任何情况下,当处理器检测到有事件发生时,就会通过一张叫做异常表的跳转表,进行一个间接过程调用(异常),到操作系统中一个专门用来处理这类事件的子程序(异常处理程序)。
当异常处理程序完成处理后,根据引起异常的事件的类型,会发生以下三种情况中的一种:
系统为每种可能的异常都分配了一个唯一的非负整数的异常号:
在系统启动时,操作系统分配和初始化一张异常表,使得表目 k 包含异常 k 的处理程序的地址。
系统在执行某个程序时,处理器检测到发生了一个事件,并确定了对应的异常号 k,就会触发异常。
触发异常:执行间接过程调用,通过异常表的表目 k,转到相应的处理程序。异常号是到异常表中的索引,异常表的起始地址放在一个特殊 CPU 寄存器——异常表基地址寄存器中。
异常类似过程调用,但有一些不同:
一旦硬件触发了异常,剩下的工作就是由异常处理程序在软件中执行。
异常处理结束后,会执行一条特殊的“从中断返回”指令,可选地返回到被中断的程序,该指令将适当的状态弹回到处理器的控制和数据寄存器中,然后将控制返回给别终端的程序。
异常可以分为 4 类:中断(interrupt)、陷阱(trap)、故障(fault)和终止(abort),具体特性如下图所示
中断是异步异常,是来自处理器外部的 I/O 设备中的信号的结果。硬件中断不是由指令造成的,因此它是异步的。硬件中断的异常处理程序常常叫做中断处理程序。
I/O 设备,例如网络适配器、磁盘控制器和定时器芯片,通过向处理器芯片上的一个引脚发信号,并将异常号放到系统总线上,来触发中断,这个异常号标识了引起中断的设备。
在当前指令完成执行后,处理器注意到中断引脚的电压变高了,就从系统总线读取异常号,调用对应的中断处理程序。当处理程序返回时,它就将控制返回给下一条指令。结果是程序继续执行,就好像没有发生过中断一样。
陷阱是有意的异常,是执行一条指令的结果。陷阱最重要的用途是在应用程序和内核之间提供一个接口,叫做系统调用。
用户程序经常需要向内核请求服务,比如读文件(read)、创建进程(fork)、加载程序(execve)、终止进程(exit)等。为了允许对这些内核服务的受控访问,处理器提供了一条特殊的 ”syscall n“ 指令,当用户程序想要向内核请求服务 n 时,就执行这条指令。执行 syscall 指令会导致一个到异常处理程序的陷阱(异常),这个处理程序解析参数,并调用适当的内核程序。
从程序员角度看,系统调用和普通的函数调用是一样的。但是实现上大不相同。它们分别允许在内核模式和用户模式。
**故障由错误情况引起。**故障发生时,处理器将控制转移给故障处理程序,如果处理程序能够修正错误,就把控制返回到引起故障的指令,否则返回给内核中的 abort 例程,abort 会终止当前的应用程序。
缺页异常
缺页异常是一种经典的故障(页面是虚拟内存中一个连续的块,典型值是 4KB)。当指令引用一个虚拟地址,而与该地址对应的物理页面不在内存中,必须要从磁盘取出时,就会发生缺页异常。
然后缺页处理程序会从磁盘加载适当的页面,然后将控制返回给引起故障的指令。当指令再次执行时,相应的物理页面就在内存中了。
理解:从存储器层次结构的角度看,缺页异常似乎可以看作是内存不命中的惩罚。
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误。终止处理程序将控制返回给一个 abort 例程,该例程会终止这个应用程序。
理解:运行程序时遇到了 abort 表明发生了故障或终止异常。
x86-64 系统中有 256 种不同的异常类型,其中 0~31 号是 Intel 架构师定义的异常(任何x86-64系统都一样),32~255对应的是操作系统定义的中断和陷阱。
理解:0-31 号是故障或终止,32~255 号都是操作系统定义的中断或系统调用。
!
Linux/x86-64 故障和终止
**除法错误。**当应用试图除以零时,或者当一个除法指令的结果对目标操作数来说太大了,就会发生除法错误。
**一般保护故障。**有许多原因,通常是因为一个程序引用了一个未定义的虚拟内存区域,或试图写一个只读的文本段。
**缺页异常。**此类故障会尝试恢复并重新执行产生故障的指令。
**机器检查。**在告知故障的指令执行中检测到致命的硬件错误时发生。
Linux/x86-64 系统调用
Linux 提供几百种系统调用,供应用程序请求内核服务时使用。(其中有一部分在 unistd.h 文件中)
系统中有一个跳转表(类似异常表)。每个系统调用都有一个唯一的整数号,对应一个到内核中跳转表的偏移量。
C 程序使用 syscall 函数可以直接调用任何系统调用。但是没必要这么做,C 标准库为大多数系统调用提供了包装函数。这些包装函数也是系统级函数。
在x86-64系统上,系统调用时通过一条称为syscall的陷阱指令来提供的。所有 Linux 系统调用的参数都是通用寄存器而不是栈传递的。一般寄存器 %rax 包含系统调用号。
hello程序,用系统级函数write实现:
int main()
{
write(1, "hello, world\n", 13);
_exit(0);
}
hello程序,用汇编实现:
实现方式:直接使用syscall指令来调用write和exit系统调用。第9-13行调用write函数。首先,第9行将系统调用write的编号存放在%rax中,第10-12行设置参数列表。然后第13行使用syscall指令来调用系统调用。同理,第14-16调用exit系统调用。
异常是允许操作系统内核提供进程概念的基本构造块。
进程的经典定义就是一个执行中程序的实例。系统中的 每个程序都运行在某个进程的上下文(context)中 。
上下文是由程序正确运行所需的状态组成的。这个状态包括 存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量、打开文件描述符的集合。
当用户向 shell 输入一个可执行目标文件的名字,运行程序时, shell 就会创建一个新的进程,然后在这个新进程的上下文中运行该可执行文件。
进程提供给应用程序的关键抽象:
使用调试器单步执行程序时会看到一系列的程序计数器(PC)值,这个 PC 的值的序列叫做逻辑控制流,简称逻辑流。
PC 的值唯一地对应于包含在程序的可执行目标文件中的指令,或包含在运行时动态链接到程序的共享对象中的指令。
关键点:进程是轮流使用处理器的。
计算机系统中逻辑流有许多不同的形式,异常处理程序、进程、信号处理程序、线程等都是逻辑流的例子。
当一个逻辑流的执行在时间上与另一个流重叠,就称为并发流(concurrent flow),这两个流称为并发地运行。
例如上述图中,进程A和B时并发的,A和C也是,但是B和C没有并发的运行。
并行流是并发流的真子集,如果两个流并发地运行在不同的处理器核或不同的计算机上时,就称为并行流。
一个进程和其他进程轮流运行的概念叫做多任务。
一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此多任务也叫做时间分片。
进程为每个程序提供它自己的私有地址空间。一般而言,和这个私有地址空间中某个地址相关联的那个内存字节是不能被其他进程读或写的。
不同进程的私有地址空间关联的内存的内容一般不同,但是每个这样的空间都有相同的通用结构。
x86-64 Linux 进程的地址空间的组织结构如下图所示:
地址空间的顶部保留给内核(操作系统常驻内存的部分),包含内核在代表进程执行指令时(比如当执行了系统调用时)使用的代码、数据、堆和栈。
地址空间的底部留给用户程序,包括代码段、数据段、运行时堆、用户栈、共享库等。代码段总是从地址 0x400000 开始。
理解:可以看出,内核栈和用户栈是分开的。
处理器使用某个控制寄存器中的一个模式位(mode bit)来区分用户模式与内核模式。进程初始时运行在用户模式,当设置了模式位时,进程就运行在内核模式。
进程从用户模式变为内核模式的方法是通过 中断、故障、陷阱(系统调用就是陷阱)这样的异常 。异常发生时,控制传递给异常处理程序,处理器将模式从用户模式转变为内核模式。
/proc 文件系统
Linux 提供了一种叫做 /proc 文件系统的机制来允许用户模式进程访问内核数据结构的内容。
/proc 文件系统将许多内核数据结构的内容输出为一个用户程序可以读的文本文件的层次结构。
可以通过 /proc 文件系统找出一般的系统属性(如 CPU 类型:/proc/cpuinfo)或者某个特殊的进程使用的内存段(/proc//maps)。
2.6 版本的 Linux 内核引入了 /sys 文件系统,它输出关于系统总线和设备的额外的低层信息。
上下文切换是一种较高层形式的异常控制流,它是建立在中断、故障等较低层异常机制之上的。
**系统通过上下文切换来实现多任务。**内核为每个进程维持一个上下文, 上下文是内核重新启动一个被挂起的进程所需的状态。
上下文由一些对象的值(是这些对象的值而非对象本身)组成,这些对象包括:通用目的寄存器、浮点寄存器、状态寄存器、程序计数器、用户栈、内核栈和各种内核数据结构(如描述地址空间的页表、包含有关当前进程信息的进程表、包含进程已打开文件的信息的文件表)。
内核挂起当前进程,并重新开始一个之前被挂起的进程的决策叫做调度,是由内核中的调度器完成的。
内核使用上下文切换来调度进程:
当内核代表用户执行系统调用时,可能发生上下文切换。如果系统调用因为等待某个事件而阻塞(比如 sleep 系统调用显式地请求让调用进程休眠,或者一个 read 系统调用要从磁盘度数据),内核就可以让当前进程休眠,切换到另一个进程。即使系统调用没有阻塞,内核也可以进行上下文切换,而不是将控制返回给调用进程。
中断也可能引发上下文切换。如所有的系统都有一种定时器中断机制,即产生周期性定时器中断,通常为 1ms 或 10ms。当发生定时器中断,内核就判定当前进程已经运行了足够长时间,该切换到新的进程了。
当 Unix 系统级函数遇到错误时,它们通常会返回 -1,并设置全局整数变量 errno 来表示什么出错了。
程序员应该总是检查错误
strerror 函数返回一个文本串,描述了和某个 errno 值相关联的错误。使用 strerror 来查看错误
'调用 Unix fork 时检查错误'
if((pid = fork()) < 0) //如果发生错误,此时 errno 已经被设置为对应值了
{
fprintf(stderr, "fork error: %s\n", strerror(errno));//strerror(errno) 返回描述当前 errno 值的文本串
exit(0);
}
错误处理包装函数
许多人因为错误检查会使代码臃肿、难读而放弃检查错误。可以通过定义错误报告函数及对原函数进行包装来简化代码。
对于一个给定的基本函数,定义一个首字母大写的包装函数来检查错误。
'错误报告函数'
void unix_error(char *msg)
{
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(0);
}
'fork 函数的错误处理包装函数 Fork'
pid_t Fork(void)
{
pid_t pid;
if ((pid = fork()) < 0)
unix_error("Fork error"); //调用上面定义的包装函数
return pid;
}
每个进程都有一个唯一的非零正整数表示的进程 ID,叫做 PID。有两个获取进程 ID 的函数:
#include
#include
pid getpid(void);
pid getppid(void);
进程总是处于以下三种状态之一:
信号是一种软件中断的形式。
终止进程
#include
void exit(int status); // status 指定进程终止时的退出状态。
exit 函数以 status 退出状态来终止进程(另一种设置退出状态的方法是从主程序中返回一个整数值。理解:是否指 main 函数的返回值)。
创建进程
父进程通过调用 fork 函数创建一个新的运行的子进程。
fork 函数只被调用一次,但是会返回两次:一次返回是在父进程中,一次是在新创建的子进程中。父进程中返回子进程的 PID,子进程中返回 0。
因为 fork 创建的子进程的 PID 总是非零的,所以可以根据返回值是否为 0 来分辨是当前是在父进程还是在子进程。
#include
#include
pid_t fork(void); //子进程返回 0,父进程返回子进程的 PID,如果出错,返回 -1
子进程与父进程几乎完全相同:
子进程和父进程之间的最大区别在于 PID 不同。
当一个进程终止时,内核并不会立即把它删除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收。
当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后清除子进程。
僵死进程:一个终止了但还未被回收的进程。
init 进程:系统启动时内核会创建一个 init 进程,它的 PID 为 1,不会终止,是所有进程的祖先。
如果一个父进程终止了,init 进程会成为它的孤儿进程的养父。init 进程会负责回收没有父进程的僵死子进程。
长时间没有运行的程序,总是应该回收僵死子进程。即使僵死子进程没有运行,也在消耗系统的内存资源。
waitpid 函数
一个进程可以通过调用 waitpid 函数来等待它的子进程终止或停止。
#include
#include
pid_t waitpid(pid_t pid, int *statusp, int options); //如果成功,返回对应的已终止的子进程的 PID;如果其他错误,返回 -1
//只有当参数 options=WNOHANG 时,才有可能返回 0;其他情况要么返回子进程 PID,要么返回 -1
waitpid 函数比较复杂。默认情况下 options = 0,此时 waitpid 会挂起调用进程的执行,直到它的等待集合中的一个子进程终止。如果等待集合中的一个进程在刚调用时就已经终止了,那么 waitpid 就立即返回。waitpid 的返回值是对应的已终止的子进程的 PID,此时该子进程会被回收,内核从系统中删除掉它的所有痕迹。
1 判定等待集合的成员
等待集合的成员是由参数 pid 确定的:
2 修改默认行为
默认情况下 options=0,可以将 options 设置为常量 WNOHANG, WUNTRACED, WCONTINUED 的各种组合来修改默认行为:
3 检查已回收子进程的退出状态
如果 statusp 参数是非空的,那么 waitpid 就会在 status 中放上关于导致 waitpid 返回的子进程的状态信息,status 是 statusp 指向的值。
wait.h 头文件定义了解释 status 参数的几个宏:
4 错误条件
如果调用进程没有子进程,那么 waitpid 返回 -1,并设置 errno 为 ECHILD。如果 waitpid 函数被一个信号中断,那么它返回 -1,并设置 errno 为 EINTR。
5 wait函数
wait 函数是 waitpid 函数的简单版本。
#include
#include
pid_t wait(int *statusp); //如果成功,返回子进程的 PID,如果出错,返回 -1
sleep函数:sleep 函数将一个进程挂起一段指定的时间。
#include
unsigned int sleep(unsigned int secs); //返回还要休眠的秒数
pause函数:pause 函数让调用函数休眠,直到该进程收到一个信号。
#include
int pause(void);
execve函数:execve 函数在当前进程的上下文中加载并运行一个新程序(是程序不是进程)。
#include
int execve(const char *filename, const char *argv[], const char *envp[]); //如果成功,则不返回,如果错误,返回 -1。
execve 函数功能: 加载并运行可执行目标文件 filename**,并带一个参数列表 argv 和一个环境变量列表 envp。**
execve 调用一次并从不返回(区别于 fork 调用一次返回两次)。
参数列表和变量列表:
参数列表:argv 指向一个以 null 结尾的指针数组,其中每个指针指向一个字符串。
环境变量列表:envp 指向一个以 null 结尾的指针数组,其中每个指针指向一个环境变量字符串,每个串都是形如 “name=value” 的名字-值对。
execve函数的执行过程
execve 函数调用加载器加载了 filename 后,设置用户栈,并将控制传递给新程序的主函数(即 main 函数)。
main 函数:main 函数有以下形式的原型,两种是等价的。
int main(int argc, char **argv, char **envp);
int main(int argc, char *argv[], char *envp[]);
main 函数有三个参数:
argc 和 argv 的值都是从命令行中获取的,如果命令行中只有该可执行文件的名字,没有其他参数,则 argc=1,argv 的第一个元素的值即为该可执行文件的文件名(包含路径)
注意 argv[] 数组和 envp 数组最后一个元素都是 NULL,可以使用 NULL 作为循环终止条件来遍历数组。
操作环境变量数组的函数:
#include
char *getenv(const char *name); //在环境变量列表中搜索字符串 "name=value",如果搜到了返回指向 value 的指针,否则返回 NULL
int setenv(const char *name, const char *newvalue, int overwrite); //若成功返回 0,否则返回 -1。如果环境变量列表中包含一个形如 ”name=value" 的字符串,setnv 会用 newvalue 替代原来的 value,如果不存在,直接添加一个 "name=newvalue" 到数组中。
void unsetenv(const char *name); //如果环境变量列表中包含一个形如 ”name=value" 的字符串,unsetnv 会删除它。
区分程序与进程
程序:程序是一堆代码和数据,程序可以作为目标文件存在于磁盘上,或作为段存在于虚拟地址空间中。
进程:进程是执行中程序的一个具体的实例。
程序总是运行在某个进程的上下文中。
区分 fork 和 execve
fork 函数是在新的子进程中运行相同的程序,新的子进程是父进程的一个复制品。
execve 函数是在当前进程的上下文中加载并运行一个新的程序,它会覆盖当前进程的地址空间,但没有创建一个新的进程。
像 Unix shell 和 Web 服务器这样程序大量使用了 fork 和 execve 函数
一个简单的 shell 的实现方式
shell 会打印一个命令行提示符,等待用户在 stdin 上输入命令行,然后对这个命令行求值。
shell 的 main 例程
#include "csapp.h"
#define MAXARGS 128
int main()
{
char cmdline[MAXLINE]; /* Command line */
while (1)
{
/* Read */
printf("> ");
Fgets(cmdline, MAXLINE, stdin); //读取用户的输入
if (feof(stdin))
exit(0);
/* Evaluate */
eval(cmdline); //解析命令行
}
}
解释并执行一个命令行
/* eval - Evaluate a command line */
void eval(char *cmdline)
{
char *argv[MAXARGS]; /* Argument list execve() */
char buf[MAXLINE]; /* Holds modified command line */
int bg; /* Should the job run in bg or fg? */
pid_t pid; /* Process id */
strcpy(buf, cmdline);
bg = parseline(buf, argv); //调用 parseline 函数解析以空格分隔的命令行参数
if (argv[0] == NULL) //表示是空命令行
return; /* Ignore empty lines */
//调用 builtin_command 检查第一个命令行参数是否是一个内置的 shell 命令。如果是的话返回 1,并在函数内就解释并执行该命令。
if (!builtin_command(argv)) //如果返回 0,即表明不是内置的 shell 命令
{
if ((pid = Fork()) == 0) //创建一个子进程
{ /* Child runs user job */
if (execve(argv[0], argv, environ) < 0) //在子进程中执行所请求的程序
{
printf("%s: Command not found.\n", argv[0]);
exit(0);
}
}
/* Parent waits for foreground job to terminate */
if (!bg) // bg=0 表示是要在前台执行的程序,shell 会等待程序执行完毕
{
int status;
if (waitpid(pid, &status, 0) < 0) //等待子进程结束回收该进程
unix_error("waitfg: waitpid error");
}
else // bg=1 表示是要在后台执行的程序,shell 不会等待它执行完毕
printf("%d %s", pid, cmdline);
}
return;
}
一个极简的 shell 程序包括以下几个函数:main 函数、eval 函数、parseline 函数、buildin 函数,它们的各自的主要职责如下:
信号是一种更高层次的软件形式的异常,它允许进程和内核中断其他进程。
一个信号就是一条消息,它通知进程系统中发生了某件事情。
每种信号类型都对应于某种系统事件。信号可以简单分为两类:
一类信号对应低层的硬件异常:
一类信号对应于内核或其他用户进程中叫高层的软件事件。
传送一个信号到目的进程包含两个步骤:
发送信号有两种原因:
待处理信号
待处理信号是指发出但没有被接收的信号。
一种类型最多只会有一个待处理信号。如果一个进程已经有一个类型为 k 的待处理信号,接下来发给他的类型为 k 的信号都会直接丢弃。
进程可以有选择地阻塞接收某种信号。当一种信号被阻塞,它仍可以发送,但是产生的待处理信号不会被接收。
一个待处理信号最多只能被接收一次。内核为每个进程在 pending 位向量中维护着待处理信号的集合,在 blocked 位向量中维护着被阻塞的信号集合。
只要传送了一个类型为 k 的信号,内核就会设置 pending 中的第 k 位,只要接收了一个类型为 k 的信号,内核就会清除 blocked 中的第 k 位。
Unix 系统提供了大量向进程发送信号的机制。这些机制都是基于**进程组(process group)**的概念。
进程组:每个进程都只属于一个进程组,进程组由一个正整数进程组 ID 来标识。
可以使用 getpgrp 函数获取当前进程的进程组 iD,可以使用 setpgid 函数改变自己或其他进程的进程组。
默认情况下,子进程和它的父进程同属于一个进程组。
#include
pid_t getpgrp(void); // 返回调用进程的进程组 ID
int setpgid(pid_t pid, pid_t pgid); // 将进程 pid 的进程组改为 pgid。若成功返回 0,错误返回 -1。
// 如果 pid=0,就表示使用当前进程的 pid,如果 pgid=0,就表示要将使用第一个参数 pid 作为进程组 ID。
用/bin/kill程序发送信号
可以用 Linux 中的 kill 程序向另外的进程发送任意的信号。
正的 PID 表示发送到对应进程,负的 PID 表示发送到对应进程组中的每个进程。
linux> /bin/kill -9 15213 # 发送信号9(SIGKILL)给进程 15213。
linux> /bin/kill -9 -15213 # 发送信号9(SIGKILL)给进程组 15213 中的每个进程。
上面使用了完整路径,因为有些 Unix shell 有自己内置的 kill 命令。
从键盘发送信号
Unix shell 使用作业这个抽象概念来表示为对一条命令行求值而创建的进程。在任何时刻,至多只有一个前台作业,后台作业可能有多个。
linux> ls | sort # 这会创建两个进程组成的前台作业,这两个进程通过 Unix 管道连接起来:一个进程运行 ls 程序,另一个运行 sort 程序
shell 为每个作业创建一个独立的进程组,进程组 ID 通常取作业中父进程中的一个。
上图是一个包含一个前台作业与两个后台作业的 shell。
在键盘上输入 Ctrl+C 会导致内核发送一个 SIGINT 信号到前台进程组中的每个进程,默认情况下会终止前台作业。
输入 Ctrl+Z 会发送一个 SIGTSTP 信号到前台进程组中的每个进程,默认情况下会停止(挂起)前台作业。
用kill函数发送信号
进程可以通过调用 kill 函数发送信号给其他进程(包括自己)。
#include
#include
int kill(pid_t pid, int sig); //若成功则返回 0,若错误则返回 -1
pid 取值有三种情况:
用alarm函数发送信号
进程可以通过调用 alarm 函数向他自己发送 SIGALRM 信号。
#include
unsigned int alarm(unsigned int secs); //返回前一次闹钟剩余的描述,如果以前没有设定闹钟,就返回 0。
alarm 函数安排内核在 secs 秒后发送一个 SIGALRM 信号给调用进程。如果 secs=0,则不会调度安排新的闹钟。
在任何情况下,对 alarm 的调用都将取消任何待处理的闹钟,并返回任何待处理的闹钟在被发送前还剩下的秒数。如果没有待处理的闹钟,就返回 0。
发送信号的方法总结
当内核把进程 p 从内核模式切换到用户模式时(比如从系统调用返回),他会检查进程 p 的未被阻塞的待处理信号的集合。如果集合为空,内核就将控制传递到 p 的逻辑控制流中的下一条指令;如果集合非空,内核就选择集合中的某个信号(通常是最小的 k),并强制 p 接收信号 k。
进程 p 收到信号会触发 p 采取某种行为,等进程完成了这个行为,控制就传递回 p 的逻辑控制流中的下一条指令。
每个信号类型都有一个预定义的默认行为,是下面中的一种:
进程可以通过 signal 函数修改和信号相关联的默认行为,其中 SIGSTOP 和 SIGKILL 的默认行为不能修改。
signal 是在 C 标准库的头文件 signal.h 中定义的。
#include
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler); //若成功返回指向前次处理程序的指针,若出错则返回 SIG_ERR(不设置 errno)。
signal 函数接受两个参数:信号值和函数指针,可以通过下列三种方法之一来改变和信号 signum 相关联的行为:
当处理程序执行它的 return 语句时,控制传递回控制流中进程被信号接收中断位置处的指令。
sigaction函数的功能是检查或修改与指定信号相关联的处理动作
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
struct sigaction结构体介绍
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}
信号处理程序可以被其他信号处理程序中断。
一个信号处理程序的例子
#include "csapp"
void sigint_handler(int sig) //定义了一个信号处理程序
{
printf("Caught SIGINT!\n");
exit(0);
}
int main()
{
/* Install the SIGINT handler */
if(signal(SIGINT, sigint_handler)) == SIGERR)
unix_error("signal error");
pause(); // wait for the receipt of signal
return 0;
}c++
Linux 提供两种阻塞信号的机制:
sigprocmask函数
sigprocmask 函数改变当前阻塞的信号集合(blocked 位向量),具体行为依赖 how 的值:
#include
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
如果 oldset 非空,blocked 位向量之前的值保存在 oldset 中。
其他辅助函数
辅助函数用来对 set 信号集合进行操作:
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(segset_t *set, int signum);
一个临时阻塞 SIGINT 信号的例子
sigset_t mask, prev_mask;
Sigemptyset(&mask);
Sigaddset(&mask, SIGINT); //将 SIGINT 信号添加到 set 集合中
Sigprocmask(SIG_BLOCK, &mask, &prev_mask); //阻塞 SIGINT 信号,并把之前的阻塞集合保存到 prev_mask 中。
... //这部分的代码不会被 SIGINT 信号所中断
Sigprocmask(SIG_SETMASK, &prev_mask, NULL); //恢复之前的阻塞信号,取消对 SIGINT 的阻塞
8.6 非本地跳转
C 语言提供了一种用户级异常控制流形式,称为非本地跳转,它将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用-返回序列(即 C 标准库 setjmp.h 的内容)。
非本地跳转通过 setjmp 和 longjmp 函数来完成
sigjmp函数
#include //c 标准库中的头文件
int setjmp(jmp_buf env); //返回 0,setjmp 的返回值不能被赋值给变量,但可以用在条件语句的测试中
int sigsetjmp(sigjmp_buf env, int savesigs);
setjmp 函数在 env 缓冲区中保存当前调用环境,以供后面的 longjmp 使用。
调用环境包括程序计数器、栈指针和通用目的寄存器。
longjmp函数
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 函数被调用一次,但从不返回。
非本地跳转的一个重要应用:允许从一个深层嵌套的函数调用中立即返回,而不需要解开整个栈的基本框架,通常是由检测到某个错误情况引起的。
C++ 和 Java 提供的 try 语句块异常处理机制是较高层次的,是 C 语言的 setjmp 和 longjmp 函数的更加结构化的版本。可以把 throw 语句看作 longjmp 函数,catch 子句看作 setjmp 函数
Linux 系统提供了大量的监控和操作进程的工具: