【Linux】进程信号(一):信号的产生与信号的保存
📝前言:
这篇文章我们来讲讲Linux——进程信号:
🎬个人简介:努力学习ing
📋个人专栏:Linux
🎀CSDN主页 愚润求学
🌄其他专栏:C++学习笔记,C语言入门基础,python入门基础,C++刷题专栏
目录
- 一,认识信号
- 1. 查看信号
- 2. 信号处理动作
- 2.1 默认处理动作
- term 和 core 的区别
- 2.2 自定义处理动作
- signal
- 示例
- 3. 前台进程和后台进程
- 区别
- 切换
- 二,信号的产生
- 1. 四种方式基本介绍
- 1. 异常产生信号
- 2. 软件条件产生信号
- 三,信号的保存
- 1. 基本概念
- 阻塞
- 2. pending表
- 3. block表
- 3.1 sigset_t
- 信号集操作函数
- 信号集“上传”函数
- 3.2 . 自定义处理动作sigaction
- 4. handler表
一,认识信号
1. 查看信号
kill -l
查看信号:
- 每个信号都有⼀个编号和⼀个宏定义名称:左边是信号编号,右边是对应的宏
- 其中
1-31
号信号是普通信号,34 - 64
为实时信号 - 普通信号:可以不立即处理
- 实时信号:立即处理
signal.h
中可以看到:编号和宏对应
2. 信号处理动作
按下 Ctrl + c
组合键会向当前前台进程发送 SIGINT
(2 号信号),此时进程会终止。让进程终止是该2号信号的默认处理动作。
信号处理的动作有三种:默认、自定义和忽略
2.1 默认处理动作
man 7 signal
我们可以去杂项章节看一下信号的具体描述:
Action
是信号的默认处理动作
term / core
→ 终止(但有区别)Ign
→ 忽略Cont
→ 继续Stop
→ 暂停
term 和 core 的区别
core
在term
的基础上多了一个核心转储功能,会生成核心转储文件(Core Dump)。- 用途:用于后续
debug
,分析程序崩溃原因- 如,
test1
崩了,我们gdb ./test1 core
就可以直接定位到崩溃的位置
- 如,
- 生产环境上(如:云服务器),
core dump
会被禁止,因为容易产生大量core
文件占据磁盘空间
waitpid()
的输出型参数的wstatus
里面就有一个core dump
标记位记录,当前信号是否是core
终止的,是否生成core
文件
ulimit -a
可以查看允许生成的core
文件的大小:
可以看到默认core
文件的大小是0
个blocks,即:禁止
ulimit -c <数字>
:可以重新设置大小
2.2 自定义处理动作
signal
signal
用来自定义信号处理动作
signum
:要自定义的信号的编号(也可以传对应的宏,本质都是数字)handler
:自定义函数的指针- 同时要求我们的自定义函数
- 返回值
void
- 一个参数:接受信号编号
- 返回值
示例
void handler(int signum)
{cout << "我收到了 " << signum << " 号信号" << endl;
}int main()
{signal(2, handler);while (true){cout << "进程PID: " << getpid() << endl;sleep(1);}return 0;
}
这时候可以killed -9 8968
把这个进程杀掉,因为9
号是强杀信号,不能被自定义动作,类似的不能被自定义的还有19
…
3. 前台进程和后台进程
在可执行程序执行后面带 &
,运行的就是后台进程。
区别
- 前台进程可以从标准输入中获取数据,但是后台进程不行(即:我们的键盘输入没办法发给后台)
- 前后台进程都可以往标准输出打印
- 在一个
bash
下:某一时刻,只能有一个前台进程,后台进程可以有多个(因为标准输入只有一个,不能同时有多个进程抢着读,会乱) - 每个
bash
进程都有自己独立的作业列表,不同的bash
进程之间的作业是相互隔离的 bash
本身是前台进程,当我们执行前台进程的时候,bash
就会被切换后台
示例:
./test1 &
把test1
放到后台运行
可见键盘输入Ctrl + c
,test1
就收不到了,但是ls
命令bash
还可以收到,最后我们kill -9 9193
就可以把这个进程杀掉
切换
jobs
可以看当前bash
的后台进程fg + 任务号
:把后台进程切换到前台Ctrl + z
:暂停前台进程,并把前台进程切换到后台bg + 任务号
:恢复暂停的后台进程,重新运行
二,信号的产生
1. 四种方式基本介绍
一个信号,要经历三个阶段:
信号的产生方式有多种:
- 键盘产生
- 如:
ctrl + c
给前台进程发信号
- 如:
- 系统调用(命令)产生
- 如:
int kill(pid_t pid, int sig)
,给pid
进程发一个sig
信号(命令kill
就是调这个的) - 如:
int raise(int sig)
,自己给自己发sig
信号 - 如:
abort()
:给自己发 6 号信号
- 如:
- [硬件]异常产生
- 如:
\0
或野指针
错误产生异常就会发对应的信号(实际上是先硬件异常)
- 如:
- 软件条件产生
- 如:管道文件写端继续,读端关闭。此时写是没有意义的,系统就会产生
SIGPIPE
信号 - 如:
alarm
:用于设置一个定时器(闹钟),在指定的秒数后向当前进程发送一个SIGALRM
信号
- 如:管道文件写端继续,读端关闭。此时写是没有意义的,系统就会产生
不管信号怎么产生,都要直接 / 间接的由OS来发对应的信号
1. 异常产生信号
出现异常的时候,其实最先变化的是硬件!
核心逻辑是:
硬件通过寄存器标记异常并主动触发 CPU 异常机制 → CPU 借助操作系统内核处理异常 → 操作系统将硬件事件转化为软件信号通知进程。
2. 软件条件产生信号
这里以alarm
为例,设计一个定期向进程发送信号,驱动进程完成对应工作的程序
void handler(int signum)
{cout << "执行任务 1 " << endl;cout << "执行任务 2 " << endl;cout << "#########################" << endl;alarm(2);
}int main()
{alarm(2);signal(SIGALRM, handler);while (true){}return 0;
}
每隔两秒,handler
就会在“闹钟”的驱动下被执行一次。
OS的调度原理也是类似:通过定期传递信号,被动被驱动,然后运行对应的进程。简单理解:
- 如果这个“闹钟”本身是一个任务结构体,里面还记录了时间片。
- 将所有任务用最小堆组织起来,每次选取任务的时候,用现在的时间对比堆顶任务的最小时间,如果超时了(就是“闹钟响了”),就运行堆顶的任务。
- 运行的同时,对应任务结构体内的时间片–。这就是调度算法
三,信号的保存
1. 基本概念
阻塞
- 信号递达:实际执行信号的处理动作
- 信号未决:信号已递达进程,但尚未被处理的状态。又可以细分成下面两种。
- 未被阻塞:可立即被递达的(取决于进程的处理方式)。
- 被阻塞:需先解除阻塞才能被递达的(是否阻塞由进程自己决定)
进程的信号保存主要依赖于三张表:
下面依次讲解
2. pending表
未决信号集:记录进程接受到的,但是未被处理信号,本质是一张位图。
- 当OS给进程发信号的时候,就是把对应信号下标的位置由 0 → 1
- 当一个信号被递达时,是
pending
表中对应位置先 → 0,然后才执行处理函数递达
3. block表
阻塞信号集:记录当前进程要阻塞的信号,本质也是一张位图。
1
代表,接受到该信号以后阻塞该信号。- 对于普通信号
0 - 31
,若信号被阻塞,在阻塞期间多次产生该信号,则未决信号集中(pending
)仅记录一次。 - 系统默认的block表是全
0
的 - 对应普通信号,当自定义处理函数在执行时,该时期内是不能接受到同编号的信号的,即:当一个信号被递达时,
block
表里对应的位置会置为1
3.1 sigset_t
sigset_t
C语言给我们提供的位图类型,我们无法直接修改信号集,需要利用sigset
先创建用户层位图,然后间接修改。
一般我们创建了sgset_t
位图时,要先清空,然后再自行修改好用户层的位图,再调用函数修改内核层的信息集。
信号集操作函数
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
介绍一些常用的(我们使用时的 组合拳):
sigemptyset
:清空set
,全置为0
sigfillset
:全设置成1
sigaddset
:加入signo
信号,即set
中对应位置设置成1
sigaddset
:删除signo
信号sigismember
:检查信号是否在信号集中- 信号存在:返回
1
- 信号不存在:返回
0
- 信号存在:返回
信号集“上传”函数
sigprocmask
:将用户空间的信号集配置传递到内核
- 原型:
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
how
:如何修改SIG_BLOCK
:将 set 中的信号(为1
的位置)添加到当前阻塞集。SIG_UNBLOCK
:从当前阻塞集中移除 set 中的信号SIG_SETMASK
:用set
完全替换当前阻塞集
set
:用户层的信号集oset
:输出型参数:返回原来的信号集,如果不需要设置成:nullptr
3.2 . 自定义处理动作sigaction
sigaction
也是用来自定义信号处理函数的,但是它更强大。
原型:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum
:信号编号act
:sigaction
类型的结构体(里面可以存自定义方法、sigset_t
表等)oldact
:输出型参数
和signal
的区别就在于这个sigacion
类型的结构体:
- 其中,
sa_handler
就是自定义函数指针,sa_mask
是信号集,用来设置在执行这个自定义函数时的block
表,用来阻塞特定的信号
示例:
void handler(int signum)
{cout << "进程:" << getpid() << "捕抓到: " << signum << endl;while(true){}
}int main()
{signal(3, handler);sigset_t my_block;sigemptyset(&my_block);sigaddset(&my_block, 3);struct sigaction act2;act2.sa_handler = handler;act2.sa_flags = 0;act2.sa_mask = my_block;sigaction(2, &act2, nullptr);while(true){}return 0;
}
运行:
进入2
信号的自定义处理函数后,2
号和3
号信号都被阻塞了。
4. handler表
handler
表是一个函数指针数组,里面存储的就是对应的信号的处理函数。
- 当我们
signal
/sigaction
自定义函数的时候,其实改的就是对应下标的函数指针。
🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!