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

Linux——进程信号(2)(函数信号与软件信号与硬件中断)

进程信号

  • 信号生成函数
    • kill 函数
    • raise 函数
    • abort 函数
  • 软件条件产生的信号
    • SIGPIPE信号
    • alarm函数与SIGALRM信号
      • alarm函数
      • SIGALRM信号
      • 实验对比:IO效率对程序性能的影响
      • 设置重复闹钟的实现
    • 软件条件的深入理解
    • 扩展知识:更精确的定时器
  • 硬件异常产生信号
    • 硬件异常信号概述
    • 典型硬件异常信号分析
    • Core Dump机制
    • 硬件异常处理流程
    • 关键问题思考
      • 信号集的基本概念(补充)
    • 扩展知识:CPU异常处理机制
    • 常见问题解答

信号生成函数

kill 函数

1.基本功能

用于向指定进程发送指定信号。是Linux系统中kill命令的实现基础。

2.函数原型

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

3.参数说明
pid:目标进程ID

  • pid > 0:发送给指定PID的进程

  • pid = 0:发送给与调用进程同组的所有进程

  • pid = -1:发送给所有有权限发送的进程(除init进程外)

  • pid < -1:发送给进程组ID等于pid绝对值的所有进程

sig:要发送的信号编号(如SIGTERM=15,SIGKILL=9)

4.返回值
成功:返回0

失败:返回-1,并设置errno

5.示例代码

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

int main(int argc, char *argv[]) {
    if(argc != 3) {
        std::cerr << "Usage: " << argv[0] << " -signumber pid" << std::endl;
        return 1;
    }
    int signum = std::stoi(argv[1]+1); // 跳过'-'字符
    pid_t pid = std::stoi(argv[2]);
    return kill(pid, signum);
}

6.补充说明
权限要求:发送进程必须有权限向目标进程发送信号

特殊信号:发送信号0(空信号)可用于检查目标进程是否存在

常用信号

  • SIGTERM(15):温和终止信号,可被捕获和处理

  • SIGKILL(9):强制终止信号,不可被捕获或忽略

  • SIGINT(2):终端中断信号(通常由Ctrl+C产生)

raise 函数

1.基本功能

向当前进程(自己)发送指定信号,相当于kill(getpid(), sig)。

2.函数原型

#include <signal.h>

int raise(int sig);

3.参数说明
sig:要发送的信号编号

4.返回值
成功:返回0

失败:返回非零值

5.示例代码

#include <iostream>
#include <unistd.h>
#include <signal.h>

void handler(int signum) {
    std::cout << "Received signal: " << signum << std::endl;
}

int main() {
    signal(SIGINT, handler); // 设置SIGINT(2)的信号处理函数
    while(true) {
        sleep(1);
        raise(SIGINT); // 每隔1秒向自己发送SIGINT信号
    }
}

6.补充说明
线程安全:在多线程环境中,raise()会向调用线程而不是整个进程发送信号

用途:常用于触发信号处理程序或测试信号处理逻辑

abort 函数

1.基本功能
使当前进程异常终止,发送SIGABRT信号(6号信号)给自己。

默认行为是终止进程并生成核心转储文件

2.函数原型

#include <stdlib.h>

void abort(void);

3.返回值

无返回值(函数不会返回)

4.示例代码

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

void handler(int signum) {
    std::cout << "Received signal: " << signum << std::endl;
    // 即使捕获了SIGABRT,进程仍会终止
}

int main() {
    signal(SIGABRT, handler);
    sleep(1);
    abort(); // 发送SIGABRT信号
    std::cout << "This line will never be executed" << std::endl;
    return 0;
}

5.补充说明
不可阻挡:即使捕获或忽略了SIGABRT信号,abort()仍会终止进程。

清理工作:abort()不会调用atexit()注册的函数。

与exit的区别

exit()是正常终止,会执行清理工作。

abort()是异常终止,可能生成核心转储。

用途:通常在检测到严重错误(如断言失败)时调用。

软件条件产生的信号

SIGPIPE信号

1.产生条件
当进程向一个已经关闭读端的管道或socket写入数据时产生。

是一种由软件条件触发的信号。

2.默认行为
终止进程。

3.处理建议
通常应该捕获并处理SIGPIPE信号。

或者在写操作前检查管道/套接字状态。

alarm函数与SIGALRM信号

alarm函数

1.功能
设置一个定时器(闹钟),在指定秒数后向进程发送SIGALRM信号

2.函数原型

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

3.参数
seconds:定时时间(秒)

0表示取消之前设置的闹钟

4.返回值
之前设置的闹钟剩余时间(秒)

如果没有之前设置的闹钟则返回0

5.特点
每个进程只能有一个活跃的alarm定时器

新的alarm调用会覆盖之前的设置

SIGALRM信号

1.默认行为
终止进程

2.常见用途
实现超时机制。

周期性任务调度。

性能测试(如计算CPU运算速度)。

实验对比:IO效率对程序性能的影响

高IO版本(效率低)

#include <iostream>
#include <unistd.h>

int main() {
    int count = 0;
    alarm(1);
    while(true) {
        std::cout << "count: " << count << std::endl;
        count++;
    }
    return 0;
}

结果:1秒内只能计数到约10万次

低IO版本(效率高)

#include <iostream>
#include <unistd.h>
#include <signal.h>

int count = 0;

void handler(int) {
    std::cout << "count: " << count << std::endl;
    exit(0);
}

int main() {
    signal(SIGALRM, handler);
    alarm(1);
    while(true) count++;
    return 0;
}

结果:1秒内能计数到约5亿次

结论
IO操作(如打印输出)会显著降低程序执行速度.

减少IO可以大幅提高CPU密集型任务的性能.

设置重复闹钟的实现

1.实现方法

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <vector>
#include <functional>

using func_t = std::function<void()>;
std::vector<func_t> gfuncs;

void handler(int) {
    for(auto &f : gfuncs) f();
    alarm(1); // 重新设置闹钟
}

int main() {
    // 添加周期性任务
    gfuncs.push_back([](){ /* 任务1 */ });
    gfuncs.push_back([](){ /* 任务2 */ });
    
    signal(SIGALRM, handler);
    alarm(1); // 初始设置
    
    while(true) {
        pause(); // 等待信号
    }
}

2.关键点
在信号处理函数中重新设置alarm。

使用pause()挂起进程等待信号。

可以注册多个周期性任务。

软件条件的深入理解

1.定义
软件条件是指由程序内部状态特定软件操作触发的信号产生机制,区别于硬件产生的信号(如SIGSEGV)。

2.常见软件条件信号
SIGALRM:由alarm/setitimer等定时器函数触发

SIGPIPE:向断开连接的管道/套接字写入数据

SIGURG:套接字上出现紧急数据

SIGCHLD:子进程状态改变

3.操作系统实现原理
定时器管理:

内核维护定时器数据结构(如timer_list)

使用时间轮或堆结构高效管理大量定时器

定时器超时后触发中断处理程序

信号传递:

内核检查目标进程的信号屏蔽字(信号是可以设置屏蔽的)

将信号加入待处理信号集

在适当时机(如系统调用返回前)递送信号

扩展知识:更精确的定时器

1.setitimer函数

#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value,
              struct itimerval *old_value);

提供更精确的定时控制(微秒级)

支持三种定时器类型

  • ITIMER_REAL:真实时间,产生SIGALRM

  • ITIMER_VIRTUAL:进程虚拟时间,产生SIGVTALRM

  • ITIMER_PROF:进程虚拟时间+系统时间,产生SIGPROF

2.现代替代方案
timer_create:POSIX定时器API

epoll/select:可用于实现高精度定时

timerfd:Linux特有的定时器文件描述符

3. 实际应用场景

实现超时机制

void operation_with_timeout(int seconds) {
    signal(SIGALRM, [](int){ /* 超时处理 */ });
    alarm(seconds);
    // 执行可能阻塞的操作
    alarm(0); // 取消超时
}

周期性任务调度

void schedule_periodic_task(int interval) {
    signal(SIGALRM, [](int){ 
        /* 执行任务 */
        alarm(interval); // 重新调度
    });
    alarm(interval);
}

性能基准测试

void benchmark() {
    volatile int count = 0;
    signal(SIGALRM, [](int){ 
        std::cout << "Operations per second: " << count << std::endl;
        exit(0);
    });
    alarm(1);
    while(true) count++;
}

4. 注意事项

信号安全性

信号处理函数应只使用异步信号安全函数

避免在信号处理函数中进行复杂操作

竞态条件

alarm返回值和实际信号递送之间可能存在竞争

考虑使用sigprocmask进行信号屏蔽

多线程环境

在多线程程序中,信号处理变得更加复杂

建议使用专门的信号处理线程

可移植性

alarm的精度有限(秒级)

考虑使用POSIX定时器API提高可移植性

硬件异常产生信号

硬件异常信号概述

1.产生机制
硬件检测到异常(如除零、非法内存访问)后通知内核

内核将硬件异常转换为相应信号发送给进程

2.常见硬件异常信号

信号编号触发条件默认动作
SIGFPE8算术异常(如除零、溢出)Core
SIGSEGV11无效内存引用(段错误)Core
SIGILL4非法指令Core
SIGBUS7总线错误(内存对齐错误等)Core
SIGTRAP5断点/陷阱指令Core

典型硬件异常信号分析

1.SIGFPE(浮点异常)

模拟除零异常

#include <stdio.h>
#include <signal.h>

void handler(int sig) {
    printf("Caught signal: %d\n", sig);
    // 注意:不退出会导致无限循环接收信号
}

int main() {
    signal(SIGFPE, handler);  // 注册信号处理函数
    sleep(1);
    
    int a = 10;
    a /= 0;  // 触发除零异常
    
    while(1);  // 保持进程不退出
    return 0;
}

关键现象
会不断收到SIGFPE信号

原因:CPU状态寄存器未被清除,异常状态持续存在

2.SIGSEGV(段错误)
模拟野指针访问

#include <stdio.h>
#include <signal.h>

void handler(int sig) {
    printf("Caught signal: %d\n", sig);
}

int main() {
    signal(SIGSEGV, handler);  // 注册信号处理函数
    sleep(1);
    
    int *p = NULL;
    *p = 100;  // 触发段错误
    
    while(1);  // 保持进程不退出
    return 0;
}

关键现象
会不断收到SIGSEGV信号

原因:非法内存访问状态未被修复

Core Dump机制

1.基本概念
进程异常终止时将用户空间内存数据保存到core文件

用于事后调试(Post-mortem Debug)
在这里插入图片描述

2.配置方法

# 查看当前core文件限制
ulimit -a

# 设置core文件大小限制(单位:KB)
ulimit -c 1024

# 取消限制(允许生成任意大小的core文件)
ulimit -c unlimited

在这里插入图片描述
在这里插入图片描述

3.产生Core Dump的信号

  • SIGQUIT(Ctrl+\)

  • SIGABRT

  • SIGFPE

  • SIGSEGV

  • SIGILL

  • SIGBUS

4.示例:获取子进程退出状态

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main() {
    if (fork() == 0) {  // 子进程
        sleep(1);
        int a = 10;
        a /= 0;  // 触发除零异常
        exit(0);
    }
    
    int status = 0;
    waitpid(-1, &status, 0);
    printf("Exit signal: %d, core dump: %d\n", 
           status & 0x7F, 
           (status >> 7) & 1);
    return 0;
}

硬件异常处理流程

1.异常发生:

CPU执行指令时检测到异常

2.硬件响应:

保存当前上下文

跳转到内核异常处理程序

3.内核处理:

分析异常类型

转换为相应信号

检查进程的信号处理方式

4.信号递送:

如果捕获了信号,调用用户注册的处理函数

否则执行默认动作(终止+可能core dump)

关键问题思考

1.为什么必须由OS处理信号?
OS是进程的管理者,具有最高权限

只有OS能访问硬件和CPU状态寄存器

保证系统安全和稳定性

2.信号是否立即处理?
不是立即处理,而是在从内核态返回用户态时处理

内核会在以下时机检查并处理待处理信号:

  • 系统调用返回时

  • 中断处理完成时

  • 进程从睡眠状态被唤醒时

3.信号如何暂存?
内核为每个进程维护两个信号集:

  • pending:已产生但未递送的信号

  • blocked:被阻塞的信号

使用位图结构高效存储

信号集的基本概念(补充)

1. 信号集定义
信号集(sigset_t)是一个位掩码数据结构,用于表示一组信号的集合。每个信号对应一个位,位值为1表示信号在集合中,0表示不在集合中。

2. 信号集的数据类型
在Linux中,信号集通常定义为:

typedef struct {
    unsigned long sig[_NSIG_WORDS];
} sigset_t;

3. 信号集的作用
表示被阻塞的信号集合

表示等待处理的信号集合

表示信号处理函数的信号掩码


4.进程如何知道信号处理方式?

a.每个进程的PCB中保存信号处理表
包含每个信号的处理方式:

  • 忽略

  • 默认

  • 自定义处理函数

b.完整的信号处理流程
信号产生

硬件异常/软件条件/其他进程发送

信号记录

内核将信号加入目标进程的pending集合

信号递送

在合适时机检查pending信号

对于未阻塞的信号

执行默认动作,或调用用户注册的处理函数

处理完成

从信号处理函数返回后恢复原执行流程

扩展知识:CPU异常处理机制

1.CPU异常分类

故障(Fault)

可修复的异常(如页故障)

修复后重新执行指令

陷阱(Trap)

指令执行后触发(如断点)

用于调试和系统调用

中止(Abort)

严重错误(如硬件故障)

通常导致进程终止

2.关键寄存器
CR0-CR4:控制寄存器

EFLAGS:状态标志寄存器

TF(Trap Flag):单步调试

IF(Interrupt Flag):中断使能

IDTR:中断描述符表寄存器

常见问题解答

1.为什么有些信号会不断重复触发?

因为异常状态未被清除

解决方案:

在信号处理函数中修复异常状态,或直接终止进程

2.如何区分不同类型的SIGSEGV?
通过si_addr(访问地址)和si_code(错误代码):

复制
void handler(int sig, siginfo_t *info, void *ucontext) {
    printf("Fault address: %p\n", info->si_addr);
    printf("Reason: %d\n", info->si_code);
}

3.为什么有时看不到core文件?

可能原因:

  • ulimit限制

  • 文件系统权限

  • 存储空间不足

  • 进程工作目录不可写


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

相关文章:

  • 问题:md文档转换word,html,图片,excel,csv
  • 《Git江湖录·分支篇》
  • 阿里巴巴1688类网站高保真原型设计
  • 文献分享: ColXTR——将ColBERTv2的优化引入ColXTR
  • Stable Diffusion 3.0 :一键开启你的AI绘画之旅
  • Rust从入门到精通之入门篇:6.函数
  • SpringBoot中安全的设置阿里云日志SLS的accessKey
  • 26考研——栈、队列和数组_栈(3)
  • 三维空间中点、线、面的关系
  • 详细介绍Qt中用于断言的宏 Q_ASSERT
  • k8s存储介绍(五)PV与PVC
  • 【Rust】使用 Rust 语言实践完整的 TDD(测试驱动开发)流程
  • RK3568 驱动和设备匹配的几种方法
  • STM32F103_LL库+寄存器学习笔记04 - GPIO设置输出模式
  • 6.4考研408数据结构图论核心知识点深度解析
  • 《Oracle DBA入门实战:十大高频问题详解与避坑指南》
  • 卷积神经网络 - AlexNet
  • DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加行拖拽排序功能示例3,TableView16_03 拖拽视觉反馈示例
  • [算法笔记]一段数量变化的无序区间的中位数查找--双堆法
  • 【区块链安全 | 第六篇】NFT概念详解