当前位置: 首页 > article >正文

【Linux】25.进程信号(1)

文章目录

  • 1. 信号入门
    • 1.1 进程与信号的相关知识
    • 1.2 技术应用角度的信号
    • 1.3 注意
    • 1.4 信号概念
    • 1.5 信号处理常见方式概览
  • 2. 产生信号
    • 2.1 通过终端按键产生信号
    • 2.2 调用系统函数向进程发信号
    • 2.3 由软件条件产生信号
    • 2.4 硬件异常产生信号
    • 2.5 信号保存
  • 3. 阻塞信号
    • 3.1 信号其他相关常见概念
    • 3.2 在内核中的表示
    • 3.3 sigset_t
    • 3.4 信号集操作函数
      • sigprocmask
      • sigpending


1. 信号入门

1.1 进程与信号的相关知识

  1. 进程 必须 识别+能够处理信号(信号没有产生,也要具备处理信号的能力)信号的处理能力,属于进程内置功能的一部分

  2. 进程即便是没有收到信号,也能知道哪些信号该怎么处理

  3. 当进程真的收到了一个具体的信号的时候,进程可能并不会立即处理这个信号,需要等到合适的时候

  4. 一个进程,当信号产生到信号开始被处理,就一定会有时间窗口,进程具有临时保存哪些信号已经发生了的能力


1.2 技术应用角度的信号

用户输入命令,在Shell下启动一个前台进程。

用户按下Ctrl+C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程

前台进程因为收到信号,进而引起进程退出

ctrl+c为什么能够杀掉我们前台进程呢?

Linux中,一次登陆中,一个终端一般会配上一个bash,每一个登陆,只允许一个进程是前台进程,可以允许多个进程是后台进程。

键盘输入首先是被前台进程收到的。(这是前台进程和后台进程的本质区别)

ctrl +c本质是被进程解释成为收到了信号。ctrl+c 会触发SIGINT信号(信号编号2),然后终端驱动程序捕获这个按键组合,将SIGINT信号发送给前台进程组的所有进程。

  1. 前台进程特性

    • 与终端关联

    • 能够接收终端输入

    • 属于当前终端的前台进程组

  2. 只能终止前台进程的原因

    • 终端只与前台进程组关联

    • 后台进程组收不到终端产生的信号

关键点:ctrl+c 本质是通过信号机制来终止进程的,而不是直接"杀死"进程。

b1cca7f77b86b1a13f67caf132fd63bf

1-31是普通信号,34-64是实时信号。

信号的处理方式:

  1. 默认动作

  2. 忽略

  3. 自定义动作(信号的捕捉)

例如红灯亮了就等绿灯是默认动作,不管红灯闯红灯就是忽略,红灯了唱歌跳舞就是自定义动作。

进程收到2号信号的默认动作,就是终止自己。

不是所有的信号都是可以被signal捕捉的,比如:9,19。

但是无论信号如何产生,最终一定是谁发送给进程的?

OS,因为OS是进程的管理者。


1.3 注意

  1. Ctrl+C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
  2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl+C 这种控制键产生的信号。
  3. 前台进程在运行过程中用户随时可能按下 Ctrl+C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

1.4 信号概念

信号是进程之间事件异步通知的一种方式,属于软中断。


1.5 信号处理常见方式概览

(sigaction函数稍后详细介绍),可选的处理动作有以下三种:

  1. 忽略此信号。
  2. 执行该信号的默认处理动作。
  3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。

2. 产生信号

2.1 通过终端按键产生信号

SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump。

Core Dump

首先解释什么是Core Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是core,这叫做Core Dump。

进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。

默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。


2.2 调用系统函数向进程发信号

首先在后台执行死循环程序,然后用kill命令给它发SIGSEGV信号。

kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。

raise函数可以给当前进程发送指定的信号(自己给自己发信号)。

#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
这两个函数都是成功返回0,错误返回-1

abort函数使当前进程接收到信号而异常终止。

#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。

2.3 由软件条件产生信号

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。

2.4 硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。以下是几种常见的硬件异常及其对应的信号:

  1. 除零异常(SIGFPE):
    当程序执行除以0的操作时,CPU的算术逻辑单元会检测到这个异常。例如:
int a = 1;
int b = 0;
int c = a / b;  // 触发SIGFPE信号
  1. 段错误(SIGSEGV):
    当程序访问了非法内存地址时,内存管理单元(MMU)会产生异常。例如:
int *p = NULL;
*p = 1;         // 访问空指针,触发SIGSEGV信号

int arr[10];
arr[10000] = 1; // 数组越界,可能触发SIGSEGV信号
  1. 非法指令(SIGILL):
    当CPU执行了非法指令时产生此信号:
void (*bad_func_ptr)() = (void (*)())0x12345678;
bad_func_ptr();  // 执行非法地址的代码,触发SIGILL信号
  1. 总线错误(SIGBUS):
    当访问未对齐的内存地址时可能产生此信号:
char *ptr = (char *)0x12345;
int *iptr = (int *)ptr;
*iptr = 1;      // 可能触发SIGBUS信号

在系统层面,这些硬件异常的处理流程是:

  1. 硬件检测到异常
  2. 触发CPU中断
  3. CPU切换到内核态
  4. 内核将硬件异常转换为相应的信号
  5. 内核向进程发送信号
  6. 如果进程注册了信号处理函数,则执行该函数
  7. 如果没有注册处理函数,则执行信号的默认处理动作(通常是终止进程)

这就是为什么C/C++中的很多运行时错误(如除零、空指针解引用、数组越界等)最终都表现为进程收到信号并终止。这种机制让操作系统能够及时发现并处理程序中的严重错误,防止错误程序继续运行可能造成的更大危害。


2.5 信号保存

为什么要信号保存?

进程收到信号之后,可能不会立即处理这个信号。信号不会被处理,就要有一个时间窗口。


3. 阻塞信号

3.1 信号其他相关常见概念

  • 实际执行信号的处理动作称为信号递达(Delivery)

  • 信号从产生到递达之间的状态,称为信号未决(Pending)。

  • 进程可以选择阻塞 (Block )某个信号。

  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.

  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。


3.2 在内核中的表示

信号在内核中的表示示意图

fca6ee5b6f978ae7d0db9dec37b27c60

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。


3.3 sigset_t

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。

因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。


3.4 信号集操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_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所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。

  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。

  • 注意:在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

b5469757cf1306db6a8ca96d07d949ea

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。


sigpending

#include <signal.h>
sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1

代码:

void printsigset(sigset_t *set)
{
    // 打印信号集中的信号状态,用1表示信号在集合中,0表示不在
    for(int i=1; i<32; i++) {
        if (sigismember(set, i)) {  // 判断信号i是否在信号集set中
            putchar('1');
        } else {
            putchar('0');
        }
    }
    puts("");
}

int main()
{
    sigset_t s, p;
    sigemptyset(&s);        // 初始化信号集s为空集
    sigaddset(&s, SIGINT);  // 将SIGINT信号添加到信号集s中,Ctrl+C
    sigprocmask(SIG_BLOCK, &s, NULL);  // 设置信号屏蔽字,阻塞SIGINT信号
    
    while(1) {
        sigpending(&p);     // 获取未决信号集
        printsigset(&p);    // 打印未决信号集
        sleep(1);
    }
    return 0;
}

程序运行时,每秒钟把各信号的未决状态打印一遍,由于我们阻塞了SIGINT信号,按Ctrl+C将会使SIGINT信号处于未决状态。按Ctrl+\仍然可以终止程序,因为SIGQUIT信号没有阻塞。

代码:

// 打印未决信号集的函数
void PrintPending(sigset_t &pending)
{
    // 从31号信号到1号信号逐个检查
    for (int signo = 31; signo >= 1; signo--)
    {
        if (sigismember(&pending, signo))
            cout << "1";  // 信号处于未决状态
        else
            cout << "0";  // 信号不在未决集中
    }
    cout << "\n\n";
}

// 信号处理函数
void handler(int signo)
{
    cout << "catch a signo: " << signo << endl;
}

int main()
{
    // 4. 屏蔽所有可屏蔽信号
    sigset_t bset, oset;
    sigemptyset(&bset);    // 清空信号集
    sigemptyset(&oset);    // 清空旧信号集
    for (int i = 1; i <= 31; i++)
    {
        sigaddset(&bset, i); // 将所有信号添加到屏蔽集
    }
    sigprocmask(SIG_SETMASK, &bset, &oset); // 设置信号屏蔽字

    // 循环检测未决信号
    sigset_t pending;
    while (true)
    {
        int n = sigpending(&pending);  // 获取未决信号集
        if (n < 0)
            continue;
        PrintPending(pending);         // 打印未决信号集
        sleep(1);
    }


    // // 0. 对2号信号进行自定义捕捉
    // signal(2, handler);

    // // 1. 先对2号信号进行屏蔽 --- 数据预备
    // sigset_t bset, oset; // 在哪里开辟的空间???用户栈上的,属于用户区
    // sigemptyset(&bset);
    // sigemptyset(&oset);
    // sigaddset(&bset, 2); // 我们已经把2好信号屏蔽了吗?并没有设置进入到你的进程的task_struct
    // // 1.2 调用系统调用,将数据设置进内核
    // sigprocmask(SIG_SETMASK, &bset, &oset); // 我们已经把2好信号屏蔽了吗?ok

    // // 2. 重复打印当前进程的pending 0000000000000000000000000
    // sigset_t pending;
    // int cnt = 0;
    // while (true)
    // {
    //     // 2.1 获取
    //     int n = sigpending(&pending);
    //     if (n < 0)
    //         continue;
    //     // 2.2 打印
    //     PrintPending(pending);

    //     sleep(1);
    //     cnt++;
    //     // 2.3 解除阻塞
    //     if(cnt == 20)
    //     {
    //         cout << "unblock 2 signo" << endl;
    //         sigprocmask(SIG_SETMASK, &oset, nullptr); // 我们已经把2好信号屏蔽了吗?ok
    //     }
    // }
    // // 3 发送2号 0000000000000000000000010

    return 0;
}

被注释的代码:

// 0. 设置2号信号(SIGINT)的处理函数
signal(2, handler);

// 1. 先对2号信号进行屏蔽 --- 数据预备
sigset_t bset, oset;    // 在用户栈上创建信号集
sigemptyset(&bset);     // 初始化为空集
sigemptyset(&oset);     // 保存旧的信号屏蔽字
sigaddset(&bset, 2);    // 只添加2号信号到屏蔽集
// 调用系统调用,将数据设置进内核
sigprocmask(SIG_SETMASK, &bset, &oset);

// 2. 监控未决信号状态
// 重复打印当前进程的pending 0000000000000000000000000
sigset_t pending;
int cnt = 0;
while (true)
{
    int n = sigpending(&pending);  // 获取未决信号
    if (n < 0)
        continue;
    PrintPending(pending);         // 打印未决信号状态

    sleep(1);
    cnt++;
    // 20秒后解除2号信号的屏蔽
    if(cnt == 20)
    {
        cout << "unblock 2 signo" << endl;
        // 恢复原来的信号屏蔽字,即解除屏蔽
        sigprocmask(SIG_SETMASK, &oset, nullptr);
    }
}
// 3 发送2号 0000000000000000000000010

两个场景的区别:

  1. 当前执行的代码:
    • 屏蔽所有可屏蔽信号
    • 持续监控所有信号的未决状态
    • 信号会一直保持在未决状态
  2. 注释掉的代码:
    • 只屏蔽SIGINT(2号)信号
    • 设置了SIGINT的自定义处理函数
    • 20秒后解除屏蔽,让信号能够被处理
    • 可以观察到SIGINT信号从未决变为已处理的过程

注释中的重要说明:

  • task_struct:进程描述符,在内核中保存进程的信号屏蔽字
  • 信号集虽然在用户栈上定义,但实际的屏蔽操作是在内核中完成
  • 通过注释分步骤展示了信号屏蔽、监控和解除屏蔽的完整流程

http://www.kler.cn/a/535640.html

相关文章:

  • 深入浅出 NRM:加速你的 npm 包管理之旅
  • 车载以太网__传输层
  • 禅道社区版项目管理软件部署(记录篇)
  • 【ABB阀门定位器EDP300如何进行自整定】
  • 优化fm.jiecao.jcvideoplayer_lib中视频横竖屏自动适配原视频方案
  • OpenEuler学习笔记(十六):搭建postgresql高可用数据库环境
  • GGML、GGUF、GPTQ 都是啥?
  • MySQL 主从复制原理及其工作过程
  • unity学习28:灯光light相关 类型type,模式mode等
  • Java面试常见问题总结
  • 【苍穹外卖 Day1】前后端搭建 Swagger导入接口文档
  • JVM为什么要指针压缩?为什么能指针压缩?原理是什么?
  • 【1】高并发导出场景下,服务器性能瓶颈优化
  • 3D图形学与可视化大屏:什么是片段着色器,有什么作用。
  • 保姆级教程Docker部署KRaft模式的Kafka官方镜像
  • Sentinel 断路器在Spring Cloud使用
  • 【AI编程】从实践出发,分享“儿童时钟学习”小程序的改版历程
  • 【Linux】26.进程信号(2)
  • 解密 Java Lambda 表达式中的 “effectively final“ 陷阱
  • AI大模型训练实战:分布式与微调指南
  • 精选五款报表工具:提升企业决策效率和数据洞察
  • Mybatis篇
  • OPENGLPG第九版学习 - 着色器基础
  • 为什么在springboot中使用autowired的时候它黄色警告说不建议使用字段注入
  • Python-基于PyQt5,Pillow,pathilb,imageio,moviepy,sys的GIF(动图)制作工具(进阶版)
  • Spring MVC整体结构介绍,图文+代码