第10章:信号(Signals)
一、导读
1.1 本章学习目标
第10章是UNIX编程的核心章节,讲解信号机制——一种异步事件通知方式。信号是软件层面的中断,用于通知进程发生了某种事件(如用户按Ctrl+C、子进程终止、定时器到期、除零错误等)。信号的处理是UNIX编程中最复杂也最容易出错的部分之一,因为信号可以在程序执行的任何时刻到达,处理函数与主程序并发执行,带来了竞态条件和重入问题。
本章的核心学习目标包括:
- 理解信号的本质:信号是异步事件通知,进程无法预测信号到达的时间
- 掌握常见信号的含义:SIGINT、SIGQUIT、SIGKILL、SIGTERM、SIGCHLD、SIGALRM等
- 学会注册信号处理函数:
signal和sigaction的区别,为什么推荐使用sigaction - 理解信号处理的安全性:信号处理函数中只能调用异步信号安全函数
- 掌握信号阻塞和等待:
sigprocmask、sigsuspend、sigpending的使用 - 了解信号的高级应用:定时器、异步I/O通知、进程间通信
1.2 为什么本章如此重要
信号是UNIX异步事件处理的基础机制。很多重要的系统功能都依赖于信号:
- 用户中断:Ctrl+C发送SIGINT,Ctrl+Z发送SIGTSTP
- 子进程管理:子进程终止时发送SIGCHLD
- 定时器:
alarm和setitimer到期时发送SIGALRM - 错误处理:除零(SIGFPE)、非法内存访问(SIGSEGV)、非法指令(SIGILL)
- 进程间通信:
kill命令发送信号通知其他进程 - 终端控制:终端断开时发送SIGHUP
信号处理函数如果编写不当,可能导致数据损坏、死锁、程序崩溃。理解信号的安全编程是编写健壮UNIX程序的关键。
1.3 核心问题引导
在阅读本章时,请带着以下问题思考:
信号处理函数什么时候被执行?它和主程序是并发执行的吗?
为什么printf不能在信号处理函数中使用?哪些函数是安全的?
signal和sigaction有什么区别?为什么POSIX推荐使用sigaction?
什么是可重入函数?为什么信号处理函数应该是可重入的?
如何阻塞信号?阻塞和忽略信号有什么区别?
如何实现"原子地"解除信号阻塞并等待信号?
二、核心概念详解
2.1 信号的概念
信号(signal)是一个短小的消息,通知进程发生了某个事件。信号是异步的——进程无法预测信号何时到达。
2.1.1 信号的生命周期
产生(generation):某个事件发生,内核为进程设置信号
递送(delivery):内核将信号通知给进程
处理(handling):进程对信号做出响应
在信号产生到递送之间,信号处于未决(pending)状态。
2.1.2 信号的处理方式
进程可以为每个信号指定一种处理方式:
| 处理方式 | 描述 |
|---|---|
| 默认动作 | 大多数信号的默认动作是终止进程 |
| 忽略 | 忽略信号(SIGKILL和SIGSTOP不能被忽略) |
| 捕获 | 调用用户指定的信号处理函数 |
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
// 信号处理函数
void handler(int sig) {
// 注意:只能调用异步信号安全函数
// write是安全的,printf不是
const char msg[] = "Signal caught!\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1);
}
int main() {
// 注册信号处理函数
signal(SIGINT, handler);
printf("Press Ctrl+C to trigger SIGINT\n");
while (1) {
pause(); // 等待信号
}
return 0;
}2.2 常见信号
| 信号 | 值 | 默认动作 | 来源 |
|---|---|---|---|
| SIGHUP | 1 | 终止 | 控制终端断开 |
| SIGINT | 2 | 终止 | Ctrl+C |
| SIGQUIT | 3 | 终止+core | Ctrl+\\ |
| SIGKILL | 9 | 终止 | 强制终止(不可捕获) |
| SIGTERM | 15 | 终止 | 请求终止 |
| SIGCHLD | 17 | 忽略 | 子进程状态改变 |
| SIGSTOP | 19 | 暂停 | 暂停进程(不可捕获) |
| SIGTSTP | 20 | 暂停 | Ctrl+Z |
| SIGCONT | 18 | 继续 | 继续执行 |
| SIGALRM | 14 | 终止 | 定时器到期 |
| SIGUSR1 | 10 | 终止 | 用户自定义信号1 |
| SIGUSR2 | 12 | 终止 | 用户自定义信号2 |
| SIGSEGV | 11 | 终止+core | 非法内存访问 |
| SIGPIPE | 13 | 终止 | 写入已关闭的管道 |
| SIGFPE | 8 | 终止+core | 算术错误(如除零) |
# 查看所有信号
kill -l
# 发送信号
kill -9 PID # 发送SIGKILL
kill -TERM PID # 发送SIGTERM
kill -USR1 PID # 发送SIGUSR12.3 signal函数
signal是最简单的信号注册函数:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);handler可以是:
- 用户定义的函数指针
SIG_IGN:忽略信号SIG_DFL:恢复默认动作
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void sigint_handler(int sig) {
printf("\nCaught SIGINT! (count++)\n");
}
int main() {
// 注册SIGINT处理函数
signal(SIGINT, sigint_handler);
// 忽略SIGQUIT
signal(SIGQUIT, SIG_IGN);
printf("Running... Press Ctrl+C\n");
while (1) {
sleep(1);
}
return 0;
}signal的问题:
行为不一致:在不同UNIX系统上,signal的行为可能不同(特别是信号处理函数是否自动重置)
不可靠:信号处理函数执行期间,如果同一信号再次到达,行为未定义
无法精确控制:不能指定信号掩码、标志等
2.4 sigaction函数
sigaction是POSIX推荐的信号注册函数,提供了更精确的控制:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int); // 信号处理函数
sigset_t sa_mask; // 信号掩码(处理期间阻塞的信号)
int sa_flags; // 标志位
void (*sa_restorer)(void); // 已废弃
};#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void handler(int sig) {
printf("\nCaught signal %d\n", sig);
}
int main() {
struct sigaction sa;
sa.sa_handler = handler;
// 在处理SIGINT期间,阻塞SIGQUIT
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGQUIT);
// SA_RESTART:被信号中断的系统调用自动重启
sa.sa_flags = SA_RESTART;
sigaction(SIGINT, &sa, NULL);
printf("Running... Press Ctrl+C\n");
while (1) {
pause();
}
return 0;
}常用标志:
| 标志 | 含义 |
|---|---|
| SA_RESTART | 被信号中断的系统调用自动重启 |
| SA_NOCLDSTOP | 子进程被停止时不产生SIGCHLD |
| SA_NOCLDWAIT | 子进程终止时不产生僵尸进程 |
| SA_SIGINFO | 使用三参数处理函数(带siginfo_t) |
2.5 信号集操作
#include <signal.h>
int sigemptyset(sigset_t *set); // 清空信号集
int sigfillset(sigset_t *set); // 填充所有信号
int sigaddset(sigset_t *set, int sig); // 添加信号
int sigdelset(sigset_t *set, int sig); // 删除信号
int sigismember(const sigset_t *set, int sig); // 检查信号是否在集合中sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
if (sigismember(&set, SIGINT)) {
printf("SIGINT is in the set\n");
}2.6 信号阻塞
进程可以阻塞信号的递送。被阻塞的信号在解除阻塞前保持未决状态。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);how参数:
- SIG_BLOCK:将
set中的信号添加到阻塞集 - SIG_UNBLOCK:从阻塞集中移除
set中的信号 - SIG_SETMASK:将阻塞集设置为
set
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void handler(int sig) {
printf("Caught SIGINT\n");
}
int main() {
signal(SIGINT, handler);
// 阻塞SIGINT
sigset_t set, oldset;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_BLOCK, &set, &oldset);
printf("SIGINT is blocked. Press Ctrl+C...\n");
sleep(5);
// 检查未决信号
sigset_t pending;
sigpending(&pending);
if (sigismember(&pending, SIGINT)) {
printf("SIGINT is pending!\n");
}
// 解除阻塞
printf("Unblocking SIGINT...\n");
sigprocmask(SIG_SETMASK, &oldset, NULL);
// 此时被阻塞的SIGINT被递送
return 0;
}2.7 sigsuspend
sigsuspend原子地替换信号掩码并等待信号:
#include <signal.h>
int sigsuspend(const sigset_t *mask);sigsuspend等价于:
sigprocmask(SIG_SETMASK, &mask, &oldmask);
pause();
sigprocmask(SIG_SETMASK, &oldmask, NULL);但sigsuspend是原子操作,避免了竞态条件。
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
volatile sig_atomic_t got_signal = 0;
void handler(int sig) {
got_signal = 1;
}
int main() {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGUSR1, &sa, NULL);
sigset_t mask, oldmask;
sigemptyset(&mask);
sigaddset(&mask, SIGUSR1);
// 阻塞SIGUSR1
sigprocmask(SIG_BLOCK, &mask, &oldmask);
printf("Waiting for SIGUSR1...\n");
// 原子地解除阻塞并等待
while (!got_signal) {
sigsuspend(&oldmask);
}
printf("Got SIGUSR1!\n");
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return 0;
}2.8 信号处理函数的安全性
信号处理函数与主程序并发执行,如果两者访问相同的共享数据,可能导致数据不一致。因此,信号处理函数必须遵循严格的安全规则。
2.8.1 异步信号安全函数
信号处理函数中只能调用异步信号安全(async-signal-safe)的函数。POSIX标准定义了一个安全函数列表:
安全的函数(部分):
_exit、write、readsignal、sigaction、sigprocmaskfork、exec系列open、closemalloc(某些实现)、free(某些实现)pipe、dup、dup2
不安全的函数:
printf、sprintf、fprintfmalloc、free(在某些实现中)syslog- 大多数标准I/O函数
2.8.2 安全的信号处理模式
模式1:只设置标志位
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
volatile sig_atomic_t got_sigint = 0;
void handler(int sig) {
got_sigint = 1; // sig_atomic_t保证原子性
}
int main() {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
while (!got_sigint) {
printf("Working...\n");
sleep(1);
}
printf("Caught SIGINT, cleaning up...\n");
// 执行清理操作
printf("Done.\n");
return 0;
}模式2:使用自管道(self-pipe trick)
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
int pipefd[2];
void handler(int sig) {
char c = 'S';
write(pipefd[1], &c, 1); // write是异步信号安全的
}
int main() {
pipe(pipefd);
fcntl(pipefd[0], F_SETFL, O_NONBLOCK);
fcntl(pipefd[1], F_SETFL, O_NONBLOCK);
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGINT, &sa, NULL);
// 主循环使用select/poll等待管道可读
// ...
}2.9 可重入函数
可重入函数(reentrant function)是可以安全地被多个执行流(包括信号处理函数)并发调用的函数。可重入函数不访问全局/静态数据,不使用锁,不调用不可重入函数。
// 不可重入的函数
int counter = 0;
void increment() {
counter++; // 访问全局变量,不可重入
}
// 可重入的函数
void increment_r(int *counter) {
(*counter)++; // 使用参数,可重入
}POSIX定义了一组可重入函数(_r后缀),如localtime_r、getpwnam_r等。
2.10 定时器
alarm和setitimer可以在指定时间后发送SIGALRM信号:
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
void alarm_handler(int sig) {
printf("Alarm!\n");
}
int main() {
signal(SIGALRM, alarm_handler);
alarm(3); // 3秒后发送SIGALRM
printf("Waiting for alarm...\n");
while (1) {
pause();
}
return 0;
}setitimer提供更精确的定时器:
#include <sys/time.h>
#include <signal.h>
#include <stdio.h>
void handler(int sig) {
printf("Timer!\n");
}
int main() {
signal(SIGALRM, handler);
struct itimerval timer;
timer.it_value.tv_sec = 1; // 1秒后第一次触发
timer.it_value.tv_usec = 0;
timer.it_interval.tv_sec = 2; // 之后每2秒触发
timer.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &timer, NULL);
while (1) {
pause();
}
return 0;
}三、实战案例
3.1 优雅退出
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
volatile sig_atomic_t running = 1;
volatile sig_atomic_t reload_config = 0;
void sigterm_handler(int sig) {
running = 0;
}
void sighup_handler(int sig) {
reload_config = 1;
}
void load_config() {
printf("Loading configuration...\n");
// 加载配置文件...
}
int main() {
struct sigaction sa;
// SIGTERM: 优雅退出
sa.sa_handler = sigterm_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
// SIGHUP: 重新加载配置
sa.sa_handler = sighup_handler;
sigaction(SIGHUP, &sa, NULL);
// 忽略SIGPIPE
signal(SIGPIPE, SIG_IGN);
load_config();
printf("Server running (PID: %d)\n", getpid());
while (running) {
if (reload_config) {
reload_config = 0;
load_config();
}
// 模拟工作
sleep(1);
}
printf("\nShutting down gracefully...\n");
// 清理资源...
printf("Goodbye.\n");
return 0;
}3.2 超时读取
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
volatile sig_atomic_t timeout = 0;
void alarm_handler(int sig) {
timeout = 1;
}
int read_with_timeout(int fd, char *buf, size_t count, int seconds) {
struct sigaction sa, old_sa;
sa.sa_handler = alarm_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGALRM, &sa, &old_sa);
timeout = 0;
alarm(seconds);
ssize_t n = read(fd, buf, count);
alarm(0); // 取消定时器
sigaction(SIGALRM, &old_sa, NULL); // 恢复原来的处理函数
if (timeout) {
errno = ETIMEDOUT;
return -1;
}
return n;
}
int main() {
char buf[256];
printf("Enter something (10 second timeout): ");
fflush(stdout);
ssize_t n = read_with_timeout(STDIN_FILENO, buf, sizeof(buf), 10);
if (n < 0) {
if (errno == ETIMEDOUT) {
printf("\nTimeout!\n");
} else {
perror("read");
}
} else {
buf[n] = '\0';
printf("Read: %s", buf);
}
return 0;
}3.3 使用sigaction实现可靠的信号处理
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
// 安全的信号处理函数注册
int setup_signal(int sig, void (*handler)(int)) {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handler;
sigfillset(&sa.sa_mask); // 处理期间阻塞所有信号
sa.sa_flags = SA_RESTART;
if (sigaction(sig, &sa, NULL) == -1) {
perror("sigaction");
return -1;
}
return 0;
}
volatile sig_atomic_t sigint_count = 0;
volatile sig_atomic_t sigterm_count = 0;
void sigint_handler(int sig) {
sigint_count++;
}
void sigterm_handler(int sig) {
sigterm_count++;
}
int main() {
setup_signal(SIGINT, sigint_handler);
setup_signal(SIGTERM, sigterm_handler);
signal(SIGPIPE, SIG_IGN);
printf("Signal handler installed. Press Ctrl+C multiple times.\n");
printf("Send SIGTERM with: kill -TERM %d\n", getpid());
while (1) {
printf("SIGINT count: %d, SIGTERM count: %d\n",
sigint_count, sigterm_count);
sleep(2);
}
return 0;
}四、本章小结
第10章深入讲解了UNIX信号机制。核心知识点包括:
信号的本质:异步事件通知,进程无法预测到达时间。信号有产生、未决、递送、处理四个阶段。
常见信号:SIGINT(Ctrl+C)、SIGTERM(请求终止)、SIGKILL(强制终止)、SIGCHLD(子进程状态改变)、SIGALRM(定时器)、SIGPIPE(写入关闭的管道)。
signal vs sigaction:signal行为不一致且不可靠,sigaction提供精确控制(信号掩码、标志位)。POSIX推荐使用sigaction。
信号处理安全性:只能调用异步信号安全函数(如write),不能调用printf、malloc等。最佳模式是只设置volatile sig_atomic_t标志。
信号阻塞:sigprocmask阻塞/解除阻塞信号。sigsuspend原子地替换掩码并等待信号。
定时器:alarm设置一次性定时器,setitimer设置周期性定时器。到期时发送SIGALRM。
五、思考与练习
思考题
为什么printf不能在信号处理函数中使用?如果一定要输出信息,应该用什么函数?
volatile sig_atomic_t的volatile和sig_atomic_t分别有什么作用?去掉其中一个会怎样?
为什么SA_RESTART标志很重要?没有它,被信号中断的read会返回什么?
信号可以排队吗?如果同一信号在阻塞期间到达多次,解除阻塞后会收到几次?
编程练习
实现一个程序,捕获SIGINT和SIGTERM信号,实现优雅退出(打印退出信息、清理资源)。
使用alarm实现一个sleep命令的简化版。
编写一个程序,使用SIGUSR1和SIGUSR2实现简单的进程间通信。
实现一个超时执行器:运行指定命令,如果在N秒内未完成则发送SIGKILL终止。