信号基本概念
信号就是一个软件中断,打断当前正在运行的进程,让该进程去处理信号的事件,当前执行的程序陷入死循环时,摁下ctr+c,死循环界面就会结束,这一现象就是一个中断现象
信号介绍
每一个信号都有属于自己的一个编号和宏定义,这些宏定义都可以在signal.h中找到,在man手册中有这些信号的详细信息,只需要在使用时及时查看即可
产生信号的方式
- 通过键盘的组合键产生,比如ctrl+c产生SIGINT信号中断当前的前台进程,ctrl+\产生SIGQUIT信号让进程崩溃并产生coredump文件,ctrl+z产生SIGTSTP信号让进程暂停
- kill命令:kill -signum pid
当kill命令不带-signum参数时(即只是kill pid),默认的信号是15) SIGTERM,而kill -9 pid则是一个强大的“强杀”命令,能杀死kill pid杀不掉的处于T状态的进程 - 通过函数产生
int kill(pid_t pid, int sig);
kill命令的系统调用接口(在代码中使用kill)
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
//kill()系统调用可用于将任何信号发送到任何进程组或进程
//第一个参数是一个进程的PID,第二个参数则是信号编号
int raise(int signum);
raise是一个库函数(#include <signal.h>),作用是发送信号到调用这个函数的进程/线程
在单线程程序中,它等效于kill(getpid(), sig);
在多线程程序中,它等效于pthread_kill(pthread_self(), sig);
void abort();
abort是一个库函数(#include <stdlib.h>),作用是造成进程异常中止
在进程中调用abort()就相当于调用了raise(3)
unsigned int alarm(unsigned int seconds);
alarm是一个系统调用接口(#include <unistd.h>),在seconds秒后会将SIGALRM信号传递到调用进程
信号注册
信号注册的前提
1.在内核中task_struct结构体中,有一个struct sigpenging的对象pending,struct sigpengding这个结构体中保存两个元素,第一个元素:struct list_head list;第二个元素:sigset_t signal
2.内核还有一个sigqueue队列,该队列的每个元素都对应着一个信号处理节点
信号注册可分为两种情况:
1.非可靠信号:1~31;
更改sig位图当中对应的比特位为1,在sigqueue队列当中增加对应信号所对应的节点;当多次收到同一个信号时,只添加一次节点,相当于丢弃了第一个信号以外的信号
2.可靠信号:34~64
第一次收到信号:更改sig位图当中对应的比特位为1,并且在sigqueue队列当中增加对应信号所对应的节点
第二次收到同样信号:当多次收到同一个信号时,先判断sig位图中比特位是否为1,并且在sigqueue队列当中增加对应信号所对应的节点
信号的注销过程
1.非可靠信号的注销:
将sig位图当中对应信号的比特位置为0并且将sigqueue队列当中节点删除
2.可靠信号的注销:
首先查看在sigqueue中是否还有当前信号对应的节点,若有,则不改变sig位图中对应比特位;反之若没有,则将sig位图中对应比特位置为0
信号的捕捉处理
1.信号的处理方式:
默认处理方式:SIG_DEL----->执行一个函数
忽略:SIG_IGN----->不做任何处理
自定义:自己写的处理函数,
ege:sig_t signal(int signum,sig_t handler);
表头文件#include<signal.h>
功 能:设置某一信号的对应动作
函数原型:void (*signal(int signum,void(* handler)(int)))(int);
或者:typedef void (*sig_t)( int );
sig_t signal(int signum,sig_t handler);
参数说明:
第一个参数signum指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。
第二个参数handler描述了与信号关联的动作,它可以取以下三种值:
(1)一个无返回值的函数地址
此函数必须在signal()被调用前申明,handler中为这个函数的名字。当接收到一个类型为signum的信号时,就执行handler 所指定的函数。这个函数应有如下形式的定义:
void func(int sig);
(2)SIG_IGN
这个符号表示忽略该信号,执行了相应的signal()调用后,进程会忽略类型为sig的信号。
(3)SIG_DFL
这个符号表示恢复系统对信号的默认处理。
函数说明:
signal()会依参数signum 指定的信号编号来设置该信号的处理函数。当指定的信号到达时就会跳转到参数handler指定的函数执行。当一个信号的信号处理函数执行时,如果进程又接收到了该信号,该信号会自动被储存而不会中断信号处理函数的执行,直到信号处理函数执行完毕再重新调用相应的处理函数。但是如果在信号处理函数执行时进程收到了其它类型的信号,该函数的执行就会被中断。
返回值:返回先前的信号处理函数指针,如果有错误则返回SIG_ERR(-1)。
演示代码
1 #include<stdio.h>
2 #include<signal.h>
3
4 void func(int s_num)
5 {
6 printf("func()\n");
7
8 }
9
10
11 int main()
12 {
13 signal(SIGINT,func);
14 while(1)
15 {
16 sleep(1);
17 }
18 return 0;
19 }
打印结果
[1@localhost 进程信号]$ ./s_out
^Cfunc()
^Cfunc()
^Cfunc()
^Cfunc()
^Cfunc()
^Cfunc()
^Cfunc()
^Z
[1]+ 已停止 ./s_out
sigaction()函数
sigaction(查询或设置信号处理方式)
表头文件
#include<signal.h>
定义函数
int sigaction(int signum,const struct sigaction *act ,struct sigaction *oldact);
函数说明
signum需要更改的信号,参数signum可以指定SIGKILL和SIGSTOP以外的所有信号。
struct sigaction结构体:
struct sigaction {
void (*sa_handler)(int);//操作系统为每个信号定义的默认调用函数
void (*sa_sigaction)(int, siginfo_t *, void *);
//这个函数指针一般是预留给sa_flags使用,当sa_flag为SA_SIGINFO时,操作系统就会调用该函数指针当中保存的函数地址
sigset_t sa_mask;//在当前进程正在处理信号时,用于接收新的信号,也就是将新的信号存在sa_mask中
int sa_flags;//配合sa_sigaction使用
void (*sa_restorer)(void);//预留信息
};
act:表示把signum信号修改为act处理方式
oldact:表示是操作系统之前被signum所定义的处理方式
信号处理函数可以采用void (*sa_handler)(int)或void (*sa_sigaction)(int, siginfo_t *, void *)。到底采用哪个要看sa_flags中是否设置了SA_SIGINFO位,如果设置了就采用void (*sa_sigaction)(int, siginfo_t *, void *),此时可以向处理函数发送附加信息;默认情况下采用void (*sa_handler)(int),此时只能向处理函数发送信号的数值。
sa_handler此参数和signal()的参数handler相同,代表新的信号处理函数,其他意义请参考signal()。
sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置。
sa_restorer 此参数没有使用。
sa_flags 用来设置信号处理的其他相关操作,下列的数值可用。
sa_flags还可以设置其他标志:
SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号 [1]
sigaction
演示代码
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<signal.h>
4 void func(int signo)
5 {
6 printf("signo:%d\n",signo);
7 }
8
9
10
11 int main()
12 {
13 struct sigaction s1_act;
14 s1_act.sa_handler = func;
15
16 sigemptyset(&s1_act.sa_mask);
17 s1_act.sa_flags=0;
18 struct sigaction s2_act;
19 sigaction(2,&s1_act,&s2_act);
20 while(1)
21 {
22 sleep(1);
23 }
24 return 0;
25 }
打印
[1@localhost 进程信号]$ make
gcc signal.c -o s_out
gcc sigaction.c -o ss_out
[1@localhost 进程信号]$ ./ss_out
^Csigno:2
^Csigno:2
^Csigno:2
^Z
[2]+ 已停止 ./ss_out
2.信号的捕获流程
举例说明上图:
int main()
{
signal(2,sigcallback);
while(1)
{
sleep(0);
}
}
在执行该程序时,收到ctrl+c,也就是2号信号此时操作系统如下:
1.收到ctrl+c,得到2号信号
2.程序执行sleep函数时,从用户态切换到内核态,执行内核代码
3.执行完sleep后,需要调用do_signal函数,处理程序所收到的信号
3.1没有收到2号信号,直接调用sysreturn函数返回用户态
3.2程序收到2号信号,切换用户态去执行自定义函数
4.执行完毕,调用sigreturn函数切换到内核态,再次调用do_signal函数,重复第3步,直至当前程序收到的信号被处理完
5.调用sysreturn函数返回用户态继续执行代码
程序从内核态----->用户态,一定会调用do_signal 函数处理程序收到的信号
程序什么时候进入内核态
1.调用系统调用
2.调用库函数时,该库函数底层使用了系统调用
3.程序异常:访问空指针、内存访问越界,double free
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<string.h>
4
5 int main()
6 {
7 char* tf=(char*)malloc(10);
8 memset(tf,'\0',10);
9 memcpy(tf,"he-ni-hao",9);
10 printf("%s\n",tf);
11 free(tf);
12 //free(tf);//会产生一个报错信号
13 tf=NULL;
14 free(tf);
15 //free NULL 程序不会崩溃,也不会收到信号,
//无论是在lunx下还是windos中,这样的操作都不会让程序奔溃
16 return 0;
17 }
正常打印:
[1@localhost 进程信号]$ ./tf_out
he-ni-hao
double free:
[1@localhost 进程信号]$ ./tf_out
he-ni-hao
*** Error in `./tf_out': double free or corruption (fasttop): 0x000000000208b010 ***
======= Backtrace: =========
/lib64/libc.so.6(+0x81299)[0x7f2f283d1299]
./tf_out[0x4006d3]
/lib64/libc.so.6(__libc_start_main+0xf5)[0x7f2f28372555]
./tf_out[0x4005a9]
======= Memory map: ========
00400000-00401000 r-xp 00000000 fd:00 3870698 /home/1/Linux/进程信号/tf_out
00600000-00601000 r--p 00000000 fd:00 3870698 /home/1/Linux/进程信号/tf_out
00601000-00602000 rw-p 00001000 fd:00 3870698 /home/1/Linux/进程信号/tf_out
0208b000-020ac000 rw-p 00000000 00:00 0 [heap]
7f2f24000000-7f2f24021000 rw-p 00000000 00:00 0
7f2f24021000-7f2f28000000 ---p 00000000 00:00 0
7f2f2813a000-7f2f2814f000 r-xp 00000000 fd:00 85 /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7f2f2814f000-7f2f2834e000 ---p 00015000 fd:00 85 /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7f2f2834e000-7f2f2834f000 r--p 00014000 fd:00 85 /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7f2f2834f000-7f2f28350000 rw-p 00015000 fd:00 85 /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7f2f28350000-7f2f28514000 r-xp 00000000 fd:00 219299 /usr/lib64/libc-2.17.so
7f2f28514000-7f2f28713000 ---p 001c4000 fd:00 219299 /usr/lib64/libc-2.17.so
7f2f28713000-7f2f28717000 r--p 001c3000 fd:00 219299 /usr/lib64/libc-2.17.so
7f2f28717000-7f2f28719000 rw-p 001c7000 fd:00 219299 /usr/lib64/libc-2.17.so
7f2f28719000-7f2f2871e000 rw-p 00000000 00:00 0
7f2f2871e000-7f2f28740000 r-xp 00000000 fd:00 219292 /usr/lib64/ld-2.17.so
7f2f28924000-7f2f28927000 rw-p 00000000 00:00 0
7f2f2893c000-7f2f2893f000 rw-p 00000000 00:00 0
7f2f2893f000-7f2f28940000 r--p 00021000 fd:00 219292 /usr/lib64/ld-2.17.so
7f2f28940000-7f2f28941000 rw-p 00022000 fd:00 219292 /usr/lib64/ld-2.17.so
7f2f28941000-7f2f28942000 rw-p 00000000 00:00 0
7ffddbea1000-7ffddbec2000 rw-p 00000000 00:00 0 [stack]
7ffddbf0c000-7ffddbf0e000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
已放弃(吐核)
信号的阻塞
1.信号的未决和阻塞状态
实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)
进程可以选择阻塞(Block)某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
每个信号都有两个标志位分别表示阻塞和未决,还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
2.sigset_t
未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态。
在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略
3.函数操作
sigprocmask()函数
头文件
#include<signal.h>
函数原型
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oldset);
函数说明
一个进程的信号屏蔽字规定了当前阻塞而不能递送给该进程的信号集。
sigprocmask()可以用来检测或改变信号屏蔽字,其操作依参数how来决定,
如果参数oldset不是NULL指针,那么信号屏蔽字会由此指针返回。
如果set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。
每个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的所有信号在递送到进程后都将被阻塞。
设:
block(old):0000 0000 block(old):1111 1111
set: 1111 1111 set: 1111 1111
block(new):1111 1111 ~set: 0000 0000
block(new):0000 0000
参数how的取值不同,带来的操作行为也不同,该参数可选值如下:
1.SIG_BLOCK: 该值代表的功能是将newset所指向的信号集中所包含的信号加到当前的信号掩码中,作为新的信号屏蔽字。block(new)=block(old)|set
2.SIG_UNBLOCK:将参数newset所指向的信号集中的信号从当前的信号掩码中移除。block(new)=block(old)&(~set)
3.SIG_SETMASK:设置当前信号掩码为参数newset所指向的信号集中所包含的信号。block(new)=set
注意事项:sigprocmask()函数只为单线程的进程定义的,在多线程中要使用pthread_sigmask变量,在使用之前需要声明和初始化。
返回值
执行成功返回0,失败返回-1。
非可靠信号,收到多个同样的非可靠信号,只会增加一次sigqueue节点,也就是只会处理一次
可靠信号
证明如下:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<signal.h>
4 void func(int signo)
5 {
6 printf("signo %d\n",signo);
7 }
8
9 int main()
10 {
11
12 signal(2,func);
13 signal(35,func);
14 sigset_t set,oldset;
15 sigemptyset(&set);//初始化为全0
16
17 sigemptyset(&oldset);
18 sigfillset(&set);//全部变为1
19
20 sigprocmask(SIG_BLOCK,&set,&oldset);
21 getchar();
22 sigprocmask(SIG_UNBLOCK,&set,NULL);
23 while(1)
24 {
25 sleep(1);
26 }
27 return 0;
28 }