野指针——发送11号信号
文件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识别到报错,转化成信号。
4.软件条件
当软件条件被触发时,OS会发送对应的信号。
管道——13号信号SIGPIPE
我们之前在了解管道时,有讲到一种情况:当读端关闭时,OS会立即终止写端。OS是向写端进程发送13号信号,即当管道的读端关闭软件条件触发是,OS会向进程发送13号信号。
定时器——4号信号SIGALRM
定时器软件条件: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个闹钟。
小结
- 上面说的所有产生信号的方式,本质都是由OS来执行发送信号的工作,因为OS是进程的管理者。
- 信号是在合适的时间进行处理,如果不是被立即处理,那么该信号就需要被记录下来,记录在进程PCB中
- 一个进程在未收到信号之前就知道自己应该如如何对该信号进行处理,这是程序员默认在系统中编写好的。
- OS向进程发送信号的本质是修改目标进程PCB中的信号位图。
四、捕捉信号的方法
1.signal
通过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的对应动作是不会被修改的)
2.sigaction
它的作用域和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 }
- 当我们在传递一个信号的期间,它同类型的信号是无法传递的。
例子中,当前信号正在被捕捉,系统会自动将当前信号加入到进程的信号屏蔽字,即在block表中自动将2号信号屏蔽。 - 当系统完成对当前信号的捕捉,会自动解除对该信号的屏蔽。
一般信号被解除屏蔽,如果该信号已经被pending的话,系统会自动传递当前屏蔽信号,否则就不做任何动作。 - 进程处理信号的原则是:串行的处理同类型信号,不允许递归式处理。
- 特殊的:如果我们在屏蔽了一个信号的同时还想屏蔽另一个信号,则可以像下面的例子:
sigemptyset(&act.sa_mask);当前我们正在处理当前信号的同时,我们还想同时屏蔽另一个信号,例如3号信号,可以用下面的代码 sigaddset(&act.sa_mask, 3);
总结
以上就是今天要讲的内容,本文从现实中的信号引入,介绍了进程信号的部分内容,包括进程信号的基本概念、进程中有什么信号,如何查看进程中的信号、信号是如何产生的、如何捕捉信号(信号的自定义动作)等相关知识。
本文作者目前也是正在学习Linux相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。
最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!