10UNIX环境高级编程(第3版)

信号

异步事件

信号处理信号集sigaction

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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不能被忽略)
捕获调用用户指定的信号处理函数
c
#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 常见信号

信号值默认动作来源
SIGHUP1终止控制终端断开
SIGINT2终止Ctrl+C
SIGQUIT3终止+coreCtrl+\\
SIGKILL9终止强制终止(不可捕获)
SIGTERM15终止请求终止
SIGCHLD17忽略子进程状态改变
SIGSTOP19暂停暂停进程(不可捕获)
SIGTSTP20暂停Ctrl+Z
SIGCONT18继续继续执行
SIGALRM14终止定时器到期
SIGUSR110终止用户自定义信号1
SIGUSR212终止用户自定义信号2
SIGSEGV11终止+core非法内存访问
SIGPIPE13终止写入已关闭的管道
SIGFPE8终止+core算术错误(如除零)
bash
# 查看所有信号
kill -l

# 发送信号
kill -9 PID      # 发送SIGKILL
kill -TERM PID   # 发送SIGTERM
kill -USR1 PID   # 发送SIGUSR1

2.3 signal函数

signal是最简单的信号注册函数:

c
#include <signal.h>

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

handler可以是:

  • 用户定义的函数指针
  • SIG_IGN:忽略信号
  • SIG_DFL:恢复默认动作
c
#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推荐的信号注册函数,提供了更精确的控制:

c
#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);   // 已废弃
};
c
#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 信号集操作

c
#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);  // 检查信号是否在集合中
c
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);

if (sigismember(&set, SIGINT)) {
    printf("SIGINT is in the set\n");
}

2.6 信号阻塞

进程可以阻塞信号的递送。被阻塞的信号在解除阻塞前保持未决状态。

c
#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

how参数:

  • SIG_BLOCK:将set中的信号添加到阻塞集
  • SIG_UNBLOCK:从阻塞集中移除set中的信号
  • SIG_SETMASK:将阻塞集设置为set
c
#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原子地替换信号掩码并等待信号:

c
#include <signal.h>

int sigsuspend(const sigset_t *mask);

sigsuspend等价于:

c
sigprocmask(SIG_SETMASK, &mask, &oldmask);
pause();
sigprocmask(SIG_SETMASK, &oldmask, NULL);

但sigsuspend是原子操作,避免了竞态条件。

c
#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、read
  • signal、sigaction、sigprocmask
  • fork、exec系列
  • open、close
  • malloc(某些实现)、free(某些实现)
  • pipe、dup、dup2

不安全的函数:

  • printf、sprintf、fprintf
  • malloc、free(在某些实现中)
  • syslog
  • 大多数标准I/O函数

2.8.2 安全的信号处理模式

模式1:只设置标志位

c
#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)

c
#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)是可以安全地被多个执行流(包括信号处理函数)并发调用的函数。可重入函数不访问全局/静态数据,不使用锁,不调用不可重入函数。

c
// 不可重入的函数
int counter = 0;
void increment() {
    counter++;  // 访问全局变量,不可重入
}

// 可重入的函数
void increment_r(int *counter) {
    (*counter)++;  // 使用参数,可重入
}

POSIX定义了一组可重入函数(_r后缀),如localtime_r、getpwnam_r等。

2.10 定时器

alarm和setitimer可以在指定时间后发送SIGALRM信号:

c
#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提供更精确的定时器:

c
#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 优雅退出

c
#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 超时读取

c
#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实现可靠的信号处理

c
#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终止。


10/12信号
0%