【Linux】信号的艺术:深入理解 Linux 进程信号
【摘要】 创建信号的目的不只是为了控制进程,还要便于管理进程,进程的终止原因有很多种,如果一概而就话,对于问题的分析就很不友好,所以才会将信号进行细分,目的是方便定位、分析、解决问题。
1.什么是信号
1.1 生活角度的信号
在一个平平无奇的早晨,你点了一份外卖打算作为午餐。下完单后你就等待外卖的到来,为什么你会等外卖呢?因为你知道它会来的。
当外卖员到了你的楼下,你的手机也受到了外卖到来的通知,但是你正在打游戏,需要几分钟之后才能去拿外卖,那么在这几分钟内,你就没有下去拿外卖,但是你知道你的外卖已经到了。也就是拿外卖这一个动作并不是要立即执行的,可以理解为“在合适的时候去取”。
在收到通知,再到你去拿外卖期间,是有一个时间窗口的,再这段时间内,你没有去拿外卖,但是你知道外卖已经到了,本质上是你得到了一个“你有一份外卖要取”的信号。
如果你现在有空,拿到外卖后,就要开始处理外卖了。现在你有3个选择去处理外卖:
- 执行默认动作,直接开炫。
- 执行自定义动作,(突然没胃口了,留给弟弟吃)
- 忽略外卖,(把外卖放一边,继续打游戏)
外卖到来的整个过程,对你来讲都是异步的,你不能确定外卖员什么时候给你打电话。
1.2 技术角度的信号
我们先写一段死循环的程序
#include <iostream>
#include <unistd.h>
using namespace std;
int main(){
while(true){
cout<<"hello i am a precess!\n";
sleep(1);
}
return 0;
}
这是一个死循环程序,当我们运行程序后,默认是一个前台进程。
然后它就会在屏幕上死循环打印“hello i am a precess!”。
如果我们想要把它关闭,就需要给它发生一个信号,可能有人会说,什么发生信号啊,我就是ctrl+c
直接终止的。有没有一种可能ctrl+c
就是一种信号呢。
当我们在键盘输入ctrl+c
,就会产生一个硬件中断,被操作系统捕获,发生给前台进程,前台进程因为收到信号,然后引起进程退出。
如果我们结合上面的例子,那么进程就是你,外卖员就是操作系统,信号就是外卖。
值得注意的是:
ctrl+c
产生的信号只能发给前台进程(一个命令后面加&就可以让其在后台运行)- shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像
ctrl+c
这种控制键产生的信号。 - 前台进程在运行过程中用户随时可能按下
ctrl+c
而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT
信号而终止,所以信号相对于进程的控制流程来说是异步的
2. 信号的概念
当在终端输入kill -l
时,你就可以看到在Linux下的所有信号了。
一共62个信号,其中1~31
号信号为普通信号,用于分时操作系统;剩下的34~64
号信号为实时信号,用于实时操作系统。
这个时候肯定久有小伙伴有疑惑了,怎么31号信号后面就是34号了,32号和33号都去哪了。
关于32号和33号
在 Linux 系统中,信号编号从 1 开始到 31 是传统的 标准信号,而从 34 开始的信号是 实时信号。信号编号从 32 到 33 被跳过,这是因为 编号 32 和 33 是为 LinuxThreads(旧版 POSIX 线程库)保留的,用于线程间的内部通信。
信号 32 和 33 被跳过是因为它们被线程库保留用于内部使用,而实时信号从编号 34 开始,以避免混淆和冲突。这种设计是 Linux 内核的一种历史兼容性处理。
回到分时操作系统和实时操作系统
关于分时操作系统和实时操作系统
- 分时操作系统(标准信号)
适合处理一些不需要严格实时性的任务。具有以下特点:
- 信号数量有限:标准信号有固定数量
- 根据时间片实行公平调度,适用于个人电脑。
- 实时操作系统
适合处理实时性要求高的任务。具有以下特点:
- 高响应,适合任务较少,需要快速处理的平台,比如汽车车机、火箭发射控制台等等。
注意:普通信号保存它有无产生,实时信号可以保存很长时间。
本篇文章,不会涉及实时信号的讲解
2.1 各个信号的作用
要了解各个型号的作用,我们可以查看man手册。
输入指令:man 7 signal
页面下滑就可以看到各个信号的作用了。
节选一部分:
有没有简单易懂的中文介绍呢?有的,有的
以下表格引用自:Linux中的31个普通信号
信号编号 | 信号名 | 信号含义 |
---|---|---|
1 | SIGHUP | 如果终端接口检测到一个连接断开,则会将此信号发送给与该终端相关的控制进程,该信号的默认处理动作是终止进程。 |
2 | SIGINT | 当用户按组合键(一般采用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文件。 |
2.2 进一步了解信号
观察上面的信号作用,可以发现大多数信号的默认动作都是终止进程,对于进程来讲,动作无非就是哪几种:终止进程,暂停进程,恢复进程,那为什么要搞出这么多的信号呢?
创建信号的目的不只是为了控制进程,还要便于管理进程,进程的终止原因有很多种,如果一概而就话,对于问题的分析就很不友好,所以才会将信号进行细分,目的是方便定位、分析、解决问题。
你知道为什么普通信号是31个吗?
这是因为这31个信号可以全部存储到int
中,非常的方便。
为什么上面都说讲上面,默认的动作呢?
这是因为进程的执行动作是能够修改的,默认维系统预设的默认动作。
当我们处理一个信号时,一个有这3种处理方式:
- 忽略该信号。
- 执行该信号的默认处理动作。
- (自定义处理方式)提供一个信号的处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕获(Catch)一个信号。
关于自定义处理方式,后面的内容会讲的。
现在让我们来看看,操作系统是怎么管理信号的。众所周知,操作系统为了管理一样东西都会采用先描述再组织的方法。
所以我们可以再操作系统中找到关于信号的数据结构。
以下源码及总结来自:北海
以下为部分源码:
struct signal_struct {
atomic_t sigcnt;
atomic_t live;
int nr_threads;
wait_queue_head_t wait_chldexit; /* for wait4() */
/* current thread group signal load-balancing target: */
struct task_struct *curr_target;
/* shared signal handling: */
struct sigpending shared_pending;
/* thread group exit support */
int group_exit_code;
/* overloaded:
* - notify group_exit_task when ->count is equal to notify_count
* - everyone except group_exit_task is stopped during signal delivery
* of fatal signals, group_exit_task processes the signal.
*/
int notify_count;
struct task_struct *group_exit_task;
/* thread group stop support, overloads group_exit_code too */
int group_stop_count;
unsigned int flags; /* see SIGNAL_* flags below */
/*
* PR_SET_CHILD_SUBREAPER marks a process, like a service
* manager, to re-parent orphan (double-forking) child processes
* to this process instead of 'init'. The service manager is
* able to receive SIGCHLD signals and is able to investigate
* the process until it calls wait(). All children of this
* process will inherit a flag if they should look for a
* child_subreaper process at exit.
*/
unsigned int is_child_subreaper:1;
unsigned int has_child_subreaper:1;
//……
};
3.总结
- 信号是执行的动作的信息载体,程序员再设计进程的时候,早就设计了其对信号的识别能力。
- 信号对于进程来说是异步的,随时可能产生,如果信号产生时,进程在处理优先级更高的事情,那么信号就不能被立即处理,此时进程需要保存信号,后续再处理。
- 进程可以将多个信号或者还未处理的信号存储再signal_struct这个数据结构中,具体信号编号存储再uint32_t signals这个位图结构中。
- 所谓的"发送"信号,其实就是写入信号,修改进程中对应的比特位,由
0
变1
就是该信号产生了。signal_struct
属于内核数据结构,只能由操作系统进行同一修改,无论信号是如何产生的,最终都需要借助操作系统进行发送。- 信号并不是立即处理的,它会再合适的时间进行统一处理。
所以进程信息可以分为3步:
- 信号产生
- 信号保存(存储在signal_struct中)
- 信号处理(信号并不是立即被处理的,而是在合适的时间才处理)
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
作者其他文章
评论(0)