- 你怎么能识别信号呢?识别信号是内置的,进程识别信号,是内核程序员写的内置特性。
- 信号产生之后,你知道怎么处理吗?知道。如果信号没有产生,你知道怎么处理信号吗?知道。所以,信号的处理方法,在信号产生之前,已经准备好了。
- 处理信号,立即处理吗?我可能正在做优先级更高的事情,不会立即处理?什么时候?合适的时候。
- 信号到来 | 信号保存 | 信号处理
- 怎么进行信号处理啊?a.默认 b.忽略 c.自定义, 后续都叫做信号捕捉。
cpp
// 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)
signal
cpp
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号信号时不执行默认的函数,转而使用自定义函数,预期结果:打印信号而不是杀死进程
cpp
#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.默认处理的动做
cpp
#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.忽略处理
cpp
#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文件,用于事后调试(后面详谈)
给所有比特位都注册信号
cpp
#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.
cpp
#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 函数可以给当前进程发送指定的信号(自己给自己发信号)。
cpp
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.
cpp
#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 函数使当前进程接收到信号而异常终止。
cpp
NAME
abort - cause abnormal process termination
SYNOPSIS
#include <stdlib.h>
void abort(void);
RETURN VALUE
The abort() function never returns.
// 就像exit函数⼀样,abort函数总是会成功的,所以没有返回值。
cpp
#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)信号给自己,要求进程必须处理,目的就是终止进程
由软件条件产生信号
SIGPIPE 是一种由软件条件产生的信号,在"管道"中已经介绍过了。本节主要介绍 alarm 函数
和 SIGALRM 信号。
cpp
NAME
alarm - set an alarm clock for delivery of a signal
SYNOPSIS
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
RETURN VALUE
alarm() returns the number of seconds remaining until any previously
scheduled alarm was due to be delivered, or zero if there was no previ‐
ously scheduled alarm.
- 调用 alarm 函数可以设定一个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发
SIGALRM 信号,该信号的默认处理动作是终止当前进程。 - 这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,"以前设定的闹钟时间还余下的时间"就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
基本alarm验证-体会IO效率问题
程序的作用是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止。必要的时候,对SIGALRM信号进行捕捉
1.IO多
cpp
// IO 多
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{
int count = 0;
alarm(1);
while(true)
{
std::cout << "count : "
<< count << std::endl;
count++;
}
return 0;
}

2.IO少
cpp
// IO 少
#include <iostream>
#include <unistd.h>
#include <signal.h>
int count = 0;
void handler(int signumber)
{
std::cout << "count : " <<
count << std::endl;
exit(0);
}
int main()
{
signal(SIGALRM, handler);
alarm(1);
while (true)
{
count++;
}
return 0;
}

明显下面的count计数更快,上面是每加一次IO一次,下面是加完在IO,IO频繁得读写文件更慢
总结:
- 闹钟会响一次,默认终止进程
- 有IO效率低
一次性闹钟
不断打点,1s后收到SIGALRM信号后打出该信号然后停留一秒,然后无限制打点
cpp
// 一次性闹钟
#include <iostream>
#include <unistd.h>
#include <signal.h>
int count = 0;
void handler(int signumber)
{
std::cout << "获得了一个信号:" << SIGALRM << std::endl;
sleep(1);
}
int main()
{
signal(SIGALRM, handler);
alarm(1);
while (true)
{
std::cout << "." << std::endl;
}
return 0;
}

重复闹钟
在自定义函数内继续发闹钟
cpp
// 一次性闹钟
#include <iostream>
#include <unistd.h>
#include <signal.h>
int count = 0;
void handler(int signumber)
{
std::cout << "获得了一个信号:" << signumber << "pid:" << getpid() << std::endl;
alarm(1);
}
int main()
{
signal(SIGALRM, handler);
alarm(1);
while (true)
{
std::cout << "." << "pid:" << getpid() << std::endl;
sleep(1);
}
return 0;
}

pause
等待一个信号出错时返回-1,没有信号时暂停,信号捕捉时返回时才会返回
cpp
NAME
pause - wait for signal
SYNOPSIS
#include <unistd.h>
int pause(void);
DESCRIPTION
pause() causes the calling process (or thread) to sleep until a signal
is delivered that either terminates the process or causes the invoca‐
tion of a signal-catching function.
RETURN VALUE
pause() returns only when a signal was caught and the signal-catching
function returned. In this case, pause() returns -1, and errno is set
to EINTR.
进程处于暂停状态,一收到信号,转而实现各种功能,每隔1s收到一个信号,周期性打印
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include<functional>
#include<vector>
using func_t=std::function<void()>;
std::vector<func_t>funcs;
///
void sche()
{
std::cout<<"我是进程调度"<<std::endl;
}
void memmanger()
{
std::cout<<"我是周期性内存管理"<<std::endl;
}
void Fflush()
{
std::cout<<"我是刷新程序"<<std::endl;
}
///
void handler(int signumber)
{
for(auto f:funcs)
{
f();
}
int n= alarm(1);
}
int main()
{ funcs.push_back(sche);
funcs.push_back(memmanger);
funcs.push_back(Fflush);
signal(SIGALRM, handler);
alarm(1);
while (true)
{
pause();
}
return 0;
}
进程处于暂停状态,由外部驱动发送信号从而实现特定功能,这也是操作系统
4.异常
1.除零错误
程序跑起来变成进程了,进程因为语法错误或使用错误导致崩掉了从而发送异常信号
cpp
#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就能找到对于的进程从而发送信号

2.野指针访问
cpp
#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号信号,终止进程
信号全部都是由操作系统发送的,程序犯错了,操着系统识别到了,根据犯错类型发送信号回该进程执行对于的函数
信号产生之后并不是立即处理,所以要求进程必须把信号记入下来