👦个人主页:Weraphael
✍🏻作者简介:目前正在学习c++和算法
✈️专栏:Linux
🐋 希望大家多多支持,咱一起进步!😁
如果文章有啥瑕疵,希望大佬指点一二
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍
目录
- 一、生活角度的信号
- 二、进程信号的理解
- 三、31个普通信号的作用
- 四、进程收到信号现象
- 4.1 前台进程和后台进程
- 4.2 Ctrl + c热键
- 4.3 硬件中断
- 五、信号的概念
一、生活角度的信号
在我们日常生活中,信号是随处可见的,比如:
- 红绿灯
- 上下课铃声
- 发令枪
...
因为我们都接受过教育(识别信号),当产生这些信号时,会立马想到对应的处理动作,但不一定会立马去处理,因为现在可能正在做更重要的事,直到在合适的时候处理。比方说:闹钟一响就要起床学习【进程信号】,可是被窝太香了,起不来。所以,在信号产生后到信号处理这段期间内,必须记住(保存)信号。
二、进程信号的理解
- 类似地,进程也必须能够识别和处理信号的能力。程序员们给操作系统内置了一批指令,一个指令表示一种特殊动作,而这些指令就是信号(进程信号)
- 当进程收到一个具体信号的时候,进程可能并不会立即处理这个信号,等到合适的时候处理。
- 一个进程从信号产生后到信号处理这段期间内,必须保存信号,等到合适的时候处理。
在Linux
中,可以通过以下命令查看当前系统中的信号列表
kill -l
以上是当前系统中的 进程信号,一共 62
个,其中 1~31
号信号为 普通信号(重点学习);剩下的 34~64
号信号为 实时信号(不考虑)。
- 普通信号:根据时间片实行公平调度,适用于个人电脑。(识别信号后可以不立即处理)
- 实时信号:高响应,适合任务较少、需要快速处理的平台,比如汽车车机、火箭发射控制台。(识别信号后必须立即处理)
注意:在Linux
中,信号本质上就是一个数字,诸如:SIGKILL
、SIGINT
等这些其实是宏。
三、31个普通信号的作用
信号编号 | 信号名 | 信号含义 |
---|---|---|
1 | SIGHUP | 如果终端接口检测到一个连接断开,默认处理动作是终止进程。 |
2 | SIGINT | 它的含义是Interrupt (中断)。这个信号通常由用户通过键盘(Ctrl + c )发送,默认是中断当前进程的运行。 |
3 | SIGQUIT | 当用户按组合键(一般采用Ctrl+\ )时,该信号不仅终止前台进程组,同时会产生一个core 文件。 |
4 | SIGILL | 此信号表示进程已执行一条非法指令,该信号的默认处理动作是终止进程,同时产生一个core 文件。 |
5 | SIGTRAP | 该信号由断点指令或其他trap 指令产生,该信号的默认处理动作是终止进程,同时会产生一个core 文件。 |
6 | SIGABRT | 调用abort 函数是产生此信号,进程异常终止,同时会产生一个core 文件。 |
7 | SIGBUS | 当出现某些类型的内存故障时,常常产生该信号,该信号的默认处理动作是终止进程,同时产生一个core 文件。 |
8 | SIGFPE | 此信号表示一个算术运算异常,比如除0 、浮点溢出等,该信号的默认处理动作是终止进程,同时产生一个core 文件。 |
9 | SIGKILL | 该信号不能被捕捉或忽略,它向系统管理员提供了一种可以杀死任一进程的可靠方法。 |
10 | SIGUSR1 | 这是一个用户定义的信号,即程序员可以在程序中定义并使用该信号,该信号的默认处理动作是终止进程。 |
11 | SIGSEGV | 指示进程进行了一次无效的内存访问(比如访问了一个未初始化的指针),该信号的默认处理动作是终止进程并产生一个core 文件。 |
12 | SIGUSR2 | 这是另一个用户定义的信号,与SIGUSR1 相似,该信号的默认处理动作是终止进程。 |
13 | SIGPIPE | 如果在管道的读进程已终止时对管道进行写入操作,则会收到此信号,该信号的默认处理动作是终止进程。 |
14 | SIGALRM | 当用alarm 函数设置的定时器超时时产生此信号,或由setitimer 函数设置的间隔时间已经超时时也产生会此信号。 |
15 | SIGTERM | 该信号是由应用程序捕获的,使用该信号让程序有机会在退出之前做好清理工作。与SIGKILL 信号不同的是,该信号可以被捕捉或忽略,通常用来表示程序正常退出。 |
16 | SIGSTKFLT | 该信号指示协处理器上的堆栈故障(未使用),该信号的默认处理动作是终止进程。 |
17 | SIGCHLD | 在一个进程终止或停止时,SIGCHLD 信号被发送给其父进程。按系统默认,将忽略此信号。如果父进程希望被告知其子进程的这种状态改变,则应捕捉此信号。信号捕捉函数中通常要调用一种wait 函数以取得子进程id 及其终止状态。 |
18 | SIGCONT | 可以通过发送该信号让一个停止的进程继续运行。 |
19 | SIGSTOP | 这时一个作业控制信号,该信号用于停止一个进程,类似于交互停止信号(SIGTSTP ),但是该信号不能被捕捉或忽略。 |
20 | SIGTSTP | 交互停止信号,当用户按组合键(一般采用Ctrl+Z )时,终端驱动程序产生此信号并发送至前台进程组中的每一个进程。 |
21 | SIGTTIN | 后台进程读终端控制台时,由终端驱动程序产生此信号并发送给该后台进程,该信号的默认处理动作是暂停进程。 |
22 | SIGTTOU | 后台进程向终端控制台输出数据,由终端驱动程序产生此信号并发送给该后台进程,该信号的默认处理动作是暂停进程。 |
23 | SIGURG | 套接字上有紧急数据时,向当前正在运行的进程发出此信号,报告有紧急数据到达,该信号的默认处理动作是忽略。 |
24 | SIGXCPU | 进程执行时间超过了分配给该进程的CPU 时间,系统产生该信号并发送给该进程,该信号的默认处理动作是终止进程,同时会产生一个core 文件。 |
25 | SIGXFSZ | 如果进程写文件时超过了文件的最大长度设置,则会收到该信号,该信号的默认处理动作是终止进程,同时会产生一个core 文件。 |
26 | SIGVTALRM | 虚拟时钟超时时产生该信号,与SIGALRM 信号类似,但是该信号只计算该进程占用CPU 的使用时间,该信号的默认处理动作是终止进程。 |
27 | SIGPROF | 该信号类似与SIGVTALRM ,它不仅包括该进程占用CPU 的时间还包括执行系统调用的时间,该信号的默认处理动作是终止进程。 |
28 | SIGWINCH | 当窗口大小发生变化时,内核会将该信号发送至前台进程组,该信号的默认处理动作是忽略。 |
29 | SIGIO | 此信号指示一个异步I/O 事件,该信号的默认处理动作是终止进程。 |
30 | SIGPWR | 电源故障,该信号的默认处理动作是终止进程。 |
31 | SIGSYS | 该信号指示一个无效的系统调用,该信号的默认处理动作是终止进程,同时会产生一个core 文件。 |
四、进程收到信号现象
4.1 前台进程和后台进程
- 前台进程:进程默认情况下都是以前台方式运行,它会将输出结果直接发送到终端。通常,用户当前正在与之交互的进程称为前台进程。用户可以通过在终端按
ctrl + c
或者使用kill
命令可以向进程发送信号9
(SIGKILL
)终止前台运行的进程。
kill -9 <pid>
- 后台进程:当进程以后台方式运行时,它不会占用终端并且不会阻塞用户的输入输出。用户可以通过以下命令将进程置于后台运行。但如果想杀掉进程只能用
kill
命令(命令如上)。
./myprocess &
注意:
- 在
Linux
中,一个终端会配上一个命令行解释器bash
,并且一个终端只允许一个进程是前台进程,但可以允许多个进程为后台进程。 - 默认情况下,
bash
就是前台进程。当一个前台进程运行起来后,bash
就变成了后台进程。 - 键盘输入首先被前台进程接收。
Ctrl + c
不会终止bash
进程。因为bash
进程对其做了特殊处理。
4.2 Ctrl + c热键
比方说我写了如下一个死循环代码
#include <iostream>
using namespace std;
int main()
{
while (true)
{
cout << "我是一个进程,我在做死循环操作" << endl;
}
return 0;
}
当此程序运行起来后,不出意外就会在屏幕上疯狂打印。而以前在学习指令的时候说过,遇到这种类似的情况直接无脑Ctrl + c
即可终止程序。
那么为什么就终止了呢?
当死循环程序运行起来后,就成为了前台进程,而bash
则变成了后台进程。又因为键盘输入首先是被前台进程收到的,因此,Ctrl + c
本质上是被前台进程解释成为了收到某种信号。
这里其实发出了一个 2
号信号SIGINT
。
而进程信号的处理方式有三种:
- 系统默认动作
- 忽略处理(不处理)
- 用户自定义处理(信号捕捉)
首先可以确定的是 Ctrl + c
收到信号的默认处理动作就是终止自己。那我能不能自定义处理信号动作呢?答案是当然可以的。
有一个系统调用接口signal
用于自定义设置信号处理方式,其函数原型如下:
#include <signal.h>
typedef void (*sighandler_t)(int);
void (*signal(int signum, sighandler_t handler);
说明:
signum
:要处理的信号的编号。单纯地传递信号名也是可以的,因为信号名其实就是信号编号的宏定义。handler
:是一个函数指针(函数地址)。
void handler(int)
{
// 自定义处理方式
}
显然,signal
函数是一个回调函数,当某个信号被捕捉时,会去调用相应的函数,也就是执行相应的动作,此时signal
函数的第一个参数会传递给handler
函数做参数。
注意:
signal
函数可以设置在任何位置,一般建议在程序开始处设置。signal
函数设置后,进程整个生命周期都有效。- 触发
signal
函数不是本身,而是在执行后序代码的过程中触发的。(只有进程收到了信号signum
,才会被调用。) - 但并不是所有的信号都可以进行自定义处理。比如
SIGKILL
和SIGSTOP
等。 - 如果
handler
函数体内不写处理方式,那么信号的处理方式是系统默认的那一套。
比方说,我可以修改Ctrl + c
信号的处理方式。自定义处理发生为:打印出自己收到了几号信号后,再退出进程。
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void myhandler(int signum)
{
cout << "信号编号为:" << signum << endl;
exit(1);
}
int main()
{
signal(SIGINT, myhandler);
while (true)
{
cout << "我是一个进程,我在做死循环操作" << endl;
sleep(1);
}
return 0;
}
【程序结果】
4.3 硬件中断
键盘数据(ctrl + c
)是如何输入给操作系统的?当用户在键盘上输入完数据,操作系统是怎么知道键盘上有数据?
-
在数据层面(指的是实际的数据存储和处理过程),
CPU
不从外设拿数据(不和外设打交道),而从内存中拿(因为内存访问速度比外设要快得多)。 -
但在控制层面,
CPU
是可以读取外设的相关状态。这是通过与外部设备的控制器进行通信来实现的。外部设备控制器负责管理与外设的交互,包括数据的传输和状态的监测。 -
当键盘有数据时,操作系统是通过硬件中断机制先知道的,这是因为计算机系统中的输入设备(如键盘)与
CPU
和操作系统之间的通信是通过中断来实现的。当外设有数据时,操作系统会采用中断的方式来通知CPU
有重要事件发生(例如数据已经准备好等),其中包含硬件的中断号(是CPU
用来识别不同中断类型的编号,存储在CPU
的寄存器中)。 -
CPU
收到中断请求后,会暂停当前正在执行的任务,并根据中断号找到对应的中断向量表(本质上就是数组)中对应中断号的条目。中断向量表是一个存储在内存中的数据结构,由操作系统维护,表中的条目是调用硬件设备驱动程序的方法的地址,从而实现与硬件设备的交互。例如,处理硬盘中断的中断服务程序可能会调用硬盘驱动程序的函数来读取read
或写入数据write
。 -
中断处理程序调用对应硬件的方法将输入数据处理后存储到内存的输入缓冲区中。最后通过文件描述符传输给相应的上层应用程序。例如,终端程序通过文件描述符从操作系统读取用户输入的命令和操作。
所以,我们学习的进程信号其实就是模拟硬件中断实现的。后面的学习大家就能体会到。
ctrl + c
又是如何变成信号的?
- 在操作系统中,键盘输入可以包含普通字符(如字母等)和特殊控制字符(
ctrl + c
等)。对于前者是可以回显的,而后者不回显。 - 因此操作系统首先会判断用户输入的是什么类型的字符,对于普通字符就直接从键盘文件拷贝到内核缓冲区;如果是特殊控制字符,操作系统会将解释成为某个信号发送给前台进程。
五、信号的概念
信号的产生和我们自己的代码的运行是 异步。属于软中断(软件中断)
- 异步:程序不必按照严格的顺序依次执行。比方说:老师上课叫一名同学去办公室拿“小蜜蜂”,老师不等他回来而是继续上课。
- 同步:程序按照预定顺序依次执行,每一步必须等待上一步完成后才能执行。就比方说:老师上课叫一名同学去办公室拿“小蜜蜂”,等他/她回来了才开始上课。
- 换言之,信号什么时候产生我们并不清楚,但是我们只需继续运行我们的代码,等着未来某个时刻信号的到来。
注意:信号一定是由操作系统(管理者)发送给进程的 ~