进程信号机制深度解析

1. 进程信号基本理解

  • 信号是一种事件的异步通知机制
    • 信号的产生,相对于进程的运行是异步的
    • 信号是要发送给进程的
  • 在信号产生之前,信号的处理方法已经准备好了
  • 信号产生 -》 信号保存 -》信号处理

1.1 查看信号

linux 复制代码
$ kill -l

1.2 查看信号的作用

linux 复制代码
$ man 7 signal
  • Term(终止)
  • Core(终止)
  • Ign(忽略)
  • Stop(暂停)
  • Cont(继续)

1.3 signal 改变信号执行动作

cpp 复制代码
signal(SIGINT/*2*/, handler); //让2号信号的处理动作变成执行handler函数
  • SIG_IGN:忽略信号
  • SIG_DFL:信号的默认处理动作
  • handler:自定义的处理函数
cpp 复制代码
typedef void (*__sighandler_t) (int); //handler要遵守这个格式

2. 产生信号

2.1 通过终端按键产生信号

  • 给前台进程发信号
  • ctrl + c(SIGINT):发送终止信号
  • ctrl + \(SIGQUIT):发送终止信号并生成core dump文件,用于事后调试
  • ctrl+ z(SIGTSTP):发送停止信号,将前台进程挂起到后台
    • jobs:查看后台进程
    • bg 任务号:让进程恢复执行
    • fg 任务号:把进程提到前台

2.2 调用系统命令向进程发信号

  • 例如
linux 复制代码
$ kill -9 进程号 # 关闭进程

2.3 使用函数产生信号

2.3.1 kill

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

int kill(pid_t pid, int sig);
  • 参数列表
    • pid:要对哪个进程发送信号
    • sig:要发送什么信号
  • 返回值:
    • 成功,返回 0
    • 失败,返回 -1

2.3.2 raise

  • raise 函数可给当前进程发送指定的信号(自己给自己)
cpp 复制代码
#include <signal.h>

int raise(int sig)

2.3.3 abort

  • abort 函数是当前进程接收到信号而异常终止(6号信号)
cpp 复制代码
#include <stdlib.h>

void abort(void);

2.4 由软件条件产生信号

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

unsigned int alarm(unisgned int seconds);
  • 功能:告诉内核在 seconds 秒之后给当前进程发送 14)SIGALRM 信号,该信号的默认处理动作是终止当前进程
  • 闹钟设置一次起效一次

2.5 硬件异常产生信号

  • 除0:8号信号
  • 内存越界:11号信号

3. 保存信号

3.1 信号其他相关常见概念

  • 实际信号的处理动作称为信号递达
  • 信号从产生到递达之间的状态,称为信号未决
  • 进程可以选择阻塞某个信号
  • 被阻塞的信号产生时将保持在未决状态,知道进程解除对此信号的阻塞,才执行递达的动作
  • 阻塞和忽略
    • 阻塞:只要信号被阻塞就不会递达
    • 忽略:在递达之后可选的一种处理动作
  • 9号信号不可被捕捉,不可被阻塞

3.2 在内核中的表示

  • block 是阻塞表,1表示第几号信号阻塞
    • 默认情况下,进程不对任何信号进行屏蔽
    • 置1的信号不会被进程处理(挂起)
  • pending 是未决表,1 表示收到了第几号信号
    • 常规信号在递达之前产生多次只记一次
  • handler:函数指针数组,表示如何处理
    • SIG_DFL:默认
    • SIG_IGN:忽略
    • 自定义处理动作

3.3 sigset_t

  • sigset_t 称为信号集
  • 阻塞信号即也叫做当前进程的信号屏蔽字(Signal Mask)
  • 每个信号只有一个 bit 的未决标志,非 0 即 1

3.4 信号集操作函数

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); // 判断一个信号集的有效信号中是否包含某种信号 

3.4.1 sigprocmask

  • sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)
cpp 复制代码
#include <signal.h>

int sigprocmask(int how,const sigset_t *set,sigset_t *oset);
  • 返回值:
    • 成功为0
    • 失败为-1
  • 参数列表
    • how:如何更改
      • SIG_BLOCK:set 为添加信号屏蔽字的信号,设置1
      • SIG_UNBLOCK:set 为要解除阻塞的信号,设置0
      • SIG_SETMASK:将 set 指向信号屏蔽字设置为对应的值(比较方便,之间设置1,0)
    • set:对哪个信号集进行操作
    • oset:输出型参数

3.4.2 sigpending

  • 准备递达之前,要先情况 pending 信号集即中对应的信号位图 1->0
  • 如果每个信号产生多次,只能记录一次
cpp 复制代码
#include <signal.h 

int sigpending(sigset_t *set);
  • 功能:读取当前进程的未决信号集,通过 set 参数传出

4. 捕捉信号

4.1 捕捉信号的流程

  • 合适的时候处理信号
    • 进程从内核态,返回到用户态的时候进行信号检测
  • 如果信号是默认的呢?忽略呢?
    • 忽略的话把pending的对应位置置0就行,默认就去执行默认动作

4.2 sigaction

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

int sigaction((int signo, const struct sigaction *act, struct sigaction *oact);
  • 功能:读取和修改与指定信号相关联的处理动作
  • 参数列表:
    • signo:信号编号
    • act:如何处理信号(自定义捕捉方法)
    • oact:输出型参数,历史动作
  • 返回值:
    • 成功:0
    • 失败:-1
  • 当某个信号的处理函数被调用的时候,内核自动将当前信号加入进程的信号屏蔽字,当信号处理处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号的时候,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止

4.3 操作系统是如何运行的

4.3.1 硬件中断

  • OS 不需要关注外部设备是否准备好了,而是外部设备准备好了会叫操作系统
    • 当没有中断时OS是暂停的
  • 信号是纯软件,本质是用软件来模拟硬件中断

4.3.2 时钟中断

  • 以固定的、特定的频率向CPU发送特定的中断
  • 操作系统就是在硬件时钟中断的驱动下进行调度
    • 操作系统是基于中断进行工作的软件

4.3.3 死循环

  • 操作系统的本质就是一个死循环!

4.3.4 软中断

  • 让CPU内部触发中断逻辑
  • 系统调用过程中,其实就是先让 int 0x80、syscall 陷入内核,本质就是触发软中断
  • OS 不提供任何系统调用接口!OS 只提供系统调用号
  • 缺页中断、内存碎片处理、除零、野指针等等
    • 全部都会转换成为CPU内部的软中断
  • CPU内部的软中断,比如 int 0x80、syscall 叫做陷阱(主动发起,以及发起的目的合法)
  • CPU内部的软中断:比如除零/野指针等,叫做异常(不合法)

4.4 如何理解用户态和内核态

  • 操作系统无论如何切换进程,都能找到同一个操作系统
  • 用户态就是执行用户 [0,3]GB 所处的状态
  • 内核态就是执行内核 [3,4]GB 所处的状态(所有进程共享一份!)
    • 所有进程执行的内核态的位置相同
  • OS 为了保护自己,不相信任何人!必须以系统调用的方式访问 [3,4]GB
    • 用户态:以用户身份,只能访问自己的 [0,3GB]
    • 内核态:以内核身份,运行通过系统调用的方式,访问OS [3,4GB]
  • 系统中,用户或者OS自己,怎么直到当前处于内核态,还是用户态?
    • 先不考虑,很复杂。简单来说就是,由硬件决定,
    • 有个cs寄存器(其中的CPL字段)0:表示内核态;3表示用户态

5. 可重入函数

  • 如果⼀个函数只访问自己的局部变量或参数,则称为可重入函数
  • 如果一个函数符合下列条件之一则是不可重入的
    • 调用了 malloc 或 free ,因为 malloc 也是用全局链表来管理堆的
    • 调用了标准 I/O 库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构

6. volatile

  • 极端情况下,例如编译器的优化级别比较高,将 main 函数中未被修改的变量优化到了寄存器(读取比较快)中,会覆盖进程看到的变量的真实情况
  • 每次都从内存中找数据,保证内存空间的可见性
  • 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作
cpp 复制代码
volatile int flag = 0;

7. SIGCHLD

  • 子进程在终止时会给父进程发 SIGCHLD 信号,默认处理动作是忽略
    • 父进程可以自定义 SIGCHLD 处理函数
  • 系统默认行为:对于 SIGCHLD 信号,系统的默认动作(SIG_DFL)是忽略,但这种 "忽略" 是特殊的:
    • 子进程终止后,内核会保留其退出状态,等待父进程调用 wait() 或 waitpid() 来 "收尸"。
    • 如果父进程不处理,子进程就会变成僵尸进程(Zombie),其进程描述符会一直占用系统资源。
    • 显式设置 SIG_IGN:当你通过 sigaction() 或 signal() 将 SIGCHLD 的处理动作显式设置为 SIG_IGN 时,这是一种完全不同的行为:
      • 子进程终止时,内核会直接丢弃其退出状态,并自动回收所有资源,不会产生僵尸进程。
      • 父进程也不会收到任何通知,调用 wait() 会立即失败并返回 -1。
cpp 复制代码
signal(SIGCHLD, SIG_IGN); // 忽略子进程发送的SIGCHLD信号(丢弃退出状态,自动回收资源,不会产生)
相关推荐
踏着七彩祥云的小丑10 小时前
pytest——Mark标记
开发语言·python·pytest
Dream of maid10 小时前
Python12(网络编程)
开发语言·网络·php
W230357657310 小时前
经典算法:最长上升子序列(LIS)深度解析 C++ 实现
开发语言·c++·算法
Y40900110 小时前
【多线程】线程安全(1)
java·开发语言·jvm
菜菜艾10 小时前
基于llama.cpp部署私有大模型
linux·运维·服务器·人工智能·ai·云计算·ai编程
重生的黑客10 小时前
Linux开发工具:条件编译、动静态库与 make/makefile 入门
linux·运维·服务器
不爱吃炸鸡柳11 小时前
Python入门第一课:零基础认识Python + 环境搭建 + 基础语法精讲
开发语言·python
minji...11 小时前
Linux 线程同步与互斥(三) 生产者消费者模型,基于阻塞队列的生产者消费者模型的代码实现
linux·运维·服务器·开发语言·网络·c++·算法
Dxy123931021611 小时前
Python基于BERT的上下文纠错详解
开发语言·python·bert