1. Linux信号机制概述
在Linux系统中,信号是一种重要的进程间通信机制,用于通知进程发生了特定事件。就像我们日常生活中遇到的各种信号(如交通信号灯、手机来电提醒)一样,操作系统通过信号来告知进程需要处理的事件。
信号处理通常分为三个阶段:
- 信号产生:某个事件触发了信号(如用户按下Ctrl+C)
- 信号保存:进程暂时存储接收到的信号
- 信号执行:进程在合适的时间处理信号
1.1 信号的基本特性
Linux系统中的信号具有以下特点:
- 每个信号都有唯一的编号和宏定义名称(如SIGINT对应2号信号)
- 信号分为普通信号(1-31)和实时信号(34以上)
- 进程可以预先设置对信号的处理方式
信号的处理方式有三种:
- 默认动作:执行系统预设的行为(如终止进程)
- 忽略信号:完全不做任何处理
- 自定义处理:执行用户定义的函数
2. 信号的产生与处理
2.1 键盘输入与信号
当我们按下Ctrl+C时,终端会向前台进程发送SIGINT(2)信号。这个过程涉及以下步骤:
- 键盘输入被硬件检测到并产生中断
- 操作系统通过中断向量表找到对应的处理程序
- 终端驱动程序识别组合键并生成相应信号
- 操作系统将信号传递给前台进程
cpp复制#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int sig) {
std::cout << "Received signal: " << sig << std::endl;
}
int main() {
signal(SIGINT, handler); // 捕获Ctrl+C
while(1) {
std::cout << "Running..." << std::endl;
sleep(1);
}
return 0;
}
2.2 信号处理函数
signal()函数用于设置信号处理方式:
cpp复制#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);
参数说明:
- sig:信号编号
- func:处理函数指针(可以是SIG_DFL、SIG_IGN或自定义函数)
注意:signal()函数在不同Unix系统中有行为差异,建议使用更现代的sigaction()函数。
3. 信号的发送方式
3.1 kill系统调用
kill()函数可以向指定进程发送信号:
cpp复制#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
实现一个简单的kill命令:
cpp复制#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
int main(int argc, char* argv[]) {
if(argc != 3) {
std::cerr << "Usage: " << argv[0] << " <signal> <pid>" << std::endl;
return 1;
}
int sig = atoi(argv[1]);
pid_t pid = atoi(argv[2]);
if(kill(pid, sig) == -1) {
perror("kill failed");
return 1;
}
return 0;
}
3.2 raise和abort函数
raise()向当前进程发送信号:
cpp复制#include <signal.h>
int raise(int sig);
abort()使进程异常终止(发送SIGABRT):
cpp复制#include <stdlib.h>
void abort(void);
示例代码:
cpp复制#include <iostream>
#include <csignal>
#include <cstdlib>
void handler(int sig) {
std::cout << "Caught signal: " << sig << std::endl;
}
int main() {
signal(SIGABRT, handler);
std::cout << "Before abort" << std::endl;
abort();
std::cout << "After abort" << std::endl; // 不会执行
return 0;
}
4. 异常产生的信号
4.1 除零错误(SIGFPE)
当进程执行整数除以零操作时,CPU的算术逻辑单元(ALU)会检测到错误并设置溢出标志位。操作系统通过检查程序状态字(PSW)发现异常,向进程发送SIGFPE(8)信号。
cpp复制#include <iostream>
#include <csignal>
void handler(int sig) {
std::cout << "Caught SIGFPE: " << sig << std::endl;
// 注意:处理完信号后程序会继续执行导致无限循环
}
int main() {
signal(SIGFPE, handler);
int a = 10;
a = a / 0; // 触发SIGFPE
return 0;
}
4.2 段错误(SIGSEGV)
访问非法内存地址(如空指针解引用)会触发SIGSEGV(11)信号。这是由于MMU在地址转换时发现页错误导致的硬件异常。
cpp复制#include <iostream>
#include <csignal>
void handler(int sig) {
std::cout << "Caught SIGSEGV: " << sig << std::endl;
exit(1); // 必须退出,否则会无限触发
}
int main() {
signal(SIGSEGV, handler);
int* p = nullptr;
*p = 10; // 触发SIGSEGV
return 0;
}
4.3 管道破裂(SIGPIPE)
当进程向已关闭读端的管道写入数据时,会收到SIGPIPE(13)信号。这是一种软件产生的信号,而非硬件异常。
cpp复制#include <unistd.h>
#include <signal.h>
#include <iostream>
void handler(int sig) {
std::cout << "Caught SIGPIPE: " << sig << std::endl;
}
int main() {
signal(SIGPIPE, handler);
int fd[2];
pipe(fd);
close(fd[0]); // 关闭读端
write(fd[1], "test", 4); // 触发SIGPIPE
return 0;
}
5. 定时器信号(SIGALRM)
alarm()函数设置定时器,到期后发送SIGALRM信号:
cpp复制#include <unistd.h>
unsigned int alarm(unsigned int seconds);
示例:实现周期性定时任务
cpp复制#include <iostream>
#include <unistd.h>
#include <signal.h>
void alarm_handler(int sig) {
std::cout << "Alarm triggered!" << std::endl;
alarm(2); // 重新设置2秒定时器
}
int main() {
signal(SIGALRM, alarm_handler);
alarm(2); // 首次设置2秒定时器
while(1) {
pause(); // 等待信号
}
return 0;
}
6. 核心转储(Core Dump)
当进程因某些信号终止时,系统可以生成核心转储文件(core dump),包含进程终止时的内存状态,用于调试。
6.1 启用核心转储
bash复制ulimit -c unlimited # 解除核心文件大小限制
6.2 分析核心转储
cpp复制#include <iostream>
int main() {
int* p = nullptr;
*p = 10; // 触发段错误
return 0;
}
编译并运行:
bash复制g++ -g test.cpp -o test
./test
使用gdb分析核心文件:
bash复制gdb test core.<pid>
7. 信号处理注意事项
- 不可捕获的信号:SIGKILL(9)和SIGSTOP(19)不能被捕获、忽略或阻塞
- 信号处理函数限制:信号处理函数中只能调用异步信号安全的函数
- 信号丢失:相同信号在短时间内多次发送可能被合并
- 竞态条件:信号可能在任何时间点到达,需要考虑并发问题
7.1 更安全的sigaction
推荐使用sigaction替代signal:
cpp复制#include <signal.h>
void handler(int sig) {
// 信号处理
}
int main() {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
while(1) pause();
return 0;
}
8. 实际应用案例
8.1 优雅地处理Ctrl+C
cpp复制#include <iostream>
#include <csignal>
#include <unistd.h>
volatile sig_atomic_t stop_flag = 0;
void handler(int sig) {
stop_flag = 1;
}
int main() {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
while(!stop_flag) {
std::cout << "Working..." << std::endl;
sleep(1);
}
std::cout << "Shutting down gracefully..." << std::endl;
return 0;
}
8.2 超时控制
cpp复制#include <iostream>
#include <csignal>
#include <unistd.h>
volatile sig_atomic_t timeout = 0;
void alarm_handler(int sig) {
timeout = 1;
}
int main() {
signal(SIGALRM, alarm_handler);
alarm(5); // 设置5秒超时
std::cout << "Starting long operation..." << std::endl;
// 模拟耗时操作
for(int i = 0; i < 10; ++i) {
if(timeout) {
std::cout << "Operation timed out!" << std::endl;
return 1;
}
sleep(1);
std::cout << "Progress: " << (i+1)*10 << "%" << std::endl;
}
alarm(0); // 取消定时器
std::cout << "Operation completed successfully" << std::endl;
return 0;
}
9. 信号与多线程
在多线程程序中,信号处理需要特别注意:
- 信号可以发送到特定线程
- 每个线程有自己的信号掩码
- 建议在主线程中统一处理信号
示例:
cpp复制#include <iostream>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>
void handler(int sig) {
std::cout << "Thread " << pthread_self() << " received signal " << sig << std::endl;
}
void* thread_func(void*) {
// 阻塞所有信号
sigset_t set;
sigfillset(&set);
pthread_sigmask(SIG_BLOCK, &set, NULL);
while(1) {
sleep(1);
}
return NULL;
}
int main() {
// 设置信号处理
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
// 创建线程
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
// 主线程等待信号
while(1) {
pause();
}
return 0;
}
10. 信号处理最佳实践
- 保持处理函数简单:信号处理函数应尽可能简单,避免复杂操作
- 使用volatile变量:信号处理函数和主程序共享的变量应声明为volatile
- 避免全局状态:尽量减少信号处理函数对全局状态的依赖
- 考虑可重入性:确保使用的函数是异步信号安全的
- 正确处理EINTR:系统调用可能被信号中断,需要检查errno == EINTR
cpp复制#include <iostream>
#include <csignal>
#include <unistd.h>
#include <errno.h>
volatile sig_atomic_t flag = 0;
void handler(int sig) {
flag = 1;
}
int main() {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
while(!flag) {
int result = sleep(10); // 可能被信号中断
if(result != 0 && errno == EINTR) {
std::cout << "Sleep interrupted by signal" << std::endl;
}
}
std::cout << "Exiting due to signal" << std::endl;
return 0;
}
理解Linux信号机制对于开发稳定可靠的系统程序至关重要。通过合理使用信号,可以实现进程间通信、异常处理和资源清理等功能。在实际开发中,应当遵循信号处理的最佳实践,确保程序的健壮性和可靠性。