Linux信号深度解剖:5种产生、3张表、4次切换

文章目录

1、信号的认识

信号是一种由外部、其他人或硬件发送给进程的异步事件通知机制。严格来说也算进程间通信方式,但与IPC有很大不同,信号只负责通知进程。

linux下有32个普通信号,本文不探究34到64的实时信号。

信号的生命周期有三个阶段,1、信号产生;2、信号保持;3、信号处理。

信号的处理有三种方式,1、忽略;2、默认处理(退出进程);3、自定义(捕捉)处理。

其中,9号信号(管理员信号)和19号信号不可被捕捉。

signal系统调用用于自定义信号处理行为:

cpp 复制代码
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

参数:

  • signaum:需要捕捉的信号。
  • handler:函数指针类型,指向用于处理的函数,其参数为信号。
    返回值:
  • 成功:返回指向上一个处理信号函数的函数指针。
  • 失败:返回SIG_ERR,并设置errno。

信号的处理是由进程自己处理的。

更详细的信号内容使用man 7 signal查看。

2、信号产生

信号的本质:PCB中的一个位图。信号产生的本质:操作系统向位图写数字。无论那种产生方式,都是由操作系统完成的。

2.1、shell命令产生

kill -signal PID用于向目标进程发送信号,一段demo测试。

cpp 复制代码
#include <signal.h>
#include <iostream>
#include <unistd.h>

void hander(int sig)
{
    std::cout << "收到信号:" << sig << std::endl;
    exit(1);
}

int main()
{
    signal(2, hander);
    while(1)
    {
        std::cout << "pid: " << getpid() << " run..." << std::endl;
        sleep(1);
    }
    return 0;
}

发送2号信号后就进行自定义处理方式。

kill命令最灵活,可以发送任意号信号给任意进程。

2.2、键盘产生

快捷键 信号 行为
Ctrl + c SIGINT(2) Term(退出进程)
Ctrl + </kbd> SIGQUIT(3) Core(退出并产生core dump)
Ctrl + z SIGSTOP(19) Stop(暂停进程)

细节1:core dump,会生成一个core文件,包含了调试信息,使用gdb program core可直接跳到程序崩溃位置,免去了打断点的过程。在云服务器中被云服务厂商禁用了,避免过多占用磁盘。

细节2:Core是异常退出,Term是用户发送信号退出。

细节3:19号信号暂停的进程变成后后台进程,不能读取键盘信息,只能用kill命令发送18号信号(SIGCONT)恢复。

2.3、硬件异常产生

常见的硬件异常有:除零异常和段错误。

CPU中的ALU 算术逻辑单元负责计算,当遇到除零异常时,ALU异常,操作系统一定要知道,因为操作系统是硬件的管理者。

CPU中CR3负责存储指向页表的地址,当访问空指针时,CR3和MMU异常(段错误),操作系统也能够知道。

使用signal捕捉后,证明除零异常和访问空指针会收到信号:

cpp 复制代码
#include <signal.h>
#include <iostream>
#include <unistd.h>

void hander(int sig)
{
    std::cout << "收到信号:" << sig << std::endl;
    exit(sig);
}

int main()
{
    signal(8 , hander);
    signal(11, hander);

    // 除零异常
    int a = 3;
    a /= 0;

    // 访问空指针
    // *(int*)nullptr = 1;
    return 0;
}

2.4、系统调用

kill:指定进程发送指定信号。

cpp 复制代码
int kill(pid_t pid, int sig);

参数:

  • pid:目标进程。
  • sig:发送的信号。

手写kill命令:

cpp 复制代码
#include <signal.h>
#include <iostream>

int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        std::cout << "Usage:" << argv[0] << "sig who" << std::endl;
        exit(1);
    }
    int sig   = atoi(argv[1]);
    pid_t pid = atoi(argv[2]);
    int n = kill(pid, sig);
    if(n == 0)
        std::cout << "Sucess!" << std::endl;
    return 0;
}

raise(C标准库函数):特定进程(调用的进程)发送指定信号。

cpp 复制代码
int raise(int sig);

参数:

  • sig:发送的信号。

该函数相当于kill(getpid(), sig)


abort:特定进程(调用的进程)发送特定信号(6号SIGABRT)。

cpp 复制代码
[[noreturn]] void abort(void);

相当于kill(getpid(), 6)。会参数core dump,常用于工程中主动留下调试信息。6号信号即使捕捉后不退出,也会再捕捉后自动退出。

2.5、软件条件产生

管道通信时,读端关闭,操作系统会发送信号杀死写端进程。详细见【管道通信深度剖析:从匿名管道到命名管道,手写进程池】

alarm闹钟:

cpp 复制代码
unsigned int alarm(unsigned int seconds);

参数:

  • seconds:非0表示seconds秒后发送14号信号(SIGALRM)。为0则取消还没有发送的信号。

返回值:

  • 上一个闹钟超时剩余的时间。若没有上一个闹钟则返回0。

常见误区:在循环中设置闹钟,可能反复重置超时时间,永远不出发闹钟。

2.6、常用信号汇总

信号名 编号 默认动作 说明
SIGINT 2 Term Ctrl + c
SIGQUIT 3 Core Ctrl + </kbd>
SIGABRT 6 Core abort()
SIGFPE 8 Core 除零/浮点异常
SIGKILL 9 Term 强制终止
SIGSEGV 11 Core 段错误
SIGPIPE 13 Term 无读端的管道写
SIGALRM 14 Term alrm闹钟
SIGCONT 18 Cont 唤醒进程
SIGSTOP 19 Stop 暂停进程

3、信号保存

3.1、信号相关概念

  1. 信号递达(Delivery):内核处理信号的动作。
  2. 信号未决(Pending):从信号产生到信号递达之间的状态。
  3. 阻塞(Block):被阻塞的信号将保持在未决状态,解除阻塞才能被递达。

阻塞不等于忽略,忽略是信号递达的具体一种处理方式,而被阻塞的信号将不会递达。

3.2、内核结构

内核对于阻塞、未决、递达维护了三张表(对应pending、blocked(block)、sighand(handler))。

sigset_t是一个位图,又叫信号集,可以表示信号是否有效。 对信号的操作变成了对三张表的增删查改。pending和block表都是用sigset_t类型实现的。

handler表中存储函数指针类型,记录处理信号方法,默认为SIG_DFL(清理进程资源并退出进程,由0强转),忽略信号为SIG_IGN(由1强转)。

三张表关系:

block pending handler
0 0 不执行
0 1 执行
1 任何 不执行

block(阻塞信号集)又叫屏蔽字信号集,类比于umask。

3.3、信号集操作函数

cpp 复制代码
#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);
  1. sigemptyset:初始化set指向的信号集,使位图中的每一个数位清0。
  2. sigfillset:初始化set指向的信号集,使位图中每一位数位置1。
  3. sigaddset:set指向的信号集中,使signo信号对应数位置1。
  4. sigdelset:set指向的信号集中,使signo信号对应数位置0。
  5. sigmember:判断set指向的信号集中signo是否为1。

读取或更改屏蔽字信号集:

cpp 复制代码
int sigprocmask(int how, const sigset_t *_Nullable restrict set,sigset_t *_Nullable restrict oldset);

参数:

  • how:通常传递宏SIG_BLOCK(新增屏蔽字,block |= set)、SIG_UNBLOCK(删除屏蔽字,block &= ~set)、SIG_SETMASK(覆盖屏蔽字信号集,block = set)。
  • set:输入型参数。
  • oldset:输出型参数,被设置为修改前的屏蔽字信号集。

sigpending用于获取当前进程的pending表位图:

cpp 复制代码
int sigpending(sigset_t *set);

参数:

  • set:输出型参数,将内核中pending位图复制到set指向的信号集中。

3.4、查看屏蔽与未决

按照一下流程可看到信号集变化的过程。
#mermaid-svg-bQceTgtT7dQtfa0B{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bQceTgtT7dQtfa0B .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bQceTgtT7dQtfa0B .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bQceTgtT7dQtfa0B .error-icon{fill:#552222;}#mermaid-svg-bQceTgtT7dQtfa0B .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bQceTgtT7dQtfa0B .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bQceTgtT7dQtfa0B .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bQceTgtT7dQtfa0B .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bQceTgtT7dQtfa0B .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bQceTgtT7dQtfa0B .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bQceTgtT7dQtfa0B .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bQceTgtT7dQtfa0B .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bQceTgtT7dQtfa0B .marker.cross{stroke:#333333;}#mermaid-svg-bQceTgtT7dQtfa0B svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bQceTgtT7dQtfa0B p{margin:0;}#mermaid-svg-bQceTgtT7dQtfa0B .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-bQceTgtT7dQtfa0B .cluster-label text{fill:#333;}#mermaid-svg-bQceTgtT7dQtfa0B .cluster-label span{color:#333;}#mermaid-svg-bQceTgtT7dQtfa0B .cluster-label span p{background-color:transparent;}#mermaid-svg-bQceTgtT7dQtfa0B .label text,#mermaid-svg-bQceTgtT7dQtfa0B span{fill:#333;color:#333;}#mermaid-svg-bQceTgtT7dQtfa0B .node rect,#mermaid-svg-bQceTgtT7dQtfa0B .node circle,#mermaid-svg-bQceTgtT7dQtfa0B .node ellipse,#mermaid-svg-bQceTgtT7dQtfa0B .node polygon,#mermaid-svg-bQceTgtT7dQtfa0B .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bQceTgtT7dQtfa0B .rough-node .label text,#mermaid-svg-bQceTgtT7dQtfa0B .node .label text,#mermaid-svg-bQceTgtT7dQtfa0B .image-shape .label,#mermaid-svg-bQceTgtT7dQtfa0B .icon-shape .label{text-anchor:middle;}#mermaid-svg-bQceTgtT7dQtfa0B .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bQceTgtT7dQtfa0B .rough-node .label,#mermaid-svg-bQceTgtT7dQtfa0B .node .label,#mermaid-svg-bQceTgtT7dQtfa0B .image-shape .label,#mermaid-svg-bQceTgtT7dQtfa0B .icon-shape .label{text-align:center;}#mermaid-svg-bQceTgtT7dQtfa0B .node.clickable{cursor:pointer;}#mermaid-svg-bQceTgtT7dQtfa0B .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bQceTgtT7dQtfa0B .arrowheadPath{fill:#333333;}#mermaid-svg-bQceTgtT7dQtfa0B .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bQceTgtT7dQtfa0B .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bQceTgtT7dQtfa0B .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bQceTgtT7dQtfa0B .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bQceTgtT7dQtfa0B .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bQceTgtT7dQtfa0B .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bQceTgtT7dQtfa0B .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bQceTgtT7dQtfa0B .cluster text{fill:#333;}#mermaid-svg-bQceTgtT7dQtfa0B .cluster span{color:#333;}#mermaid-svg-bQceTgtT7dQtfa0B div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bQceTgtT7dQtfa0B .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bQceTgtT7dQtfa0B rect.text{fill:none;stroke-width:0;}#mermaid-svg-bQceTgtT7dQtfa0B .icon-shape,#mermaid-svg-bQceTgtT7dQtfa0B .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bQceTgtT7dQtfa0B .icon-shape p,#mermaid-svg-bQceTgtT7dQtfa0B .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bQceTgtT7dQtfa0B .icon-shape .label rect,#mermaid-svg-bQceTgtT7dQtfa0B .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bQceTgtT7dQtfa0B .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bQceTgtT7dQtfa0B .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bQceTgtT7dQtfa0B :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 捕捉2号信号
查看信号集
屏蔽2号信号
发送2号信号
查看信号集
解除屏蔽
查看信号集

具体实现如下:

cpp 复制代码
#include <iostream>
#include <unistd.h>

void PrintSig(sigset_t& set)
{
    for(int i = 31; i > 0; --i)
    {
        if(sigismember(&set, i))
            std::cout << 1;
        else
            std::cout << 0;
    }
    std::cout << std::endl;
}

// 全局变量方便handler函数使用。
sigset_t pendding, block, oldblock;
void hander(int sig)
{
    std::cout << "收到信号:" << sig << std::endl;
    PrintSig(pendding);
}

int main()
{
    signal(2, hander);
    
    sigpending(&pendding);
    PrintSig(pendding);

    sigemptyset(&block);
    sigaddset(&block, 2);
    sigprocmask(SIG_BLOCK, &block, &oldblock); 
    
    kill(getpid(), 2);

    sigpending(&pendding);
    PrintSig(pendding);

    sigprocmask(SIG_SETMASK, &oldblock, nullptr);

    sigpending(&pendding);
    PrintSig(pendding);
    return 0;
}

结果:

bash 复制代码
0000000000000000000000000000000
0000000000000000000000000000010
收到信号:2
0000000000000000000000000000010
0000000000000000000000000000000

得出结论:解除信号后,信号立即被处理;先处理信号,后将pending相关位置0。

4、信号处理

4.1、信号处理时机

虚拟地址空间(32位)中,0~3GB称为用户态空间;3GB~4GB被称为内核态空间。

信号处理都是内核做的,因此无论怎么处理都是在内核态空间完成的。

典型场景:

  1. 系统调用完成后。
  2. 中断处理完成后。
  3. 进程被调度再次获得CPU时。

内核在返回用户态之前调用do_signal()函数,检查当前进程的pending和block表。

  • 若无信号可递达,则直接返回。
  • 若有信号且未被阻塞,则递达信号。

4.2、信号处理流程

默认处理:清理task_struct,并退出进程。

忽略:清理pending表后直接返回用户态。


自定义处理流程:

自定义处理需要4次状态切换:

  1. 进入内核态处理某种事件,用户态->内核态;
  2. 处理完后,判断是否执行信号处理,保存上下文,返回用户态执行处理逻辑,内核态->用户态;
  3. 处理完后,进入内核态加载上下文,用户态->内核态;
  4. 加载完后,返回用户态继续执行主流程,内核态->用户态;

5、总结

  1. 信号定义:异步时间通知机制,不传数据,与信号量无关。
  2. 生命周期:产生->未决->递达。
  3. 产生方式:键盘、命令、硬件异常、系统调用、软件条件。
  4. 内核三张表:block(屏蔽字)、pending(未决)、handler(处理函数)。
  5. 阻塞 不等于 忽略:阻塞阻止信号递达,忽略时递达后的一种处理方式。9、19号信号不可忽略/捕捉/阻塞。
  6. 处理时机:内核态->用户态时检查。自定义捕捉4次状态切换。
相关推荐
倔强的石头1061 小时前
【Linux指南】Linux快捷键与系统实用技巧
linux·运维·服务器
番茄地瓜1 小时前
Linux 配置静态 IP 步骤
linux·运维·服务器
liulilittle1 小时前
论 Linux 内核态全局稳态带宽的卡尔曼估计与工程实现
linux·服务器·网络·c++·计算机网络·tcp·通信
XBodhi.1 小时前
Visual Studio C++ 语法错误: 缺少“;”(在“return”的前面)
开发语言·c++·visual studio
Irissgwe2 小时前
五、应用层协议HTTP
linux·网络·网络协议·http·状态码·url
三品吉他手会点灯2 小时前
C语言学习笔记 - 43.运算符与表达式 - 运算符1 - 运算符的分类和简单介绍
c语言·笔记·学习·算法
.千余2 小时前
【Linux】 传输层协议UDP:从端口号到传输机制
linux·运维·udp
囚~徒~3 小时前
轻量化的虚拟机
linux·运维·服务器
SteveSenna3 小时前
Ubuntu 20.04 安装 Isaac Sim 4.5 + Isaac Lab
linux·运维·服务器