生活中有各种各样的信号,比如:闹钟、红绿灯、上下课铃声……我们可以知道信号产生时对应的要做些什么,幼儿园的小朋友也明白红灯停、绿灯行的道理。
但是,人是怎么识别出这些信号的呢?人是只有通过认识,才能产生行为:有人通过教育的手段让我们在大脑里记住了红绿灯属性及其对应行为。
但是,当信号产生时,我们并不是总能及时去处理这个信号。信号的发生是随时的(异步),但是我们去处理信号并不都是即时的。因为,我们在信号来临时可能会有其他更重要的事情要做(优先级更高的事情),所以从信号发生到信号被处理中间会有一个时间窗口,当然我们在未处理这个信号时需要将这个信号记录下来,等能处理时再处理。
当我们处理信号时,处理信号的方式也是有所不同的(不同的信号有不同的处理方式,不同的人对对同一个信号的处理方式也可能不同,相同的人对相同的信号在不同的场景下处理信号方式也可能不同)。处理信号的方式大致分为以下三种:
前言中,我们通过生活中的信号引入了进程中的信号,下面我们简单了解以下进程信号的概念。进程本身是被程序员编写的代码,是属性和逻辑的组合,所以进程处理信号的识别和对应的动作都是程序员所赋予的。
task_struct
)中,我们用比特位来表示信号的编号,比特位的内容表示是否收到对应信号(0表示没收到,1表示收到了)。
查看系统定义的信号列表;
每一个信号都有与之对应的编号和宏定义名称,这些宏定义可以在signal.h中找到。
查看信号详细信息。
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 }
ctrl + c:热键,它实际上是个组合键,OS会将它解释为2信号。
进程对3信号的默认行为是终止进程,所以当我们按ctrl + c时当前运行的进程将被直接终止。
用ctrl + c:
用kill -2
ctrl + z:热键,实际上是20号信号(即,按ctrl + \和kill -20 (进程pid)是一样的)。
ctrl + \:热键,实际上是3信号。
用键盘向前台进程发送信号,前台进程会影响shell,Linux规定跟shell交互时只允许有一个前台进程,实际上当我们运行自己的进程时,我们的进程就变成了前台进程,而sbash会被自动切到后台(默认情况下bash也是一个进程)。
当然,除了用键盘向前台进程发送信号外,我们可以用系统调用向进程发送信号。
发送信号的能力是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执行的。
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 }
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 }
不同的信号代表不同的事件,但是对事件发送后的处理动作是可以一样的。大多数信号处理的默认动作都是终止进程。
信号的产生,不一定非要用户显示的发送,有些情况下,信号会自动在OS内部产生。
文件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 }
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号信号。
文件test1.cc
1 #include<iostream>
2 using namespace std;
3 #include<unistd.h>
4 #include<signal.h>
5 int main(int argc, char* argv[])
6 {
7 while(true)
8 {
9 cout<<"i am running..."<<endl;
10 sleep(1);
11 int* p;
12 *p = 10;
13 }
14 return 0;
15 }
野指针的使用,导致程序崩溃,进程收到了来自OS的11号信号。
文件test1.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(11, catchSig);//捕捉任意信号
12 while(true)
13 {
14 cout<<"i am running..."<<endl;
15 sleep(1);
16 int* p;
17 *p = 10;
18 }
19 return 0;
20 }
OS会给当前进程发送11号信号,11号信号代表非法的内存引用。
OS怎么知道野指针?
访问野指针会导致虚拟地址到物理内存之间转化时对应的MMU报错,进而OS识别到报错,转化成信号。
当软件条件被触发时,OS会发送对应的信号。
我们之前在了解管道时,有讲到一种情况:当读端关闭时,OS会立即终止写端。OS是向写端进程发送13号信号,即当管道的读端关闭软件条件触发是,OS会向进程发送13号信号。
定时器软件条件:alarm():设定闹钟。
调用alarm函数可以设定一个闹钟,即告诉内核在seconds秒后给当前进程发送SIGALRM信号,该信号的默认处理动作时终止当前进程。
该函数的返回值是:0或者之前设定的闹钟事件剩余的秒数。
例如,早上设定的6:30的闹钟,但是在6:20被人吵醒了,想着再睡一会睡到6:40,于是将闹钟设定为20分钟后再响。
于是,重新设定的闹钟为20分钟后响,以前设定的闹钟还剩余的时间为10分钟。如果seconds值为0,表示取消之前设定的闹钟,函数返回值仍然是之前设定的闹钟的剩余秒数。
这份代码的意义是统计1s左右,我们的计算机可以将数据累积多少次。但,实际上这种方式效率较低,因为打印在屏幕上是需要访问外设的,而外设的运行速度较慢。
如果不进行打印:
文件test.cc
1 #include<iostream>
2 using namespace std;
3 #include<unistd.h>
4 #include<signal.h>
5 int cnt = 0;
6 void catchSig(int signo)
7 {
8 cout<<"获取到一个信号,他的编号是:"<<signo<<",在1秒内计算机累加了:"<<cnt<<"次"<<endl;
9 }
10 int main()
11 {
12 signal(SIGALRM, catchSig);
13 alarm(1);//软件条件
14 while(1)
15 {
16 cnt++;
17 }
18 return 0;
19 }
理解闹钟是软件条件
“闹钟”其实就是用软件实现的,任意一个进程都可以通过alarm系统调用在内核中设置闹钟。OS内可能会存在很多的“闹钟”,因此需要对“闹钟”进行管理:先描述,再组织。因此,在OS内部设置闹钟时,需要为闹钟创建特定的数据结构对象。
OS会周期性检查这些闹钟。
curr_timestamp > alarm.when;//超时了,OS会发送SIGALRM ->alarm.p
struct alarm{
uint64_t when; //未来到的超时时间
int type; //闹钟类型(一次性、周期性)
task_struct* p;
stryct alarm* next;
//…
};
内核管理闹钟的数据结构是堆:大堆或者小堆。例如,100个闹钟,可以根据100个闹钟的when建小堆,最小的在堆顶。只要堆顶的没有超时,其他的闹钟自然也没有超时,所以只需要检查堆顶即可管理好这100个闹钟。
通过signum方法设置回调函数,来设置某一个信号的对应动作
练习:
文件test.cc
1 #include<iostream>
2 using namespace std;
3 #include<sys/types.h>
4 #include<unistd.h>
5 #include<signal.h>
6 void catchSig(int signo)
7 {
8 cout<<"获取到一个信号,信号编号是:"<<signo<<endl;
9 }
10 int main(int argc, char* argv[])
11 {
12 signal(2, catchSig);//捕捉任意信号
13 while(true)
14 {
15 cout<<"i am running...,my pid = "<<getpid()<<endl;
16 sleep(1);
17 }
18 return 0;
19 }
可以看到此时向进程发送2号信号或者按ctrl + c都能捕捉到信号(之所以在发送信号时没有终止进程,是因为我们将默认动作改为自定义动作,如果想让进程也终止,可以加上exit(0);或者直接kill -9 ,注意killl -9的对应动作是不会被修改的)
它的作用域和signal一样,对特定的信号设置特定的回调方法。
一个正在运行的进程,势必会收到大量同类型的信号。那么问题来了,如果收到同类型的信号,但当前进程正在处理相同的信号,此时会出现什么情况?OS是否会运行频繁进行重复的信号提交?
文件test.cc
1 #include<iostream>
2 using namespace std;
3 #include<stdio.h>
4 #include<sys/types.h>
5 #include<unistd.h>
6 #include<signal.h>
7 void Count(int cnt)
8 {
9 while(cnt)
10 {
11 printf("cnt:%2d\r", cnt);
12 fflush(stdout);
13 cnt--;
14 sleep(1);
15 }
16 printf("\n");
17 }
18 void handler(int signo)
19 {
20 cout<<"get a signo:"<<signo<<"正在处理中…"<<endl;
21 Count(20);
22 }
23 int main(int argc, char* argv[])
24 {
25 cout<<"i am "<<getpid()<<endl;
26 struct sigaction act, aact;
27 act.sa_handler = handler;
28 act.sa_flags = 0;
29 sigemptyset(&act.sa_mask);
30 sigaction(SIGINT, &act, &aact);
31 while(true) sleep(1);
32 return 0;
33 }
sigemptyset(&act.sa_mask);当前我们正在处理当前信号的同时,我们还想同时屏蔽另一个信号,例如3号信号,可以用下面的代码
sigaddset(&act.sa_mask, 3);
以上就是今天要讲的内容,本文从现实中的信号引入,介绍了进程信号的部分内容,包括进程信号的基本概念、进程中有什么信号,如何查看进程中的信号、信号是如何产生的、如何捕捉信号(信号的自定义动作)等相关知识。
本文作者目前也是正在学习Linux相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。
最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!