【Linux笔记】——进程信号的产生
🔥个人主页🔥:孤寂大仙V
🌈收录专栏🌈:Linux
🌹往期回顾🌹:【Linux笔记】进程间通信——system v 共享内存
🔖流水不争,争的是滔滔不
- 一、进程信号简介
- 二、引入信号
- 三、信号的产生
- 3.1 自定义捕捉动作
- 3.2 前台进程与后台进程
- 3.2 理解给进程发信号
- 信号产生,硬件异常
- 信号产生,软件产生
一、进程信号简介
Linux信号:程序世界的“紧急通知”机制
在Linux系统中,信号(Signal) 是一种简单而强大的通信机制,如同程序之间的“紧急通知”。当进程遇到外部事件(如用户按下Ctrl+C)、程序错误(如内存越界)或定时任务触发时,系统会通过发送信号快速打断进程当前操作,要求其立即响应。
信号的设计初衷是处理异步事件——这些事件可能在程序运行的任何时刻发生,无法预测。例如:
-
用户中断:SIGINT(Ctrl+C)通知进程终止。
-
致命错误:SIGSEGV(段错误)意味着程序访问了非法内存。
-
进程控制:SIGTERM请求进程优雅退出,SIGKILL则强制终止。
-
后台任务:SIGCHLD通知父进程子任务状态变化。
开发者可以为信号绑定自定义处理逻辑,比如保存数据后退出,或直接忽略某些信号。但信号处理需谨慎——不当操作可能导致程序崩溃或资源泄漏。
为什么理解信号至关重要?
它是实现可靠服务(如优雅退出、防止僵尸进程)的基石。帮助开发者调试程序崩溃、理解系统底层行为。在多进程协作、系统管理中广泛应用。信号机制如同程序的“神经系统”,虽轻量却贯穿系统运行的每个角落。
二、引入信号
生活中的信号:闹钟、红绿灯、上课铃声、狼烟、电话闹铃等等。
信号就是中断正在做的事情,是一种事件的异步通知机制,信号是一种给进程发送的,用来进行事件异步通知的机制。注意信号的产生相对于进程的进程是异步的。信号是发给进程的。
基本结论
- 信号处理,进程在信号没有产生的时候,早就知道信号该如何处理了。
- 信号的处理,不是立即处理而是等一会再处理,合适的时候进行信号的处理。
- 人能认识信号,是提前被"教育"过的,进程也是如此,进程是人设计的进程已经内置了对于信号的识别和处理方式。
- 信号源非常多给进程的产生信号的信号源也非常多。
三、信号的产生
当我们杀掉一个进程用ctrl+c进程操作,其实当我们按下ctrl+c的时候就是给目标进程发送信号的,这个信号的处理动作就是让进程终止。其实还有好多信号,通过kill -l进行查看
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义 #define
SIGINT 2。编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal
kill -l命令可以察看系统定义的信号列表
3.1 自定义捕捉动作
进程收到信号,处理信号的动作有三种
- 默认处理动作
- 自定义信号处理动作
- 忽略处理
我们用ctrl+c杀掉进程,采用的是信号的默认处理动作,但是系统给我们提供了函数可以自定义信号处理动作
#include <signal.h>typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);
自定义把二号信号SIGINT自定义修改为打印获得一个信号,运用后发现在用ctrl+c之前终止进程的组合键现在变为了打印是几号信号。
#include<iostream>
#include<signal.h>
using namespace std;void handler(int sig)
{cout<<"获得一个信号"<<sig<<endl;
}
int main()
{signal(SIGINT,handler);int cnt=0;while(true){cout<<"haha"<<cnt++<<endl;sleep(1);}return 0;
}
执行自定义信息处理的动作,叫为自定义捕捉动作。
3.2 前台进程与后台进程
前台进程与后台进程是操作系统中管理任务执行的两种模式,尤其在命令行环境(如Linux/Unix)中广泛应用。它们的核心区别在于 是否占用终端控制权 以及 与用户的交互方式。
键盘产生的信号,只能发送给前台进程。而键盘产生的信号给后台进程,后台进程不做处理。所以前台进程能从键盘中获取标准输入,后台进程无法从标准输入中获取内容,但是这二者都能向标准输入中打印。键盘只有一个,输入数据一定是给一个确定的进程的,前台进程必须只有一个,后台进程可以有多个。前台进程的本质就是从键盘获取数据。
刚才写的那个自定义捕捉的程序,我们输入其他的Linux的指令发现没反应,这是因为当这个可执行程序已经变成了前台进程,前台进程只能有一个,bash进程自动被放到后台进程了。
ctrl+z 前台进程切换到后台
jobs 查看所有的后台进程
fg+任务号将特定的进程提到前台
bg + 任务号 让后台进程也运行
这时发现用ctrl+c没反应,把这个后台进程先提到前台在试就可以了。
对上面的的命令进行一下总结
jobs查看所有的后台进程
fg+任务号将特定的进程提到前台
bg+任务号让后台进程也运行
ctrl+z前台进程切换到后台
3.2 理解给进程发信号
信号产生之后,不是立即处理的,所以进程必须把信号记录下来等到合适的时候进行处理。记录在一个叫struct_task_struct 的结构体中,(位图结构)比特位的位置表示信号标号,比特位的内容表示是否收到。发送信号本质就是向目标进程写信号,修改位图。上面提到的struct_task_struct 是操作系统内的数据结构对象,修改位图的本质就是修改操作系统内核的数据,所以这就表明不管信号怎么产生发送信号在底层必须让操作系统发送,操作系统发送信号就是进行进行系统调用。
举一个例子,发送kill信号
#include <iostream>
#include <string>
#include <sys/types.h>
#include <signal.h>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 << "send " << signum << "to" << target << " success.";}return 0;
}
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>void handlerSig(int sig)
{std::cout << "获得了一个信号: " << sig << std::endl;exit(13);
}int main()
{signal(SIGINT, handlerSig);int cnt = 0;while (true){std::cout << "hello world, " << cnt++ << " ,pid: " << getpid() << std::endl;sleep(1);}
}
手动实现和测试“信号发送与接收”机制,也就是在验证 kill 命令和 signal 信号处理机制。
信号产生,硬件异常
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前
进程执行了除以0的指令, CPU的运算单元会产生异常, 内核将这个异常解释为SIGFPE信号发送给进
程。再比如当前进程访问了非法内存地址, MMU会产⽣异常,内核将这个异常解释为SIGSEGV信号发送
给进程。
信号全是操作系统发送的,程序出错了,操作系统识别到程序犯错了。在一方面操作系统通过硬件方式知道。
-
CPU执行指令时随时在“检测”CPU每条指令在执行时,都会自动进行检查,包括:除法时检查除数是不是0。访存时检查地址是否合法(分页机制、段机制)。特权指令检查当前运行级别(用户态不能干内核的事。这些检查是CPU电路里直接完成的,不需要程序参与,速度是纳秒级。
-
一旦发现问题 → 异常信号(Exception)比如除0时:
CPU内部“除法单元”发现除数为0。立刻触发一个“除法错误异常”(通常是#DE异常)。再比如访问了不存在的虚拟地址:CPU查页表发现PTE无效。立刻触发一个“缺页异常”(#PF Page Fault)。
⚡ 本质上:这些“异常”就是一种特殊的“中断”,区别只是来源不同(内部检测 vs 外部中断引脚)。 -
CPU 触发异常后会干嘛?(硬件动作)
挂起当前指令流。把当前上下文信息(寄存器、EIP、标志寄存器)压栈。跳转到IDT(中断描述符表)里登记的对应异常处理函数。例如,#DE异常会跳到“除0异常”的处理入口。这个处理函数是操作系统写好的内核代码(用户写不了)。➡️ 硬件到这里的活儿就干完了,后续接力棒交给内核。
信号产生,软件产生
在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产生机制。这些条
件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据
产⽣的SIGPIPE信号)等。当这些软件条件满足时,操作系统会向相关进程发送相应的信号,以通知
进程进行相应的处理。简而言之,软件条件是因操作系统内部或外部软件操作而触发的信号产⽣。
调用 alarm 函数可以设定⼀个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发SIGALRM 信号,该信号的默认处理动作是终止当前进程。
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡⼀觉,设定闹钟为30分钟之后响,20分钟后被⼈吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
系统闹钟,其实本质是OS必须自身具有定时功能,并能让用户设置这种定时功能,才可能实现闹钟这
样的技术。
现代Linux是提供了定时功能的,定时器也要被管理:先描述,在组织。内核中的定时器数据结构是:
struct timer_list {struct list_head entry;unsigned long expires;void (*function)(unsigned long);unsigned long data;struct tvec_t_base_s *base;
}
操作系统管理定时器,采勇的是时间轮的做法,但是我们为了简单理解,可以把它在组织成为"堆结构"。