Zach的博客

Signal

Signal, Linux进程间通信

信号(Signal)是Linux的一种进程间通信机制,信号由内核或者其他进程发送给接收进程,接收到信号的进程相应地采取行动来处理信号。信号是一种软中断,即当一个进程收到一个信号时,它停止自己的执行流程,转而去处理信号。
值得注意的是信号是可重入的,即信号处理程序可以被其他信号中断。

信号的种类

常用的信号并不是很多,信号的名称在<signal.h>中定义,常用的信号及解释如下:

  • SIGALRM:由alarm()系统调用发送给接收进程,alarm()系统调用可以定时发送一个SIGALRM信号给进程,以触发某个定时操作。
  • SIGHUP:该信号表示某一个操作关闭了终端,运行在该终端下的程序会收到SIGHUP信号,默认的信号处理程序会结束收到的信号的程序。
  • SIGINT:用户输入了Ctrl-C就会发送一个SIGINT给前台应用。
  • SIGILL:这是一个异常信号,表示系统执行时遇到了一个非法程序,当加载动态链接一个被破坏的函数库时可能会产生该信号。注意,这个信号不能被捕获或者忽略,一般在shell中用于强制终止异常程序。
  • SIGABRT:程序中调用abort()系统调用会产生一个SIGABRT信号
  • SIGSEGV:这也是一个异常信号,当程序访问了一个不属于它的内存空间是内核会发送一个SIGSEGV信号给程序。
  • SIGPIPE:Broken Pipe,如果程序尝试往管道写入数据,而管道并没有与之对应的读数据的进程,就会产生SIGPIPE信号。
  • SIGTREM:这个信号告诉进程结束自身的运行,kill命令默认发送SIGTERM信号
  • SIGHLD:当一个子进程结束运行时,父进程就会收到一个SIGHLD信号。
  • SIGUSR1 & SIGUSR2:用户可自定义的信号。

注意SIGILLSIGSTOP(用于调试)的处理行为不能被更改。

POSIX信号的语义

符合POSIX的系统上信号的处理总结为:

  • 一旦安装了信号处理函数,它就一直安装着
  • 在一个信号处理函数运行期间,正被递交的信号是阻塞的。而且安装处理函数时,POSIX保证传递给sigactionsa_mask信号集中指定的任何信号都是被阻塞的。
  • 如果一个信号在被阻塞期间产生了一次或多次,那么该信号在被解阻塞之后通常只递交一次,也就是说,Unix信号默认是不排队的。
  • 利用sigprocmask函数选择性地阻塞或解阻塞一组信号是可能的,我们可以在一段临界代码执行期间,防止捕获某些信号,以此保护这段代码。

信号的处理

signal函数

signal函数是一个年代久远的信号处理函数,其函数原型如下:

1
void (*signal(int, void (*handler)(int)))(int);

可以看到,signal是一个函数,其返回值是一个函数指针,signal函数注册一个新的信号处理函数,并返回原先注册的信号处理函数。

一个signal函数的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void sig_handler(int signum) {
printf("Receive Signal : %d\n", signum);
}

int main() {
signal(SIGINT, sig_handler);
sleep(10);

return 0;
}

结果截图:
signal_test.png

可以看到当我们按下Ctrl-C时触发了自定义的信号处理函数。

sigaction函数

signal函数的行为根据UNIX版本的不同而不同,在不同的Linux版本中亦是如此。所以我们应该使用一个更加健壮的接口--sigaction函数。sigaction的原型为:

1
2
3
#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

sigaction的结构定义如下:

1
2
3
4
5
6
7
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_flags被设置成SA_SIGINFO时,信号处理函数是sa_sigaction函数,否则是sa_handler函数。sa_flags的其他值的解释如下:

  • SA_NOCLDSTOP:子进程停止时不产生SIGHLD信号
  • SA_RESETHAND:将对此信号的处理方式在信号处理函数入口处重置为SIG_DFL
  • SA_RESTART:重启可中断的函数而不是给出EINTER错误
  • SA_NODEFER:捕获到信号时不将它添加到屏蔽字中。

当我们使用sa_sigaction函数作为信号处理函数时,我们可以得到发送信号的进程的更多信息,信息存储在siginfo_t中,其结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
siginfo_t {
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused
hardware-generated signal
(unused on most architectures) */

pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time
sigval_t si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count; POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
long si_band; /* Band event (was int in
glibc 2.3.2 and earlier) */

int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address
(since Linux 2.6.32) */

}

sa_mask中设置被进程屏蔽的信号。

一个使用sigaction的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>

void sig_handler(int signum) {
printf("Receive signal : %d\n", signum);
}

int main() {
struct sigaction act;
memset(&act, 0, sizeof(act));

act.sa_handler = sig_handler;
act.sa_flags = SA_RESETHAND;
/* 清除屏蔽字 */
sigemptyset(&act.sa_mask);

sigaction(SIGINT, &act, 0);

while (1) {
printf("Test\n");
sleep(2);
}

return 0;
}

结果如下:

sigaction_test.png

可以看到,程序第一次收到SIGINT信号时执行的是自定义的信号处理函数,第二次就被重置为默认的信号处理函数了。

kill函数

kill系统调用函数的原型如下:

1
2
3
4
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

它的作用是发送一个信号(由sig参数指定)给一个特定进程。默认发送SIGTERM信号。

信号集处理函数

信号集处理函数主要用于处理sigset_t,从而让进程屏蔽某些信号。函数的原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <signal.h>

int sigemptyset(sigset_t *);

int sigfillset(sigset_t *);

int sigaddset(sigset_t *);

int sigdelset(sigset_t *);

int sigismember(sigset_t *sigset, int signal);

int sigpromask(int how, const sigset_t *set, sigset_t *osigset);

int sigpending(sigset_t *);

int sigsuspend(const sigset_t *);

sigpromask可以根据how指定的值来修改进程的信号屏蔽字。如果set参数为空,则how没有意义,但是如果此时osigset不为空,那么当前信号屏蔽字保存在osigset指向的地址中。

how的不同取值和对应的操作:

  • SIG_BLOCK:把参数set中的信号加入到进程的信号屏蔽字中
  • SIG_SETMASK:把进程的信号屏蔽字设置为set中的信号
  • SIG_UNBLOCK:从进程的信号屏蔽字中删除set中指定的信号

注意,调用这个函数才能修改进程的信号屏蔽字,它之前的函数都只是改变一个变量,对进程的信号屏蔽字并没有直接影响。

sigpending函数将被阻塞的信号中停留在待处理状态的信号写入参数指定的地址中。

sigsuspend函数首先将进程的屏蔽字换成参数指定的信号,然后挂起进程。

一个例子实验一下以上的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <unistd.h>  
#include <signal.h>
#include <sys/types.h>
#include <stdlib.h>
#include <stdio.h>
void handler(int sig)
{

printf("Handle the signal %d\n", sig);
}

int main()
{

sigset_t sigset;//用于记录屏蔽字
sigset_t ign;//用于记录被阻塞的信号集
struct sigaction act;
//清空信号集
sigemptyset(&sigset);
sigemptyset(&ign);
//向信号集中添加信号SIGINT
sigaddset(&sigset, SIGINT);

//设置处理函数和信号集
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGINT, &act, 0);

printf("Wait the signal SIGINT...\n");
pause();//挂起进程,等待信号

//设置进程屏蔽字,在本例中为屏蔽SIGINT
sigprocmask(SIG_SETMASK, &sigset, 0);
printf("Please press Ctrl+c in 10 seconds...\n");
sleep(10);
//测试SIGINT是否被屏蔽
sigpending(&ign);
if(sigismember(&ign, SIGINT))
printf("The SIGINT signal has ignored\n");
//在信号集中删除信号SIGINT
sigdelset(&sigset, SIGINT);
printf("Wait the signal SIGINT...\n");
//将进程的屏蔽字重新设置,即取消对SIGINT的屏蔽
//并挂起进程
sigsuspend(&sigset);

printf("The app will exit in 5 seconds!\n");
sleep(5);
exit(0);
}

运行结果:

sigset_test.png

可以看到当我们屏蔽了SIGINT信号后,如果SIGINT发送给进程,那么信号就被阻塞编程待处理状态,当我们取消对信号的阻塞后该信号马上就被处理,于是程序就马上打印

1
The app will exit in 5 seconds!