什么是信号
1.在现实生活中,我们过马路看到的红绿灯就是一种信号,铃声也是一种信号,根据不同的信号我们有着不同的处理方式。
2.在Linux中信号也是类似的,就是提供一种机制告诉进程 该怎么做。
信号列表
1.通过kill -l命令可以查看系统定义的信号:
[xutingting@localhost Desktop]$ 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
2.如果没有仔细看,会觉得有64个信号,其实一共只有有62个信号(标号为1~31,34~64),中间缺省的是为了分隔,其中1~31号信号为普通信号,34~64号信号为实时信号。
对于普通信号来说,如果当前进程刚收到了该信号,还没有处理,接下来再次收到该信号,则只会处理一次,而实时信号来一次处理一次。
3.每个信号都有一个编号和一个宏定义的名称,在signal.h头文件里面可以找到这些宏的定义。
信号的产生方式
1.从通过终端按键产生信号(例如从键盘按下Ctrl-C 产生2号信号);
2.通过系统函数向进程发送信号;
3.由软件条件产生信号;
1.从通过终端按键产生信号
(1)通过键盘产生的信号是发送给前台进程的。
- 前台进程:前台进程是目前正在屏幕上显示的进程和一些系统进程,也就是和用户正在交互的进程。
(2)通过键盘按下Ctrl-C,系统就会向当前进程发送2号SIGINT信号,使得进程终止执行。
(3)通过ctrl-\ 发送 SIGQUIT 信号给前台进程,终止前台进程并生成 core 文件。
- 现在来解释一下什么是core文件
- 首先介绍一下Core Dump:
①Core Dump为核心转储文件(类似于临终遗言,用来保护现场)
②默认是不产生Core Dump文件的。(通过ulimit -a命令,用来查看当前shell的资源信息,可以看到core文件的size为0,所以默认不产生,如果想要产生首先先修改core文件可用的size)
[root@localhost 信号]# ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 7881
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 10240
cpu time (seconds, -t) unlimited
max user processes (-u) 7881
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
- 可以修改当前bash资源信息里的core文件大小,就可以产生core文件。
[root@localhost 信号]# ulimit -c 1024 //因为core文件对应-c,将其大小改为1024
[root@localhost 信号]# ulimit -a
core file size (blocks, -c) 1024
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
- ③Core Dump可以用来事后调试。
gdb test core.3363 //test表示可执行程序,core.3363表示core文件
2.通过系统函数向进程发送信号
(1)kill函数
- 用于给某个进程发送某个信号(kill命令底层调用kill函数)
①kill函数原型:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
//pid表示进程id,sig表示哪种信号,既可以填写信号的序号也可以填写信号对应的宏
②使用示例:
#include<stdio.h>
#include<signal.h>
int main()
{
kill(4036,9); //给pid为4036的进程发送9号信号
return 0;
}
③通过kill函数自己模拟实现kill命令
- kill命令的使用方式为kill 信号 pid
- 则自己编写的Mykill,使用方式为./Mykill 信号 pid,这三个参数通过命令行参数进行传递
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>
//./kill signo pid,命令行参数中argv[1]为信号,argv[2]为进程id
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("Usage ./kill [signo] [pid]\n");
return 1;
}
int signo = atoi(argv[1]);
int pid = atoi(argv[2]);
int ret = kill(pid,signo);
if(ret < 0)
{
perror("kill\n");
return 2;
}
return 0;
}
运行结果:(执行Mykill之前,当前包含两个进程,执行./Mykill 9 11581之后,当前就只包含一个进程,pid为11581的进程已经不在了)
[root@localhost 信号]# ps aux | grep 'hello'
root 11581 12.3 0.0 1864 292 pts/4 R 17:33 0:01 ./hello
root 11929 0.0 0.0 4420 772 pts/4 S+ 17:33 0:00 grep hello
[root@localhost 信号]# ./Mykill 9 11581
[root@localhost 信号]# ps aux | grep 'hello'
root 13534 0.0 0.0 4420 776 pts/4 S+ 17:34 0:00 grep hello
[1]+ Killed ./hello
(2)raise
- 用于给自己发送信号
①raise函数原型(相当于 kill(getpid(),sig)
)
#include <signal.h>
int raise(int sig);
②使用示例
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
int main()
{
printf("hello\n");
sleep(3);
raise(SIGQUIT); //给自己发送SIGQUIT信号
return 0;
}
(3)abort
- 用于给自己发送SIGABRT信号,使得当前进程异常终止
- 例如assert如果参数为false则会调用该函数,C++里面的异常如果没有捕获也会调用该函数,导致进程异常终止
#include <stdlib.h>
void abort(void);
3.由软件条件产生信号
(1)SIGPIPE
- 例如在进程间通信方式的管道中,当写者一直往管道里面写,读者不仅不读还将自己的读端关闭,则系统会给写者发送SIGPIPE信号。
(2)SIGALRM信号与alarm函数
①alarm函数
- 调用alarm函数会设置一个闹钟,表示多少秒之后向当前进程发送SIGALRM信号,SIGALRM信号用于终止当前进程。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//参数:参数表示设置的秒数,如果参数为0,表示取消之前设置的闹钟
//返回值:要么为0要么返回之前设置的闹钟剩下的秒数
② 作用:告诉内核在seconds秒之后给当前进程发送SIGALRM信号
③使用示例:
include<stdio.h>
#include<unistd.h>
int main()
{
printf("before\n");
alarm(10);
sleep(12);
printf("after\n");
return 0;
}
运行结果:
- 可以发现10秒之后,进程终止并且屏幕显示Alarm clock,而且可以发现没有输出”after“,说明当时间到了,就会给进程发送SIGALRM信号,导致进程异常终止,就不会执行后面的语句。
- 如果没有sleep(12),则会输出”after“,因为还没有到闹钟设置的时间,当前进程已经结束执行。
- 所以当没有到闹钟设置的时间,程序都会正常往后执行,直到到了闹钟设置的时间,才会收到一个SIGALRM信号。
[root@localhost 信号]# ./alarm
before
Alarm clock
④如果想要使程序正常进行,则必须对信号进行捕捉.(通过signal函数,稍后介绍信号捕捉的相关知识)
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void handle(int signo)
{
printf("handle\n");
}
int main()
{
signal(SIGALRM,handle); //捕捉SIGALRM信号,对应信号的处理函数为handle
printf("before\n");
alarm(10);
sleep(12);
printf("after\n");
return 0;
}
运行结果如下:(可以看到设置一个闹钟,时间为10秒,当对信号进行捕捉了之后,先回去处理信号,然后就会继续执行main函数后面的程序)。
[root@localhost 信号]# ./alarm
before
handle
after
⑤基于alarm函数实现sleep。
- 首先:通过调用alarm函数,注册一个闹钟,并设置好时间;
- 然后:在前面需要对信号进行捕捉,设置一个handel函数使其等待闹钟信号的到来;
- 其次:在信号到来之前需要挂起等待(可通过pause函数);
- 最后:一旦信号来了,捕捉之后就继续往后执行。
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void handle(int signo)
{
}
int main()
{
signal(SIGALRM,handle);
printf("hello\n");
alarm(4);
pause();
printf("ok\n");
return 0;
}
信号的相关概念
1.递达:执行信号的处理动作称之为递达;
未决:信号的产生到被处理之前,称之为未决;
阻塞:屏蔽该信号,就会让当前信号被阻塞。
2.信号的表示
(1)信号是操作系统发送给某个进程的,对于一个进程来说,它有一个PCB,在PCB里面有两个位图和一个函数指针数组。
(2)一个位图用来表示当前进程接收到了哪些信号;另一个位图表示当前进程对该进程是否阻塞。函数指针数组表示当前进程对该信号执行什么样的操作。
(3)当系统给某个进程发送信号时就是将该信号对应的pending位图里对应的位置为1。
3.信号的处理方式:
①默认
②忽略
③自定义
注意:
信号的阻塞和忽略是不同的概念,阻塞就表示当前信号永远不会递达,忽略表示信号已经递达后的处理动作;阻塞是一种状态,而忽略是一种行为。
信号的捕捉
1.通过signal函数
①signal函数原型:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
②参数解析:
signum:表示哪个信号
handler:表示信号处理函数 ,sighandler_t为函数指针,信号处理函数的参数是int,返回值是void的函数。
③返回值:返回该信号前一个信号处理函数
2.信号捕捉
(1)流程:
①在用户层,当在主函数里发现某条指令触发了信号;
②这时会进入内核层,去处理递达地信号;
③当发现该信号地处理方式为用户自定义地函数时,又会回到用户层;
④执行用户层里信号处理函数。
⑤当信号处理函数执行完之后,又会回到内核态;
⑥当发现没有新地信号递达就会回到用户层,继续执行main函数。
(2)注意点:
①信号捕捉整体执行顺序(类似于无穷大符号);
②信号处理函数与main函数是两个执行流;
③信号处理函数执行完之前,原有的main函数等待。
3.当某个信号地处理函数被调用后,如果再接收到这个信号,则会将将该信号加入进程的信号屏蔽字。(阻塞信号集也叫做信号屏蔽字)
信号集的相关操作
- 未决和阻塞标志都可以用一个数据结构来存储,这个数据结构是sigset_t类型,sigset_t定义的变量就叫做信号集,对于信号集而言,相当于一个位图,不同的位表示不同的信号,每一位上的数字0或1表示当前信号的状态。
1.信号集就相当于一个位图,对信号集操作就是对位图进行操作。
#include <signal.h>
int sigemptyset(sigset_t *set); //初始化并将所有位都置为0
int sigfillset(sigset_t *set); //将所有位都置为1
int sigaddset(sigset_t *set, int signum); //将某位置为1 ,signum对应的就是某一位
int sigdelset(sigset_t *set, int signum); //将某一位置为0
int sigismember(const sigset_t *set, int signum); //判断某一位是1还是0
2.sigprocmask
(1)用于读取或修改当前进程的信号屏蔽字(阻塞信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
(2)参数解析:
oldset用于读取当前进程的信号屏蔽字;
set用于设置当前进程的信号屏蔽字,当两者都不为空时,将当前的信号屏蔽字备份至oldset里面,然后通过第一个参数how和set修改当前进程的信号屏蔽字。
3.sigpending
用于读取当前进程未决信号集,只能读不能写,因为未决是系统决定的。
#include <signal.h>
int sigpending(sigset_t *set); //set为输出型参数,将读取到的信号未决集放在set里面
4.利用上面的相关函数,编写一个程序,一直读取当前进程的未决信号集,并且对Ctrl-C(SIGINT信号)屏蔽掉。
#include<signal.h>
#include<unistd.h>
//打印信号集的函数,打印按照左边是低位右边是高位进行打印,与普通的二进制的位顺序不太一样
void printsigset(sigset_t* s)
{
int i = 0;
for(;i<32;++i) //因为有32个位
{
if(sigismember(s,i)) //判断第i位的值是否为1,也就是判断i位对应信号的状态
{
printf("1");
}
else
{
printf("0");
}
}
printf("\n");
}
int main()
{
sigset_t set; //定义一个信号集
sigemptyset(&set);
sigaddset(&set,SIGINT); //将SIGINT信号添加至set里
sigset_t oldset;
sigemptyset(&oldset);
sigprocmask(SIG_BLOCK,&set,&oldset); //设置当前进程的阻塞信号集,并将之前的阻塞信号集放在oldset里
while(1)
{
sigpending(&oldset); //将当前进程的未决信号集放至参数oldset里
printsigset(&oldset); //打印未决信号集
sleep(1);
}
return 0;
}
运行结果:
[root@localhost 信号]# ./sigset
10000000000000000000000000000000
10000000000000000000000000000000
10000000000000000000000000000000
10000000000000000000000000000000
^C10100000000000000000000000000000 //当按下Ctrl-C后当前进程并没有终止,并且将当前进程的信号屏蔽字里对应的SIGINT信号那一位(SIGINT对应序号为2)设置成了1
10100000000000000000000000000000
10100000000000000000000000000000
SIGCHILD信号
1.在前面我们学过进程的相关概念,其实当子进程退出后,会给父进程发送一个SIGCHILD信号,该信号的默认处理动作是忽略。父进程可以定义SIGCHILD信号的处理行为,就可以让父进程去做其它事,当父进程接收到该信号时进行再进行进程等待,就不会造成僵尸进程。
2.在Linux中,将SIGCHILD信号的处理方式设置为SIG_IGN,也会解决僵尸问题。(因为这样当子进程终止时,就会自动清理掉),注意:该方式只对Linux有效。