承接上一章。本章描述的重点时信号处理函数。同时或拓展20.4节所发起的讨论。本章涵盖主题如下。
- 如何设计信号处理器函数,其中对可重入性以及异步信号安全函数的探讨必不可少。
- 从信号处理器函数中正常返回的各种途径,特别是非本地跳转技术的运用。
- 利用备选栈处理信号。
- 记住与带有SA_SIGINFO标准的sigactiob()函数,处理器函数能够获取引发起调用信号的更多详细信息。
- 信号处理器函数如何中断处于阻塞状态的系统调用,以及如何重启该系统调用。
21.1设计信号处理器函数
一般而言京信号处理器函数设计的越简单越好。其中的一个重要原因就在于,这将降低引发竞争条件的风险。下面时针对信号处理器函数的两种常用设计。
- 信号处理器函数设置全局标志变量并退出。主程序对此标志进行周期性检查,一旦置位随机采取相应动作。(主程序若因监控一个或对各文件描述符I/O状态而无法进行这种周期性检查时,则可令信号处理器函数向一专用管道写入一个字节的数据,同时将该管道的读取段置于主程序所监控的文件描述符范围之内。63.5.2节展示了这一技术的运用。)
- 信号处理器函数执行某种类型的清理动作,接着终止进程或者使用非本地跳转(21.2.1节)将站内接剋并控制发挥到主程序的预定位置。
21.1.1 再论信号的非队列化处理
20.10节已经提及,在执行某i信号的处理器函数时会阻塞同类信号的传递(除非在带哦用sigaction()指定了SA_NODEFER标志)。如果在执行处理器函数时(再次)产生同类信号,那么会将该信号表姐为等待状态并在处理器函数返回之后再进行传递。前一章节还曾指出,不会对信号进行排队处理。在处理器函数执行期间,如果多次产生同类信号,那么仍然会将其标记为等待状态,但稍后只会传第一次。
信号的这种“失踪”方式无疑将影响对信号处理器函数的设计。首先,无法对信号的产生穿刺术进行可靠技术。其次,在为信号处理器函数编码时可能需要考虑处理同类信号多次产生的情况。26。3.1节在塔伦SOGCHILD信号时会有相关示例。
21.1.2可重入函数和异步信号安全函数
在信号处理器函数中,并非所有系统调用以及可函数均可语义安全调用。要想了解来龙去脉,就需要解释以下两种概念:可重入(reentrant)函数和异步信号安全(async-signal-safe)函数。
可重入和非可重入函数
要解释可重入函数为何物,首先要区分单线程和多线程程序。带女性UNIX程序都具有、一条执行线程,贯穿程序始终,CPU围绕单挑执行逻辑来处理指令。而对于多线程程序威严,同一进程却存在多条独立、并发的执行逻辑流。
第29章将会展示如何显式创建一个包含多条线程的程序,不过,多执行线程的概念与使用了信号处理器函数的程序也有关联。因为信号处理器函数可能会在任一时检点异步中断程序的执行,从而在同一个进程实际形成了两条(即主程序和信号处理函数)独立(虽然不是并发)的执行线程。
如果同一个进程的多条线程可以同时安全的执行某一个函数,那么该函数就是可重入的。此处,“安全”意味着,无论其他线程调用该函数的执行状态如何,函数均可产生预期结果。
SUv3对可重入函数的定义是:函数由两条或多条线程调用时,即便时交叉执行,其效果与各线程以未定义顺序依次调用时一致。
更新全局变量或静态数据结构的函数可能时不可重入的。(只用到本地变量的函数可定是可重入的。)如果对函数的两个调用(例如:分别由两条执行线程发起)同时试图更新同一全局变量或数据类型,那么二者可能会相互干扰产生不正确的结果。例如,假设某线程正在为一链表数据结构添加一个新的链表项,而另一线程也正试图更新同意链表。由于链表添加新项设计对多枚指针的更新,一旦另一线程中断这些步骤并修改了相同的指针,那么链表可能会遭到破坏。因此,malloc()函数族以及使用他们的其他函数库都是不可重入的。
还有一些函数库之所以不可重入,是因为他们使用了经静态分配的内存来返回信息。此类函数的例子包括(crypt()、getpwnam()、gethostbyname()以及getserverbyname()。如果信号处理器用到了这类函数,那么将会覆盖主程序中上一次调用同一函数所返回的信息(反之亦然)。
将静态数据结构用于内部记账的函数也是不可重入的。其最明显的例子就是stdio函数库成员(printf()、scanf()等),他们会为缓冲区I/O更新内部数据结构。所以,如果在信号处理器函数中调用了printf().而主程序中又在调用printf()或其他stdio函数期间遭到了处理器函数的中断,那么有时就会看到奇怪的输出,甚至导致程序崩溃或者数据的损坏。
即使并未使用不可重入的库函数,可重入问题依然不可忽视。如果信号处理器函数和主程序都要更新由程序员自定义的全局性数据结构,那么对于主程序而言,这种信号处理器就是不可重入的。
如果函数时不可重入的,那么其手册页通常会明或暗地给出提示。对于那些使用或返回静态分配变量地函数,需要特别留意。
示例程序
程序清单21-1展示了函数crypt()(8.5节)不可重入地本来面目。该程序接受两个字符串作为命令行参数,执行步骤如下。
- 调用crypt()加密第一个命令行参数地字符串,并使用strdup()将结果复制到独立缓冲区中。
- 为SIGINT信号(按下Ctrl-C产生)创建处理器函数。处理器函数调用crypt()加密第二个命令行参数所提供地字符串。
- 进入无限for循环,使用crypt()加密第一个命令行参数中地字符串,并检查其返回字符串与第一步保存地结果是否一致。
在不产生信号地情况下,第三步地检查结果z将总是匹配。然而,一旦收到SIGINT信号,而主程序又卡在for循环内的crypt调用之后,字符串地匹配检查之前遭到京信号处理器函数地中断,这是就会发生字符串不匹配地情况。程序运行结果如下:
由对上述输出mistach和handled值地比较可知,在大多数情况下,处理器会在main()中地crydiaoyong-与字符串比较之间去覆盖静态的分配地缓冲区
程序清单21-1:在main()以及信号处理函数中调用不可重入函数
#define _XOPEN_SOURCE 600
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
static char *str2; /* Set from argv[2]*/
static int handled = 0; /*Counts number of calls to handler*/
static void handler(int sig)
{
crypt(str2,"xx");
handled++;
}
int main(int argc,char *argv[])
{
char *cr1;
int callNum,mismatch;
struct sigaction sa;
if(argc !=3)
{
printf("%s str1 str2 \n",argv[0]);
return -1;
}
str2 = argv[2]; //make argv[2] avalicale to handler
cr1 = strdup(crypt(argv[1],"xx")); /*copr statically allocated string to another buffer*/
if(cr1 == NULL)
{
perror("strdup1:");
return -1;
}
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = handler;
if(sigaction(SIGINT,&sa,NULL) == -1)
{
perror("sigaction:");
return -1;
}
/*Repeatedly call crypt() using argv[1].if interrupted by a signal handler,
then the static storage returned by crypt() will be overwritten by the
results of encryting argv[2],and strcmp() will detect a mismatch whith the value in 'cr1'*/
for(callNum =1,mismatch = 0;;callNum++){
if(strcmp(crypt(argv[1],"xx"),cr1) !=0){
mismatch ++;
printf("Mismatch on call %d (mismatch =%d handled = %d)\n",
callNum,mismatch,handled);
}
}
}
标准地异步信号安全函数
异步信号安全的函数是指当从信号处理器函数调用时,可以保证其实现时安全的。如果某一函数是可重入的,又或者信号处理器函数无法将其中断时,就成该函数是异步信号安全的。
表21-1所列为各种标准要求实现为异步安全的函数。其中名称后未跟v2或v3字符串的函数是由POSIX.1-1990规定为异步信号安全的。带有v2标记的函数由SUSv2加入,带有v3标记的的则由SUSv3加入。个别UNIX实现可能会将其他某些函数实现为异步信号安全的,但所有符合标准的UNIX 实现都必须保证至少表中这些函数是异步信号安全的(假设由实现来提供这些函数,Linux并未实现所有这些函数)。
SUSv4对表21-1做了如下修改。
- 移除如下函数:fpathconf()、pathconf()和sysconf()
- 添加如下函数:
表21-1:
SUSv3强调,表21-1之外的所有函数对于信号而言都是不安全的,但同时指出,仅当信号处理器函数中断了不安全函数的执行,且处理器函数自身也调用了这个不安全函数时,该函数才是不安全的。换言之,编写信号处理器函数有如下两种选择。
- 确保信号处理器函数的代码本身是可重入的,且只调用异步信号安全的函数。
- 当主程序执行不安全函数或是去操作信号处理器啊汉纳树也可能更新的全局数据结构时,阻塞信号的传递。
第二种方法的问题是,在一个复杂的程序中,要想确保主程序对不安全函数的调用不为信号处理器函数所中断,这有些困难。处于这一原因,通常就上述规则简化为信号处理器函数中绝不调用不安全的函数。
如果使用同一处理器函数来处理多个不同信号,或者在调用sigaction()时设置了SA_NODEFER标志,那么处理器函数就有可能中断自己。因此处理器函数如果恒心了全局(或静态变量),即使主程序硧这些变量,那么他们仍然是不可重入的。
信号处理器函数内部对errno的使用
由于可能会更新errno,调用表21-1中函数依然会导致信号处理器函数不可重入,因为他们可能会覆盖自谦主程序调用函数时所设置的errno值。有一种变通方法,即当信号处理器函数使用了表21-1所列函数时,可在其入口函数处保存errno值,并在出口处恢复errno的旧有值,请看下面例子
void handler(int sig)
{
int savedErrno;
savedErrno = errno;
/*Now we can execute a function that might modify errno*/
errno = savedErrno;
}
在本书示例中使用不安全函数
虽然printf()不是异步信号安全的函数,但却频频现身于本书各种示例的信号处理函数中。之所以如此,是因为在展示对信号处理器的调用,以及显示处理器相关变量的内容时,printf()都不失为一种简单而快捷的方式。处于类似原因,在信号处理其函数中偶尔会用到其他一些不安全的函数,包括其他的stdio函数以及strsignal()。
真正的应用程序应当避免在信号处理器函数中调用非异步信号安全的函数。为了明确这一点,每当示例的信号处理器调用这些函数时,代码注释中都会注明这一用法是不安全的。
printf("Some message\n"); //UNSAFE
21.1.3 全局变量和sig_atomic_t数据类型
尽管存在可重入问题,有时仍需要在主程序和信号处理器函数之间共享全局变量。信号处理器函数可能会随时修改全局变量--只要主程序能够正确处理这种可能性,共享全局变量就是安全的。例如,一种常见的设计是,信号处理器函数只做一件事情,设置全局标志。主程序会周期性检查这一标志,并采取相应动作来响应信号传递(同时清楚标志)。当信号处理器函数以此方式来访问全局变量时,应该总是在声明变量时使用volatile关键字,从而防止编译器将其优化到寄存器中。
对全局变量的读写可能不止一条机器指令,而信号处理器函数就可能在这些指令序列之间将主程序中断(也可将此类变量访问称为非原子操作)。因此C语言标准以及SUSv3定义了一种整型数据类型sig_atomic_t.意在保证读写操作的原子性。因此,所有主程序与信号处理器函数之间共享的全局变量都应声明如下:
volatile sig_atomic_t flag;
程序清单22-5提供了使用sig_atomic_t 数据乐星的一个例子。
注意C语言的递增(++)和递减(--)caozuofu-并不在sig_atomic_t所提供的保障范围之内。这些操作在某些硬件架构上可能不是原子操作(更多细节参考30.1节)。在使用sig_atomic_t变量时唯一能做的就是在信号处理器中进行设置,在主信号处理器中进行设置,在主程序中进行检查(反之依可)。
C99和SUSv3规定,实现应当(在<stdint.h>中)定义两个常量SIG_ATOMIC_MIN和SIG_ATOMIC_MAX,用于规定可赋给sig_atomic_t类型的范围。标准要求,如果将sig_atomic_t表示为有符号值,其值范围至少应该在-127~127之间,如果作为无符号值,则应该在0~255之间。在linux中,这两个常量分别等于有符号32位整型数的负、正极限值。
21.2 终止信号处理器函数的其他办法
目前为止所看到的信号处理器函数都是以返回主程序而终结。不过,只是简单地从信号处理器函数中返回并不能满足需要,有时候甚至没什么用处。(22.4节在讨论硬件产生地信号时会举出这方便地例子。)
以下是从信号处理器函数中终止地其他地一些方法。
- 使用exit()终止进程。处理器函数实现可以做一些清理工作。注意,不要使用exit()来终止信号处理其器函数,因为他不在21-1列表所列地安全函数之中,之所以不安全,是因为该函数堆在调用_exit()函数之前刷新stdio()地缓冲区。
- 使用kill()信号来杀掉进程(即信号地默认动作是终止进程)
- 从信号处理器中执行非本地跳转
- 使用abort()函数终止进程,并产生核心转储。
21.2.1 在信号处理器函数中执行非本地跳转
6.8节曾提及使用setjmp()和longjmp来执行非本地跳转,以便从一个函数跳转至该函数地某个调用者。在信号处理器函数中也可以使用这种技术。这也是因硬件异常(例如内存访问错误)而导致信号传递之后地一条回复路径,允许信号捕获并把控制返回到程序中地某个特定位置。例如一三收到SIGINT信号,shell执行一个非本地跳转,将控制返回到主输入循环中(以便读取下一条命令)。
然而,使用标准longjump()函数从处理器函数中退出存在一个问题。之前曾经提及,在进入信号处理器函数时,内核会自动将引发调用地信号以及由act.sa_mask所指定地人一信号添加到进程地信号掩码中,并在处理器正常返回时再将它们从掩码中清除。
如果使用longjmp()来推出信号处理器函数,那么信号掩码会发生什么情况呢?这取决于特定UNIX实现地血统。在System V一脉中,longjmp()不会将信号掩码恢复,亦即在离开处理器函数时不会对遭遇祖泽地信号解除阻塞。Linux遵循System V这一特性。(这通常并非所希望地行为,因为引发对信号处理器调用地信号仍将保持阻塞状态。)在源于BSD一脉地实现中,setjmp()将信号掩码保存在恶女参数中,而信号掩码地保存值由longjmp()恢复。(继承自BSD地实现还提供另外两个拥有System V语义地函数:_setjmp()和_longjmp()。)换言之,使用longjump()来推出信号处理器函数将有损于程序地可移植性。
监狱两大UNIX流派之间地差异,POSIX.1-1990选择不对setjmp()和longjump()地信号掩码处理进行规范,而是定义了一组新函数:sigsetjmp()和siglongjmp(),针对非本地跳转时地信号掩码进行显式控制。
#include <setjmp.h>
int sigsetjmp(sigjmp_buf env,int savesigs);
Returns 0 on initial call,nonzero on return via siglongjmp()
void siglongjmp(sigjmp_buf env,int val);
函数sigset()和siglongjmp()地操作与setjmp()和longjmp()类似,唯一区别式参数地类型不同(是sigjmp_buf而不是jmp_buf),而且sigsetjmp()多出一个参数savesigs。如果指定savesigs为非0,那么将会调用sigsetjmp()时进程地当前信号掩码保存于env中,之前通过指定相同env参数地silongenv()调用进行恢复。如果savesigs为0,则不会保存和恢复进程地信号掩码。
函数longjmp()和siglongjmp()都不在表21-1所列异步信号安全函数地范围之内,因为与在信号处理器函数中调用这些函数一样,在执行非本地跳转之后去调用任何非异步信号安全地函数也需要冒同样地风险。此外如果信号处理器函数中断了正在更新数据结构地主程序,那么执行非本地跳转跳转退出处理器函数之后,这种不完整地更新动作很可能会将数据结构置于不一致状态,规避这一问题地一种技术是在程序对敏感数据进行更新时,借助于sigprocmask()临时将信号阻塞起来。
示例程序
程序清单21-2展示了两种类型地非本地跳转在处理信号掩码上地差异。该程序为SIGINT创建处理器函数,并允许选择setjmp()+longjmp()组合或者sigsetjmp()+siglongjmp()组合地方式来退出信号处理器函数,具体采用何种函数组合取决于编译时是否对宏USE_SIGSETJMP进行了定义。程序会在进入信号处理器函数时,亦即非本地跳转将控制信号处理器交还给主程序后,显示信号掩码地当前设置。
如果想利用longjmp()来推出信号处理器函数,其结果如下:
红框圈出地地方按下Control -C已不再影响,因为SIGINT 已经被阻塞 按下Control -\ 退出
由程序输出结果可知,信号处理器函数调用longjmp()之后地信号掩码设置与进入处理器函数时保持一致,
上述shell会话中构建程序所使用地makefile由随本书发布地源码提供。选项 -s 不要显示正在执行地命令。使用该选项意在避免对绘画日志地显示产生干扰。
如果对同一源文件来床架 利用siglongjmp()退出信号处理器函数地程序,则结果如下:
(编译时并没有使用makefile 只是在文件中简单定义了宏,有兴趣读者可以使用源码中地makefile进行编译,随书源码在另一篇资源中,可免费下载)。
由上述输出可知,在这里并没有将SIGINT信号阻塞,因为siglongjump()恢复了原来地信号掩码。接着,再次按下Ctrl-C,会再次调用该信号处理器函数。siglongjump()将信号掩码恢复到调用sigsetjump()时地值(即一个空信号集)。
程序清单21-2还展示了信号处理器函数执行非本地跳转地一种实用技术。信号随时可能产生,所以有可能发生于sigsetjmp()(或是setjmp())设置非跳转目标之前。为杜绝这种可能(这将导致处理器函数使用未初始化地env缓冲区来执行非本地跳转),程序启用了守卫变量canJmp,来表征env缓冲区地初始化与否。如果canJmp不为真,处理器函数将不执行跳转直接返回。另一种方法时调整程序代码,在创建信号处理器函数之前去调用sigsetjmp()或setjmp().不过对于复杂地程序而言,渴求这样地步骤执行顺序可能会困难,而是用守卫变量会更简单一些。
注意在编写程序清单21-2程序时使用#ifdef是使其编码风格符合标准地最简单地手段。特别是当无法运用下面地运行时间差代码来取代#ifdef时。
if(useSiglongjmp)
s = sigsetjmp(senv,1);
else
s = setjmp(env);
if(s == 0)
....
这一做法有违规范,因为SUSv3不允许在赋值语句(6.8节)中调用setjmp()和sigsetjmp()。
程序清单21-2:在信号处理器函数中执行非本地跳转
#define _GNU_SOURCE /*Get strsignal() declaration from <string.h>*/
#include <string.h>
#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#define USE_SIGSETJMP
static volatile sig_atomic_t canJmp = 0;
/*Set to 1 once "env" buffer has been initialized by [sig]setjmp()*/
#ifdef USE_SIGSETJMP
static sigjmp_buf env;
#else
static jmp_buf env;
#endif
void /* Print list of signals within a signal set */
printSigset(FILE *of, const char *prefix, const sigset_t *sigset)
{
int sig, cnt;
cnt = 0;
for (sig = 1; sig < NSIG; sig++) {
if (sigismember(sigset, sig)) {
cnt++;
fprintf(of, "%s%d (%s)\n", prefix, sig, strsignal(sig));
}
}
if (cnt == 0)
fprintf(of, "%s<empty signal set>\n", prefix);
}
int /* Print mask of blocked signals for this process */
printSigMask(FILE *of, const char *msg)
{
sigset_t currMask;
if (msg != NULL)
fprintf(of, "%s", msg);
if (sigprocmask(SIG_BLOCK, NULL, &currMask) == -1)
return -1;
printSigset(of, "\t\t", &currMask);
return 0;
}
static void handler(int sig)
{
/*UNSAFE:this handler uses non-async-signal-safe functions
(printf(),strsignal(),printSigMask(); see Section 21.1.2)*/
printf("Received signal %d(%s),signal mask is :\n",sig,
strsignal(sig));
printSigMask(stdout,NULL);
if(!canJmp){
printf("'env' buffer not yet set,doing a simple return\n");
return ;
}
#ifdef USE_SIGSETJMP
siglongjmp(env,1);
#else
longjmp(env,1);
#endif
}
int main(int argc,char *argv[])
{
struct sigaction sa;
printSigMask(stdout,"Signal at startup:\n");
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = handler;
if(sigaction(SIGINT,&sa,NULL) == -1)
{
perror("sigaction:");
return -1;
}
#ifdef USE_SIGSETJMP
printf("Calling sigsetjmp()\n");
if(sigsetjmp(env,1) == 0)
#else
printf("Calling setjmp()\n");
if(setjmp(env) == 0)
#endif
canJmp = 1; /*EXCuTED AFTER [SIG]setjmp*/
else
printSigMask(stdout,"After jump from handler,signal mask is :\n");
for(;;) //wait for signals until killed
pause();
}
21.2.2 异常终止进程:abort
函数abort()终止其调用进程,并生成核心转储。
#include <stdlib.h>
void abort(void);
函数abort()通过产生SIGABRT信号来终止调用进程。对SIGABRT地默认动作是产生核心转储文件并终止进程。调试器可以利用核心转储文件来检测调用abort()时地程序状态。
SUSv3要求,无论阻塞或者忽略SIGABRT信号,abort()调用均不受影响。同时规定,除非进程捕获SIGABRT信号后信号处理器函数尚未返回,否则abort(0必须终止进程。后一句话值得三思。21.2节所藐视地信号处理器函数地终止方法中,与此相关地就是使用本地跳转退出处理器函数。这一做法将掉abort()地效果。否则,abort()总是终止进程。在大多数实现中,终止时可确保发生如下之间:若进程在发出一次SIGABRT信号后仍未终止(即,处理器捕捉信号并未返回,以便恢复执行abort()),则abort()会将对SIGABRT信号地处理重置为SIG_DFL,并再度发出SIGABRT信号,从而确保将进程杀死。
如果abort()成功终止了进程,那么还将刷新stdio流并将其关闭。
21.3 在备选栈中处理信号:signalstack()
在调用信号处理器函数时,内核同巡航会在进程栈中为其创建一帧。不过,如果进程对栈地扩展突破了对栈大小地限制时,这种做法就不打可行了。例如,栈地增长过大,以至于会触及到一片映射内存(48.5节)或者向上增长地堆,又或者栈地大小已经直逼RLIMIT_STACK(36.3节)资源限制,这些都会导致这种情况地发生。
当进程对栈地扩展试图突破其上限时,内核将为该进程产生SIGSEGV信号。不过,因为栈空间依然耗尽,内核也就无法再为进程已经安装地SIGSEGV处理器函数创建栈帧,结果是,处理器函数得不到调用,而进程也就终止了(SIGSEGV地默认动作)。
如果希望这种情况下确保对 SIGSEGV信号处理器函数地调用,就需要做如下工作。
- 分配一块被称为“备选信号栈”的内存区域,作为信号处理器函数的栈帧。
- 调用signaltstack(),告之内核该备选信号栈的存在。
- 在创建信号处理器函数时指定SA_ONSTACK标志,亦即通知内核在备选栈上为处理器函数创建函数帧,
利用系统调用sigaltstack(),既可以创建一个备选信号栈、,也可以将已创建备选信号栈的相关信息返回。
#include <signal.h>
int sigaltstack(const stack_t *sigstack,stack_t *old_sigstack);
Returns 0 on success,or -1 on error
参数sigstack所指向的数据结构描述了新备选信号栈的为止及属性。参数old_sigstack指向的结构则用于返回上一备选信号栈的相关信息(如果存在)。两个参数之一均可为NULL。例如,将参数sigstack设为NULL可以发现现有备选信号栈,并且不用将其改变。不为NULL时,这些参数所指向的数据结构类型如下:
typedef struct{
void *ss_sp; /*Starting address of alternate stack*/
int ss_flags; //Flags:SS_ONSTACK,SS_DISABLE
size_t ss_size; //Size of alternate stack
}stack_t
字段ss_sp和ss_size分别指定了备选信号栈的为止和大小,在实际使用信号栈式,内核会将ss_sp值自动对齐与硬件架构相适宜的地址边界。
备选信号栈通常既可以静态分配,也可以在堆上动态分配。SUSv3规定将常量SIGSTKSZ作为备选栈大小的典型值,而将MINSSIGSTKSZ作为调用信号处理器函数所需的最小值。在linux/x86-32系统上,分别将这两个值定义为8192和2048.
内核不会重新划分备选栈的大小。如果溢出了分配给它的空间。就会产生混乱(例如,写变量超出了对栈的限制)。这通常不是一个问题,因为一般情况下会利用备选栈来处理标准栈溢出的特殊情况,常常只在这个栈上分配为数不多的几帧。SIGSEGV处理器函数的工作不是在执行清理动作后终止进程,就是使用非本地跳转解开标准栈。
ss_flag可以包含如下值之一:
SS_ONSTACK
如果在获取已创建备选信号栈的当前信息时该标志位已经置位,就表明进程正在备选信号栈上执行。当进程已经在备选栈上运行时,试图调用signaltstack()来创建一个新的备选信号栈会产生一个错误(EPERM)。
SS_DISABLE
在old_sigstack中返回,表示当前不存在一创建的备选信号栈。如果在sigstack中指定,则会禁用当前已创建的备选信号栈。
在程序清单21-3演示了备选信号栈的创建和使用。在创建一个新的备选信号栈以及SIGSEGV的信号处理器之后,程序将调用一个无限递归函数,这回导致栈溢出,同时系统会向进程发送SIGEGV信号。该程序的运行结果如下。
在这一会话中,明空ulinit负责移除shell之前可能设置的任何RLIIMT_STACK资源限制。36.3节会解释这种资源限制。
#define _GNU_SOURCE //Get strsignal() declaration from string.h
#include <string.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
static void sigsegvHandler(int sig)
{
int x;
/* UNSAFE: This handler uses non-async-signal-safe functions
(printf(), strsignal(), fflush(); see Section 21.1.2) */
printf("Caught signal %d (%s)\n",sig,strsignal(sig));
printf("Top of handler stcak near %10p\n",(void *)&x);
fflush(NULL);
_exit(EXIT_FAILURE); /*can't return after SIGSEGV*/
}
static void overflowStack(int callNum) //a recursive function that overfolws the stack
{
char a[1000000000]; //make this stack frame large
printf("Call %4d - top of stack near %10p\n",callNum, &a[0]);
overflowStack(callNum+1);
}
int main(int argc,char *argv[])
{
stack_t sigstack;
struct sigaction sa;
int j;
printf("Top of standard stack is near %10p\n",(void *)&j);
/*Allocate alternate satck and inform kernel of its existence"*/
sigstack.ss_sp = malloc(SIGSTKSZ);
if(sigstack.ss_sp == NULL)
{
perror("malloc:");
return -1;
}
sigstack.ss_size = SIGSTKSZ;
sigstack.ss_flags = 0;
if(sigaltstack(&sigstack,NULL) == -1)
{
perror("sigaltstack:");
return -1;
}
printf("Alternate stack is at %10p-%p\n",sigstack.ss_sp,(char *)sbrk(0)-1);
sa.sa_handler = sigsegvHandler; //Establish handler for SIGSEGV
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_ONSTACK; //Handler uses alternate stack
if(sigaction(SIGSEGV,&sa,NULL) == -1)
{
perror("sigaction:");
return -1;
}
overflowStack(1);
}
21.4 SA_SIGINFO 标志
如果在使用sigaction()创建处理器函数时设置了SA_SIGINFO标志,那么在收到信号时处理器函数可以获取该信号的一些附加信息。为获取这一信息,需要将处理器函数声明如下:
void handler(int sig,siginfo_t *siginfo,void *ucontext);
如同标准信号处理器一样,第一个参数sig表示信号编号。第二个参数siginfo是用于提供信号附加信息的一个结构。该结构会与最后一个参数ucontext一起,在下面做详细说明。
因为上述信号处理器函数的原型不同于标准处理器函数,依照C语言得类型规则,将无法利用sigaction结构的sa_hanler字段来指定处理器函数地址。此时需要另一个字段:sa_sigaction。换言之,sigaction结构比20.13节所展示的要稍微复杂一些。其完整定义如下:
struct sigaction{
union{
void(*sa_handler)(int);
void(*sa_sigaction)(int,siginfo_t *,void *)
}_sigaction_handler;
sigset_t sa_mask;
int sa_dlags;
void (*sa_restorer)(void)
};
/*Following defines make the union fields look like simple fields
in the parent structure*/
#define sa_handler __sigaction_handler.sa_handler
#define sa_sigaction __sigaction_handler.sa_sigaction
结构sigaction使用联合体来合并sa_sigaction和sa_handler。(大部分其他UNIX实现也采用相同的方式。)之所以使用联合体,是因为对sigaction()的特定调用只会用到其中一个字段。(不过,如果天真的认为可以彼此独立地设置sa_handler和sa_sigaction,就有可能导致一些奇怪地bug。可能地原因是在为不同的信号创建处理器函数时,多次对sigaction()调用服用了同一个sigaction结构。)
这里使用SA_SIGINFO创建信号处理器函数地一个例子:
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO;
if(sigaction(SIGINT,&act,NULL) == -1)
{
perror("sigaction:");
return -1;
}
至于使用SA_SIGINFO标志完整的例子,请参考程序22-3和22-5
结构 siginfo_t
在以SA_SIGINFO标志创建地信号处理器函数中,结构siginfo_t是其第2个参数,格式如下:
typedef struct{
int si_signo; //Signal number
int si_code; //Signal code
int si_trapno; //Trap number for hardware-generated signal
//unsed on most architectures
union sigval si_value; /*Accopanying data from sigqueue()*/
pid_t si_pid; /*process ID of sending process*/
pid_t si_uid; /*Real user ID of sender*/
int si_errno; /*Error number (generally unsed)*/
void *si_addr; /*Address that generated signal
(hardware-generated signals only)*/
int si_oberrun; /*overrun count (Linux 2.6 POSIX timers)*/
int si_timerid; /*(kernel-internal) Timer ID
(Linux 2.6,POSIX timers)*/
long si_band; /*Band event (SIGPOLL/SIGIO)*/
int si_fd; /*File descriptor (SIGPOLL/SIGIO)*/
int si_status; /*Exit status or signal (SIGCHLD)*/
clock_t si_utime; /*User CPU time(SIGCHLD)*/
clock_t si_stime; /*System CPUctime (SIGCHLD)*/
}siginfo_t;
要获取<signal.h>对siginfo_t地声明,必须将特性测试宏_POSIX_C_SOURCE地值定义为大于或等于199309.
如同大部分UNIX实现一样,在Linux系统中,siginfo_t结构很多地字段都是联合体,因为对每个信号而言,并非所有信号都有必要。
一旦进入信号处理器函数,对结构siginfo_t中字段地设置如下。
si_signo
需要为所有信号设置。内含引发处理器函数调用地信号编号---与处理器函数sig参数相同。
si_code
需要为所有信号设置。如表21-2所示,所含代码提供了关于信号来源地深入信息。
si_value
该字段包含调用sigqueque()发信号时地伴随数据。22.8.1节将讨论sigqueque()。
si_pid
对于经由kill()会sigqueque()发送地信号,该字段保存了发送进程地进程ID。
si_uid
对于经由kill()会sigqueque()发送地信号,该字段保存了发送进程的真实用户ID。系统之所以提供真实用户ID,是因为其信息量比之有效用户ID更为丰富。会与20.5节所述关于信号发送的权限规则,如果有效用户ID授予发送者信号的权力,那么发送方的用户ID必须要么为0(特权用户),要么与接收进程的真实用户ID或者保存设置用户ID(saved set-user-ID)相同。这时,接收者了解发送者的真实用户ID就很有用,因为他可能不同于有效用户ID(例如,发送这是一个set-user-ID程序)。
si_errno
如果将该字段设置为非0值,则其所包含一错误号(类似errno),标志信号的产生原因。Linux通常不用该字段。
si_addr
仅针对由硬件产生的SIGBUG、SIGSEGV、SIGILL和SIGFPE信号设置该字段,对于SIGBUS和SIGSEGV而言,该字段内涵引发无效内存引用的地址。对于SIGILL和SIGFPE信号,则包含导致信号产生的程序指令地址。
以下字段均属非标准的Linux扩展,仅当POSIX定时器(23.6节)到其而产生信号传递时设置:
si_timerid
内含供内核内部使用的ID,以标识定时器。
si_overrun
设置该字段为定时器的溢出次数。
仅当收到SIGIO信号(63.3节)时,才会设置下年两个字段。
si_band
该字段包含与I/O时间相关的“带事件”值。(直到glicc2.3.2,si_band的类型都是int型。)
si_fd
该字段包含与I/O时间相关的文件描述符编号。SUSv3并未定义这一字段,不过许多其他实现都予以了支持。
仅当收到SIGCHLD信号(26.3节)时才会对以下字段进行设置。
si_status
该字段包含子进程的退出状态(当si_code=CLD_EXITED时)或者发送给子进程的信号编号(即26.1.3节所述终止或者停止子进程的信号编号)。
si_utime
该字段包含子进程使用的用户CPU时间,在版本2.6以前,以及2.6.27以后的内核版本中,对该字段的度量以系统时钟抵达除以sysconf(_SC_CLK_TCK)的返回值作为基本单位。而在版本2.6.27之前的2.6内核中存在bug,该字段在报告时间时采用的度量单位为(可由用户配置的)jiffy(10.6节)。SUSv3没有定义该字段,但许多其他实现都予以支持。
si_stime
该字段包含了子进程使用的系统CPU时间。可参考si_utime的描述。同样,SUSv3并未定义该字段,不过许多其他实现都予以支持。
si_code字段提供了关于信号来源的更多信息,其值如表21-2所示。表中第2列列出的信号特有值(特别是由硬件产生的四种信号:SIGBUS、SIGEGV、SIGILL和SIGFPE)不会悉数现身于所有的UNIX实现以及硬件架构上--尽管Linux定义了所有常量,而且SUSv3也定义了其中的大部分。
关于表21-2中所示各值,还需要注意以下几点附加说明。
- 关于SI_KERNEL和SI_SIGIO为Linux所特有,既未或SUSv3定义,也未获其他UNIX实现支持
- SIG_SIGIO仅在Linux2.2用到,子内核2.4起,Linux转而采用表中的POLL_*常量。
表21-2:结构siginfo_t中si_code字段返回值一览表
SUSv4 定义了功用与psignal()(20.8节)相仿的psiginfo()函数。函数psiginfo()带有两个参数,分别是指向sigiinfo_t结构的指针和一个消息字符串。该函数在标准错误设备上输出字符串消息,接着显示描述于siginfo_t结构中的信号信息。glibc自2.10版开始提供psiginfo()函数。glibc实现会显示信号的描述信息及来源(根据si_code字段所示),对于某些信号,还会列出siginfo_t结构中的其他字段,函数psiginfo()是SUSv4中的新丁,并非所有系统都予以支持
参数 ucontext
以SA_SIGINFO标志所创建的信号处理器函数,其最后一个参数是ucontext,一个指向ucontext_t类型结构(定义于<ucontext.h>)的指针,因为SUSv3并未规定该参数的人格细节,所以定义未void类型指针。该结构提供了所谓的用户上下文信息,用于描述调用信号处理其函数前的进程状态,其中包括上一个进程信号掩码以及寄存器的保存值,例如程序计数器(cp)和栈指针寄存器(sp)。信号处理器函数很少用到此类信息。
使用结构ucontext_t的其他函数有getcontext()、makeecontext()、setcontext()和swapcontext(),分别对应的功能是允许进程去接收、创建、改变以及交换执行上下文。(这些操作优点类似setjmp和longjmp()但更为通用。)可以使用这些函数来实现写成(coroutines),另进程的执行线程在两个(或多个)函数之间交替。SUSv3规定了这些函数,但将他们标记为废止。SUSv4则将其删去,并建议POSIX线程来重写旧有的应用程序。glibc手册页提供了关于这些函数的深入信息。
21.5 系统调用的中断和重启
考虑如下场景。
- 为某信号创建处理器函数。
- 发起一个阻塞的系统调用(blocking system call),例如,从终端设备调用的read()就会阻塞到有数据为止。
- 当系统调用遭到阻塞时,之前创建了信号处理器函数的信号传递了过来,随即引发对处理器函数的调用。
信号处理器函数返回后又发生什么》某人情况下,系统调用失败,并将errno置为EINTR.这是一种有用的特性。23.3节将会描述如何使用定时器(会产生SIGALRM信号)来设置read()之类阻塞系统调用的超时。
不过,更为常见的情况时希望遭到中断的系统调用得以继续运行。为此可在系统调用早信号处理器中断的事件中,利用如下代码来手动重启系统调用。
while((cnt = read(fd,buf,BUF_SIZE)) == -1 && errno == EINTR)
continue; //do nothing loop body
if(cnt == -1)
{
perror("read:");//read() failed with other than EINTR
return -1;
}
如果需要频繁使用上述代码,那么定义成如下宏会很方便:
#define NO_EINTR(stmt) while((stmt) == -1 && errno == EINTR);
//使用该宏,可以将早先对read()的调用改写如下:
NO_EINTR(cnt = read(fd,buf,BUF_SIZE));
if(cnt == -1) //read() failed with other than EINTR
{
perror("read()");
return -1;
}
即使采用了类似NO_EINTR()这样的宏,让信号处理器来终端系统调用还是颇为不便,因为只要有意重启阻塞的调用,就需要为每个自责的系统调用添加代码。反之,可以调用指定了SA_RESTART标志的sigaction()来创建信号处理器函数,从而令内核代表进程自动重启系统调用,还无需处理系统调用可能返回的EINTR错误。
标志SA_RESTART时针对每个信号的设置,换言之,允许某些信号的处理器函数中盾阻塞的系统调用,而其他系统调用可以自动重启。
SA_RESTART 标志对哪些系统调用(和库函数)有效
不幸的是,并非所有的系统调用都可以通过指定SA_RESTART来达到自动重启的目的。究其原因,有部分历史因素。
- 4.2BSD引入了重启系统调用的概念,包括中断对wait()和waitpid()的调用,以及如下1/O系统调用:read()、readv()、write()和阻塞的ioctrl()操作。I/O系统调用都是可中断的,所以只有在操作“慢速(slow)”设备时,才可以利用SA_RESTART标志来自动重启调用。慢速设备包括终端(terminal)、管道(pipe)、FIFO以及套接字(socket)。对于这些文件类型,各种I/O操作都有可能阻塞。(相比之下,磁盘文件并未沦入慢速设备之列,因为借助于缓冲区高速缓存,磁盘I/O请求一般都可以立即得到满足。当出现磁盘I/O请求时,内核会令该进程休眠,直至完成I/O动作为止。)
- 其他大量阻塞的系统调用则继承自System V,在其初始设计中并未提供重启系统调用的功能。
在Linux中,如果采用SA_RESTART标志来创建系统处理器函数,则如下阻塞的系统调用(以及构建于其上的库函数)在遭到中断时是可以自动重启的。
- 用来等待子进程(26.1节)的系统调用:wait(),waitpid()、wait3()、wait4()、和waittid()。
- 访问慢速设备时的I/O系统调用:read()、readv()、write()、writev()和ioctl()。如果在收集到信号时,已经传递了部分数据,那么还是会中断输入输出系统调用,但会返回成功状态:一个整型值,标识已成功传递数据的字节数。
- 系统调用open(),在可能阻塞的情况下(例如,44.7节所述,再打开FIFO时)。
- 用于套接字的各种系统调用:accept(),accept4()、connect()、send()、sendmsg()、sendto()、recv()、recvfrom()和recvmsg()。(在Linux中,如果使用setsockopt()来设置超时,这些系统调用就不会自动重启。)
- 对POSIX消息队列进行I/O系统调用:mq_receive()、mq_timedreceive()、mq_send()和mq_timedsend()。
- 用于设置文件锁的系统调用和库函数:flock()、fcntl()和lockf()。
- Linux 特有系统调用futex()的FUTEX_WAIT操作。
- 用于递减POSIX信号量的sem_wait()和sem_timedwait()函数。在一些unix实现上,如果设置了SA_RESTART标志,sem_wait()就会重启。
- 用于同步POSIX线程的函数:pthread_nutex_lock()、pthread_mutex_trylock()、pthread_mutex_timelock()、pthread_cond_wait()和pthread_cond_timedwait()。
内核2.6.22之前,不管是否设置了SA_RESTART标志,futex()、sem_wait()和sem_timedwait()遭到中断时总是产生EINTR错误。
以下阻塞的系统调用(以及构建于其上的库函数)则不会自动重启(即便指定了SA_RESTART)。
- poll()、ppoll()、select()、和pselect()这些I/O多路复用调用。(SUSv3明文规定,无论设置SA_RESTART标志与否,都不对select()和pselect()遭处理器函数中断时的行为进行定义。)
- Linux 特有的epoll_wait()和epoll_pwait系统调用。
- Linux特有的io_getevents()系统调用。
- 操作System V 消息队列和信号量的阻塞系统调用:semop()、semtimedop()、msgrecv()和msgsnd()。(虽然System V原本并未提供自动重启系统调用的功能,但在某些UNIX实现上,如果设置了SA_RESTART 标志,这些系统调用还是会重启。)
- 对inotify文件描述符发起的read()调用。
- 用于将进程挂起指定时间的系统调用和库函数:sleep()、nanosleep()和clock_nanosleep()。
- 特意设计用来等待某一信号到达的系统调用:pause()、sigsuspend()、sigtimedwait()和sigwaitinfo()。
为信号修改SA_RESTART标志
函数siginterrupt()用于改变信号的SA_RESTART设置。
#include <signal.h>
int siginterrupt(int sig,int flag);
Returns 0 on success,or -1 on error
若参数flag为真,则对信号sig的处理器函数将会中断阻塞的系统调用的执行。如果flag为假,那么在执行了sig处理器函数之后会自动重构其阻塞的系统调用。
函数siginterrupt()的工作原理是:调用sigaction()获取信号当前处置的副本调整自结构oldact中返回的SA_RESTART标志,接着再次调用sigaction()来更新信号处置。
SUSv4标记siginterrupt()已废止,并推荐使用sigaction加以代替。
对于某些Linux系统调用,未处理的停止信号会产生EINTR错误
在Linux上,即使没有信号处理器函数,某些阻塞的系统调用也会产生EINTR错误。如果系统调用遭到阻塞,并且进程因信号(SIGSTOP、SIGSTP、SIGTTIN或SIGTTOU)而停止,之后又因收到SIGCONT信号而恢复执行时,就会发生这种情况。
以下系统调用和函数具有这一行为:epoll_pwait()、epoll_wait()、对inotify文件描述符执行的read()调用、semop()、semtimedop()、sigtimedwait()和sigwaitinfo()。
内核2.6.24之前,poll也曾存在这种行为,2.6.22之前的sem_wait()、sem_timedwait()、futex(FUTEX_WAIT),2.6.9之前的msgrcv()和msgsnd(),以及Linux2.4及其之前的nanosleep()也同样如此。
在Linux2.4 及其之前的版本中,也可以使用这种方式及来中断sleep(),但是不会返回错误值,而是返回休眠所剩余的秒数。
这种行为的结果是,如果程序可能因信号而停止和重启,那么就需要添加代码来重新启动这些系统调用,即便该程序并未为停止信号设置处理器函数。
21.6总结
本章讨论了影响信号处理器函数操作与设计的一系列因素。
由于没有对信号排队,故而在为处理器编码时,有时必须要考虑特定类型信号多次发生的可能性,即使之前信号只产生过一次。可重入问题会影响到对全局变量的修改方式,还限制了可从信号处理器函数中安全调用的函数范围。
除了返回之外,信号处理器函数的终止还存在多种其他方法,其中包括:调用_exit(),发送信号来终止进程(kill(),raise()或abort()),或者执行非本地跳转。借助于sigsetjmp()和siglongjmp(),可以在执行非本地跳转时为程序提供处理信号掩码的显式控制手段。
可以使用sigaltstack()来为进程定义备选信号栈。这是调用信号处理器函数时,用来代替标准进程栈的一块内存。当标准栈因增长过大(内核会在此时向进程发送SIGSEGV信号)而消耗殆尽时,备选栈就特别有用。
如果在调用sigaction()时设置了SA_SIGINFO标志,那么所创建的信号处理器函数就能接收信号的附加信息。siginfo_t结构提供了这些信息,其地址则传递给信号处理器作为参数。
如果信号处理器函数中断了阻塞的系统调用,系统调用会产生EINTR错误。利用这种特性,就可以为阻塞的系统调用设置一个定时器。如果有意,可以手动重启遭到中断的系统调用。另外,在调用sigaction()创建信号处理器函数时,如果设置了SA_RESTART标志,那么大部分(但非全部)系统调用都将会自动重启