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.常见硬件异常信号
信号 | 编号 | 触发条件 | 默认动作 |
---|---|---|---|
SIGFPE | 8 | 算术异常(如除零、溢出) | Core |
SIGSEGV | 11 | 无效内存引用(段错误) | Core |
SIGILL | 4 | 非法指令 | Core |
SIGBUS | 7 | 总线错误(内存对齐错误等) | Core |
SIGTRAP | 5 | 断点/陷阱指令 | 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限制
-
文件系统权限
-
存储空间不足
-
进程工作目录不可写