1.大纲
目录
[① 通过终端按键(键盘)产生信号例如,Ctrl+C发送2号信号SIGINT、Ctrl+\发送3号信号SIGQUIT](#① 通过终端按键(键盘)产生信号例如,Ctrl+C发送2号信号SIGINT、Ctrl+\发送3号信号SIGQUIT)
[② 程序异常时操作系统会向程序发送信号来终止进程。](#② 程序异常时操作系统会向程序发送信号来终止进程。)
[③ 调用函数](#③ 调用函数)
[- kill系统调用:](#- kill系统调用:)
[- raise系统调用](#- raise系统调用)
[abort 系统调用](#abort 系统调用)
[⑤ 硬件异常产生信号](#⑤ 硬件异常产生信号)
原视频来自于B站里昂,总长22min。根据视频自己进行了额外的知识补充。
视频链接
进程通信方法:管道,共享内存,消息队列,信号和信号量
2.信号
对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。信号跟信号量虽然名字相似度 66.66%,但两者用途完全不一样,就好像 Java 和 JavaScript 的区别。
一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件 。比如,如果当进程在前台运行时,你键入Ctrl+C(也就是同时按下Ctrl键和 C键),那么内核就会发送一个SIGINT信号给这个前台进程。
在 Linux 操作系统中, 为了响应各种各样的事件,提供了几十种信号,分别代表不同的意义。我们可以通过 kill -l 命令,查看所有的信号:
bash
$ 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号信号为实时信号(可靠信号),处于就绪队列多个相同的实时信号全部会被响应。
每种信号类型都对应于某种系统事件,用不同的整数表示,例如SIGINT信号用号码2表示
比如,如果一个进程试图除以0 ,那么内核就发送给它一个SIGFPE信号(号码8 Floating Point Exception浮点异常)。
如果一个进程执行一条非法指令 ,那么内核 就发送给它一个SIGILL信号(号码4)。
sigill(ill生病,有病的,不健康的;不良的;不良的;〈美俚〉(因嫌疑而)被捕的,被拘留的;坏的,邪恶的,有害的;不舒服;)
kill -l
#查看系统上支持的不同类型的信号
3.信号的实现
在操作系统中,每个进程都有一个进程控制块(Process Control Block,PCB),它包含了进程的管理和控制信息.。
【例】Linux下,用一个名为task_struct的结构体类型来描述PCB,包括很多字段,如进程的状态进程的标识、进程的优先级等。每一种信息都用一个字段来实现。
struct task struct
{
...
volatile long state; /进程状态
pid_t pid; //进程ID,每个进程都有一个唯一的PID,用于区分不同进程,相当于身份证号
unsigned long rt_priority;//进程优先级
stuct mm_struct*mm,*active_mm;//与内存管理有关的数据结构,包含进程内存使用的信息
...
}
因此,我们可以将信号记录在进程的task_struct(PCB)结构体 中。
例如,我们可以用位图来表示某个信号是否产生,每个比特位代表了某个特定的信号,比特位为0代表没有收到了信号,为1则代表收到了信号。进程收到信号,本质是位图被修改
4.信号的处理
信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令)。
信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式。
①忽略信号
**不采取任何操作,**把对应的1->0,返回到main函数继续执行 **。**但是有两种信号不能被忽略:SIGKILL(9)和SIGSTOP(19) ,它们用于在任何时候中断或结束某一进程。
这样做的原因是系统管理员需要能够杀死或停止进程,如果进程能够选择忽略SIGKILL(使进程不能被杀死)或SIGSTOP(使进程不能被停止)将破坏这一权力。
②执行信号的默认操作
Linux 对每种信号都规定了默认操作,例如,上面列表中的 SIGTERM 信号,就是终止进程的意思。如果是终止的话,我们OS在内核态会直接杀死进程,不会返回到main函数了。
- SIG_DFL :默认行为,大部分进行都是进行终止,
- SIG_ING:忽略行为,
例如,进程收到SIGINT信号后会终止,本质上是向进程发送了一个编号为2的SIGINT信号,只不过这个信号是通过键盘输入的,然后经过操作系统处理后再发送给进程。
③捕获井处理信号
内核(内核态)会暂停该进程正在执行的代码,并跳转到用户注册的函数(用户态)。
- 用户态:我自己写的代码
- 内核态:执行操作系统的代码
- 两者都属于操作系统运行状态
言归正传,让我们开始讲信号捕捉的具体流程
画一个无穷符号,然后上边画一条横线,横线上的4个交点就是状态切换的4个点,所有①②③④⑤就是信号捕捉的各个过程,注意按照箭头的方向执行。
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号
由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下;
用户程序注册了 SIGQUIT 信号的处理函数 sighandler,当前正在执行 main 函数
①这时发生中断或异常切换到内核态
②在中断处理完毕后要返回用户态的 main 函数之前检査到有信号 SIGQUIT 递达:
③内核决定返回用户态后不是恢复 main 函数的上下文继续执行,而是执行 sighandler(信号处理)函数,sighandler 和 main 函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
④sighandler 函数返回后自动执行特殊的系统调用 sigreturn 再次进入内核态。
⑤如果没有新的信号要递达,这次再返回用户态就是恢复 main 函数的上下文继续执行了。
例题:
由上述信号捕获过程,易选A。
实际举例
SIGINT原本是用来结束进程的,但用signal自定义它的功能后就可以使它对进程的操作改变,
#include<singal.h>
#include<stdio.h>
#include<unistd.h>
//handle的函数实现的功能是打印can't stop!"
void handle(int sigNum){
printf("can't stop! sigNum:%d\n",sigNum);
}
int main(){
signal(SIGINT,handle);
//将SIGINT号信号的功能改成handle
while(1){
printf("a\n");
sleep(1);
}
return 0;
}
可以看到,每次按下Ctrl+C,都会打印对应内容(实现自定义的功能而不是像原来一样终止进程),而sigNum也证明Ctrl+C对应的信号值确实是2号(SIGINT)。
经常捕获的两种信号是 SIGINT 和 SIGTERM。
SIGKILL和SIGSTOP 不能被捕获,即无法通过自定义handle函数来修改其信号操作。
exp1
#include<singal.h>
#include<stdio.h>
#include<unistd.h>
//handle的函数实现的功能是打印can't stop!"
void handle(int sigNum){
printf("can't stop! sigNum:%d\n",sigNum);
}
int main(){
signal(SIGKILL,handle);
//将SIGINT号信号的功能改成handle
while(1){
printf("still alive\n");
sleep(1);
}
return 0;
}
exp2
#include<singal.h>
#include<stdio.h>
#include<unistd.h>
int count=0;
//handle的函数实现的功能是打印"count=%d, still alive"
void handle(int sigNum){
printf("count=%d, still alive sigNum:%d\n",sigNum);
}
int main(){
signal(SIGKILL,handle);
signal(SIGTERM,handle);
signal(SIGINT,handle);
//将SIGINT号信号的功能改成handle
while(1){
count++;
sleep(1);
}
return 0;
}
2:sigint
15:sigterm
9:sigkill
几个Linux支持的典型信号:
SIGCHLD
当进程终止或停止时,内核会给进程的父进程发送此信号。在默认的情况下SIGCHLD是被忽略 的,如果进程对它们的子进程是否存在感兴趣,那么进程必须显式地捕获并处理该信号。
SIGFPE
不考虑它的名字,该信号代表所有的算术异常 ,而不仅仅指浮点数运算相关的异常,异常包括溢出、下溢和除以0.
默认的操作是终止进程并形成内存转储文件,但进程可以捕获并处理该信号。
SIGILL
当进程试图执行一条非法机器指令 时,内核会发送该信号。默认操作是终止进程并进行内存转储进程可以选择捕获并处理SIGILL。
SIGINT
当用户输入中断符(通常是Ctrl-C)时,该信号被发送给所有前台进程组中的进程默认的操作是终止进程。进程可以选择捕获并处理该信号,通常是为了在终止前进行清理工作
5.信号的产生
① 通过终端按键(键盘)产生信号 例如,Ctrl+C
发送2号信号SIGINT、Ctrl+\
发送3号信号SIGQUIT
② 程序异常时操作系统会向程序发送信号来终止进程。
③ 调用函数
- kill系统调用:
kil()调用会从一个进程向另一个进程发送信号:
int kill(pid t pid,int signo);//调用kill给pid代表的进程发送信号signo。
kill命令本质上是通过系统调用实现的。
- raise系统调用
【例子】创建一个raise程序,将其2号信号的处理函数改为自定义函数,该信号每隔一段时间便会给自己发送信号
#include<singal.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
void handle(int signo){
printf("get a signal : %d\n", signo);
}
int main(){
signal(2,handle);
//将2号信号自定义。
while(1){
printf("I'm a process, pid = %d\n",getpid());
sleep(1);
raise(2);
//进程自己给自己发送2号信号。
}
return 0;
}
abort 系统调用
abort 使当前进程接受到信号而异常终止
#include<singal.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
void handle(int signo){
printf("get a signal : %d\n", signo);
}
int main(){
signal(6,handle);
//将SIGINT号信号的功能改成handle
while(1){
printf("I am a process, pid : %d\n",getpid());
sleep(1);
abort();
}
return 0;
}
④由于软件条件产生信号
例如alarm()函数可以设置定时器,当定时器倒计时结束,就会向进程发送一个SIGALARM信号。
- 系统调用:通过系统提供的函数,如 kill()、alarm(),显式地向进程发送信号。
- 用户输入:用户通过终端输入特定字符组合(如 Ctrl+C),会向进程发送 SIGINT 信号。
- 子进程状态变化:子进程结束或停止时,父进程会收到 SIGCHLD 信号,监控子进程状态。
- 定时器:通过 alarm()或setitimer()设置的定时器到期时,向进程发送 SIGALRM 信号。
exp1
#include<singal.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
void handle_alarm(int signo){
printf("Alarm clock!\n");
}
int main(){
//设置信号处理函数
signal(SIGALRM,handle_alarm);
//做其他事情或者简单地等待
while(1){
printf("running\n");
alarm(2);
sleep(2);
}
return 0;
}
exp2
pause()函数的作用是使当前进程进入睡眠状态,直到接收到一个信号为止。
int pause(void);
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
int main( ) {
pid_t child_pid = fork();
//fork一个子进程。 子进程的pid不一样。
if(child_pid < 0) {//创建新进程失败。
perror("Fork failed");
exit(1);
}
if (child pid == 0){ // 子进程
while (1){
pause(); // 等待信号
}else { // 父进程
while (1) {
sleep(2);//每2秒发送一次信号
kill(child_pid, SIGUSR1);// 向子进程发送信号
printf("Sent SlGUSR1 to child process (PID: %d)\n", child_pid);
}
return 0;
}
⑤ 硬件异常产生信号
①硬件信号触发:某些硬件操作可能触发特定信号。如,SIGFPE信号(浮点除零操作)。
②硬件异常:如内存访问越界(如段错误,SIGSEGV)或非法指令。
发生硬件异常时,它被硬件以某种方式检测到并通知内核,然后内核向当前进程发送适当的信号。
例如当前进程执行了除零的指令CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号,并将该信号发送给进程。
总之,使用信号的两个主要目的:
① 让进程知道已经发生了一个特定的事件。
② 强迫进程执行它自己代码中的信号处理程序,
注意,并不是系统中所有进程都可以向其他进程发送信号,只有核心和超级用户可以。
普通进程只可以向拥有相同uid(用户标识号)和gid(组标识号)或者在相同进程组中的进程发送信号。
当信号产生时,内核将进程taskstruct中的信号相应标志位设置为1,表明产生了该信号。
系统对置位之前该标志位已经为1的情况不进行处理,这说明进程只处理最近接收的信号。
信号产生后并不马上送给进程,它必须等待直到进程再一次运行时才交给它。
每当进程从系统调用中退出时,内核会检查它的signal和blocked字段(位图),查看是否有需要发送的非屏蔽信号,若有则立即发送信号。
如果信号的处理被设置为缺省,则系统内核将会处理该信号,否则会执行用户提供的信号处理程序。
【练习1】信号是用户按下Ctrl+C时默认发送给前台进程组的信号?
A. 'SIGKILL'
B.'SIGSTOP'
C.'SIGINT'
D.'SIGTERM'
答案:C
【练习2】哪个信号用于终止进程,并且不能被进程捕获或忽路?
A. 'SIGKILL`
B."SIGBUS"
C. 'SIGINT
D.'SIGTERM
答案:A
【练习3】在信号的进程通信机制中,以下哪个说法是正确的?
A.对于任意的信号都可以忽略
B.对于任意的信号都可以捕获
C.系统中所有进程都可以向其他进程发送信号
D.系统对置位之前该标志位已经为1的情况不进行处理,这说明进程只处理最近接收的信号
答案:D