16.5 信号
程序中所花费的时间绝大多数都是由程序本身所引发的,例如执行各种语句和请求输入。但是,有些程序必须遇到的事件却不是程序本身所引发的。一个常见的例子就是用户中断了程序。如果部分计算好的结果必须进行保存以避免数据的丢失,程序必须预备对这类事件做出反映,虽然它没有办法预测什么时候会发生这种情况。
信号就是用于这种目的。信号(signal)表示一种事件,它可能异步地发生,也就是并不与程序执行过程的任何事件同步。如果程序并未安排怎样处理一个特定的信号,那么当该信号出现时程序就做出一个缺省的反映。标准并未定义这个缺省反应是什么,但绝大多数编译器都选择终止程序。另外,程序可以调用signal函数,或者忽略这个信号,或者设置一个信号处理函数(signal handler)。当信号发生时,程序就调用这个函数。
16.5.1 信号名<signal.h>
表16.4列出了标准所定义的信号,但编译器并不需要实现所有这些信号,而且如果觉得合适,编译器也可以定义其他的信号。
SIGABRT是一个由abort函数所引发的信号,用于终止程序。至于哪些错误将引发SIGFPE信号则取决于编译器,常见的有算术上溢或下溢以及除零错误。有些编译器对这个信号进行了扩展,提供了关于引发这个信号的操作的特定信息。使用这个信息可以允许程序对这个信号做出更智能的反应,但这样做将影响程序的可移植性。
表16.4 信号
信号 含义
SIGABRT 程序请求异常终止
SIGFPE 发生一个算术错误
SIGILL 检测到非法指令
SIGSEGV 检查到对内存的非法访问
SIGINT 收到一个交互性注意信号
SIGTERM 收到一个终止程序的请求
SIGILL信号提示CPU试图执行一条非法的指令。这个错误可能由于不正确的编译器设置所导致。另一个可能的原因是程序的执行流出现了错误,例如使用一个未初始化的函数指针调用一个函数,导致CPU试图执行实际上是数据的指令(把数据段当成了代码段)。SIGSEGV信号提示程序试图非法访问内存。引发这个信号有两个最常见的原因,其中一个是程序试图访问未安装于机器上的内存或者访问操作系统未曾分配给这个程序的内存,另一个是程序违反了内存访问的边界要求。后者可能发生在那些要求数据边界对齐的机器上。例如,如果整数要求位于偶数的边界(存储的起始位置是编号为偶数的地址),一条指定在奇数边界访问一个整数的指令将违反边界规则。未初始化的指针常常会引发这类错误。前面几个信号是同步的,因为它们都是在程序内部发生的。尽管无法预测一个算术错误何时将会发生,如果使用相同的数据反复运行这个程序,则每次在相同的地方都会出现相同的错误。SIGINT和SIGTERM这两个信号则是异步的。它们在程序的外部产生,通常是由程序的用户触发,表示用户试图向程序传达一些信息。
SIGINT信号在绝大多数机器中都是在用户试图中断程序时发生。SIGTERM则是另一种用于请求终止程序的信号。在实现了这两个信号的系统中,一种常用的策略是为SIGINT定义一个信号处理函数,目的是执行一些日常维护工作(housekeeping)并在程序退出前保存数据。但是,SIGTERM则不配备信号处理函数,这样当程序终止时便不必执行这些日常维护工作。
16.5.2 处理信号<signal.h>
通常,我们关心的是怎样处理那些自主发生的信号,也就是无法预测其什么时候会发生的信号。raise函数用于显式地引发一个信号。
int raise( int sig );
调用这个函数将引发它的参数所指定的信号。程序对这类信号的反应和那些自主发生的信号是相同的。可以调用这个函数对信号处理函数进行测试。但如果误用,它可能会实现一种非局部的goto效果,因此要避免以这样的方式使用它。当一个信号发生时,程序可以使用3种方式对它做出反应。缺省的反应是由编译器定义的,通常是终止程序。程序也可以指定其他行为对信号做出反应:信号可以被忽略;或者程序可以设置一个信号处理函数,当信号发生时调用这个函数。signal函数用于指定程序希望采取的反应:
void ( *signal( int sig, void (*handler)( int ) ) )( int );
对函数进行分析。首先将省略返回类型,这样可以先对参数进行研究:
signal( int sig, void (*handler)( int ) )
第1个参数是表16.4所列的信号之一,第2个参数是希望为这个信号设置的信号处理函数。这个处理函数是一个函数指针,它所指向的函数接受一个整型参数且没有返回值。当信号发生时,信号的代码作为参数传递给信号处理函数。这个参数允许一个处理函数处理几种不同的信号。
现在从原型中去掉参数,这样函数的返回类型看上去就比较清楚:
void (*signal())( int );
signal是一个函数,它返回一个函数指针,后者所指向的函数接受一个整型参数且没有返回值。事实上,signal函数返回一个指向该信号以前的处理函数的指针。通过保存这个值,可以为信号设置一个处理函数并在将来恢复为先前的处理函数。如果调用signal失败,例如由于非法的信号代码所致,函数将返回SIG_ERR值。这个值是个宏,它在signal.h头文件中定义。
signal.h头文件还定义了另外两个宏:SIG_DFL和SIG_IGN,它们可以作为signal函数的第2个参数。SIG_DFL恢复对该信号的缺省反应,SIG_IGN使该信号被忽略。
16.5.3 信号处理函数
当一个已经设置了信号处理函数的信号发生时,系统首先恢复对该信号的缺省行为(当信号处理函数正在执行时,编译器可以选择“阻塞”信号而不是恢复缺省行为(请参阅有关文档))。这样做是为了防止如果信号处理函数内部也发生这个信号可能导致的无限循环。然后,信号处理函数被调用,信号代码作为参数传递给函数。
信号处理函数可能执行的工作类型是很有限的。如果信号是异步的,也就是说,不是因为调用abort或raise函数引起的,信号处理函数并不应该调用除signal之外的任何库函数,因为在这种情况下其结果是未定义的。而且,信号处理函数除了能向一个类型为volatile sig_atomic_t的静态变量赋一个值以外,可能无法访问任何静态数据。为了保证真正的安全,信号处理函数所能做的就是对这些变量之一进行设置然后返回。程序的剩余部分必须定期检查变量的值,看看是否有信号反应。
这些严格的限制是由信号处理的本质产生的。信号通常用于提示发生了错误。在这些情况下,CPU的行为是精确定义的,但在程序中,错误所处的上下文环境可能很不相同,因此它们并不一定能够良好定义。例如,但strcpy函数正在执行时如果产生了一个信号,原因可能是当时目标字符串暂时未以NUL字节终结;或者当一个函数被调用时如果产生一个信号,原因可能是当时堆栈处于不完整的状态。如果依赖这种上下文环境的库函数被调用,它们就可能以不可预料的方式失败,很可能引发另一个信号。
访问限制定义了在信号处理函数中保证能够运行的最小功能。类型sig_atomic_t定义了一种CPU可以以原子方式访问的数据类型,也就是不可分割的访问单位。例如,一台16位的机器可以以原子方式访问一个16位整数,但访问一个32位整数时可能需要两个操作。在访问原子数据的中间步骤时,如果产生一个可能导致不一致结果的信号,在信号处理函数中把数据访问限制为原子单位则可以消除这种可能性。
警告:
标准表示信号处理函数可以通过exit终止程序。用于处理SIGABRT之外所有信号的处理函数也可以通过调用abort终止程序。但是,由于这两个都是库函数,因为当它们被异步信号处理函数调用时可能无法正常运行。如果必须用这种方式终止程序,注意仍然存在一种微小的可能性导致它失败。如果发生这种情况,函数的失败可能破坏数据或者奇怪的症状,但程序最终将终止。
1.volatile
信号可能在任何时候发生,所以由信号处理函数修改的变量的值可能会在任何时候发生改变。因此,不能指望这些变量在两条相邻的程序语句中肯定具有相同的值。volatile关键字告诉编译器这个事实,防止它以一种可能修改程序含义的方式“优化”程序。考虑下面的程序段:
if( value ){
printf( "True\n" );
}else{
printf( "False\n" );
}
if( value ){
printf( "True\n" );
}else{
printf( "False\n" );
}
在普通情况下,大家会认为第2个测试与第1个测试具有相同的结果。如果信号处理函数修改了这个变量,第2个测试的结果可能不同。除非变量被声明为volatile,否则编译器可能会用下面的代码进行替换,从而对程序进行“优化”。这些语句在通常情况下是正确的:
if( value ){
printf( "True\n" );
printf( "True\n" );
} else{
printf( "False\n" );
printf( "False\n" );
}
2. 从信号处理函数返回
从一个信号处理函数返回,将导致程序的执行流从信号发生的地点恢复执行。这个规则的例外情况是SIGFPE。由于计算无法完成,所以从这个信号返回的效果是未定义的。
警告:
如果希望捕捉将来同种类型的信号,在从当前这个信号的处理函数返回之前,注意要调用signal函数重置设置信号处理函数;否则,只有第1个信号才会被捕捉。接下来的信号将使用缺省反应进行处理。
提示:
由于各种计算机对不可预料的错误的反应各不相同,因此信号机制的规范也比较宽松。例如,编译器并不一定要使用标准定义的所有信号,而且在调用某个信号的处理函数之前可能会也可能不会重新设置信号的缺省行为。此外,对信号处理函数所施加的严重限制反映了不同的硬件和软件环境所施加的限制的交集。
这些限制和平台依赖性的结果就是使用信号处理函数的程序比不使用信号处理函数的程序可移植性弱一些。只要当需要时才使用信号以及不违反信号处理函数的规则有助于使这种类型的程序内部固有的可移植性问题降低到最低程度。
C和指针 第16章 标准函数库 16.5 信号
于 2022-07-12 19:45:12 首次发布