前言
生活中有各种各样的信号,比如:闹钟、红绿灯、上下课铃声……我们可以知道信号产生时对应的要做些什么,幼儿园的小朋友也明白红灯停、绿灯行的道理。
但是,人是怎么识别出这些信号的呢?人是只有通过认识,才能产生行为:有人通过教育的手段让我们在大脑里记住了红绿灯属性及其对应行为。
但是,当信号产生时,我们并不是总能及时去处理这个信号。信号的发生是随时的(异步),但是我们去处理信号并不都是即时的。因为,我们在信号来临时可能会有其他更重要的事情要做(优先级更高的事情),所以从信号发生到信号被处理中间会有一个时间窗口,当然我们在未处理这个信号时需要将这个信号记录下来,等能处理时再处理。
当我们处理信号时,处理信号的方式也是有所不同的(不同的信号有不同的处理方式,不同的人对对同一个信号的处理方式也可能不同,相同的人对相同的信号在不同的场景下处理信号方式也可能不同)。处理信号的方式大致分为以下三种:
- 默认动作:例如,红灯停,绿灯行等。
- 自定义动作:例如,红灯唱歌,绿灯跳舞等。
- 忽略动作:例如,早晨闹钟响了,我们默认动作是起床,忽略动作是忽略闹钟继续睡觉。
那么,进程与人处理信号的方式有什么异同呢?信号又是如何产生的呢?本文我们来了解Linux中的进程信号。
一、进程信号
前言中,我们通过生活中的信号引入了进程中的信号,下面我们简单了解以下进程信号的概念。进程本身是被程序员编写的代码,是属性和逻辑的组合,所以进程处理信号的识别和对应的动作都是程序员所赋予的。
- 信号是给进程发送的,那么进程是如何识别信号的? 认识 + 动作。
- 进程在处理信号的时候有三种动作:默认动作、自定义动作、忽略动作。
- 处理信号也被称为信号被捕捉。
- 如果进程收到信号的时候,有优先级更高的代码需要执行,我们就不能即时的处理信号,因此进程需要有保存信号的能力。
- 进程是如何保存不能即时处理的信号的?
信号会被保存在进程的PCB(task_struct
)中,我们用比特位来表示信号的编号,比特位的内容表示是否收到对应信号(0表示没收到,1表示收到了)。 - 如何理解信号的发送和接收?
信号的发送和接收,实际上就是改变PCB中的信号位图。PCB是内核维护的数据结构对象,所以PCB的管理者是OS,因此只有OS可以改变PCB中的内容,因此无论我们之后学习到多少种发送信号的方式,本质上都是OS向目标进程发送信号。当然,这也说明了系统必须要提供发送信号、处理信号相关的系统调用。(我们之前使用的kill命令就是一种系统调用)
二、查看命令kill -l与信号解释man 7 signal
1.kill -l
查看系统定义的信号列表;
每一个信号都有与之对应的编号和宏定义名称,这些宏定义可以在signal.h中找到。
2.man 7 signal
查看信号详细信息。
Term表示正常结束(OS不会做任何额外工作);
Core表示OS出了终止的工作;
其他的见下文。
三、信号的产生
文件test.c
1 #include<stdio.h> 2 int main() 3 { 4 int cnt = 0; 5 while(1) 6 { 7 printf("hello %d, i am %d\n",cnt++, getpid()); 8 sleep(1); 9 } 10 return 0; 11 }
1.按键
ctrl + c
ctrl + c:热键,它实际上是个组合键,OS会将它解释为2信号。
进程对3信号的默认行为是终止进程,所以当我们按ctrl + c时当前运行的进程将被直接终止。
用ctrl + c:
用kill -2
ctrl + z
ctrl + z:热键,实际上是20号信号(即,按ctrl + \和kill -20 (进程pid)是一样的)。
ctrl + \
ctrl + \:热键,实际上是3信号。
2.系统调用
用键盘向前台进程发送信号,前台进程会影响shell,Linux规定跟shell交互时只允许有一个前台进程,实际上当我们运行自己的进程时,我们的进程就变成了前台进程,而sbash会被自动切到后台(默认情况下bash也是一个进程)。
当然,除了用键盘向前台进程发送信号外,我们可以用系统调用向进程发送信号。
kill——向任意进程发送信号
发送信号的能力是OS的,但是有这个能力并不一定有使用这个能力的权利,一般情况下要由用户决定向目标进程发送信号(通过系统提供的系统调用接口来向进程发送信号)。
通过kill与命令行参数相结合:
文件mykill.cc
1 #include<iostream> 2 #include<stdio.h> 3 #include<errno.h> 4 #include<stdlib.h> 5 #include<sys/types.h> 6 #include<signal.h> 7 using namespace std; 8 9 static void Usage(const string &proc) 10 { 11 cout<<"\nUsage:"<<proc<<" pid signo\n"<<endl; 12 } 13 14 int main(int argc, char* argv[]) 15 { 16 if(argc != 3) 17 { 18 Usage(argv[0]); 19 exit(1); 20 } 21 pid_t pid = atoi(argv[1]); 22 int signo = atoi(argv[2]); 23 int n = kill(pid, signo); 24 if(n != 0) 25 { 26 perror("kill"); 27 } 28 return 0; 29 }
kill命令底层实际上就是kill系统调用,信号的发送由用户发起而OS执行的。
raise——进程给自己发送任意信号
文件mysignal.cc
1 #include<iostream> 2 #include<signal.h> 3 #include<unistd.h> 4 using namespace std; 5 int main(int argc, char* argv[]) 6 { 7 int cnt = 0; 8 while(cnt <= 10) 9 { 10 sleep(1); 11 cout<<cnt++<<endl; 12 if(cnt >= 5) 13 { 14 raise(3);//发送3号信号 15 } 16 } 17 return 0; 18 }
abort——进程给自己指定的信号(6号信号)
文件mysignal.cc
1 #include<iostream> 2 #include<stdlib.h> 3 #include<signal.h> 4 #include<unistd.h> 5 using namespace std; 6 int main(int argc, char* argv[]) 7 { 8 int cnt = 0; 9 while(cnt <= 10) 10 { 11 sleep(1); 12 cout<<"cnt:"<<cnt++<<",pid:"<<getpid()<<endl; 13 if(cnt >= 5) 14 { 15 abort();//发送6号信号 16 } 17 } 18 return 0; 19 }
不同的信号代表不同的事件,但是对事件发送后的处理动作是可以一样的。大多数信号处理的默认动作都是终止进程。
3.硬件异常产生信号
信号的产生,不一定非要用户显示的发送,有些情况下,信号会自动在OS内部产生。
除零,发送8号信号
文件test.cc
1 #include<iostream> 2 using namespace std; 3 #include<unistd.h> 4 int main(int argc, char* argv[]) 5 { 6 while(true) 7 { 8 cout<<"i am running..."<<endl; 9 sleep(1); 10 int a = 10; 11 int b = 0; 12 a /= b; 13 } 14 return 0; 15 }
- 为什么进程除零会终止进程?
因为除零会导致当前进程收到来自OS的SIGFPE信号。 - OS怎么知道应该给当前进程发送8号信号呢?
因为CPU出现异常,除零错误。
CPU中由很多寄存器(eax/edx
等),执行int a = 10;int b = 0;a /= b;时CPU内除了数据保存,还要保证运行有没有问题。因此CPU内有状态寄存器,状态寄存器可以用来衡量本次运算结果,10/0的结果是无穷大,它会引起状态寄存器溢出标记位用0变为1,CPU就发送了运算异常。OS得知CPU发送运算异常,就要识别异常:状态寄存器的标记位置为1,是由当前进程导致的,因此会向当前进程发送信号,最后就终止了进程。
通过signal接口,将SIGFPE
信号自定义捕捉。
文件test.cc
1 #include<iostream> 2 using namespace std; 3 #include<unistd.h> 4 #include<signal.h> 5 void catchSig(int signo) 6 { 7 cout<<"获取到一个信号,信号编号是:"<<signo<<endl; 8 } 9 int main(int argc, char* argv[]) 10 { 11 signal(SIGFPE, catchSig);//捕捉任意信号 12 int a = 10; 13 int b = 0; 14 a /= b; 15 while(true) 16 { 17 cout<<"i am running..."<<endl; 18 sleep(1); 19 } 20 return 0; 21 }
通过上面的例子,我们可以知道:收到信号并不一定会引起进程退出。
如果进程没有退出,则还有被调度的可能。CPU内的寄存器只有一份,寄存器内的内容是独立属于当前进程的上下文,一旦出现异常,我们没有能力去修正这个问题,所以当进程被切换时,会有无数次状态寄存器被保存和恢复的过程,每一次恢复的时候都会让OS识别到CPU内的状态寄存器中的溢出标志位为1,所以每一次都会发送8号信号。