linux中,信号也是一个非常重要的知识点,接下来的博客我们将仔细探讨信号的知识,话不多说,现在就开始啦~~
1.信号是什么
我们举几个例子,比如上课铃,上课铃一响我们就知道要回去上课,这就是信号,它提醒我们该干什么事情了,再比如点外卖,外卖小哥送外卖敲门的声音,就是一个信号,他表示外卖到了,我们可以取外卖啦,在古代战争时,点狼烟可以告诉战士敌人来了,同理,狼烟也是一种信号,告诉我们敌人来临
所以什么是信号?
答:信号是一种给进程发送的,用来进行事件异步通知的机制!
那什么时异步通知呢,举个例子,在上课的时候老师发现张三还不在教室,如果此时继续上课而不等待张三,就可以说是异步,如果老师等张三回来在上课,那么就是同步
那么根据上面的描述,我们可以得到几个关于信号的基本结论:
1.信号处理,进程在信号没有产生的时候,就知道信号该如何处理了(你在外卖员未敲门前就知道敲门是因为你的外卖到了)
2.信号的处理,不是立即处理,而是可以等一会儿处理,合适的时候,进行信号处理(你的外卖到了,而此时你正在忙,你可以选择让外卖小哥把外卖放在门口,等你忙完了再去取,而不是必须他一敲门你就必须去取外卖)
3.进程早已经内置了对于信号的识别与处理方式
4.信号源非常多->给进程产生信号的信号源也非常多
2.信号的产生

信号需要先产生,然后保存,然后处理,下面我们就探讨产生
产生信号的方法非常多,这里使用键盘产生信号等来讲解
1.键盘产生信号
cpp
#include <iostream>
#include <unistd.h>
int main()
{
int cnt = 0;
while (true)
{
std::cout << "hello world : " << cnt++ << std::endl;
sleep(1);
}
return 0;
}

我们在键盘上输入的ctrl + c就是一种信号,是让该进程结束的信号

其实信号在linux里面只不过是数字,不过为了更好的表示,使用了宏替代了数字,1~31是普通信号,后面的是实时信号,不需要掌握太多,只需要了解即可,而我们的ctrl + c正是触发了2信号,其实相当一部分信号处理动作,就是让自己终止!
进程收到信号->处理信号
1.默认处理动作(信号给什么就干什么)
2.自定义信号处理动作(自己定义接下来的行为)
3.忽略处理(继续干自己的事情,不管信号)
我们依旧用外卖的例子来解释一下:
此时我们拿到了外卖,接下来该干什么呢
1.我们可以选择吃外卖(我们点外卖就是为了吃,合情合理)-> 默认处理动作
2.我们自己不吃,而是给女朋友吃(我们自己决定干接下来的事情)-> 自定义信号处理动作
3.不管外卖,继续做自己的事情(忽略处理)
那我们怎么证明进程默认处理是ctrl + c(2信号)呢,下面提供一种可以更改进程的默认信号处理的方法 -- signal
1.signal替换信号

其中第一个参数是你想改变的信号值,第二个参数是你改变的这个信号值之后他表示哪个函数(比如2->A, 你现在传入2, B,现在2->B)
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handlerSign(int sign)
{
std::cout << "获得一个信号:" << sign << std::endl;
}
int main()
{
signal(2, handlerSign);
int cnt = 0;
while (true)
{
std::cout << "hello world : " << cnt++ << std::endl;
sleep(1);
}
return 0;
}

此时发现ctrl + c对于的就是信号2
那么我们怎么关掉这个进程呢,可以使用ctrl + \
2.前后台
在 Linux 终端里,我们运行的程序分 "前台进程" 和 "后台进程",它们和键盘的交互规则,藏着终端的 "专属逻辑":
比如你在命令行敲./XXX启动程序,这个./XXX就是前台进程 ;要是加个&写成./YYY &,./YYY就会以后台进程的形式运行 ------ 而此刻你正在操作的命令行 shell,本身也是一个前台进程(所以终端里同一时间,前台进程只能有一个)
这时候键盘的 "专属权限" 就体现出来了:像 Ctrl+C 这类由键盘触发的信号,只会发给当前的前台进程。后台进程是收不到的 ------ 毕竟键盘只有一个,终端得确保 "输入的信号 / 数据,能精准给到一个确定的进程",要是前台进程能有多个,键盘信号就不知道该发给谁了
输入输出的规则也有区别:前台进程能直接从键盘获取输入(比如你运行一个需要输入密码的程序,前台才能正常读键盘);但后台进程不行,它没法从标准输入里拿内容。不过不管前后台,程序都能把内容输出到屏幕上(比如后台进程的日志,照样能打印到终端)
简单说:前台进程是终端的 "当前交互对象",占着键盘的交互权;后台进程是 "后台默默跑的",不抢键盘的交互资源 ------ 这是终端为了避免输入混乱,定好的 "单前台" 规则
就拿刚刚的程序举例子

此时我们的进程不会读取键盘内容,要想杀死这个进程,就需要启动kill -9 pid指令了
3.补充前后台命令
| 命令 / 操作 | 功能描述 | 用法示例 |
|---|---|---|
jobs |
查看当前终端所有后台任务(显示任务号、状态、命令) | 直接输入jobs,输出如[1]+ Stopped ./XXX |
ctrl+z |
将当前前台进程暂停,并切换到后台 | 前台进程运行时,按下ctrl+z组合键 |
fg 任务号 |
将指定任务号的后台任务,切换到前台运行 | 若jobs显示任务号为 1,输入fg 1 |
bg 任务号 |
让后台暂停的任务恢复运行(仍保持后台状态) | 若jobs显示任务号为 1,输入bg 1 |
3.进程发送信号本质
给进程发送信号,并非直接向进程传递数据,而是:
- 通过目标进程的
PID,找到该进程在内核中的管理结构; - 修改该结构中的 "信号位图"(某一位对应一个信号);
- 这一操作必须由操作系统完成(用户态进程无法直接修改内核数据),因此发送信号的本质是调用系统调用(如
kill)让内核修改目标进程的信号状
信号的记录方式(内核层面)
内核中每个进程对应一个struct task_struct(进程控制块,PCB),其中包含unsigned int sigs字段,该字段是信号位图结构:
- 位图的每一位对应一个信号(比如第 2 位对应 SIGINT 信号);
- 当某信号被发送给进程时,内核会将
sig中对应位置 1,以此记录 "进程收到了该信号"。
信号的处理逻辑
- 非立即处理 :信号产生后,进程不会立刻执行信号处理函数,而是先将信号记录在
sig位图中; - 合适时机处理 :进程会在 "从内核态返回用户态" 等安全时机,检查
sig位图的状态,执行对应信号的处理逻辑。
这一机制的核心是 "内核统一管理进程的信号状态"------ 因为进程的运行状态、资源都由内核维护,信号作为进程的 "事件通知",必须通过内核完成记录与触发
2.系统调用产生信号
1.kill
Signtest.cc
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handlerSign(int sign)
{
std::cout << "获得一个信号:" << sign << std::endl;
}
int main()
{
signal(2, handlerSign);
int cnt = 0;
while (true)
{
std::cout << "hello world : " << cnt++ << " pid : " << getpid() << std::endl;
sleep(1);
}
return 0;
}
mykill.cc
cpp
#include <iostream>
#include <signal.h>
int main(int argc, char* argv[])
{
if (argc != 3)
{
std::cout << "./mykill sig pid" << std::endl;
return 1;
}
int sig = std::stoi(argv[1]);
int pid = std::stoi(argv[2]);
int n = kill(pid, sig);
if (n == 0)
{
std::cout << "kill -" << sig << " " << pid << std::endl;
}
return 0;
}
运行结果


2.raise
raise() 是标准 C 库函数(非系统调用,但封装了kill系统调用),功能是向调用该函数的进程自身发送指定信号 ,等价于 kill(getpid(), sig)(getpid()获取当前进程 ID)
代码如下:
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handlerSign(int sign)
{
std::cout << "获得一个信号:" << sign << std::endl;
}
int main()
{
//signal(2, handlerSign);
for (int i = 1; i < 32; i++)
{
signal(i, handlerSign);
}
for (int i = 1; i < 32; i++)
{
raise(i);
}
int cnt = 0;
while (true)
{
std::cout << "hello world : " << cnt++ << " pid : " << getpid() << std::endl;
sleep(1);
}
return 0;
}

3.abord
abort() 是标准 C 库函数,核心功能是强制让当前进程异常终止 :它会向进程自身发送 SIGABRT 信号(6 号信号),且这个终止行为是 "不可阻挡" 的(即便你捕获了 SIGABRT,默认仍会终止进程)
#include <stdlib.h> // 必须包含的头文件(注意不是signal.h)
void abort(void);
3.产生异常
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handlerSign(int sign)
{
std::cout << "获得一个信号:" << sign << std::endl;
//exit(13);
}
int main()
{
//signal(2, handlerSign);
for (int i = 1; i < 32; i++)
{
signal(i, handlerSign);
}
/*for (int i = 1; i < 32; i++)
{
if (i != 9)
raise(i);
sleep(1);
}*/
int cnt = 0;
while (true)
{
std::cout << "hello world : " << cnt++ << " pid : " << getpid() << std::endl;
sleep(1);
//除零错误
/*int a = 10;
a /= 0;
*/
//野指针
/*
int* a = nullptr;
*a = 10;
*/
}
return 0;
}
