【Linux】进程信号(一):信号的产生与信号的保存

发布于:2025-05-26 ⋅ 阅读:(26) ⋅ 点赞:(0)

📝前言:

这篇文章我们来讲讲Linux——进程信号

🎬个人简介:努力学习ing
📋个人专栏:Linux
🎀CSDN主页 愚润求学
🌄其他专栏:C++学习笔记C语言入门基础python入门基础C++刷题专栏


一,认识信号

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 的区别
  • coreterm的基础上多了一个核心转储功能,会生成核心转储文件(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 + ctest1就收不到了,但是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_tC语言给我们提供的位图类型,我们无法直接修改信号集,需要利用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:信号编号
  • actsigaction类型的结构体(里面可以存自定义方法、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自定义函数的时候,其实改的就是对应下标的函数指针。

🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!


网站公告

今日签到

点亮在社区的每一天
去签到