- 你怎么能识别信号呢?识别信号是内置的,进程识别信号,是内核程序员写的内置特性。
- 信号产生之后,你知道怎么处理吗?知道。如果信号没有产生,你知道怎么处理信号吗?知道。所以,信号的处理方法,在信号产生之前,已经准备好了。
- 处理信号,立即处理吗?我可能正在做优先级更高的事情,不会立即处理?什么时候?合适的时候。
- 信号到来 | 信号保存 | 信号处理
- 怎么进行信号处理啊?a.默认 b.忽略 c.自定义, 后续都叫做信号捕捉。
// sig.cc
#include <iostream>
#include <unistd.h>
int main()
{
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
编写了一个死程序,按理说会无限循环下去,但是为什么用户ctrl + c会终止运行呢?
- 用户输入命令,在Shell下启动一个前台进程
- 用户按下 Ctrl+C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程
- 前台进程因为收到信号,进而引起进程退出
信号的本质就是宏,而1~31个信号分别用比特位的01代表示
而其实, Ctrl+C 的本质是向前台进程发送 SIGINT 即 2 号信号,我们证明⼀下,这里需要引入一
个系统调用函数(signal)
NAME
signal - ANSI C signal handling
SYNOPSIS
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数说明:
signum:信号编号[后⾯解释,只需要知道是数字即可]
handler:函数指针,表⽰更改信号的处理动作,当收到对应的信号,就回调执⾏handler⽅法
开始测试
编写一个死循环,同时接收到2号信号时不执行默认的函数,转而使用自定义函数,预期结果:打印信号而不是杀死进程
#include<iostream>
#include<unistd.h>
#include<signal.h>
void handlerSig(int sig)
{
std::cout<<getpid()<<"获得了一个信号:"<<sig<<std::endl;
}
int main()
{ //进程收到2号信号时转而调用自定义函数handlerSig并将sig传递给自定义函数
signal(SIGINT,handlerSig);
int cnt=0;
while(true)
{
std::cout<<getpid()<<": "<<"hello word"<<cnt++<<std::endl;
sleep(1);
}
}
此时ctrl + C杀不掉只能使用kill -9
这里进程为什么不退出?signal调用将信号编号交给默认的函数处理,而2号对于默认的函数是杀掉进程,现在转而执行自定义函数
这个例子能说明哪些问题?信号处理,不是自己处理,而是调用别的函数
注意
- 要注意的是,signal函数仅仅是设置了特定信号的捕捉作为处理方式,并不是直接调用处理动作。如果后续特定信号没有产生,设置的捕捉函数永远也不会被调用!!
- Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
- Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C这种控制键产生的信号。
- 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
- 关于进程间关系,我们在⽹络部分会专门来讲,现在就了解即可。
- 可以渗透 & 和 nohup
信号处理
处理信号有三种方式:
1.默认处理的动做
#include<iostream>
#include<unistd.h>
#include<signal.h>
void handlerSig(int sig)
{
std::cout<<getpid()<<"获得了一个信号:"<<sig<<std::endl;
}
int main()
{ //进程收到2号信号时转而调用自定义函数handlerSig并将sig传递给自定义函数
signal(SIGINT/*2*/, SIG_DFL); // 设置忽略信号的宏
int cnt=0;
while(true)
{
std::cout<<getpid()<<": "<<"hello word"<<cnt++<<std::endl;
sleep(1);
}
}
2.自定义处理动作
如之前代码
提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为自
定义捕捉(Catch)一个信号。
3.忽略处理
#include<iostream>
#include<unistd.h>
#include<signal.h>
void handlerSig(int sig)
{
std::cout<<getpid()<<"获得了一个信号:"<<sig<<std::endl;
}
int main()
{ //进程收到2号信号时转而调用自定义函数handlerSig并将sig传递给自定义函数
signal(SIGINT/*2*/, SIG_IGN); // 设置忽略信号的宏
int cnt=0;
while(true)
{
std::cout<<getpid()<<": "<<"hello word"<<cnt++<<std::endl;
sleep(1);
}
}
上面的所有内容,我们都没有做非常多的解释,主要是先用起来,然后渗透部分概念和共识,下面我们从理论和实操两个层面,来进⾏对信号的详细学习、论证和理解。为了保证条理,我们采用如下思路来进行阐述:
通过终端按键产生信号
- Ctrl+C (SIGINT) 已经验证过,这里不再重复
- Ctrl+\(SIGQUIT)可以发送终止信号并生成core dump文件,用于事后调试(后面详谈)
给所有比特位都注册信号
#include<iostream>
#include<unistd.h>
#include<signal.h>
void handlerSig(int sig)
{
std::cout<<getpid()<<"获得了一个信号:"<<sig<<std::endl;
}
int main()
{
for(int i=1;i<=31;i++)
signal(i, handlerSig); //给1~31号信号都注册自定义函数
int cnt=0;
while(true)
{
std::cout<<getpid()<<": "<<"hello word"<<cnt++<<std::endl;
sleep(1);
}
}
产生信号的方式:
1.调用系统命令kill向进程发信号
2.通过键盘
3.调用系统调用函数
kill
kill 命令是调用 kill 函数实现的。 kill 函数可以给一个指定的进程发送指定的信号。
NAME
kill - send signal to a process
SYNOPSIS
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
RETURN VALUE
On success (at least one signal was sent), zero is returned. On error,
-1 is returned, and errno is set appropriately.
#include<iostream>
#include<sys/types.h>
#include<signal.h>
// ./mykill signumber pid
int main( int argc ,char* argv[])
{
if(argc !=3)
{
std::cout<<"./mykill signumber pid"<<std::endl;
return 1;
}
int signum =std::stoi(argv[1]);
pid_t target =std::stoi(argv[2]);
int n=kill(target,signum);
if(n==0)
{
std::cout<<"发送"<<signum<<"到"<<target<<"成功"<<std::endl;
}
return 0;
}
在kill这个系统调用中,可以发送进程信号给指定进程
raise
raise 函数可以给当前进程发送指定的信号(自己给自己发信号)。
NAME
raise - send a signal to the caller
SYNOPSIS
#include <signal.h>
int raise(int sig);
RETURN VALUE
raise() returns 0 on success, and nonzero for failure.
#include<iostream>
#include<unistd.h>
#include<signal.h>
void handlerSig(int sig)
{
std::cout<<getpid()<<"获得了一个信号:"<<sig<<std::endl;
}
int main()
{
for(int i=1;i<=31;i++)
signal(i, handlerSig); //给1~31号信号都注册自定义函数
for(int i=1;i<=31;i++)
{
sleep(1);
raise(i);
}
}
到9就停止是因为9号信号是强制执行的,系统调用对他无效,即便是调用自定义函数也仍然会杀死进程,比2号进程还猛
abort
abort 函数使当前进程接收到信号而异常终止。
NAME
abort - cause abnormal process termination
SYNOPSIS
#include <stdlib.h>
void abort(void);
RETURN VALUE
The abort() function never returns.
// 就像exit函数⼀样,abort函数总是会成功的,所以没有返回值。
#include<iostream>
#include<unistd.h>
#include<signal.h>
void handlerSig(int sig)
{
std::cout<<getpid()<<"获得了一个信号:"<<sig<<std::endl;
}
int main()
{
for(int i=1;i<=31;i++)
signal(i, handlerSig); //给1~31号信号都注册自定义函数
int cnt=0;
while(true)
{
std::cout<<getpid()<<": "<<"hello word"<<cnt++<<std::endl;
abort();
sleep(1);
}
}
发送固定的6号(SIGABRT)信号给自己,要求进程必须处理,目的就是终止进程
4.异常
程序跑起来变成进程了,进程因为语法错误或使用错误导致崩掉了从而发送异常信号
#include<iostream>
#include<unistd.h>
#include<signal.h>
void handlerSig(int sig)
{
std::cout<<getpid()<<"获得了一个信号:"<<sig<<std::endl;
}
int main()
{
for(int i=1;i<=31;i++)
signal(i, handlerSig); //给1~31号信号都注册自定义函数
int cnt=1;
//除零错误
cnt/=0;
}
kill -l查表,8号是SIGFPE浮点数错误
cpu里有一个状态寄存器,在跑程序执行运算时出错了会修改标志寄存器,操作系统是软硬件资源的管理者,通过全局指针current就能找到对于的进程从而发送信号
#include<iostream>
#include<unistd.h>
#include<signal.h>
void handlerSig(int sig)
{
std::cout<<getpid()<<"获得了一个信号:"<<sig<<std::endl;
}
int main()
{
for(int i=1;i<=31;i++)
signal(i, handlerSig); //给1~31号信号都注册自定义函数
int *p =nullptr;
*p=1;//野指针
}
对野指针的访问,11号信号是SIGSEGV段错误
访问零号地址,操作系统拿到的是虚拟地址,cr3寄存器可通过页表的地址找到页表,MMU将cr3的页表地址和要访问的0号地址做虚拟的页表转化,但是页表下在零号地址没有对应的映射关系,转化失败,硬件报错,cpu的寄存器无法把100写到0号地址处,发送11号信号,终止进程
信号全部都是由操作系统发送的,程序犯错了,操着系统识别到了,根据犯错类型发送信号回该进程执行对于的函数
信号产生之后并不是立即处理,所以要求进程必须把信号记入下来