Linux下 进程信号初识和信号的产生

欢迎来到我的频道 【点击跳转专栏】

码云链接 【点此转跳】

文章目录

  • [1. 信号快速认识](#1. 信号快速认识)
    • [1.1 ⽣活⻆度的信号,什么是信号?](#1.1 ⽣活⻆度的信号,什么是信号?)
    • [1.2 信号有关操作](#1.2 信号有关操作)
      • [1.2.1 kill -l](#1.2.1 kill -l)
      • [1.2.2 kill -数字 pid](#1.2.2 kill -数字 pid)
      • [1.2.3 signal(系统调用)](#1.2.3 signal(系统调用))
  • [2. 信号的产生](#2. 信号的产生)
    • [2.1 键盘产生信号](#2.1 键盘产生信号)
    • [2.2 信号的几个细节](#2.2 信号的几个细节)
    • [2.3 硬件异常产⽣信号](#2.3 硬件异常产⽣信号)
    • [2.4 使⽤函数产⽣信号](#2.4 使⽤函数产⽣信号)
      • [1. kill(系统调用)](#1. kill(系统调用))
      • [2. 用kill函数实现⾃⼰的 kill 命令](#2. 用kill函数实现⾃⼰的 kill 命令)
      • [3. raise](#3. raise)
      • [4. abort](#4. abort)
    • [2.5 由软件条件产⽣信号](#2.5 由软件条件产⽣信号)
      • [1. alarm](#1. alarm)
      • [2. 基本alarm验证-体会IO效率问题](#2. 基本alarm验证-体会IO效率问题)
      • 3.如何简单快速理解系统闹钟
      • [4. 理解软件条件本质](#4. 理解软件条件本质)

1. 信号快速认识

1.1 ⽣活⻆度的信号,什么是信号?

在日常或者历史中 红绿灯 狼烟 闹钟 铃声 手机铃声 你考差后父母的脸色都属于信号 信号在我们生活中 处处存在!

这些事情现在并没有产生, 但是我们都知道这些信号产生后 我们应该做些什么!

结论一: 信号的处理⽅法,在信号产⽣之前,已经准备好了。

小孩子怎么识别这些信号然后判断出处理方法的呢?刚开始也许不知道,但是在常年累月的训练中,我们的大脑中构建了这些信号的产生和处理方法!

结论二: 识别信号是内置的,进程识别信号,是内核程序员写的内置特性。

我们看到绿灯不一定会直接走 我可能等一会 肚子饿的时候我也不会立即去吃饭,因为我可能做更重要的事情比如上课!

结论三: 处理信号不是立即处理的,因为进程可能在做优先级更高的事情,等合适的时候再处理!所以信号到来后要保存!

结论四: 信号到来 -> 信号保存 -> 信号处理

所以怎么处理信号??

结论五: a.默认处理(红灯亮就停止,肚子饿就吃饭,可能立即可能稍后处理,大部分进程收到信号处理方式就是终止进程)b.忽略(下课铃响 老师拖堂正常上课)c. 自定义(闹铃响了,别人起床洗漱,你选择起床跳舞);我们把自定义信号处理的动作叫做:信号捕获!!!

那么什么是信号?

信号:外部或者其他人或者硬件给进程发送的一种异步的事件通知机制!

  • 通知机制:即 告诉进程发送了什么!
  • 同步:即 上课老师要你取快递,同学和老师等你回来才接着讲课,这个叫 同步。
  • 异步:即 上课老师要你取快递,但仍然正常讲课;或者点外卖 你在打游戏,外卖员在送餐。多种事件,彼此不影响,同时发送!!
  • 事件:即 进程运行期间 终止、异常、指令退出

1.2 信号有关操作

1.2.1 kill -l

我们可以通过kill -l指令查看信号

我们把1~31号称为 普通信号 ;我们把 34~64号称为实时信号!

信号名本质是一个宏 比如#define SIGINT 2所以未来我们可以使用 数字也可以用名字。

大部分进程收到信号处理方式就是终止进程。

1.2.2 kill -数字 pid

通过

cpp 复制代码
kill -数字 pid

向目标pid的进程发送对应的信号。

1.2.3 signal(系统调用)

在 Linux 系统编程中,signal() 是 C 标准库提供的一个核心函数,用于设置或修改进程对特定信号的处理方式。

c 复制代码
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
  • signum:指定要处理的信号编号。
  • handler :指定信号到达时的处理动作,支持以下三种取值:
    • 自定义函数指针 :指向一个形如 void func(int sig) 的函数。当接收到对应信号时,进程会中断当前执行流,转而执行该函数。
    • SIG_IGN:指示进程忽略该信号(即收到信号后什么都不做)。
    • SIG_DFL:恢复系统对该信号的默认处理方式(大多数信号的默认动作是终止进程)。

函数执行成功时,返回该信号上一次 注册的处理函数指针;如果发生错误,则返回 SIG_ERR(通常为 -1)。

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

//signo: 是收到哪一个信号,才让我执行的
void handler(int signo)
{
  std::cout<< "收到了一个信号:"<<signo<<std::endl;
  //默认2号信号终止,但自定义处理信号,它不退出了
}

int main()
{
    signal(SIGINT,handler);
    while (1)
    {
        std::cout<<"test sig ...,pig:" <<getpid()<<std::endl;
        sleep(1);
    }
    return 0;
}

结论一: ctrl + c:终止进程 = 通过键盘给目标进程发送2号信号,处理动作就是终止!

2. 信号的产生

2.1 键盘产生信号

信号产生,我们已知:

  1. 用命令kill 产生
  2. 用键盘产生。
  • ctrl + c:是向目标进程发送2号信号 - - - 默认动作终止进程
  • crtl + \:是向目标进程发送3号信号 - - -默认动作终止进程
  • ctrl + z:是向目标进程发送19号信号 - - - 默认就是暂停进程

⚠️: 键盘产生的进程只能用于控制前台进程,无法控制后台进程,因为只有前台进程可以获取键盘输入!

2.2 信号的几个细节

细节一: 如1.2.3的案例代码 信号自定义捕捉方法为不退出,如果我把所有信号都自定义了,都不退出 会出什么问题??

防止有人利用信号这个机制恶意植入病毒 所以程序员定义了两个信号(9号和19号信号) 无法被捕获无法被忽略
9) SIGKILL ;19) SIGSTOP
⚠️:19号信号默认的是暂停进程 可用18号信号继续启动进程(不过进程会默认变成后台进程

细节二: 信号处理,是谁处理?有没有创建新的进程?

实际上 信号的处理是进程自己做的

复制代码
//signo: 是收到哪一个信号,才让我执行的
void handler(int signo)
{
std::cout<< "收到了一个信号:"<<signo<<"who:"<<getpid()<<std::endl;
//默认2号信号终止,但自定义处理信号,它不退出了
}

细节三: 我们怎么知道哪些信号默认的动作是什么呢?

我们可以通过 man 7 signal 查看官方手册

细节四:当我们在bash疯狂输入ctrl + c为什么bash不会被我们杀死呢?

因为bash把所有信号忽略了,默认不对任何信号产生反应,不过9和19号信号无法被忽略,所以可以用9号杀死bash 不过不建议你这么做😂

2.3 硬件异常产⽣信号

如果我们的程序 除零或者野指针,程序就会崩溃, 为什么程序会崩溃??

本质是因为: 程序因为一场,导致收到了信号,然后进程终止!除零和野指针是因为分别 收到 8和11号信号终止了进程:


问题1: 怎么证明程序异常时因为收到对应信号?

很简单我把8和11信号捕获一下不就可以了!!!

此时结果如图:

此时证明了 证明程序异常时因为收到对应信号


问题2:为什么我们对应的进程会收到对应的信号呢??

回答这个问题前,我们得知道 信号是怎么被保存的!

信号被保存在进程的task_struct结构体里!那么【1,31】号的信号有无被进程收到该用什么数据结构保存? 答案是位图 在进程的PCB里只需要有个unsignged int sigs即可存储该信息。

比特位的位置: 信号编号 。比特位的内容(0,1):是否收到信号!

所以 向目标进程发送信号的本质:修改信号位图!!


那么谁来写?

信号位图在task_struct里,修改位图本质就是在修改task_struct内核数据结构!只有OS有资格写信号!!所以 不管我们未来发送信号的方式有多少种 但是只有OS可以向目标进程写信号!

所以OS必须提供对应的系统调用来发送信号!


除零错误 -> 本质 CPU出错

*p野指针 -> 本质 MMU+页表-> MMU报错(关于MMU是什么 看下图)

而硬件报错 谁最先知道:OS!因为OS是硬件管理者,那么是谁导致硬件出问题的,肯定是当前运行的进程,所以OS可以根据报错的硬件向目标进程写信号!!


为什么 除零或者野指针,会触发硬件报错?(以除零为例子)

在CPU内 会有通用寄存器存储运算数 然后进行算逻运输 为了让我们的运算有可信度 同时CPU(x86架构下)会有个标志位寄存器(EFlags) 用于记录 CPU执行指令后的状态 会有特定标志位 表明当前计算是否可信

除0错误本质 是OF标志位置1


那么OS怎么知道,当前出错的进程是谁呢?

操作系统内部有个全局的task_struct指针 建议优化到寄存器里!

而这个指针永远会指向当前 正在运行的进程! CPU出错通过该指针 确定结构体位置 写入信号即可!


收到8号信号,我们捕捉信号(只打印,没有让进程退出)那么为什么信号会死循环呢??

因为 进程有优先级 PCB 有状态,那么该进程就会被OS继续调度!CPU内的寄存器的内容,都是当前进程的硬件上下文 因为此时硬件异常没有消除 所以当继续调用该进程的时候 就会继续给你发信号 当进程时间片到了被切换走时寄存器 内部所有数据包括 标志位寄存器的内容都会给你保存起来 当再次调度该进程的时候 寄存器内容又会恢复 一直调用 一直异常 一直发信号 一直死循环
所以不是进程死循环 而是进程不能跑 一跑就硬件报错 一报错就发信号 就一直做我们捕获后的函数(打印) 所以就看起来死循环了


关于野指针错误(简单说下)

我们在MMU查页表的时候 我们发现这个指针所指向虚拟地址无法转化为物理地址 于是就会硬件报错 识别出来就会给进程发送信号 只要错了就无法往后走 代码就不推进 OS一直识别该错误 就一直给你发信号!


问题3: core dumped 是什么?

当除0错误的时候会发送(core dumped) 而 ctrl + c终止进程发送2号信号 也没有这个 :

以下两个都是终止进程 那么这两个有什么区别吗?

core dumped本质就是进程异常退出,不过还会做个核心转储 凡事进程退出带core的都是进程自身出错!


当进程异常退出时 我们会关心 进程为什么退出?在哪里退出?

当进程运行时 大部分数据是在内存中的! 在执行某一行代码时 一旦崩溃 OS是知道的 但是为了便于用户调试 所以OS会把进程运行时的异常信息,转储到磁盘中!

我们把这种技术称为:核心转储;其目的:为了我们后续进行debug调试的!

我们可以用ulimit -a 查看当前 Shell 会话中所有系统资源的限制:

不过 我们的云服务器 一般默认把 核心转储 的功能关闭了!

我们可以用 ulimit -c 空间大小 手动把本次登录时 把核心转储功能开启!

在新机器下一般打开你也看不到生成的文件现象不明显 所以用一些比较老的云服务机器 当发生硬件异常错误后 会生成一个core文件:

当我们g++ XXX -g打入调试信息开始debug时 当我们cgdb进行调试 当我们输入 core-file core文件名 此时就会自动定位程序终止位置和终止原因:

这种debug的方式叫 事后调试!


为什么云服务器会把核心转储功能自动关闭?

因为在互联网公司中 服务器数以万计 ,程序崩溃时生成的"案发现场记录"(Core Dump)实在太大了。如果程序一直崩溃,这些巨大的文件很快就会把服务器的硬盘塞爆,导致整个系统瘫痪。而且,在崩溃时要把这些庞大的数据写进硬盘,会严重拖慢系统的运行速度,影响正常业务。


我们怎么知道,当前的进程发生了核心转储呢?

bash 创建了 子进程(我们执行的代码) 进程挂掉后的报错信息 时bash打印的:

在我们子进程退出的时候 会有个退出信息:

从0开始 第8到15位 写的是我们正常的退出状态,第0到第6位写的是退出信号,而第7位 记录的就是有没有发生core dump!

2.4 使⽤函数产⽣信号

1. kill(系统调用)

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

kill 系统调用是 Linux/Unix 系统编程中用于向进程或进程组发送信号(Signal)的核心接口。

c 复制代码
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);
  • pid(目标进程/进程组)
    • pid > 0:信号发送给指定进程 ID 的单个进程。
    • pid = 0:信号发送给与调用进程属于同一进程组的所有进程。
    • pid = -1:信号发送给调用进程有权限发送信号的所有进程(除 init 等系统特殊进程外)。
    • pid < -1:信号发送给进程组 ID 等于 pid 绝对值的所有进程。
  • sig(信号类型)
    • 指定要发送的信号编号(如 SIGTERMSIGKILL 等)。
    • 特殊用法 :当 sig = 0 时,不实际发送任何信号,仅用于检查目标进程是否存在以及调用进程是否有权限(常用于进程有效性验证)。

  • 成功 :返回 0
  • 失败 :返回 -1,并设置全局变量 errno 以指示错误原因。

2. 用kill函数实现⾃⼰的 kill 命令

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include <sys/types.h>
void Usage(const std::string &cmd)
{
    std::cout<<"Usage: "<<cmd <<"signnumber who" <<std::endl;
}

// ./mykil signumber who
int main(int argc,char * argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    int signumber=std::stoi(argv[1]);
    pid_t pid =std::stoi(argv[2]);

    int n= kill(pid,signumber);
    (void)n;

    return 0;
}

效果:

3. raise

raise 函数是 C/C++ 语言中用于向当前正在执行的程序自身发送信号 的便捷接口。它是 kill() 系统调用的简化版,专门用于进程或线程内部的信号触发。

c 复制代码
#include <signal.h>

int raise(int sig);
  • 参数 sig :要发送给自身的信号编号(如 SIGINT, SIGTERM, SIGUSR1 等)。
  • 返回值 :成功时返回 0;如果发生错误,则返回非零值,并将全局变量 errno 设置为 EINVAL(表示指定的信号无效)。

4. abort

abort() 是 C/C++ 标准库中用于强制且异常地终止当前进程 的函数。与 exit()return 不同,它通常用于程序检测到无法恢复的严重错误(如内存损坏、断言失败)时,主动"自杀"并保留现场以供事后调试。

c 复制代码
#include <stdlib.h>

void abort(void);
  • 永不返回:该函数没有返回值,调用后进程必然会被终止,不会执行其后的任何代码。
  • 触发 SIGABRT 信号abort() 的本质是向当前进程发送 SIGABRT 信号(信号编号通常为 6)。
  • 不可阻挡的终止 : 即使程序通过 signal()SIGABRT 注册了自定义处理函数,abort() 也会先执行该处理函数;但如果处理函数返回而没有终止进程,abort() 会恢复信号的默认行为并再次发送信号,确保进程最终被强制终止。
  • 生成 Core Dump :在 Linux/Unix 系统下,如果系统配置允许,abort() 会导致内核生成一个 core dump 文件,这对于事后使用 GDB 等工具定位崩溃原因至关重要。

2.5 由软件条件产⽣信号

举个例子 管道通信中 当子进程向父进程写 父进程切断读端 此时识别到软件异常,OS会向子进程发送SIGPIPE(13号)信号 杀死子进程!

发送信号的方式很多,围绕用户、硬件和软件的各种场景展开的 - - - 借助OS之手向目标进程"写"信号!

1. alarm

alarm() 是 Linux/Unix 系统编程中用于设置"软件定时器"的核心接口。它允许进程在指定的时间后接收一个 SIGALRM(14号信号)!

c 复制代码
#include <unistd.h>

unsigned int alarm(unsigned int seconds);
  • 参数 seconds:定时器的倒计时秒数。
  • 返回值 :返回上一次 设置的闹钟距离触发还剩余的秒数。如果之前没有设置过闹钟,则返回 0

核心特性与工作原理:

  • 触发 SIGALRM 信号 :经过指定的 seconds 秒后,内核会向当前调用进程发送 14号信号 SIGALRM。如果不捕获或忽略该信号,其默认动作是直接终止该进程。
  • 唯一性(覆盖旧闹钟) :每个进程有且仅有一个闹钟定时器。如果在旧闹钟未到期前再次调用 alarm(),新的倒计时将覆盖旧的,并且函数会返回旧闹钟的剩余时间。
  • 一次性触发alarm() 是一次性定时器,触发一次后自动失效。如果需要实现周期性任务,必须在自定义的信号处理函数中重新调用 alarm()
  • 取消闹钟 :当传入参数 seconds = 0 时,表示取消当前进程中所有未到期的闹钟。

2. 基本alarm验证-体会IO效率问题

程序的作⽤是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终⽌。

必要的时候,对SIGALRM信号进⾏捕捉

cpp 复制代码
// IO 多
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{
int count = 0;
alarm(1);
while(true)
{
std::cout << "count : "
<< count << std::endl;
count++;
}
return 0;
}

效果:

shell 复制代码
... ...
count : 107148
count : 107149
Alarm clock

cpp 复制代码
// IO 少
#include <iostream>
#include <unistd.h>
#include <signal.h>
int count = 0;
void handler(int signumber)
{
std::cout << "count : " <<
count << std::endl;
exit(0);
}
int main()
{
signal(SIGALRM, handler);
alarm(1);
while (true)
{
count++;
}
return 0;
}
shell 复制代码
$ g++ alarm.cc -o alarm
whb@bite:~/code/test$ ./alarm
count : 492333713

结论:

  • 闹钟会响⼀次,默认终⽌进程
  • 有IO效率低

3.如何简单快速理解系统闹钟

系统闹钟,其实本质是OS必须⾃⾝具有定时功能,并能让⽤⼾设置这种定时功能,才可能实现闹钟这样的技术。

现代Linux是提供了定时功能的,多个进程,多个定时器,定时器也要被管理:先描述,在组织。内核中的定时器数据结构是:

cpp 复制代码
struct timer_list
{
struct list_head entry;
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
struct tvec_t_base_s *base;
};

用个简单类似的结构说明一下吧:

各个闹钟 都有个未来过期时间 每一秒超时检查不需要每个都查到 只需要O(1) 找到一个离未来超时最近的即可 我们可以根据其超时时间 用最小堆进行管理! 超时后用void (*callback)()进行回掉 让OS对目标进程发送14号信号即可!然后将该节点释放后 整理堆即可!

当然OS并没有用堆 用的是时器链表 用堆说是为了方便理解!

4. 理解软件条件本质

在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产生机制。这些条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产生的SIGPIPE信号)等。当这些软件条件满足时,操作系统会向相关进程发送相应的信号,以通知进程进行相应的处理。简而言之,软件条件是因操作系统内部或外部软件操作而触发的信号产生。

相关推荐
阿坤带你走近大数据1 小时前
Linux中管道符的作用
java·linux·服务器
爱装代码的小瓶子1 小时前
安工大Linux考点分类真题解析(含知识点是试卷原题了)
linux·服务器·网络·c
hweiyu001 小时前
Linux命令:sudoedit
linux·运维·服务器
qq_163135751 小时前
Linux 【03-nl命令超详细教程】
linux
ShGamu2 小时前
自动化输送设备公司选型参考与核心维度梳理
运维·自动化·自动化输送设备
qq_163135752 小时前
Linux文件基本属性【权限】
linux
bloglin999992 小时前
docker镜像构建及部署样例
运维·docker·容器
SLD_Allen2 小时前
基于docker搭建sub2api图文教程
运维·docker·容器
我科绝伦(Huanhuan Zhou)2 小时前
文件备份系统已开源
运维·服务器