
◆ 博主名称: 小此方-CSDN博客 大家好,欢迎来到小此方的博客。
⭐️Linux系列个人专栏: 【主题曲】Linux
⭐️此方的GitHub: github_此方
⭐️ Re系列专栏:我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)
文章目录
- 概要&序論
- 一、信号的概念
- 二、信号的产生
-
- 2.1键盘产生信号
- 2.2目标进程是什么?
- 2.3给进程发送信号的本质是什么
- 插曲:讲一讲前台进程组的概念
-
- [2.3.1 什么是进程组?](#2.3.1 什么是进程组?)
- [2.3.2 Ctrl + C 的批量传递机制](#2.3.2 Ctrl + C 的批量传递机制)
- 2.4系统调用和系统命令产生信号
概要&序論
Hello,大家好我是此方,结束进程间通信,来到了Linux的倒数第二个话题------信号。今天主要介绍信号的基础认知以及信号是如何产生的等问题。好,我们开始吧。
一、信号的概念
1.1感性理解信号的概念
我们在先前讲到过信号量的概念,但是今天我们要讲的信号,和信号量的关系就像老婆和老婆饼,雷锋和雷锋塔------毫无关系。
信号与IPC的辩证关系
闹钟响了你要起床,绿灯亮了你要通过,在这些语境中,闹钟和绿灯都是信号。信号中断了人正在做的事情 ------因为你有新的事情要做了!那么,把人换成进程,信号就是中断进程当前任务的"异步通知机制 "。
详细解释:"异步"------说的是信号的发送过程相对于进程的执行是异步的。
李老师在上课,突然由通知让李老师去拿卷子,李老师派小刚去拿卷子。"好的孩子们,我们先自习一会儿,等小刚回来我们再上课"------同步。"好的孩子们,我们不等小刚,继续上课,小刚在执行拿卷子,李老师同时在执行上课"------异步。
1.2如何查看所有的信号
我们有哪些信号?通过指令kill -l来查看。信号就是一个整数数字一个宏,我们一共有62个信号。 其中前31个信号是普通信号,后31个是实时信号,本篇重点讲解普通信号。
- 1-31普通信号不必立即处理。
- 34-64实时信号必须立即处理。

信号在源代码中
详细看到每一个信号的细节,可以用man 9 signal查看:

从左向右依次是信号的名称,信号遵循的标准版本,信号的默认行为,信号的行为详细信息。
信号的默认行为有:终止(Term)核心转储(Core)停止(Stop)忽略(Ign)。具体放在后续详细讲解。
1.3综上得出关于信号的基础结论
- 进程在信号没有产生的时候,早就知道信号该如何处理了。------你知道红灯亮了要停车,绿灯亮了要前进。
- 信号的处理,不是立即处理,而是可以等一会在处理,合适的时候,进行信号的处理。------你在直播游戏,外卖到了你必须记住这个信号,然后在这局游戏结束后下楼取外卖。
- 人能识别信号,是提前被"教育"过的,进程也是如此,OS程序员设计的进程,进程早已经内置了对于信号的识别和处理方式。
- 生活中的信号源非常多->给进程产生信号的信号源,也非常多!(信号源有哪些我们后面讲。)
于是我们接下来的三篇关于信号的文章,主要以以下这条线叙事。信号产生,捕捉,保存,处理。

二、信号的产生
2.1键盘产生信号
信号产生的方式有很多种,键盘产生信号就是其中之一,我们以前使用的CTRL + C 就通过产生信号来终止进程。
2.2.1信号处理的三种抉择与自定义信号处理
验证前须知: 收到信号的处理动作有默认处理,自定义处理和忽略处理三种抉择。 如何进行自定义处理?


自定义信号处理的系统调用signal通过传入函数指针进行回调。 返回值:
- 成功时:返回该信号上一次关联的处理函数(handler)的值。例如,如果该信号之前的处理方式是 SIG_IGN,那么返回值也会是 SIG_IGN。
- 失败时:返回 SIG_ERR,并且会将全局变量 errno 设置为相应的错误原因(如 EINVAL,表示信号无效)。

科普 操作系统在设计的时候,为了防止恶意进程通过篡改信号处理机制使得其无法被任何手段暂停和终止。 不允许9号和19号信号被设置自定义处理。
2.1.2如何验证键盘产生了信号以及产生了何种信号
你想怎么验证?我想看到信号处理的过程------通过修改信号处理的默认操作。 代码如下:
cpp
#include<iostream>
#include<string>
#include<signal.h>
#include<unistd.h>
void Hander(int n){
std::cout<<"收到信号:"<<n << std::endl;
exit(1);
}
int main(){
for(int i = 0 ; i < 32 ; i++)
signal(i , Hander);
while(1){
std::cout<<"进程运行中。。。"<<std::endl;
sleep(1);
}
return 0 ;
}
运行,当我们按CTRL+C的时候,进程接收到信号2并打印, 当我们按CTRL + \ 进程收到三号信号 并打印。


2.2目标进程是什么?
回归定义截取------"信号是发送给目标进程的 " ,那么目标进程是什么呢?直接说答案:前台进程。后台进程可以有很多个,但是前台进程只能有一个。
键盘知道它自己要把信号发送给哪个进程吗?------唯一指向前台进程。前台进程的一个本质就是可以从键盘中获取数据的进程。
- 前台进程可以获取标准输入的内容。
- 后台进程不可获取标准输入的内容。
- 两者都可以向标准输入写入内容,
科普 在 Windows 操作系统中,我们看似能同时看到浏览器、文档等多个窗口在"前台"并存 但这只是视觉上的多任务 。由于键盘只有一个,Windows 引入了" 输入焦点 "机制。本质上,只有被你用鼠标点击选中的那个当前活跃窗口,才是唯一真正的"前台进程",独占键盘的输入数据;而其他未被选中的窗口,哪怕肉眼可见,在输入层面也属于"后台进程",无法获取你的键盘敲击。 这与 Linux 终端下前台进程只有一个的底层逻辑完全一致。
2.2.1如何切换进程的前后台
方法一:在运行进程的时候在后面加上取址符。
bash
# ./XXX --- 前台进程
# ./YYY & --- 后台进程
方法二:指令操作。
jobs : 查看所有的后台任务。
fg 任务号 : 特定的进程,提到前台。
ctrl+z :进程切换到后台。(暂停进程,暂停的进程必须被切换到后台)
bg 任务号:让后台进行回复运行。
2.2.2后台进程不响应键盘的输入
这里举两个常见的案例:我们./proc执行一个死循环打印的进程,此时我们载用键盘输入指令ls ,shell不会回应,因为shell从前台切换到了后台。
父进程提前退出,子进程孤儿化,被操作系统切换到后台进程,此时响应不了键盘,无法通过ctrl+c杀掉。
2.3给进程发送信号的本质是什么
一个信号发出去,不会立即处理而是存起来对不对?那么存在那里呢?在进程PCB中有这么一个字段:
cpp
struct task_struct{
unsigned int sigs;
}
这是一个32位的位图,刚好可以存放31个信号的二元状态信息。
- 位的位置标识哪一个信号。
- 位的0/1标识信号是否被收到。
所以我们就可以知道,信号发送的本质就是:修改为位图。
联系起来 :我们的键盘输入ctrl+c,操作系统作为硬件的管理者,识别出ctrl+c是发送信号。于是将这个信号写入前台目标进程的位图 ,前台目标进程在指定的时间查看自己的位图是2号信号,杀掉自己。
好,继续回来,unsigned int sigs;在task_struct ,它属于操作系统的数据结构对象,修改数据本质就是修改内核数据。而修改内核数据必须要通过系统调用------不管信号如何产生,在底层必须通过系统调用发送信号 。所以我们引出kill:(跳转到2.4)

插曲:讲一讲前台进程组的概念
在理解了前台进程后,我们往往会产生疑问:既然前台进程"有且仅有一个",那为什么按下 Ctrl + C 时,前台的父子进程会同时挂掉 ?
其实,操作系统底层的精确表述是:当前终端有且仅有一个"前台进程组"。
2.3.1 什么是进程组?
在 Linux 内核中,进程是"抱团"运行的。每当我们在终端启动一个程序,系统就会分配一个进程组 ID(PGID):
- 组长进程:由命令行直接启动的父进程担任,其 PID 就是整个进程组的 PGID。
- 组员进程 :父进程通过
fork()创建的所有子进程,默认都会自动加入该进程组,共享同一个 PGID。
终端在管理输入和信号时,正是以"进程组"为单位进行独占和调度的。
2.3.2 Ctrl + C 的批量传递机制

当操作系统识别出 Ctrl + C 的 2 号信号(SIGINT )后,它在底层并不是发给某个单一的 PID,而是直接锁定当前处于前台的整个进程组 。
随后,内核会遍历该前台进程组内的所有进程(含父进程和所有子进程),将信号分别写入它们各自 PCB 的信号位图中。父子进程在各自的调度时机检测到信号后同时执行退出。
2.4系统调用和系统命令产生信号
系统调用和系统命令是产生信号的第二种方式。 主要的系统调用有kill,raise,abort。系统命令主要是kill。系统命令kill我们一直在用就不讲了。
2.4.1系统调用kill发送信号

cpp
#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include<unistd.h>
void Hander(int x)
{
std::cout<<"捕捉到信号"<< x << std::endl ;
}
int main()
{
for(int i = 1 ; i< 32 ; i ++)
signal(i , Hander);
for(int i = 1 ; i < 32 ; i ++) {
kill(getpid(),i);
sleep(1);
}
return 0 ;
}

2.4.2系统调用raise发送信号
专门给自己发送信号的系统调用。raise(sig) 等价于kill(getpid(),sig);

cpp
#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include<unistd.h>
void Hander(int x)
{
std::cout<<"捕捉到信号"<< x << std::endl ;
}
int main()
{
for(int i = 1 ; i< 32 ; i ++)
signal(i , Hander);
for(int i = 1 ; i < 32 ; i ++) {
raise(i);
sleep(1);
}
return 0 ;
}
2.4.3系统调用abort发送信号
注意是abort不是about
异常终止当前进程,并产生一个 Core Dump(如果系统允许),abort() 会向自己发送 SIGABRT 信号。


我们发现Hander无法组织abort结束进程。为什么?因为POSIX 对 abort() 有明确规定:如果 SIGABRT 被捕获,并且处理函数返回,abort() 必须仍然终止进程。
Gemini对此提供了一个不错的解释:
还没有讲完,下一篇将带大家了解另外两种信号产生机制
好的本期内容就到这里,如果对你有帮助,还不要忘记点赞三联支持。我是此方,我们下期再见。bye! Linux、C++、算法持续连载中,欢迎关注WeChat Official Account 【此方的技术栈】。


