信号的本质是异步。异步一这个词,听着高端大气上档次,又让人云山雾绕,其则不然。其实我们想想,我们这个世界是异步的,每个人干事儿,并不总是A->B->C->D这种。比如我在网上买了东西,我其实并不知道快递几时能到。我可能在公司里面,在喝水,在回邮件,在查bug,在写代码,突然收到了快递小哥的电话,注意这就是信号的delivery。由于快递的到来,我不得不停下我手头的活儿,去签收快递。这就是传说中的典型的异步。我不知道快递小哥几时给我电话,但是我收到电话就去签收,这是我的信号处理函数。更高级一点,如果我在参加重要的会议,我可能需要屏蔽快递小哥的电话(假如我知道其电话),这已经是linux下信号的高级应用(sigprocmask)了。
信号是一种机制,是在软件层次对中断机制的一种模拟,内核让某进程意识到某特殊事情发生了。强迫进程去执行相应的信号处理函数。至于信号的来源可能来自硬件如按下键盘或者硬件故障(如ctrl+c发送SIGINT),可能来自其他进程(kill,sigqueue),可能来自自己进程(raise)。
信号的本质是一种进程间的通信,一个进程可以向另一个进程发送信号,至少传递了signo这个int值。实际上,通信的内容,可以远不止是signo,可以通过SA_SIGINFO标志位通知进程去取额外的信息。
我痛恨片汤话儿,可是上面一大坨片汤话儿,却真真的道出了信号的本质。
前面也提到了,signal是个让人爱恨交加的feature,原因在于沉重的历史包袱。下面我将一一道来。
在上古时期,UNIX就已经有了signal这个feature,但是当时的signal存在几个问题:
1 传统的信号处理函数是一次性的,而非永久性的。
linux为了向下兼容,依然实现了这个有缺陷的signal系统调用。你可看到signal系统调用的内核代码中有SA_ONESHOT这个标志位。
-
#ifdef __ARCH_WANT_SYS_SIGNAL
-
/*
-
* For backwards compatibility. Functionality superseded by sigaction.
-
*/
-
SYSCALL_DEFINE2(signal, int, sig, __sighandler_t, handler)
-
{
-
struct k_sigaction new_sa, old_sa;
-
int ret;
-
-
new_sa.sa.sa_handler = handler;
-
new_sa.sa.sa_flags = SA_ONESHOT | SA_NOMASK;
-
sigemptyset(&new_sa.sa.sa_mask);
-
-
ret = do_sigaction(sig, &new_sa, &old_sa);
-
-
return ret ? ret : (unsigned long)old_sa.sa.sa_handler;
-
}
- #endif /* __ARCH_WANT_SYS_SIGNAL */
- #define SA_ONESHOT SA_RESETHAND
上图反映了内核如何传递信号。基本就是选择一个挂起信号,然后处理一个信号。get_signal_to_deliver 是在进程中选择一个信号来handle。代码在kernel/signal.c,其中有如下code:
-
if (ka->sa.sa_handler == SIG_IGN) /* Do nothing. */
-
continue;
-
if (ka->sa.sa_handler != SIG_DFL) {
-
/* Run the handler. */
-
*return_ka = *ka;
-
-
if (ka->sa.sa_flags & SA_ONESHOT)
-
ka->sa.sa_handler = SIG_DFL;
-
-
break; /* will return non-zero "signr" value */
- }
值得一提的是,glibc的signal函数,调用的已经不是传统的signal系统调用,而是rt_sigaction系统调用,这种一次性的缺陷早已经解决了。怎么证明:
-
manu@manu-hacks:~/code/c/self/signal$ cat signal_fault_1.c
-
#include <stdio.h>
-
#include <stdlib.h>
-
#include <signal.h>
-
#include <string.h>
-
#include <errno.h>
-
-
#define MSG "OMG , I catch the signal SIGINT\n"
-
#define MSG_END "OK,finished process signal SIGINT\n"
-
int do_heavy_work()
-
{
-
int i ;
-
int k;
-
srand(time(NULL));
-
-
for(i = 0 ; i < 100000000;i++)
-
{
-
k = rand()%1234589;
-
}
-
-
}
-
-
void signal_handler(int signo)
-
{
-
write(2,MSG,strlen(MSG));
-
do_heavy_work();
-
write(2,MSG_END,strlen(MSG_END));
-
}
-
-
int main()
-
{
-
char input[1024] = {0};
-
-
#if defined TRADITIONAL_SIGNAL_API
if(syscall(SYS_signal ,SIGINT,signal_handler) == -1)
#elif defined SYSTEMV_SIGNAL_API
if(sysv_signal(SIGINT,signal_handler) == -1)
#else
if(signal(SIGINT,signal_handler) == SIG_ERR)
#endif
-
-
{
-
fprintf(stderr,"signal failed\n");
-
return -1;
-
}
-
-
printf("input a string:\n");
-
if(fgets(input,sizeof(input),stdin)== NULL)
-
{
-
fprintf(stderr,"fgets failed(%s)\n",strerror(errno));
-
return -2;
-
}
-
else
-
{
-
printf("you entered:%s",input);
-
}
-
-
return 0;
-
-
- }
- rt_sigaction(SIGINT, {0x8048736, [INT], SA_RESTART}, {SIG_DFL, [], 0}, 8) = 0
-
manu@manu-hacks:~/code/c/self/signal$ gcc -o signal_glibc signal_fault_1.c
-
manu@manu-hacks:~/code/c/self/signal$ ./signal_glibc
-
input a string:
-
input^COMG , I catch the signal SIGINT
-
^COK,finished process signal SIGINT
-
OMG , I catch the signal SIGINT
-
OK,finished process signal SIGINT
-
^COMG , I catch the signal SIGINT
-
OK,finished process signal SIGINT
-
^COMG , I catch the signal SIGINT
-
OK,finished process signal SIGINT
-
^Z
- [1]+ Stopped ./signal_glibc
我们如何体验下老古董的signal,glibc提供了一个sysv_signal接口,manual中这样描述:
-
However sysv_signal() provides the System V unreliable signal semantics, that is: a) the disposition of the sig‐
-
nal is reset to the default when the handler is invoked; b) delivery of further instances of the signal is not
-
blocked while the signal handler is executing; and c) if the handler interrupts (certain) blocking system calls,
- then the system call is not automatically restarted.
- gcc -DSYSTEMV_SIGNAL_API -o signal_sysv signal_fault_1.c
-
manu@manu-hacks:~/code/c/self/signal$ ./signal_sysv
-
input a string:
-
^COMG , I catch the signal SIGINT
-
^C
- manu@manu-hacks:~/code/c/self/signal$ man sysv_signal
- rt_sigaction(SIGINT, {0x8048756, [], SA_INTERRUPT|SA_NODEFER|SA_RESETHAND}, {SIG_DFL, [], 0}, 8) = 0
- #define SA_ONESHOT SA_RESETHAND
-
gcc -DTRADITIONAL_SIGNAL_API -o signal_traditional signal_fault_1.c
-
manu@manu-hacks:~/code/c/self/signal$ ./signal_traditional
-
input a string:
-
^COMG , I catch the signal SIGINT
- ^C
- signal(SIGINT, 0x8048736) = 0 (SIG_DFL)
如何证明这一点呢?我上面的例子中故意在信号处理函数中做了很heavy很耗时的操作,从而容易造出处理信号A的时候,另一信号A又被deliver的场景。
因为do_heavy_work是个很耗费时间的操作,信号处理完成我们会在标准错误上输出处理完成的语句,这就表征了信号处理结束了没有。
我们看下传统signal的,收到一个SIGINT的信号的情况:
-
manu@manu-hacks:~/code/c/self/signal$ ./signal_traditional
-
input a string:
-
^COMG , I catch the signal SIGINT
-
OK,finished process signal SIGINT
-
fgets failed(Interrupted system call)
- manu@manu-hacks:~/code/c/self/signal$
-
manu@manu-hacks:~/code/c/self/signal$ ./signal_traditional
-
input a string:
-
^COMG , I catch the signal SIGINT
-
^C
- manu@manu-hacks:~/code/c/self/signal$
那么我们现在的glibc的signal函数如何?
strace又来帮忙了?
- rt_sigaction(SIGINT, {0x8048736, [INT], SA_RESTART}, {SIG_DFL, [], 0}, 8) = 0
-
SYSCALL_DEFINE4(rt_sigaction, int, sig,
-
const struct sigaction __user *, act,
-
struct sigaction __user *, oact,
-
size_t, sigsetsize)
-
-
struct sigaction {
-
union {
-
__sighandler_t _sa_handler;
-
void (*_sa_sigaction)(int, struct siginfo *, void *);
-
} _u;
-
sigset_t sa_mask;
-
unsigned long sa_flags;
-
void (*sa_restorer)(void);
- }
-
manu@manu-hacks:~/code/c/self/signal$ ./signal_glibc
-
input a string:
-
^COMG , I catch the signal SIGINT
-
^C^C^C^COK,finished process signal SIGINT
-
OMG , I catch the signal SIGINT
-
^C^COK,finished process signal SIGINT
-
OMG , I catch the signal SIGINT
-
OK,finished process signal SIGINT
-
^COMG , I catch the signal SIGINT
-
OK,finished process signal SIGINT
-
^COMG , I catch the signal SIGINT
-
^Z
- [2]+ Stopped ./signal_glibc
在上图的handle_signal函数的末尾,调用了signal_delivered函数:
-
/**
-
* signal_delivered -
-
* @sig: number of signal being delivered
-
* @info: siginfo_t of signal being delivered
-
* @ka: sigaction setting that chose the handler
-
* @regs: user register state
-
* @stepping: nonzero if debugger single-step or block-step in use
-
*
-
* This function should be called when a signal has succesfully been
-
* delivered. It updates the blocked signals accordingly (@ka->sa.sa_mask
-
* is always blocked, and the signal itself is blocked unless %SA_NODEFER
-
* is set in @ka->sa.sa_flags. Tracing is notified.
-
*/
-
void signal_delivered(int sig, siginfo_t *info, struct k_sigaction *ka,
-
struct pt_regs *regs, int stepping)
-
{
-
sigset_t blocked;
-
-
/* A signal was successfully delivered, and the
-
saved sigmask was stored on the signal frame,
-
and will be restored by sigreturn. So we can
-
simply clear the restore sigmask flag. */
-
clear_restore_sigmask();
-
-
sigorsets(&blocked, ¤t->blocked, &ka->sa.sa_mask);
-
if (!(ka->sa.sa_flags & SA_NODEFER))
-
sigaddset(&blocked, sig);
-
set_current_blocked(&blocked);
-
tracehook_signal_handler(sig, info, ka, regs, stepping);
- }
- 当进程执行一个信号处理程序的函数时,通常屏蔽相应的信号,即自动阻塞这个信号,直到处理程序结束。因此,所处理的信号的另一次出现,并不能中断信号处理程序,所以信号处理函数不必是可以重入的。
那么传统的signal系统调用和sysv_signal又如何?为何他们存在信号的可重入问题?
-
SYSCALL_DEFINE2(signal, int, sig, __sighandler_t, handler)
-
{
-
struct k_sigaction new_sa, old_sa;
-
int ret;
-
-
new_sa.sa.sa_handler = handler;
-
new_sa.sa.sa_flags = SA_ONESHOT | SA_NOMASK;
-
sigemptyset(&new_sa.sa.sa_mask);
-
-
ret = do_sigaction(sig, &new_sa, &old_sa);
-
-
return ret ? ret : (unsigned long)old_sa.sa.sa_handler;
- }
#define SA_NOMASK SA_NODEFER
- rt_sigaction(SIGINT, {0x8048756, [], SA_INTERRUPT|SA_NODEFER|SA_RESETHAND}, {SIG_DFL, [], 0}, 8) = 0
3 早期的signal,会中断系统调用。
何意?
某些系统调用可能会被信号中断,此时系统调用返回错误EINTR,表示被信号中断了。非常多的系统调用都会被中断,我前面有篇博文重启系统调用探究,就详细介绍了系统被信号中断的问题,传统的signal会出现这个问题。那么glibc的signal函数有没有这个问题?答案是没有这个问题,glibc的signal函数很不错。
- rt_sigaction(SIGINT, {0x8048736, [INT], SA_RESTART}, {SIG_DFL, [], 0}, 8) = 0
-
manu@manu-hacks:~/code/c/self/signal$ ./signal_traditional
-
input a string:
-
^COMG , I catch the signal SIGINT
-
OK,finished process signal SIGINT
-
fgets failed(Interrupted system call)
-
manu@manu-hacks:~/code/c/self/signal$ ./signal_sysv
-
input a string:
-
^COMG , I catch the signal SIGINT
-
OK,finished process signal SIGINT
-
fgets failed(Interrupted system call)
- manu@manu-hacks:~/code/c/self/signal$
-
static void
-
handle_signal(unsigned long sig, siginfo_t *info, struct k_sigaction *ka,
-
struct pt_regs *regs)
-
{
-
/* Are we from a system call? */
-
if (syscall_get_nr(current, regs) >= 0) {
-
/* If so, check system call restarting.. */
-
switch (syscall_get_error(current, regs)) {
-
case -ERESTART_RESTARTBLOCK:
-
case -ERESTARTNOHAND:
-
regs->ax = -EINTR;
-
break;
-
-
case -ERESTARTSYS:
-
if (!(ka->sa.sa_flags & SA_RESTART)) {
-
regs->ax = -EINTR;
-
break;
-
}
-
/* fallthrough */
-
case -ERESTARTNOINTR:
-
regs->ax = regs->orig_ax;
-
regs->ip -= 2;
-
break;
-
}
- }
- 。。。
- }
但是存在一个问题,就会可移植性。由于不同的平台可能不同。单就linux平台而言,glibc的signal函数还不错。
那么signal还有什么问题呢?为啥有引入了实时信号?那是下一篇内容。
参考文献
1 深入理解linunx内核
2 linux内核源代码情景分析
3 signal ppt 蘇維農
4 linux系统编程