信号操作与检测函数

2014-09-05 07:37:07   最后更新: 2014-09-18 08:58:42   访问数量:1350




调用 sigprocmask 函数可以检测或更改进程的信号屏蔽字

int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);

 

定义于 signal.h 中,调用成功返回0,否则返回-1

 

该函数的参数和返回值有如下几种情况

  1. 若 oset 为非空指针,则当前进程的信号屏蔽字会通过 oset 返回
  2. 若 set 为非空指针,则参数 how 指示如何修改当前信号屏蔽字
  3. 若 set 为空指针,则不改变该进程的信号屏蔽字,how参数也没有意义

 

下表中是 how 参数的意义

sigprocmask 用法
how说明用途
SIG_BLOCK该进程的信号屏蔽字是其当前信号屏蔽字和 set 指向信号集的并集用来附加阻塞信号
SIG_UNBLOCK该进程的信号屏蔽字是其当前信号屏蔽字和 set 指向信号集的交集用来解除阻塞
SIG_SETMASK该进程的信号屏蔽字设置为 set 指向信号集的值设置阻塞信号

 

在调用 sigprocmask 函数后对于未决的、不在阻塞的信号,都会在 sigprocmask 函数返回前递送至少一个信号给进程

 

  • 注意:sigprocmask 是仅为单线程的进程定义的,为处理多线程的进程中信号的屏蔽有另一个单独的函数,这个函数我们后面介绍

 

实例(1)

下面的例子中,使用sigprocmask 向进程信号屏蔽字中加入了 SIGINT 信号,因此在程序执行过程中无法使用 CTRL+C 中断程序的执行

#include <stdio.h> #include <unistd.h> #include <signal.h> #include <malloc.h> int main() { sigset_t *set = (sigset_t*)malloc(sizeof(set)); sigemptyset(set); sigaddset(set, SIGINT); int ret = sigprocmask(SIG_BLOCK, set, NULL); printf("ret: %d\n", ret); sleep(15); return 0; }

 

实例(2)

#include <stdio.h> #include <signal.h> #include <errno.h> int main () { sigset_t sigset, osigset; int errno_save; sigemptyset(&sigset); sigaddset(&sigset, SIGINT); errno_save = errno; if (sigprocmask(SIG_BLOCK, &sigset, NULL) != 0) printf("set sigprocmask error\n"); if (sigprocmask(0, NULL, &osigset) != 0) printf("sigprocmask error\n"); printf("sig_mask:\n"); if (sigismember(&osigset, SIGINT)) printf("SIGINT "); if (sigismember(&osigset, SIGQUIT)) printf("SIGQUIT "); if (sigismember(&osigset, SIGUSR1)) printf("SIGUSR1 "); if (sigismember(&osigset, SIGALRM)) printf("SIGALRM "); printf("\n"); errno = errno_save; return 0; }

 

需要注意的是,如果同时提供了第二个参数和第三个参数,sigprocmask 函数会首先返回当前信号屏蔽字到第三个参数,然后根据第一个和第二个参数设置新的信号屏蔽字

 

sigpending 返回未决信号集

int sigpending(sigset_t *set);

 

调用成功则返回0,信号集由 set 参数返回,出错返回 -1

 

示例

#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> static void sig_quit(int); int main () { sigset_t newmask, oldmask, pendmask; if (signal(SIGQUIT, sig_quit) == SIG_ERR) { printf("catnot catch SIGQUIT\n"); exit(-1); } sigemptyset(&newmask); sigaddset(&newmask, SIGQUIT); if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) { printf("SIG_BLOCK error\n"); exit(-1); } sleep(5); if (sigpending(&pendmask) < 0) { printf("sigpending error\n"); exit(-1); } if (sigismember(&pendmask, SIGQUIT)) printf("\nSIGQUIT pending\n"); if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) { printf("SIG_SETMASK error\n"); exit(-1); } printf("SIGQUIT unblocked\n"); sleep(5); return 0; } static void sig_quit (int signo) { printf("caught SIGQUIT\n"); if (signal(SIGQUIT, SIG_DFL) == SIG_ERR) { printf("cannot reset SIGQUIT"); exit(-1); } }

 

执行结果如下图所示:

 

由于在信号被阻塞期间,内核向进程递送了信号,所以 SIGQUIT 信号存在于 sigpending 返回的未决信号集中,即解除阻塞后信号会被递送到进程

图中可以看到,信号并没有被排队递送而是只递送了一次

 

由于信号阻塞期间没有产生过 SIGQUIT 信号,所以在未决信号集中,也就不包含该信号了

 

sigaction 函数的功能是检查或修改与指定信号相关联的处理动作(或同时执行这两种操作)

int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);

 

定义于 signal.h 中

调用成功返回0,否则返回-1

 

参数 signo 是要检测或修改的具体动作的信号编号,若 act 指针为非空,则要修改其动作,若 oact 指针为非空,则系统经由 oact 指针返回该信号的上一动作

 

sigaction 结构体结构如下:

struct sigaction { void (*sa_handler) (int); sigset_t sa_mask; int sa_flags void (*sa_sigaction) (int, siginfo_t*, void*); }

 

 

当执行信号动作时,如果 sa_handler 字段包含一个信号捕捉函数的地址,则 sa_mask 字段说明了一个信号集,在调用该信号捕捉函数之前,这一信号集要加到进程的信号屏蔽字中

从信号捕捉函数返回时,进程的信号屏蔽字会被复位为原先值

这样就保证了在处理一个给定的信号时,如果信号再次发生则会先阻塞以完成前一个信号的处理

 

sa_flags 字段指定对信号进行处理的各个选项,如下表:

sigaction 选项说明
选项说明
SA_INTERRUPT(默认处理方式)此信号中断的系统调用不会自动重启动
SA_NOCLDSTOP若 signo 是 SIGCHLD,当子进程停止或由停止继续运行时(作业控制),不产生此信号,只有当子进程终止时才产生此信号
SA_NOCLDWAIT若 signo 是 SIGCHLD,则当调用进程的子进程终止时,不创建僵死进程,若调用进程调用了 wait,则调用进程阻塞直至所有子进程都终止,此时wait返回-1,并将errno设置为 ECHILD
SA_NODEFER当捕捉到此信号时,在执行其信号捕捉函数时,系统不自动阻塞此信号,除非sa_mask包括了此信号,注意,此种类型的操作对应于早期不可靠信号
SA_ONSTACK若用 sigaltstack 声明了一个替换栈,则将信号地送给替换栈上的进程
SA_RESETHAND在信号捕捉函数的入口处,将此信号处理方式复位为 SIG_DFL,并清除 SA_SIGINFO 标志,注意,次类型的信号对应于早期不可靠信号,但是,不能自动复位 SIGKILL 和 SIGTRAP 这两个信号的配置
SA_RESTART由此信号中断的系统调用会自动重启
SA_SIGINFO此选项对信号提供了附加信息:一个指向 siginfo 结构的指针以及一个指向进程上下文标识符的指针

 

sigaction 结构中有两个信号处理函数字段,但是应用程序一次只能使用这两个中的一个,因为系统可能使用同一存储区去实现这两者

通常使用:

void handler (int signo);

 

但是如果设置了 SA_SIGINFO 标志,则使用下列方式处理信号:

void handler (int signo, siginfo_t *info, void *context);

 

siginfo_t 结构包含了信号产生原因的相关信息,POSIX 规定至少需要包含 si_signo 和 si_code 成员,XSI 扩展要求至少实现下列字段:

struct siginfo { int si_signo; /* signal number */ int si_errno; /* if nonzero, errno value from <errno.h> */ int si_code; /* additional info (depends on signal) */ pid_t si_pid; /* sending process ID */ uid_t si_uid; /* sending process real user ID */ void *si_addr; /* address that caused the fault */ int si_status; /* exit value or signal number */ long si_band; /* band number for SIGPOLL */ }

 

si_code 如下表所示:

 

示例 -- 用 sigaction 实现 signal 函数

Sigfunc *signal(int signo, Sigfunc *func) { struct sigaction act, oact; act.sa_handler = func; sigemptyset(&act.sa_mask); act.sa_flags = 0; if (signo == SIGALRM) { #ifdef SA_INTERRUPT act.sa_flags |= SA_INTERRUPT; #endif } else { #ifdef SA_RESTART act.sa_flags |= SA_RESTART; #endif } if (sigaction(signo, &act, &oact) < 0) return SIG_ERR; return oact.sa_handler; }

 

对除 SIGALRM 信号外的所有信号,我们都尝试设置 SA_RESTART 标志,是为了让这些信号中断的系统调用能重新启动

而不希望重启动由 SIGALRM 信号中断的系统调用的原因是,我们可能希望通过 SIGALRM 信号对 IO 操作设置时间限制

 

之前我们看到,调用 setjmp 和 longjmp 函数在信号处理程序中是非常有用的,用来直接返回到程序的主循环中,而不是从信号处理程序正常返回

但是,当进入信号捕捉函数时,此时当前I型您好被自动地加到进程的信号屏蔽字中,如果信号处理程序没有正常返回,则进程将一直屏蔽该信号的产生

因此,POSIX 提供了 sigsetjmp 和 siglongjmp 两个函数

int sigsetjmp(sigjmp_buf env, int savemask); void siglongjmp(sigjmp_buf env, int val);

 

定义于 setjmp.h 中

第一个函数如果直接调用返回0,若从 siglongjmp 调用返回则返回非0

 

这两个函数与 setjmp 和 longjmp 函数相比,sigsetjmp 增加了一个参数 -- savemask

如果 savemask 非0,则sigsetjmp 在 env 中保存进程的当前信号屏蔽字,调用 siglongjmp 时,如果带非0的 savemask 的 sigsetjmp 调用已经保存了 env,则 siglongjmp 从其中恢复保存的信号屏蔽字

示例

下例展示了两个函数的具体用法:

#include "../apue.2e/include/apue.h" #include <setjmp.h> #include <time.h> static void sig_usr1(int), sig_alrm(int); static sigjmp_buf jmpbuf; static volatile sig_atomic_t canjump; int main () { if (signal(SIGUSR1, sig_usr1) == SIG_ERR) err_sys("signal(SIGUSR1) error\n"); if (signal(SIGALRM, sig_alrm) == SIG_ERR) err_sys("signal(SIGALRM) error\n"); printf("starting main\n"); if (sigsetjmp(jmpbuf, 1)) { printf("ending main\n"); return 0; } canjump = 1; while (1) pause(); } static void sig_usr1(int signo) { time_t starttime; if (canjump == 0) return; printf("starting sig_usr1\n"); alarm(3); starttime = time(0); while (1) { if (time(0) > starttime + 5) break; } printf("finishing sig_usr1\n"); canjump = 0; siglongjmp(jmpbuf, 1); } static void sig_alrm(int signo) { printf("in sig_alrm\n"); }

 

执行结果如下图:

 

 

书中叙述,如果将sigsetjmp和siglongjmp分别换成_setjmp和_longjmp,则结果的最后一行输出会变成ending main: SIGUSR1

事实上,无论是换成_setjmp 和 _longjmp 还是 setjmp 和 longjmp 结果都是一样的

是因为在信号处理函数执行过程中并不屏蔽当前信号吗?

将程序改成下面这样:

#include "../apue.2e/include/apue.h" #include <signal.h> #include <setjmp.h> #include <time.h> static void sig_usr1(int), sig_alrm(int); static sigjmp_buf jmpbuf; static volatile sig_atomic_t canjump; int main () { if (signal(SIGUSR1, sig_usr1) == SIG_ERR) printf("signal(SIGUSR1) error\n"); if (signal(SIGALRM, sig_alrm) == SIG_ERR) printf("signal(SIGALRM) error\n"); printf("starting main\n"); if (sigsetjmp(jmpbuf, 1)) { printf("ending main\n"); return 0; } canjump = 1; while (1) pause(); } static void sig_usr1(int signo) { time_t starttime; if (canjump == 0) return; printf("starting sig_usr1\n"); alarm(3); starttime = time(0); while (1) { if (time(0) > starttime + 5) break; } printf("finishing sig_usr1\n"); canjump = 0; siglongjmp(jmpbuf, 1); } static void sig_alrm(int signo) { int ret; printf("starting sig_alrm\n"); sigset_t oset; ret = sigemptyset(&oset); if (ret != 0) { printf("init sigset error\n"); return; } if ((ret = sigprocmask(0, NULL, &oset)) != 0) { printf("sigprocmask error\n"); return; } printf("sigismember SIGUSR1 ? %d\n", sigismember(&oset, SIGUSR1)); printf("sigismember SIGALRM ? %d\n", sigismember(&oset, SIGALRM)); printf("finishing sig_alrm\n"); }

 

执行结果如下图:

 

可见信号处理函数在信号处理过程中并不屏蔽该信号,如果在信号处理函数中,该信号再次产生,则可能导致程序陷入死循环

但是如果将signal函数换成 sigaction 函数,则信号在信号处理函数中确实会被屏蔽,因此在信号处理中,应使用sigaction函数,而不是使用signal函数

 

上面的程序演示了另一种技术,仅在调用 sigsetjmp 之后才将变量 canjump 设置为非0 值,在信号处理程序中检查该变量,仅当他为非0值时,才调用 siglongjmp,这样,可以保证在跳转以前 jmpbuf 一定已经被初始化过,如果 jmpbuf 尚未初始化,则不调用信号处理程序

canjmp 的数据类型是 sig_atomic_t,这是由 ISO C 定义的变量类型,保证写这种类型的变量时不会被中断,这种类型的变量总是包括 ISO 修饰符 volatitle,保证每一个操作都不会因编译器优化而被跳过

 

通过阻塞所选择的信号,可以保护不希望被信号中断的代码临界区,但有时我们希望解除一个信号的阻塞状态,然后pause等待信号的发生

下例解除并等待 SIGINT 信号

sigset_t newmask, oldmask; sigemptyset(&newmask); sigaddset(&newmask, SIGINT); if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) err_sys("SIG_BLOCK error"); if (sigprocmask(SIG_SETMASK, &oldmaks, NULL) < 0) err_sys("SIG_SETMASK error"); pause();

 

 

考虑上面的程序,可能出现在解除阻塞后,尚未执行到pause时,此时产生了一个信号,因此当最终执行到pause时,由于需要捕捉的信号已经产生过了,可能不会再次产生,而造成进程的永远挂起

因此,需要原子地先恢复信号屏蔽字,然后使进程休眠

这就是 sigsuspend 函数的功能

sigsuspen 函数

int sigsuspend(const sigset_t *sigmask);

 

返回-1,并将 errno 设置为 EINTR

 

将进程的信号屏蔽字设置为 sigmask 指向的值,并挂起进程直至有信号产生

 

下例展示了如何保护临界区不被信号中断

/* * author: 龙泉居士 * date: 2014-09-15 * file: main.c */ #include "function/function.h" #include <setjmp.h> #include <time.h> int main() { sigset_t newmask, oldmask, waitmask; struct sigaction sa, osa; sa.sa_flags = 0; sigemptyset(&(sa.sa_mask)); sa.sa_handler = sig_int; if (sigaction(SIGINT, &sa, &osa) != 0) { printf("sigaction error\n"); return -1; } pr_mask("program start: "); sigemptyset(&waitmask); sigaddset(&waitmask, SIGUSR1); sigemptyset(&newmask); sigaddset(&newmask, SIGINT); if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) != 0) { printf("SIG_BLOCK error\n"); return -1; } pr_mask("in critical region: "); if (sigsuspend(&waitmask) != -1) { printf("sigsuspend error\n"); return -1; } pr_mask("after sigsuspend: "); if (sigprocmask(SIG_SETMASK, &oldmask, NULL) != 0) { printf("SIG_SETMASK error\n"); return -1; } if (sigaction(SIGINT, &osa, NULL) != 0) { printf("recover sigaction error\n"); return -1; } return 0; }

 

/* * author: 龙泉居士 * date: 2014-09-15 * file: function.h */ #ifndef _FUNCTION_H_20140916_ #define _FUNCTION_H_20140916_ #include "../../apue.2e/include/apue.h" #define true 1 #define false 0 void sig_int(int); void pr_mask(const char*); #endif

 

/* * author: 龙泉居士 * date: 2014-09-15 * file: function.c */ #include "function.h" #include <stdio.h> #include <errno.h> void pr_mask(const char* str) { int save_errno = errno; int signo_array[] = { SIGHUP, SIGINT, SIGQUIT, SIGILL, SIGTRAP, SIGABRT, SIGBUS, SIGFPE, SIGKILL, SIGUSR1, SIGSEGV, SIGUSR2, SIGPIPE, SIGALRM, SIGTERM, SIGSTKFLT, SIGCHLD, SIGCONT, SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU, SIGURG, SIGXCPU, SIGXFSZ, SIGVTALRM, SIGPROF, SIGWINCH, SIGIO, SIGPWR, SIGSYS, SIGRTMIN, SIGRTMIN+1, SIGRTMIN+2, SIGRTMIN+3, SIGRTMIN+4, SIGRTMIN+5, SIGRTMIN+6, SIGRTMIN+7, SIGRTMIN+8, SIGRTMIN+9, SIGRTMIN+10, SIGRTMIN+11, SIGRTMIN+12, SIGRTMIN+13, SIGRTMIN+14, SIGRTMIN+15, SIGRTMAX-14, SIGRTMAX-13, SIGRTMAX-12, SIGRTMAX-11, SIGRTMAX-10, SIGRTMAX-9, SIGRTMAX-8, SIGRTMAX-7, SIGRTMAX-6, SIGRTMAX-5, SIGRTMAX-4, SIGRTMAX-3, SIGRTMAX-2, SIGRTMAX-1, SIGRTMAX }; char *sigstr_array[] = { "SIGHUP", "SIGINT", "SIGQUIT", "SIGILL", "SIGTRAP", "SIGABRT", "SIGBUS", "SIGFPE", "SIGKILL", "SIGUSR1", "SIGSEGV", "SIGUSR2", "SIGPIPE", "SIGALRM", "SIGTERM", "SIGSTKFLT", "SIGCHLD", "SIGCONT", "SIGSTOP", "SIGTSTP", "SIGTTIN", "SIGTTOU", "SIGURG", "SIGXCPU", "SIGXFSZ", "SIGVTALRM", "SIGPROF", "SIGWINCH", "SIGIO", "SIGPWR", "SIGSYS", "SIGRTMIN", "SIGRTMIN+1", "SIGRTMIN+2", "SIGRTMIN+3", "SIGRTMIN+4", "SIGRTMIN+5", "SIGRTMIN+6", "SIGRTMIN+7", "SIGRTMIN+8", "SIGRTMIN+9", "SIGRTMIN+10", "SIGRTMIN+11", "SIGRTMIN+12", "SIGRTMIN+13", "SIGRTMIN+14", "SIGRTMIN+15", "SIGRTMAX-14", "SIGRTMAX-13", "SIGRTMAX-12", "SIGRTMAX-11", "SIGRTMAX-10", "SIGRTMAX-9", "SIGRTMAX-8", "SIGRTMAX-7", "SIGRTMAX-6", "SIGRTMAX-5", "SIGRTMAX-4", "SIGRTMAX-3", "SIGRTMAX-2", "SIGRTMAX-1", "SIGRTMAX" }; sigset_t mask; if (sigprocmask(0, NULL, &mask) != 0) { printf("sigprocmask error\n"); return; } printf("%s\n", str); printf("sig mask: "); int i, is_mask = false; for (i=0; i < sizeof(signo_array)/sizeof(int); ++i) { if (sigismember(&mask, signo_array[i])) { is_mask = true; printf("%s ", sigstr_array[i]); } } if (!is_mask) { printf("no_signal_masked"); } printf("\n"); errno = save_errno; } void sig_int(int signo) { pr_mask("caught SIGINT\n"); }

 

执行结果:

 

可以看到,sigsuspend 首先将信号屏蔽字设置为了 waitset,并等待,直到 SIGINT 信号发生,在 SIGINT 信号处理函数中,阻塞了 waitset 中的信号,当处理结束之后,进程的信号屏蔽字恢复到了函数执行之前

 

sigsuspend 的另一个常用方式是等待在信号处理函数中设置一个全局变量的值,但是需要注意的是,只能够给 sig_atomic_t 类型的变量复制,否则后果是不可预期的

 

示例 -- 通过 suspend 实现父子进程同步

/* * author: 龙泉居士 * date: 2014-09-17 * file: main.c */ #include <signal.h> static volatile sig_atomic_t sigflag; static sigset_t newmask, oldmask, zeromask; static void sig_usr(int signo) { sigflag = 1; } void TELL_WAIT(void) { if (signal(SIGUSR1, sig_usr) == SIG_ERR) { printf("signal SIGUSR1 error\n"); exit(0); } if (signal(SIGUSR2, sig_usr) == SIG_ERR) { printf("signal SIGUSR2 error\n"); exit(0); } sigemptyset(&zeromask); sigemptyset(&newmask); sigaddset(&newmask, SIGUSR1); sigaddset(&newmask, SIGUSR2); if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) != 0) { printf("SIG_BLOCK error\n"); exit(0); } } void TELL_PARENT(pid_t pid) { kill(pid, SIGUSR2); } void WAIT_PARENT() { while (sigflag == 0) sigsuspend(&zeromask); sigflag = 0; if (sigprocmask(SIG_SETMASK, &oldmask, NULL) != 0) { printf("child SIG_SETMASK error\n"); exit(0); } } void TELL_CHILD(pid_t pid) { kill(pid, SIGUSR1); } void WAIT_CHILD() { while (sigflag == 0) sigsuspend(&zeromask); sigflag = 0; if (sigprocmask(SIG_SETMASK, &oldmask, NULL) != 0) { printf("parent SIG_SETMASK error\n"); exit(0); } }

 

这 5 个函数使用了两个用户定义的信号:SIGUSR1 和 SIGUSR2

 

在单线程环境中,进程只能单纯地等待信号,在多线程环境中,常常会专门安排一个信号处理线程

 






读书笔记      技术帖      linux      unix      龙潭书斋      apue      unix环境高级编程      signal      信号      sigpending      sigprocmask      sigaction      sigsetjmp      siglongjmp      sigsuspend     


京ICP备15018585号