本文首要介绍Linux信号体系和怎么运用POSIX API来呼应信号。本文中的示例适用于Linux体系和大部分POSIX兼容体系。
Linux体系中的信号
在下列状况下,咱们的运用进程或许会收到体系信号:
用户空间的其他进程调用了相似kill(2)函数
进程本身调用了相似about(3)函数
当子进程退出时,内核会向父进程发送SIGCHLD信号
当父进程退出时,一切子进程会收到SIGHUP信号
当用户经过键盘终端进程(ctrl+c)时,进程会收到SIGINT信号
当进程运转出现问题时,或许会收到SIGILL、SIGFPE、SIGSEGV等信号
当进程在调用mmap(2)的时分失利(或许是由于映射的文件被其他进程截短),会收到SIGBUS信号
当运用性能调优东西时,进程或许会收到SIGPROF。这一般是程序未能正确处理中止体系函数(如read(2))。
当运用write(2)或相似数据发送函数时,假如对方现已断开衔接,进程会收到SIGPIPE信号。
如需了解一切体系信号,拜见signal(7)手册。
信号的默许行为
每个信号都相关一个默许的行为,当进程没有捕获并处理信号时,进程会依照默许的行为处理信号。
这些默许行为包含:
完毕进程。这是最通用默许行为,包含SIGTERM、SIGQUIT、SIGPIPE、SIGUSR1、SIGUSR2等信号。
完毕并履行中心转储。包含SIGSEGV、SIGILL、SIGABRT等信号,这一般都是由于代码中存在过错。
一些信号默许会被疏忽,例如SIGCHLD。
挂起进程。SIGSTOP信号会引起进程挂起,而SIGCOND能够将挂起的进程持续运转。该进程常见于在控制台运用ctrl+z组合键。
信号处理
最传统的信号处理方法是运用signal(2)函数装载一个信号处理函数。可是这种方法现已被抛弃,首要原因是在UNIX完成中,收到信号之后,会重置回默许的信号处理行为。一起,该行为是不跨渠道的。因而,主张的信号处理方法是运用sigaction(2)函数。
sigacTIon(2)函数的原型为:
int sigacTIon (int signum, const struct sigacTIon *act, struct sigacTIon *oldact);
值得留意的是,sigaction(2)函数不直接承受信号处理函数,而需求运用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);};
其间一些要害字段:
sa_handler:信号处理函数的函数指针,其函数原型和signal(2)承受的信号处理函数相同。
sa_sigaction:另一种信号处理函数指针,它能在处理信号时获取更多信号相关的信息。
sa_mask:答应设置信号处理函数履行时需求堵塞的信号。
sa_flags:修正信号处理函数履行时的默许行为,详细可选值请参照手册。
sigaction运用示例:
#include #include #include #include static void hdl (int sig, siginfo_t *siginfo, void *context){ printf (“Sending PID: %ld, UID: %ld\n”, (long)siginfo-》si_pid, (long)siginfo-》si_uid);}int main (int argc, char *argv[]){ struct sigaction act; memset (&act, ‘\0’, sizeof(act)); /* 这儿运用sa_sigaction字段,由于该字段供给了两个额定的参数, 能够获取关于接纳信号的更多信息。 */ act.sa_sigaction = &hdl; /* SA_SIGINFO标识告知sigaction函数运用sa_sigaction字段,而非sa_handler字段*/ act.sa_flags = SA_SIGINFO; if (sigaction(SIGTERM, &act, NULL) 《 0) { perror (“sigaction”); return 1; } while (1) sleep (10); return 0;}
该示例中运用了三个参数版其他信号处理函数来呼应SIGTERM信号,编译(假定源文件名为sig.c)并履行程序,能够有以下输出:
gcc -o sig sig.c./sig &kill $!
Sending PID: 16200, UID: 1000
留意,运用三参数版别信号处理函数时,必须将sa_flags字段设置为SA_SIGINFO,不然信号处理函数将无法获取到正确的siginfo_t方针。
关于siginfo_t结构体,sigaction(2)的手册中有详细介绍,其间的几个字段十分有用:
si_code:用于标识信号的来历,例如kill(2)、raise(3)等经进程序调用产生的信号,该值为SI_USER;而由内核发送的信号,该值为SI_KERNEL。
关于SIGCHLD信号,能够从si_status字段(进程退出码)、si_utime字段(进程耗费的用户态时刻)和si_stime字段(进程耗费的内核态时刻)获取更多信息。
关于SIGILL、SIGFPE、SIGSEGV、SIGBUS等信号,能够从si_addr字段获取产生过错的内存地址。
常见问题
由于信号处理函数是异步履行且无法预知履行时刻,因而编码时需求特别留意异步履行产生的问题,尤其是主函数和信号处理函数之间同享的数据。
首先是编译器优化。假如一个变量在主函数中循环读取,信号处理函数中修正(例如一个退出标识),这时编译器优化或许导致信号处理函数中的修正无法让主函数感知到。例如如下代码:
#include #include #include #include static int exit_flag = 0;static void hdl (int sig){ exit_flag = 1;}int main (int argc, char *argv[]){ struct sigaction act; memset (&act, ‘\0’, sizeof(act)); act.sa_handler = &hdl; if (sigaction(SIGTERM, &act, NULL) 《 0) { perror (“sigaction”); return 1; } while (!exit_flag) ; return 0;}
假如运用gcc O2级其他优化,该程序会依照预期,在接纳到SIGTERM信号时退出。可是,假如优化等级调整到O3,向进程发送SIGTERM信号之后,进程还会持续运转(假定文件名为test_sig.c):
gcc -o test -O3 test_sig.c./test &killall test
这时控制台不会提示后台进程退出,运用jobs指令查看后,test进程依然存在:
jinlingjie@localhost ~/data/Downloads $ 。/test &[1] 2532jinlingjie@localhost ~/data/Downloads $ killall testjinlingjie@localhost ~/data/Downloads $ jobs[1]+ 运转中 。/test &
这是由于在O3级其他优化中,编译器发现while循环会不断读取exit_flag变量,为了加速读取速度,编译器会把该变量值直接加载到寄存器中,而不再每次从内存读取。此刻信号处理函数再修正exit_flag变量,不会被更新到寄存器中,因而进程无法退出。关于这种场景,需求给同享变量添加volatile要害字,以保证进程每次读取变量时,都去内存从头获取最新的值。
上面的示例中的场景,还需求考虑对同享变量修正的原子性。在一些渠道上int类型的读取或许写入或许不是原子的。信号体系供给sig_atomic_t方针,以保证原子的读写。
除此以外,编写信号处理函数还需求留意信号安全。由于信号处理函数调用的其他函数也有或许被信号中止,signal(7)手册的Async-signal-safe functions(异步信号安全函数)章节详细列举了一切在信号处理函数中能够安全调用的函数。
特别信号处理
SIGCHLD信号
假如父进程不需求获取子进程的退出状况码,也不需求等候子进程的退出,仅有的意图是整理僵尸进程。那么,父进程只需求处理SIGCHLD信号,并进行整理即可:
static void sigchld_hdl (int sig){ /* 等候一切现已退出的子进程。 * 这儿运用非堵塞的调用以避免子进程在代码其他当地被整理。 */ while (waitpid(-1, NULL, WNOHANG) 》 0) { }}
这是一个简略的信号处理函数,假如需求做更多的作业,请特别留意不要运用非异步信号安全的函数。
SIGBUS信号
前面说到过SIGBUS信号通常是拜访被映射(mmap(2))的内存时,无法映射到对应文件(通常是文件被截断了)。这种非正常状况下,进程的一般行为是直接退出,可是假如一定要处理SIGBUS信号仍是可行的。这时能够经过sigsetjmp(3)和siglongjmp(3)来越过产生过错的当地,然后让程序持续运转。
需求特别留意的是,信号处理函数履行了siglongjmp(3)调用之后,代码没有持续运转下去,而是直接跳转到sigsetjmp(3)方位从头开端履行。假如此刻代码依然持有锁等资源,将不会开释,假如后续代码持续去竞赛锁,或许会导致死锁的产生。
SIGSEGV信号
处理SIGSEGV(段过错)信号是或许的,但这一般是没有意义的,由于即便代码从头运转了,运转到相同的当地依然或许产生段过错。其间一种重启程序有用的状况是经过mmap(2)获取到的内存有写保护,由此产生的SIGSEGV信号(能够经过信号处理函数中的siginfo_t参数获取产生原因),或许能够经过mprotect(2)函数来去除写保护。
假如段过错是由于栈空间缺乏导致的,那么这时将无法经过信号处理函数来处理SIGSEGV信号。由于信号处理函数相同需求分配栈空间来履行。这种状况下,能够经过sigaltstack(2)函数为信号处理函数界说独立的栈空间。
SIGABRT信号
企图处理SIGABRT信号时,需求了解abort(3)函数的运转原理:该函数会先发送SIGABRT信号,假如该信号被疏忽,或许对应的信号处理函数正常回来(没有经过longjmp(3)跳转),它会将信号处理函数重置为默许方法,而且从头发送SIGABRT信号信号,这将导致进程退出。因而,处理SIGABRT信号的效果或许是在进程完毕前做一些最终的操作,或许运用longjmp(3)从头的当地开端履行。
信号和fork()
当父进程调用fork(2)函数创立子进程时,子进程不会仿制父进程的信号行列,即便此刻父进程的信号行列非空,也会独自创立一个空的信号行列。可是,子进程会承继父进程的一切信号处理函数和信号堵塞状况。因而假如父进程现已完成对信号的设置,没有特别状况子进程无须从头设置。
信号和线程
由于POSIX标准中,一切的一个进程的一切线程都有相同的进程ID(PID),向多线程进程发送信号有两种状况:
向进程发送信号(运用相似kill(2)这样的函数直接向进程发送信号):线程能够经过pthread_sigmask(2)独自设置需求堵塞的信号。因而假如有线程没有堵塞当时发送的信号,进程中的一个线程会收到该信号(可是没有特别阐明详细哪个线程会收到);假如一切的线程都堵塞了当时发送的信号,该信号会被参加进程的信号行列;假如进程没有设置当时信号的信号处理函数,而且该信号的默许行为是停止进程,那么整个进程都将被停止。
向特性线程发送信号(运用pthread_kill(2)):线程能够经过pthread_kill(2)向进程中的其他线程(或许本身)发送信号,此刻信号会发送到对应线程的信号行列中。一起操作体系也或许会向特性线程发送比如SIGSEGV信号。假如接纳信号的线程没有处理对应的信号,且该信号的默许行为是停止进程,那么该线程地点的进程都将被停止。
信号发送
向进程发送信号的方法能够有:
经过键盘交互:一些键盘的组合键,能够向控制台正在履行的进程发送信号。
CTRL+C:发送SIGINT信号,该信号默许行为是停止进程。
CTRL+\:发送SIGQUIT信号,该信好默许行为是停止进程并中心转储。
CTRL+Z:发送SIGSTOP信号,该信号默许行为是挂起进程。
kill(2):kill(2)函数承受两个参数,一个是信号发送的进程ID,一个是需求发送的信号。其间的进程ID有一些特别的约好。
0:假如PID为0,信号发送的方针是当时进程组的一切进程。
-1:假如PID为-1,信号发送的方针是一切(有权限发送信号)的进程。
《 -1:假如PID小于-1,信号发送的方针是进程ID为-PID的进程组。
向进程本身发送信号:进程能够经过调用raise(3)、abort(3)等函数向本身发送信号。
raise(3):能够向进程发送指定信号,需求留意的是,在多线程环境中,只会向当时线程发送信号。
abort(3):向当时进程发送SIGABRT信号,前文现已说到过,该函数会重置信号处理函数,因而无需关怀进程是否现已处理了SIGABRT信号。
sigqueue(2):该函数和kill(2)函数相似,可是多了一个sigval参数。因而调用者能够向信号处理函数传递一个整数或许一个指针。信号处理函数能够经过siginfo_t参数获取该参数。
信号堵塞
有些时分,咱们需求堵塞信号,避免信号打断当时程序的履行,而不是捕获和处理信号。传统的 signal(2)函数能够经过将信号处理函数设置为SIG_IGN来完成堵塞的功用。可是该方法现已抛弃,主张运用sigprocmask(2)函数来完成信号堵塞功用,由于它供给了更多的参数,能够适用于杂乱场景。
一个简略的示例:
#include #include #include #include static int got_signal = 0;static void hdl (int sig){ got_signal = 1;}int main (int argc, char *argv[]){ sigset_t mask; sigset_t orig_mask; struct sigaction act; memset (&act, 0, sizeof(act)); act.sa_handler = hdl; if (sigaction(SIGTERM, &act, 0)) { perror (“sigaction”); return 1; } sigemptyset (&mask); sigaddset (&mask, SIGTERM); if (sigprocmask(SIG_BLOCK, &mask, &orig_mask) 《 0) { perror (“sigprocmask”); return 1; } sleep (10); if (sigprocmask(SIG_SETMASK, &orig_mask, NULL) 《 0) { perror (“sigprocmask”); return 1; } sleep (1); if (got_signal) puts (“Got signal”); return 0;}
上述示例展现了经过sigprocmask(2)函数来堵塞SIGTERM信号10秒,此刻假如进程接纳到了SIGTERM信号,会被参加到进程的信号行列中。免除对SIGTERM信号的堵塞,此刻假如之前的信号行列中有SIGTERM信号,或许新收到了SIGTERM信号,就会履行对应的信号处理函数。
堵塞信号运用的一个场景便是避免信号的竞赛。一些函数(如select(2)、poll(2))会堵塞当时函数履行,这时在反常的状况下,这些函数会希望经过信号来中止当时的堵塞操作。可是,假如此刻程序还设置了其他信号处理函数,这时信号或许会被设置的信号处理函数消费,导致堵塞操作的函数依然履行,无法中止。
遇到这种状况,就需求运用sigprocmask(2)合作支撑重置sigmask的堵塞函数(如pselect(2)poll(2)),大致的示例代码片段如下:
sigemptyset (&mask);sigaddset (&mask, SIGTERM);if (sigprocmask(SIG_BLOCK, &mask, &orig_mask) 《 0) { perror (“sigprocmask”); return 1;}while (!exit_request) { /* 假如在这儿接纳到信号,信号会被堵塞, * 直到撤销堵塞(下面pselect完成) */ FD_ZERO (&fds); FD_SET (lfd, &fds); res = pselect (lfd + 1, &fds, NULL, NULL, NULL, &orig_mask); /* 下面持续文件描述符操作 */}
跋文
本文对Linux/UNIX信号体系、信号的处理、发送、堵塞等做了简略的介绍。可是整个信号体系十分杂乱,还有许多没有说到的内容,等待和我们持续沟通。