生活中处处是信号。
当早晨的闹钟响起
当赛道上的信号枪响起
当路口的红绿灯绿灯亮起
……
当信号产生时,我们就知道接下来该做什么了。
系统中的信号也如此
信号:信号是发给进程的,是进程之间事件异步通知的一种方式,属于软中断。
信号产生时为什么我们会知道接下来该做什么,本质是因为被提前灌输了“遇到这个信号时,该做什么”。同样,在早期技术人员写相关源码时就已经提前写入可能产生的信号以及处理方法,这代表着进程识别和处理信号的能力远远早于信号的产生。
进程发送信号的本质
OS将信号数据写入进程的PCB中。
当进程收到信号时,进程并不是立即处理的,有可能当前进程做着更加重要的事情,所以收到信号后,会将信号保存起来,在“合适的时机”处理。
根据上述可得出一条逻辑线路
Linux查看信号列表的命令kill -l
[YDY@VM-0-2-centos ~]$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
1~31号信号为普通信号,34 ~ 64为实时信号。以下的学习均只涉及普通信号。
进程收到信号后,有三种处理方案:
信号捕捉所用到的system call
#include
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
例如,捕捉2号信号。
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <signal.h>
4
5 void handler(int sig);
6
7 int main(void)
8 {
9 //捕捉2号信号
10 signal(2, handler);
11 sleep(20);
12 return 0;
13 }
14
15 void handler(int sig)
16 {
17 printf("get a sig :%d\n", sig);
18 }
注意:9号信号(SIGKILL)不可被捕捉.
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <signal.h>
4
5 //测试各种终端按键发送的普通信号
6
7 void handler(int sig);
8
9 int main(void)
10 {
11 //信号捕捉
12 int sign = 1;
13 for(sign = 1; sign < 32; sign++)
14 {
15 signal(sign, handler);
16 }
17 while(1);
18 //var sleep(50);
19 return 0;
20 }
21 void handler(int sig)
22 {
23 printf("get a sinal : %d\n", sig);
24 }
经过测试,在Linux上可发送信号的几个按键
按键 | 信号 |
---|---|
ctrl + z | 20 |
ctrl + c | 2 |
ctrl + 4 | 3 |
ctrl + \ | 3 |
…… | …… |
当进程异常的时候,会产生信号,导致进程退出。
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <signal.h>
4 #include <sys/types.h>
5 #include <sys/wait.h>
6 //测试进程异常产生的信号
7 int main(void)
8 {
9 if(fork() == 0)
10 {
11 int a = 10;
12 printf("%d\n", a / 0);
13 }
14
15 int status = 0;
16 pid_t ret = waitpid(-1, &status, 0);
17 if(ret > 0)
18 {
19 //wait success
20 printf("child get a sinal : %d\n", status & 0x7F);
21 }
22 return 0;
23 }
子进程,收到了8号信号(SIGFPE),导致进程崩溃并终止。
当进程异常退出后,我们最希望的就是知道哪里出错,所以需要去检查刚才的数据等信息。但是进程都终止了,数据不在内存,还如何事后检查呢?
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许
产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。
例如,在Linux上,默认不产生core文件。如果需要使用,可以用ulimit -c filesize
命令更改core文件的大小,这样就可以生成core文件,filesize的大小最大不超过1024K。
父进程使用waitpid
等待子进程时,其第二个参数(输出型参数status)反映了子进程的退出信息。这个参数只使用低16位,次低7位是一个core dump标志位
如果产生了core文件,那么core dump标志位上为1;没有产生则标志位上为0。
//测试进程异常产生的信号
int main(void)
{
if(fork() == 0)
{
int a = 10;
printf("%d\n", a / 0);
}
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
if(ret > 0)
{
//wait success
printf("child get a sinal : %d, core dump: %d\n", status & 0x7F, (statu s >> 7) & 0x7F);
}
return 0;
}
说明刚才的进程没有创建core文件。
进程异常收到信号导致终止的本质:
CPU计算过程中,导致硬件出现了问题。然后被其他的硬件检测到并通知给内核,内核随后向该进程发送合适的信号。
作用:发送一个信号给进程
#include
#include
int kill(pid_t pid, int sig);
//pid:发送给进程的PID
//sig:要发送的信号
//成功返回0,失败返回-1
测试系统调用kill
8 int main(void)
9 {
10 pid_t id = fork();
11 if(id == 0)
12 {
13 //child
14 while(1);
15 }
16 else
17 {
18 kill(id, 2);
19 int status = 0;
20 pid_t ret = waitpid(-1, &status, 0);
21 if(ret > 0)
22 {
23 //wait success
24 printf("child get a sinal : %d\n", status & 0x7F);
25 }
26 }
27 return 0;
28 }
运行结果:
在Linux上有一个kill
命令也可以发送信号,它本质上也是调用了系统调用kill.
kill命令的使用方法
kill -signal pid
signal:几号信号
pid:发送给进程,这个进程pid
例如:
.
通过某种软件,来触发信号的产生和发送。
SIGPIPE、SIGALRM信号是由软件条件产生的信号。
这里主要学习SIGALRM信号。
涉及函数
作用:通知内核在seconds秒后发送SIGALRM信号给当前进程
#include
unsigned int alarm(unsigned int seconds);
//返回值为0,或者为之前设定的闹钟时间还剩的秒数
如果设定的秒数为0,表示取消以前设定的闹钟,返回值是以前设定的闹钟还剩的秒数。
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <signal.h>
4 #include <sys/types.h>
5 #include <sys/wait.h>
6
7 void handler(int sig);
8
9 int main(void)
10 {
11 signal(14, handler); //捕捉SIGALRM信号
12 alarm(10);
13 int cnt = 0;
14 while(1)
15 {
16 cnt++;
17 printf("cnt : %d\n", cnt);
18 sleep(1);
19 }
20 return 0;
21 }
22 void handler(int sig)
23 {
24 printf("get a signal : %d\n", sig);
25 }
运行结果:10秒后收到了14号(SIGALRM)信号。
在收到信号后,进程并不是立即处理,它会在“合适的时机”进行处理,所以进程需要保存收到的信号。
以Linux为例。
Linux的进程PCB–>task_struct中存在着三张关于信号的表。
block表和pending表是位图结构。
block表:比特位的位置代表信号编号,比特位内容标识是否被阻塞。阻塞位图又称为信号屏蔽字。
pending表:比特位的位置代表信号编号,比特位内容标识是否收到该信号,是否处于未决状态。
handler表:是一个函数指针数组,第一个条目对应编号为1的信号,第二个条目对应编号为2的信号……,每个条目指向对应信号的处理方法。
以上图为例,该进程收到了2号信号,但是2号信号被阻塞。
阻塞标志(block)和未决标志(pending)可以使用同一个类型存储,这个类型为sigset_t,称为信号集。可以表示信号的“有效”和“无效”状态。这个类型中如何存储bit依赖于系统实现,用户只需调用库函数来操作sigset_t变量。
#include
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
sigemptyset
函数将信号集初始化为空,也就是将所有的bit变为一个标志位,表示当前没有收到信号状态。sigfillset
函数将信号集初始化为收到所有信号的状态。sigaddset
和sigdelset
函数,添加/删除有效信号进set的信号集。sigismember
函数,判断一个信号集的有效信号中是否包含该信号。如果包含,返回1,不包含返回0,出错返回-1sigemptyset
/sigfillset
函数初始化,让信号集处于确定状态。以上函数在“信号屏蔽字操作”做测试
#include
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
假设当前的信号屏蔽字为mask ,参数how的可以选项
值 | 含义 |
---|---|
SIG_BLOCK | 现在的阻塞信号集,是当前阻塞信号集mask和信号集set的并集。信号集set包含了我们想要添加进当前信号集的信号 |
SIG_UNBLOCK | 从当前阻塞信号集mask中删除信号集set中的信号,推导的公式:mask = mask &(~set) |
SIG_SETMASK | 使现在的阻塞信号集由mask变为set的阻塞信号集 |
注意:如果使用system call接口sigprocmask
解除了当前多个未决信号的阻塞,那么在sigprocmask
返回之前,至少有一个信号已经被递达。
测试代码(阻塞2号信号):
1 #include <stdio.h>
2 #include <signal.h>
3 #include <unistd.h>
4 #include <sys/types.h>
5
6 int main(void)
7 {
8 //解除对2号信号的阻塞
9 sigset_t set, oset;
10
11 sigemptyset(&set);
12 sigemptyset(&oset);
13
14 //添加2号信号进信号集set,用于修改当前进程的信号屏蔽字
15 sigaddset(&set, 2);
16
17 //从当前信号屏蔽字中添加信号集set中包含的信号
18 sigprocmask(SIG_BLOCK, &set, &oset);
19
20 while(1)
21 {
22 printf("i am process : %d\n", getpid());
23 sleep(1);
24 }
25
26 return 0;
27 }
运行现象:给这个进程发送2号信号,并没有看到进程对这个信号做了处理,这个信号一直处于未决状态,最后进程被我使用终端按键ctrl + \
给进程发送3号信号终止。
#include
int sigpending(sigset_t *set);
作用:读取当前进程的未决信号集,通过输出型参数set截取
返回值,成功返回0,失败返回-1
测试代码
1 #include <stdio.h>
2 #include <signal.h>
3 #include <unistd.h>
4 #include <sys/types.h>
5
6 void show_pending(sigset_t *p);
7 int main(void)
8 {
9 //解除对2号信号的阻塞
10 sigset_t set, oset;
11
12 sigemptyset(&set);
13 sigemptyset(&oset);
14
15 //添加2号信号进信号阻塞集set,用于修改当前进程的信号屏蔽字
16 sigaddset(&set, 2);
17
18 //从当前信号屏蔽字中删除阻塞信号集set中包含的信号
19 sigprocmask(SIG_BLOCK, &set, &oset);
20
21 sigset_t pending;
22 while(1)
23 {
24
25 sigemptyset(&pending);
26
27 //将当前进程的未决信号集通过pending传出(存储在pending变量中国)
28 sigpending(&pending);
29
30 //打印当前进程的未决信号集
31 show_pending(&pending);
32
33 printf("i am process : %d\n", getpid());
34 sleep(1);
35 }
36
37 return 0;
38 }
39 void show_pending(sigset_t *p)
40 {
41 printf("the pending of current process is ");
42 int i = 1;
43 for(i = 1; i < 32; i++)
44 {
45 //如果信号存在那么bit为1
46 if(sigismember(p, i))
47 {
48 printf("1");
49 }
50 else
51 {
52 printf("0");
53 }
54
55 }
56 printf("\n");
57 }
运行现象:进程在未收到信号前,其pending位图的所有比特位均为0,而后向进程发送2号信号后,pending位图的第2位比特位变成1,其余均不变,2号信号处于未决状态。
#include
int sigaction(int signo, const struct signaction *act, struct sigaction *oldact);
作用:是一个系统调用函数,用于修改信号的处理动作。
返回值:调用成功返回0,失败返回-1。
act和oldact都指向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
是一个函数指针,指向的是信号的处理方法,有三种情况:
SIG_DEF
,表示信号的处理动作为默认动作SIG_IGN
,表示信号的处理动作为忽略动作结构体内还有一个信号集mask
,希望哪些信号在执行信号的处理方法时被阻塞,就把这些信号放入mask中。
测试代码:
1 #include <stdio.h>
2 #include <signal.h>
3 #include <unistd.h>
4 #include <string.h>
5
6 void handler(int signo)
7 {
8 while(1)
9 {
10 printf("executing %d signal\n", signo);
11 sleep(1);
12 }
13 }
14
15 int main(void)
16 {
17 struct sigaction act;
18 memset(&act, 0, sizeof(act));
19 act.sa_handler = handler; //注册了一个处理方法
20
21 //将希望处理信号时屏蔽的信号添加进sa_mask中
22 sigemptyset(&act.sa_mask);
23 sigaddset(&act.sa_mask, 3);
24
25 //更改2号信号的处理方法
26 sigaction(2, &act, NULL);
27
28 printf("i am process :%d\n", getpid());
29 sleep(20);
30
31 return 0;
32 }
运行现象:在打印了i am process : 16931过后,在20秒之内给该进程发送2号信号,进程开始死循环打印"executing 2 signal", 发送3号信号没有反应。
信号的处理时机:当进程从内核态返回用户态时。
上图可抽象为一个“无穷大”。