信号
信号,什么是信号?
在现实生活中,闹钟,红绿灯,电话铃声等等;这些都是现实生活中的信号,当闹钟想起时,我就要起床;当电话铃声想起时,我就知道有人给我打电话,就要接听电话;
现实生活中的这些信号,我们接收到之后就要停止当前正在做的事;所以可以说:**号是发送给进程的,而信号是一种事件通知的异步通知机制
在计算机操作系统中,信号是发送给进程的,而信号是一种事件通知的异步通知机制
简单来说就是,进程在没有收到信号时,在执行自己的代码;信号的产生和进程的运行,是异步的
同步异步
这里简单了解以下同步异步:
同步: 任务按照顺序执行,前面任务没有完成,后面任务就要阻塞等待;
异步: 多个任务可以同时执行,也就是说事件可以同时发生。
相关概念
在深入探究信号之前,先来了解信号相关的概念:
- 在没有产生信号时,进程就已经知道如何处理信号了
就像在现实生活中一样,在闹钟没有响之前,我们就知道闹钟响了就要起床了。
- 信号处理,可以立即处理,也可以过一段时间再处理(在合适的时候处理)
- 进程当中早已内置了对于信号的识别和处理
我们知道操作系统也是程序员写的,在设计写操作系统时,进程当中已内置了如何接受信号和处理信号。
- 信号源非常多
信号是发送给进程的,那信号是谁产生发送给进程的呢?
信号的产生源非常多,就比如Ctrl + C
,Ctrl + \
,kill
指令都是给进程发送信号。
信号分类
简单了解了信号是什么,那在Linux
系统中都存在哪些信号呢?如何查看这些信号呢?
kill -l
命令用来查看所有的信号:

可以看到一共有62
个信号,对于这62
个信号可以粗略的分为两部分:
1 - 31
号信号:这部分信号可以不被立即处理(非实时信号)34 - 64
号信号:这部分信号必须被立即处理(实时信号)
信号处理
信号从产生到处理,可以分为信号产生、信号保存、信号处理三个阶段;
进程对于信号的处理方式有三种:
- 默认处理:
SIG_DFL
,进程处理信号的默认处理方式就是终止进程。- 自定义处理:我们可以修改进程对于信号的处理方式。
- 忽略处理:
SIG_IGN
信号产生
了解了信号是发送给进程的,那信号是如何产生的呢?
1. 通过终端按键(键盘)产生信号

在之前,我们通过Ctrl + C
可以终止进程,为什么呢?
这就是因为Ctrl + C
本质上就是向目标进程发送信号,而进程对于相当一部分信号的处理方式都是终止进程。
Ctrl + C
是向进程发送几号信号呢?
这里Ctrl + C
是向进程发送2
号信号。
系统调用signal

signal
用来替换进程某种信号的默认处理方式;
存在两个参数:signum
表示要替换信号的数字标号handler
是函数指针类型,表示要替换的函数

cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{
std::cout << "收到信号 " << sig << std::endl;
}
int main()
{
signal(2, handler);
int cnt = 0;
while (true)
{
printf("cnt : %d, pid : %d\n", cnt++, getpid());
sleep(1);
}
return 0;
}

可以看到,进程在收到2
号信号之后,没有执行默认处理方式,而是执行handler
函数。
按Ctrl + C
就是给进程发送2
号信号。
这里按Ctrl + C
是给进程发送2
号信号,除此之外Ctrl + \
是发送3号信号、Ctrl + Z
是发送20号信号。
这里就将进程对于1 - 31
号信号的处理方式都替换成自定义处理:
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{
std::cout << "receive signal : " << sig << std::endl;
}
int main()
{
for (int i = 1; i < 32; i++)
signal(i, handler);
int cnt = 0;
while (true)
{
printf("cnt : %d, pid : %d\n", cnt++, getpid());
sleep(1);
}
return 0;
}

可以看到Ctrl + c
、Ctrl + \
能够让进程退出就是给目标进程发送对应的信号,而进程对于信号的处理发送就是终止进程。
这里就存在一些疑问:
- 进程将对于所有的信号的处理方式都替换成自定义处理,那信号就不能杀死进程了?
Ctrl + c
、Ctrl + \
是给目标进程发送信号,那目标进程是什么呢?
首先,在信号在存在一些信号,是不能替换进程对于该信号的处理方式的,例如9
号信号(上面的进程我们依然可以发送9
号信号杀掉该进程)。
目标进程
Ctrl + c
这种通过键盘来给目标进程发送信号,那什么是目标进程呢?简单来说就是前台进程。
前台/后台进程

如上图所示,直接启动程序,默认是在前台运行的,这时我们输入指令,没有任何反应;
在程序退出后,命令才得以执行。

而在启动程序时,让程序在后台进程;也就是进程在后台运行,此时输入命令行指令,指令可以被执行。
前台进程只有一个,后台进程可以存在多个
这是因为,键盘输入只有一个,也就是同时只能存在一个进程读取键盘输入的数据;也就是前台进程。
而多个进程能够同时向一个显示器文件中写入,也就是输出到屏幕中。
相关操作
关于前台进程和后台进程,我们可以进程查看后台进程、将后台进程变成前台进程、暂停前提正在运行的进程(让它变成后台)、以及让后台进程运行起来等一系列操作。
jobs
查看后台进程

使用jobs
命令可以查看当前所有的后台进程,可以查看到所有后台进程的任务号、状态等等信息。
fg
将后台进程变成前台进程
我们可以让进程在后台运行,也可以查看后台进程;当然也可以将一个后台进程变成前台进程。

Ctrl + Z
暂停前台进程
我们知道,Ctrl + Z
可以暂停目标进程,而Ctrl + Z
也是给目标进程发送信号;本质上来说Ctrl + Z
就是给前台进程发送20
号信号
前台进程被暂停之后,就会变成后台进程
简单来说就是,前台进程要获取我们用户的输入信息,前台进程无法被暂停。
我们通过
Ctrl + Z
暂停一个前台进程之后,该进程就会变成后台进程了。
这里就不修改程序对于信号的处理方式了。
cpp
#include <iostream>
#include <unistd.h>
int main()
{
while (true)
{
std::cout << "pid : " << getpid() << std::endl;
sleep(2);
}
return 0;
}

bg
让后台进程运行起来
前台进程被暂停就会变成后台进程,那处于暂停状态的后台进程呢?
我们可以通过bg
命令来让一个暂停状态的后台进程运行起来。

OS如何管理硬件资源
先来看一下代码:
cpp
int main()
{
int x = 0;
std::cout << "in begin" << std::endl;
std::cin >> x;
std::cout << "in sucess" << std::endl;
return 0;
}
我们知道,在输入cin/printf
时,程序就会等待我们输入数据之后,才会接着运行;也就是说进程会等待键盘输入数据,进程就从运行态到阻塞态(内核数据结构从CPU
运行队列到键盘等待队列)。
等待我们输入数据时,进程才会继续运行;
那进程是如何知道键盘上输入数据了呢?
我们知道OS
管理软硬件资源,所以操作系统肯定是知道键盘上是否存在数据的,那问题是:OS
是如何知道键盘存储数据了呢?
这里并不是OS
定期排查,来看键盘是否有数据的;
简单来说,就是当键盘当中存在数据时,键盘就会向
CPU
发送硬件中断;在CPU
当中存在对应的针脚,CPU
通过识别高低锻电压来区别是否存在硬件中断;当存在硬件中断时,就CPU
就会执行操作系统处理数据的代码;而OS
就会停止当前工作,将数据读入内存。
2. 通过系统调用发送信号
信号可以由终端按键,例如Ctrl + C
目标进程发送信号;当然我们也可以通过系统调用来发送信号。
常用的系统调用有kill
、raise
、abort
等
kill

kill
系统调用可以给任意进程发送信号;
参数
pid
:指要发送信号给进程,进程的pid
sig
:指要发送几号信号,信号的标号。
了解了kill
系统调用可以给任意进程发送信号,那就可以使用kill
来实现一个自己的kill
命令:mykill
cpp
//mykill.cc
#include <iostream>
#include <string.h>
#include <signal.h>
int main(int argc, char* argv[])
{
if(argc !=3)
{
return -1;
}
int id = std::stoi(argv[2]);
char* str = argv[1]+1;
int sig = std::stoi(str);
kill(id,sig);
return 0;
}
cpp
//test.cc
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{
std::cout << "receive signal : " << sig << std::endl;
}
int main()
{
for (int i = 1; i < 32; i++)
signal(i, handler);
std::cout << "pid : " << getpid() << std::endl;
while (true)
{
sleep(1);
}
return 0;
}

raise

kill
系统调用可以给任意进程发送任意信号而
raise
是库函数,它可以给进程自己发送任意信号。
简单来说就是进程调用raise
,可以给自己发送任意信号。
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{
std::cout << "receive signal : " << sig << std::endl;
}
int main()
{
for (int i = 1; i < 32; i++)
signal(i, handler);
for (int i = 1; i < 32; i++)
{
if (i == 9 || i == 19)
continue;
std::cout << "send signal " << i << std::endl;
raise(i);
}
return 0;
}
这里9
号信号和19
号信号无法进程自定义捕捉,就不发送9和19 号信号。

abort

kill
可以给任意进程发送任意信号、raise
可以给进程自己发送任意信号;而
abort
用来给进程自己发送特定的信号(6
号信号),来终止进程。
abort
的作用就是终止进程。
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{
std::cout << "receive signal : " << sig << std::endl;
}
int main()
{
for(int i = 1; i<32;i++)
signal(i,handler);
std::cout << "pid : " << getpid() << std::endl;
abort();
while(1)
sleep(1);
return 0;
}

可以看到,abort
是给进程自己发送6
号信号;6
号信号是SIGABRT
。
但是这里是修改了进程对于1-32
的处理方式的(9
和19
无法修改),并且进程在收到abort
发送的6
号信号之后,是执行了自定义处理发送handler
的,那为什么进程还是退出了?
abort
函数的作用就是终止进程 ,这里就是修改了进程对于6
号进程的处理发送,但是abort
还是会终止进程。
3. 硬件异常
我们知道,当程序中存在/0
、野指针(越界访问)时,进程就会直接退出;那进程是如何退出的呢?
答案就是信号 ,当程序出现错误时,OS
统就会给当前进程发送信号从而杀掉进程。
操作系统是如何知道程序出错了呢?
当程序出错时,操作系统会通过信号杀掉进程,那操作系统是如何知道程序出错了呢?
例如
/0
,CPU
在执行/0
操作,寄存器就会发生浮点数溢出,就会触发硬件中断,从而执行OS
相关的方法。野指针同理,当进行野指针访问时,
CPU
在执行时发出错就会触发硬件中断,然后执行OS
相关方法。
除0
cpp
void handler(int sig)
{
std::cout<<"recive signal : "<< sig << std::endl;
exit(1);
}
int main()
{
for(int i = 1; i<32;i++)
signal(i, handler);
//除0
int x = 3;
x/=0;
while(true)
{
}
return 0;
}

野指针
cpp
void handler(int sig)
{
std::cout<<"recive signal : "<< sig << std::endl;
exit(1);
}
int main()
{
for(int i = 1; i<32;i++)
signal(i, handler);
//野指针
int* p = nullptr;
*p = 1;//访问nullptr
while(true)
{
}
return 0;
}

子进程退出core dump
还记得当子进程退出时,存在一个退出码;退出码的低7
位指子进程被哪个信号杀死,而第8
位在标识进程是否被信号杀死。
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void handler(int sig)
{
std::cout << "recive signal : " << sig << std::endl;
}
int main()
{
int id = fork();
if (id < 0)
exit(1);
else if (id == 0)
{
sleep(1);
int x = 10;
x /= 0;
}
for (int i = 1; i < 32; i++)
signal(i, handler);
int status = 0;
waitpid(-1, &status, 0);
printf("status : %d, exit signal: %d, core dump: %d\n", status, status & 0x7F, (status >> 7) & 1);
return 0;
}
在上述代码中,父进程创建子进程,子进程进行/0
操作,子进程就会被信号杀掉;
父进程修改对8(SIGFPE)
信号的处理方式,然后获取子进程的退出信息。
在子进程的退出信息中,低7位存储子进程被几号信号杀掉,第8
位标识子进程是否被信号杀掉。
4. 软件条件
软件异常产生中断,顾名思义进程软件条件不满足从而产生信号;
例如:进程间通过管道文件进行通信,读端退出,OS
系统就会杀掉写端;(通过发送信号让写端退出)。
这里简单测试一下:
cpp
//process1.cc
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define PATHNAME "./fifo"
int main()
{
// 创建管道文件
mkfifo(PATHNAME, 0666);
// 打开
int rfd = open(PATHNAME, O_RDONLY);
// 读取
char buff[1024];
int cnt = 3;
while (cnt--)
{
int x = read(rfd, buff, sizeof(buff));
buff[x] = 0;
std::cout << "read : " << buff << std::endl;
}
// 关闭
close(rfd);
return 0;
}
//process2.cc
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <signal.h>
#define PATHNAME "./fifo"
void handler(int sig)
{
std::cout << "receive signal : " << sig << std::endl;
exit(1);
}
int main()
{
for(int i =1 ;i<32;i++)
signal(i,handler);
// 打开管道文件
int wfd = open(PATHNAME, O_WRONLY);
// 写入
const char *msg = "abcd";
while (true)
{
write(wfd, msg, strlen(msg));
sleep(1);
std::cout << "write : " << msg << std::endl;
}
return 0;
}

alarm
先来看一下alarm
函数

alarm
只有一个参数seconds
,alarm
的作用就是给当前进程设置闹钟;
简单来说就是,在seconds
秒后给进程发送信号。
对于alarm
的返回值,可能为0
,也可能是上次设置闹钟的剩余时间。

cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{
std::cout << "recive signal : " << sig << std::endl;
}
int main()
{
for(int i = 1;i<32;i++)
signal(i,handler);
alarm(3);
sleep(5);
return 0;
}

可以看到alarm
设置闹钟,就是在seconds
秒过后给进程发送14
号信号。
所以,我们就可以通过给进程设定闹钟,让进程周期性的完成一些任务;
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <functional>
#include <vector>
void sch()
{
std::cout << "process scheduling" << std::endl;
}
void check()
{
std::cout << "memory check" << std::endl;
}
std::vector<std::function<void()>> task_list;
void handler(int sig)
{
for (auto &e : task_list)
{
e();
}
alarm(1);
}
int main()
{
task_list.push_back(sch);
task_list.push_back(check);
signal(14, handler);
alarm(1);
while (true)
{
pause();
}
return 0;
}

理解系统闹钟
系统闹钟,本质上就是操作系统给对应进程发送信号,所以操作系统本身就要具有定时的功能;(例如:时间戳)
而在OS
中,定时器也可能存在很多,如此多的定时器也要被管理起来;Linux
内核数据结构如下:
cpp
struct timer_list {
struct list_head entry;
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
struct tvec_t_base_s *base;
};
可以看到timer_list
也是被链表链接起来的;其中还包括exipries
定时器超时时间和function
处理方式。
管理定时器,采用的是时间轮的方法,可以简单理解成堆结构。
总结
简单总结上述内容:
信号是事件的一种异步通知机制
信号产生的方式
终端按键
系统调用:
kill
、raise
、abort
硬件异常:
/0
、野指针、子进程退出软件条件:
alarm
、软件条件不满足