【Linux】系统部分——信号的概念和产生
23.信号的概念和产生
信号的概念
信号是进程之间事件异步通知的⼀种⽅式,属于软中断。 信号的处理是异步的。进程不会立即中断自己正在做的事情去处理信号
理解:
-
信号的识别是内置的:信号的定义和编号(如
SIGINT
=2,SIGKILL
=9)是操作系统内核预先定义好的 -
信号的处理方式必须在信号产生之前就由进程预先定义好。处理方式包括:
- 默认:执行系统预设的操作(如终止、忽略、暂停等)。
- 忽略:收到信号后直接丢弃,不做任何处理。
- 自定义捕捉:让进程调用一个自己定义的函数来处理信号。
-
一个信号被产生(generated)后,并不会立即被处理。
补充:前台进程和后台进程
前台进程运行时命令行操作无法使用,但是后台进程运行时bash可以解析命令行。前台进程可以用
Ctrl C
终止,但后台进程无法用这种办法终止,,可以用信号终止kill -9 [pid]
创建后台进程需要在
./[可执行程序]
后面加上&
user@iZ7xvdsb1wn2io90klvtwlZ:~/lesson27/Sig$ ./sig &
如果不想让后台进程的输出结果在命令行窗口打印,在前面加上加上nohup,输出就会写入文件
nohup.out
user@iZ7xvdsb1wn2io90klvtwlZ:~/lesson27/Sig$ nohup ./sig & [3] 11026
使用这种方法之后会在命令行后面显示作业号以及进程的pid,如果想要终止此进程,除了使用信号,还可以使用
fg [作业号]
的方法将这个后台进程转换为前台进程,之后使用Ctrl C
,而Ctrl C
这个键盘组合键在操作系统会被转换为2号信号
信号的产生
在前面我们已经了解到信号的处理方法有三种,其中对于自定义捕捉,可以使用signal
系统调用对输入的信号编号的默认操作修改为自定义函数来处理信号。
#include <signal.h>typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);
可见,通过这个函数,我们可以吧输入的信号编号对应的默认处理方式修改为自定义函数来处理,函数第一个形参需要信号的编号或名称,第二个是函数指针
下图是常见信号的编号和名称的对应关系,其实信号的名称就是一个宏。
user@iZ7xvdsb1wn2io90klvtwlZ:~$ kill -l1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP6) 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
signal
使用举例:
#include <iostream>
#include <unistd.h>
#include <signal.h>void Hander(int sig)
{std::cout << "信号:" << sig << std::endl;
}int main()
{signal(2, Hander);while(true){std::cout << "hello linux" << std::endl;sleep(1);}return 0;
}
user@iZ7xvdsb1wn2io90klvtwlZ:~/lesson27/Sig$ make
g++ -o sig Signal.cc
user@iZ7xvdsb1wn2io90klvtwlZ:~/lesson27/Sig$ ./sig
hello linux
hello linux
^C信号:2
hello linux
hello linux
hello linux
hello linux
^\Quit (core dumped)
-
Ctrl C
本来是向前台进程发送终止信号(信号2)但是我们通过signal
函数将默认终止改成了Hander
方法,当对应信号被触发,内核会将对应信号编号,传奇给自定义方法 -
要注意的是,signal函数仅仅是设置了特定信号的捕捉⾏为处理⽅式,并不是直接调⽤处理动作。如果后续特定信号没有产⽣,设置的捕捉函数永远也不会被调⽤
-
我们可以使用
man 7 signal
查询不同信号的缺省处理方法 -
9号信号无法被自定义捕捉
进程如何获取信号
硬件产生中断,键盘的操作要先经过OS的解析,当OS将这个操作解析为发送指定信号的时候,OS会把对应信号发送给进程,这个发送的本质其实是向进程的PCB中有关信号部分位图的写入(信号发送给进程进程不会立刻进程处理,在进程中有一个位图来保存信号,bit位的位置对应信号的编号,值(0/1)表示是否接受到信号。无论以什么方式发送信号,最终都是转换到OS,让OS写入信号,因为task_struct
的唯一管理者是OS
产生信号的方式
-
系统发出指令
使用
kill
指令可以向进程发送指定信号 -
系统调用
使用系统调用
kill
可以向进程发送指定信号,系统发出指令实际上也是调用系统调用实现的raise 函数可以给当前进程发送指定的信号(⾃⼰给⾃⼰发信号)
abort 函数使当前进程接收到信号⽽异常终⽌
-
软件条件
比如管道通信时如果读端已经关闭,OS会给写端发送
SIGPIPE
信号直接终止写端进程,像这种由软件在某一种特定情况下产生信号即为由软件条件产生信号。除此之外SIGALRM
也是软件条件触发信号。在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产⽣机制。这些条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产⽣的SIGPIPE信号)等。当这些软件条件满⾜时,操作系统会向相关进程发送相应的信号,以通知进程进⾏相应的处理。简⽽⾔之,软件条件是因操作系统内部或外部软件操作⽽触发的信号产⽣。
#include <unistd.h> unsigned int alarm(unsigned int seconds);
-
调⽤ alarm 函数可以设定⼀个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发
SIGALRM
信号,该信号的默认处理动作是终⽌当前进程。 -
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。
-
如果seconds值为0,表⽰取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数
-
闹钟的设置是一次性的,如果想要这个闹钟重复执行,可以使用
signal
函数自定义捕捉,让进程调用自己定义的函数,在这个函数的结尾重新开启闹钟//伪代码 void hander(int sig) {//........alarm(tim); }int main() {signal(SIGALRM, hander);//.....alarm(tim);//.....return 0; }
-
pause
函数可以等待信号。导致调用进程(或线程)休眠,直到信号被传递,该信号要么终止进程,要么导致信号捕获函数的调用#include <unistd.h> int pause(void)
-
-
硬件异常产⽣信号
硬件异常被硬件以某种⽅式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执⾏了除以0的指令, CPU的运算单元会产⽣异常, 内核将这个异常解释为SIGFPE信号发送给进程。再⽐如当前进程访问了⾮法内存地址, MMU会产⽣异常,内核将这个异常解释为SIGSEGV信号发送给进程。
#include <stdio.h> #include <signal.h> void handler(int sig) {printf("catch a sig : %d\n", sig); } // v1 int main() {//signal(SIGFPE, handler); // 8) SIGFPEsleep(1);int a = 10;a/=0;while(1);return 0; }
catch a sig : 8 catch a sig : 8 catch a sig : 8 catch a sig : 8 catch a sig : 8 catch a sig : 8 catch a sig : 8 ^C
通过上⾯的实验,我们可能发现:发现⼀直有8号信号产⽣被我们捕获,这是为什么呢?上⾯我们只提到CPU运算异常后,如何处理后续的流程,实际上 OS 会检查应⽤程序的异常情况,其实在CPU中有⼀些控制和状态寄存器,主要⽤于控制处理器的操作,通常由操作系统代码使⽤。状态寄存器可以简单理解为⼀个位图,对应着⼀些状态标记位、溢出标记位。OS 会检测是否存在异常状态,有异常存在就会调⽤对应的异常处理⽅法。除零异常后,我们并没有清理内存,关闭进程打开的⽂件,切换进程等操作,所以CPU中还保留上下⽂数据以及寄存器内容,除零异常会⼀直存在,就有了我们看到的⼀直发出异常信号的现象。
子进程退出core dump
在我们使用man 7 signal
指令查询有哪些信号的时候,我们会发现同样执行进程终止的默认操作但是有两种不同的标识Term
和Core
Term
表示直接进程退出,不需要debug;Core
表示核心转储,会在当前目录下生成一个core
文件将进程在内存中的部分信息保存起来,便于日后调试。
-
对于云服务器,这个
core
文件一般是默认不创建的,可以使用ulimit -a
查询这个文件的大小,用ulinit -c
修改这个core
文件的大小user@iZ7xvdsb1wn2io90klvtwlZ:~/lesson27/Sig$ ulimit -a real-time non-blocking time (microseconds, -R) unlimited core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 6191 max locked memory (kbytes, -l) 206476 max memory size (kbytes, -m) unlimited open files (-n) 65535 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 6191 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited user@iZ7xvdsb1wn2io90klvtwlZ:~/lesson27/Sig$ ulimit -c 10240 user@iZ7xvdsb1wn2io90klvtwlZ:~/lesson27/Sig$ ulimit -a real-time non-blocking time (microseconds, -R) unlimited core file size (blocks, -c) 10240 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 6191 max locked memory (kbytes, -l) 206476 max memory size (kbytes, -m) unlimited open files (-n) 65535 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 6191 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited
-
⾸先解释什么是Core Dump。当⼀个进程要异常终⽌时,可以选择把进程的⽤⼾空间内存数据全部 保存到磁盘上,⽂件名通常是core,这叫做Core Dump。
-
进程异常终⽌通常是因为有Bug,⽐如⾮法内存访问导致段错误,事后可以⽤调试器检查core⽂件以查清错误原因,这叫做 Post-mortem Debug (事后调试)。
-
⼀个进程允许 产⽣多⼤的 core ⽂件取决于进程的 Resource Limit (这个信息保存 在PCB中)。默认是不允许产⽣ core ⽂件的, 因为 core ⽂件中可能包含⽤⼾密码等敏感信息,不安全。
-
在开发调试阶段可以⽤ ulimit 命令改变这个限制,允许产⽣ core ⽂件。 ⾸先⽤ ulimit 命令改变 Shell 进程的 Resource Limit ,如允许 core ⽂件最⼤为 1024K: $ ulimit -c1024