文章目录
- 生活角度的信号
- 技术应用角度的信号
- 系统中的信号
- 信号函数signal
- 调用系统函数向进程发信号
- 由软件条件产生信号
- 由硬件异常产生信号
- 总结
生活角度的信号
在我们的生活中,什么可以被称为信号呢?
那可太多啦,有红绿灯,闹钟,下课铃,倒计时,狼烟,冲锋号,肚子叫,眼色脸色,手势,外卖电话。。。。。。
联系实际,理解信号
大致从三个角度来讲,如图所示:
- 当我们看到红绿灯(信号)的时候,会有匹配的动作,红灯停,绿灯行。但是我们收到这个信号时,为什么会知道怎么做呢?那是因为曾经有人“教育”过我们,信号还没有产生,我们也知道如果收到信号该怎么做,也就是说我们具有“识别”信号的能力。推导得出——>进程就是我,信号是一个数字,进程在没有收到信号的时候,其实它早就知道一个信号该怎么被处理了!也就是说,程序员设计进程的时候,早就已经设计了对信号的识别能力。
- 同时因为信号随时都有可能产生,所以在信号产生前,我可能正在做优先级更高的事情,我可能不能立马去处理这个信号!我们需要在后续合适的时间才能处理这个信号。比如大家可能都有这个经历,当你在房间打王者的时候,正推塔呢,突然外卖员给你打电话,说你的外卖到了,但是你要打完这把游戏再去拿外卖,在这个时间窗口内你脑子里面是记得等会游戏结束要去拿外卖,也就是说你保存了“拿外卖”这个信号。同理,信号产生——>时间窗口——>信号处理,进程收到信号的时候,如果没有立马处理这个信号(处在时间窗口),需要进程具有记录信号的能力。
- 信号的产生对于进程来讲是异步的。(即外卖的到来,对你来讲是异步的,你不能确定外卖小哥具体什么时间点给你打电话)
- 当你时间合适时,顺利拿到你的外卖后,就要开始处理外卖了(处理信号),而处理外卖的一般方式有三种:(1)执行默认动作(开始幸福的吃起外卖);(2)忽略外卖(拿到外卖后,先晾在一遍,再打一局王者);(3)执行自定义动作(你突然想高歌一曲,跳支舞)。同理,进程在收到信号之后,处理信号有三种方式:(1)执行默认动作(处理信号);(2)忽略信号;(3)执行自定义操作。
技术应用角度的信号
系统中的信号
认知了上述形象生动的例子之后,我们知道若进程不及时处理信号,就需要记录保存对应产生的信号,那么到底将信号记录保存在哪里呢?
由于系统内部可能有多个进程,同时也有多个信号,对应的进程处理对应的信号,所以我们依然要采取先描述,再组织的方法,管理信号。那么怎么描述一个信号,用什么数据结构管理这个信号呢?
在linux中,我们可以通过命令kill -l
可以察看系统定义的信号列表,如图所示。
- 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2
- 编号34以下的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明:
man 7 signal
那么其实马上就可以想到之前学过的数据结构——位图。没错,这31个信号就是采用位图结构就记录的,32个比特位,0,1表示信号是否被记录。所以task_struct内部必定要存在一个位图结构,用int表示:uint32_t signals
;假设该进程收到了9号kill信号,那么该位图的第九个比特位就会由0置1,9号信号就被记录了,待该进程处理。
综上,所谓的发送信号,本质其实是向进程PCB写入信号,直接修改特定进程的信号位图中的特定比特位,0->1。task_struct 数据内核结构,只能由OS操作系统进行修改,所以无论后面我们有多少种信号产生的方式,最终都必须OS完成最后的发送过程!
信号函数signal
我们通过理解Ctrl+c,来学习一下signal信号函数
- 功能
设置某一信号的对应动作 - 函数声明
#include <signal.h>
typedef void (*sighandler_t)(int);//函数指针
sighandler_t signal(int signum, sighandler_t handler);
- 参数说明
第一个参数signum:指明了所要处理的信号类型(信号编号),它可以取除了SIGKILL和SIGSTOP外的任何一种信号。
第二个参数handler:描述了与信号关联的动作,它可以取以下三种值:
(1)SIG_IGN
:这个符号表示忽略该信号。
(2)SIG_DFL
:这个符号表示恢复对信号的系统默认处理。不写此处理函数默认也是执行系统默认操作。
(3)sighandler_t类型的函数指针
,当接收到一个类型为sig的信号时,就执行handler 所指定的函数。
Ctrl+c?
- 用户输入命令,在Shell下启动一个前台进程。用户按
Ctrl+c
,这个键盘产生一个硬件中断,被OS获取,解释成信号,发送给目标进程,前台进程因为收到信号,进而引起进程退出。 - 所以
Ctrl+c
的本质是是向前台进程发送对应的信号。那么如何证明这一点呢?我们来看如下代码:
代码:
1 #include<iostream>
2 #include<signal.h>
3 #include<unistd.h>
4 void handler(int signo)//自定义方法
5 {
6 std::cout<<"get a signal:"<< signo <<std::endl;
7 }
8 int main()
9 {
10 signal(2,handler);
11 while(true)
12 {
13 std::cout<<"我是一个进程,我正在运行......,我的pid是:"<<getpid()<<std::endl;
14 sleep(1);
15 }
16 return 0;
17 }
我们在键盘按下 Ctrl+c
会终止进程,实际是因为键盘产生硬件中断(一个硬件对应一个中断)后,OS获取并解析成了2号信号,在OS中2号信号对应的动作就是结束进程。而上述代码,我们用signal
函数改变了2号信号对应的动作,所以2号信号的动作由结束进程——>打印get a signal:2。所以我们再将程序跑起来,当我们在键盘上桥下Ctrl+c,这个进程就不会被终止,而是打印。。。看图吧
综上:
- 2号信号:进程的默认处理动作是终止进程
- signal方法:可以进行对指定的信号设定自定义处理动作
注意:signal(2,handler)调用完这个函数的时候,handler方法并没有被调用,只是更改了2号信号和对应handler方法的映射关系,并没有调动handler函数,例如以下的代码show函数并没有调用Print函数,只是接收了这个函数的参数,但没有进行回调handler函数。当2号信号产生的时候,handler函数才会被调用,执行自定义捕捉。
void Print()
{
printf("hello world\n");
}
void show(int a,func_t f)
{
printf("hello show\n");
}
int main()
{
show(10,Print);
return 0;
}
PS:前台进程与后台进程
./进程名
——运行起来的是前台进程,./进程名 &
——运行起来的是后台进程,只能kill-9
干掉后台进程。Ctrl+c
产生的信号只能发送给前台进程。如果是后台进程,则程序可以放到后台运行,即不能干掉后台进程,这样shell就不必等待进程结束,就可以接受新的命令,启动新的进程。- 前台进程在运行过程中用户随时可能按下
Ctrl+c
而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT
信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
调用系统函数向进程发信号
当我们要使用kill命令向一个进程发送信号时,我们可以用kill -信号名(信号编号) 进程ID的形式进行发送。
kill函数
实际上kill命令是通过调用kill函数实现的,kill函数可以给指定的进程发送指定的信号,kill函数的函数原型如下:
int kill(pid_t pid, int sig);
kill函数用于向进程ID为pid的进程发送sig号信号,如果信号发送成功,则返回0,否则返回-1。
我们可以用kill函数模拟实现一个kill命令,实现逻辑如下:
//测试代码loop.cc
1 #include<iostream>
2 #include<unistd.h>
3
4 int main()
5 {
6 while(true)
7 {
8 std::cout<<"我是一个进程。。。pid:"<<getpid()<<std::endl;
9 sleep(2);
10 }
11 }
1 #include<iostream>
2 #include<cstdlib>
3 #include<string>
4 #include<unistd.h>
5 #include<signal.h>
6 #include<sys/types.h>
7 #include<cstring>
8 #include<cerrno>
9 #include<cassert>
10
11 void Usage(std::string proc )
12 {
13 printf("\tUsage\n\t");
14 std::cout << proc << "信号编号 目标进程" << std::endl;
15 }
16 int main(int argc,char* argv[])
17 {
18 if(argc!=3)
19 {
20 Usage(argv[0]);
21 exit(1);
22 }
23 int signo = atoi(argv[1]);
24 int target_id = atoi(argv[2]);
25 int n = kill(target_id,signo);
26 if(n!=0)
27 {
28 std::cerr << errno <<":"<<strerror(errno) << std::endl;
29 }
30
31 }
//makefile文件
1 .PHONY:all
2 all:mykill loop
3
4 loop:loop.cc
5 g++ -o $@ $^ -std=c++11
6
7 mykill:mykill.cc
8 g++ -o $@ $^ -std=c++11
9 .PHONY:clean
10 clean:
11 rm -f mykill loop
raise函数
raise函数可以给当前进程发送指定信号,即自己给自己发送信号,raise函数的函数原型如下:
int raise(int sig);
raise函数用于给当前进程发送sig号信号,如果信号发送成功,则返回0,否则返回一个非零值。
例如,下列代码当中用raise函数每隔一秒向自己发送一个2号信号。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int signo)
{
printf("get a signal:%d\n", signo);
}
int main()
{
signal(2, handler);
while (1){
sleep(1);
raise(2);
}
return 0;
}
abort函数
raise函数可以给当前进程发送SIGABRT信号,使得当前进程异常终止,abort函数的函数原型如下:
void abort(void);
abort函数是一个无参数无返回值的函数。
例如,下列代码当中每隔一秒向当前进程发送一个SIGABRT信号。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void handler(int signo)
{
printf("get a signal:%d\n", signo);
}
int main()
{
std::cout << "begin " << std::endl;
sleep(1);
abort();//给自己发送指定信号
std::cout << "end " << std::endl;
return 0;
}
运行结果:只打印出来begin,没打印end,因为执行到abort函数时直接终止了进程。
也可以这样写:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void handler(int signo)
{
printf("get a signal:%d\n", signo);
}
int main()
{
signal(SIGABRT,handler);
while(true)
{
std::cout << "begin " << std::endl;
sleep(1);
abort();//给自己发送指定信号
std::cout << "end " << std::endl;
}
return 0;
}
但是上述代码并未产生循环,打印完get a signal之后,就aborted退出了,总之,abort可以被自定义捕捉,但是“我”依然要终止进程。
由软件条件产生信号
alarm函数 和SIGALRM信号
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动
作是终止当前进程。
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
我们先来看一段代码:
上面这段代码是用以验证算力的,算力结果如下:
但是为什么算力这么小呢?原因是因为有IO(需要打印到显示器),网络的约数限制,所以说IO的效率其实非常低。
那我们让算力安心计算,1s到了,就调用自定义函数对其捕捉。
此次运行结果:
alarm函数的自举:自己调用自己
运行结果:
由硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元(硬件)会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
除0错误的本质,就是就是触发CPU硬件异常,如将CPU的某个状态寄存器置1,代表本次计算有溢出问题,然后OS找到发生错误的进程,向该进程的PCB内写入对应的信号,使该进程异常退出。
运行结果:我们的进程确实是收到了:8信号导致崩溃的
对空指针的解引用问题:导致MMU内存管理单元出现硬件异常问题。
总结
信号的产生:
- 键盘
- 系统调用
- 指令
- 软件条件
- 硬件异常
都是借助OS之手向目标进程发送信号,即向目标进程pcb写信号位图。