6. 信号机制.md 16 KB

6 信号机制

6.1 完整概念讲解

什么是信号

信号是Linux系统中最古老的进程间通信机制之一,本质上是一种软件中断。当某个事件发生时,内核会向目标进程发送一个信号,进程收到信号后会中断当前执行流程,转去执行预设的信号处理函数。

信号的核心特点:

  • 异步性:信号的产生和处理是异步的,进程无法预知信号何时到来
  • 简单性:信号只携带一个整数编号,不携带额外数据
  • 内核介入:信号的发送和投递由内核完成
  • 默认行为:每个信号都有默认处理动作(终止、忽略、暂停、继续等)

为什么需要信号

  1. 进程控制:终止进程(SIGTERM/SIGKILL)、暂停进程(SIGSTOP/SIGTSTP)
  2. 事件通知:子进程退出(SIGCHLD)、定时器到期(SIGALRM)
  3. 用户交互:Ctrl+C(SIGINT)、Ctrl+Z(SIGTSTP)
  4. 异常处理:段错误(SIGSEGV)、除零错误(SIGFPE)
  5. 资源限制:CPU时间耗尽(SIGXCPU)

信号的生命周期

产生(Delivery) → 排队(Pending) → 抛递(Delivery) → 处理(Disposition)
  • 产生:硬件异常、内核事件、kill()调用、软件条件等
  • 排队:信号到达后进入pending状态(非实时信号只保留一次)
  • 抛递:内核将信号投递给目标进程
  • 处理:执行信号处理函数或默认动作

信号分类

类别 信号范围 特点
非实时信号 1-31 不支持排队,多次发送只保留一次
实时信号 34-64 支持排队,可携带额外数据

6.2 核心API/语法

signal() - 简单信号处理

#include <signal.h>

typedef void (*sighandler_t)(int signum);

sighandler_t signal(int signum, sighandler_t handler);
参数 说明
signum 信号编号
handler SIG_IGN(忽略)、SIG_DFL(默认)、自定义处理函数指针
返回值 说明
成功 之前的信号处理函数指针
失败 SIG_ERR

注意:signal()在不同Unix系统中行为不一致,Linux下会自动重启系统调用,但handler可能被重置为默认(不可靠)。

sigaction() - 可靠信号处理

#include <signal.h>

int sigaction(int signum, const struct sigaction *act,
              struct sigaction *oldact);

struct sigaction 结构体

struct sigaction {
    union {
        void (*sa_handler)(int signum);           // 简单处理函数
        void (*sa_sigaction)(int signum, siginfo_t *info, void *context);  // 高级处理函数
    };
    sigset_t sa_mask;           // 信号屏蔽字,处理期间阻塞的信号
    int sa_flags;               // 选项标志
    void (*sa_restorer)(void);  // 已废弃,不要使用
};
sa_flags 说明
SA_SIGINFO 使用sa_sigaction作为处理函数(可获取信号详细信息)
SA_RESTART 自动重启被中断的系统调用
SA_NOCLDSTOP 子进程停止时不产生SIGCHLD
SA_NOCLDWAIT 子进程退出时不产生僵尸进程
SA_NODEFER 处理期间不自动屏蔽当前信号
SA_RESETHAND 处理一次后恢复为默认动作

siginfo_t 结构体(需SA_SIGINFO标志):

typedef struct {
    int si_signo;       // 信号编号
    int si_code;        // 信号来源
    int si_errno;       // 错误码
    pid_t si_pid;       // 发送进程PID
    uid_t si_uid;       // 发送进程UID
    void *si_addr;      // 内存地址(SIGSEGV)
    int si_status;      // 退出状态(SIGCHLD)
    // ... 其他字段
} siginfo_t;

kill() 和 raise()

#include <signal.h>

int kill(pid_t pid, int sig);    // 发送信号给进程
int raise(int sig);              // 发送信号给自己
int sigqueue(pid_t pid, int sig, const union sigval value);  // 实时信号+数据

kill() 的 pid 参数

pid值 含义
pid > 0 发送给指定PID的进程
pid == 0 发送给同进程组的所有进程
pid == -1 发送给所有有权限发送的进程
pid < -1 发送给进程组ID为-pid的进程组

信号屏蔽

#include <signal.h>

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

sigprocmask 的 how 参数

how 说明
SIG_BLOCK 将set中的信号添加到屏蔽字
SIG_UNBLOCK 从屏蔽字中移除set中的信号
SIG_SETMASK 将屏蔽字设置为set

信号集操作函数

sigemptyset(sigset_t *set);      // 清空信号集
sigfillset(sigset_t *set);       // 填满信号集
sigaddset(sigset_t *set, int signum);   // 添加信号
sigdelset(sigset_t *set, int signum);   // 删除信号
sigismember(const sigset_t *set, int signum);  // 检查信号是否在集合中

6.3 代码示例

示例1:signal()基本用法

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

void sigint_handler(int signum) {
    printf("收到SIGINT信号(%d),程序即将退出...\n", signum);
    _exit(0);
}

int main() {
    // 注册SIGINT信号处理函数
    signal(SIGINT, sigint_handler);

    printf("程序运行中,按Ctrl+C测试...\n");
    while(1) {
        sleep(1);
    }
    return 0;
}

编译命令

gcc -o signal_demo signal_demo.c
./signal_demo

示例2:sigaction()完整示例

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

// 高级信号处理函数
void sig_handler(int signum, siginfo_t *info, void *context) {
    printf("信号编号: %d\n", signum);
    printf("发送进程PID: %d\n", info->si_pid);
    printf("发送进程UID: %d\n", info->si_uid);
    printf("信号来源代码: %d\n", info->si_code);

    if (signum == SIGSEGV) {
        printf("发生段错误,地址: %p\n", info->si_addr);
    }
}

int main() {
    struct sigaction sa;

    // 清空并设置sigaction结构体
    sigemptyset(&sa.sa_mask);
    sa.sa_sigaction = sig_handler;
    sa.sa_flags = SA_SIGINFO;  // 使用高级处理函数

    // 注册SIGSEGV信号处理
    if (sigaction(SIGSEGV, &sa, NULL) == -1) {
        perror("sigaction");
        exit(1);
    }

    printf("程序运行中,触发段错误测试...\n");

    // 触发段错误
    int *p = NULL;
    *p = 100;

    return 0;
}

编译命令

gcc -o sigaction_demo sigaction_demo.c
./sigaction_demo

示例3:信号屏蔽与sigsuspend

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

volatile int received_sigusr1 = 0;

void sigusr1_handler(int signum) {
    received_sigusr1 = 1;
    printf("收到SIGUSR1信号\n");
}

int main() {
    struct sigaction sa;
    sigset_t block_set, old_set, wait_set;

    // 设置信号处理函数
    sigemptyset(&sa.sa_mask);
    sa.sa_handler = sigusr1_handler;
    sa.sa_flags = 0;
    sigaction(SIGUSR1, &sa, NULL);

    // 创建要屏蔽的信号集
    sigemptyset(&block_set);
    sigaddset(&block_set, SIGUSR1);

    // 阻塞SIGUSR1
    sigprocmask(SIG_BLOCK, &block_set, &old_set);

    printf("PID: %d,等待SIGUSR1信号...\n", getpid());

    // 等待信号(原子操作:解除阻塞并等待)
    sigemptyset(&wait_set);
    sigsuspend(&wait_set);

    printf("收到信号,继续执行...\n");

    // 恢复原来的信号屏蔽字
    sigprocmask(SIG_SETMASK, &old_set, NULL);

    return 0;
}

编译命令

gcc -o signal_block signal_block.c
./signal_block &
kill -USR1 <PID>

示例4:SIGALRM定时器与SA_RESTART

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

volatile int alarm_count = 0;

void alarm_handler(int signum) {
    alarm_count++;
    printf("ALRM: %d\n", alarm_count);

    // 每次触发后重新设置5秒定时器
    if (alarm_count < 5) {
        alarm(5);
    } else {
        printf("定时器结束\n");
    }
}

int main() {
    struct sigaction sa;
    char buf[100];

    // 设置SIGALRM处理
    sigemptyset(&sa.sa_mask);
    sa.sa_handler = alarm_handler;
    sa.sa_flags = SA_RESTART;  // 自动重启被中断的read
    sigaction(SIGALRM, &sa, NULL);

    // 设置5秒定时器
    alarm(5);

    printf("程序运行中,等待定时器...\n");
    printf("也可以在终端输入内容测试EINTR:\n");

    // 测试SA_RESTART效果
    // 如果没有SA_RESTART,read会被信号中断返回-1
    while (1) {
        printf("> ");
        fflush(stdout);

        if (read(STDIN_FILENO, buf, sizeof(buf)) == -1) {
            if (errno == EINTR) {
                printf("read被信号中断 (EINTR)\n");
                continue;
            }
            perror("read");
            break;
        }

        if (strncmp(buf, "quit", 4) == 0) {
            break;
        }

        printf("输入: %s", buf);
    }

    return 0;
}

编译命令

gcc -o alarm_demo alarm_demo.c
./alarm_demo

示例5:SIGCHLD处理子进程退出

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

volatile int child_exited = 0;

void sigchld_handler(int signum) {
    int status;
    pid_t pid;

    // 使用WNOHANG循环回收,避免遗漏
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status)) {
            printf("子进程%d正常退出,状态码: %d\n", pid, WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("子进程%d被信号%d终止\n", pid, WTERMSIG(status));
        }
        child_exited = 1;
    }
}

int main() {
    struct sigaction sa;
    pid_t pid;

    // 设置SIGCHLD处理
    sigemptyset(&sa.sa_mask);
    sa.sa_handler = sigchld_handler;
    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;  // 不处理停止的子进程
    sigaction(SIGCHLD, &sa, NULL);

    // 创建子进程
    pid = fork();
    if (pid == 0) {
        // 子进程
        printf("子进程PID: %d,3秒后退出\n", getpid());
        sleep(3);
        exit(42);  // 退出状态码42
    }

    // 父进程
    printf("父进程PID: %d,子进程PID: %d\n", getpid(), pid);
    printf("等待子进程退出...\n");

    while (!child_exited) {
        pause();  // 挂起等待信号
    }

    printf("所有子进程已回收\n");
    return 0;
}

编译命令

gcc -o sigchld_demo sigchld_demo.c
./sigchld_demo

示例6:实时信号与sigqueue

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>

void sigrt_handler(int signum, siginfo_t *info, void *context) {
    printf("收到实时信号 %d, 数据值: %d\n", signum, info->si_value.sival_int);
}

int main() {
    struct sigaction sa;
    union sigval val;
    pid_t pid;

    // 设置实时信号处理
    sigemptyset(&sa.sa_mask);
    sa.sa_sigaction = sigrt_handler;
    sa.sa_flags = SA_SIGINFO;  // 必须使用SA_SIGINFO
    sigaction(SIGRTMIN, &sa, NULL);

    printf("PID: %d\n", getpid());
    printf("等待实时信号...\n");

    // 等待一下,然后发送测试信号
    sleep(2);

    // 使用sigqueue发送实时信号+数据
    val.sival_int = 12345;
    if (sigqueue(getpid(), SIGRTMIN, val) == -1) {
        perror("sigqueue");
    }

    // 多次发送,实时信号会排队
    for (int i = 0; i < 3; i++) {
        val.sival_int = 100 + i;
        sigqueue(getpid(), SIGRTMIN, val);
    }

    sleep(1);
    return 0;
}

编译命令

gcc -o realtime_sig realtime_sig.c
./realtime_sig

6.4 注意事项与易错点

问题 说明 解决方案
signal()不可靠 handler可能被重置为SIG_DFL 使用sigaction()替代
信号处理函数中避免非异步安全函数 printf、malloc、free等不可重入 使用write()、_exit(),或设置标志位
信号屏蔽遗漏 信号处理期间收到相同信号导致递归 使用sa_mask自动屏蔽当前信号
僵尸进程 子进程退出后父进程未回收 处理SIGCHLD或设置SA_NOCLDWAIT
EINTR错误 系统调用被信号中断 使用SA_RESTART或检查errno
实时信号丢失 非实时信号只保留一次 使用SIGRTMIN及以上的实时信号
kill()权限不足 非root进程只能发给同用户进程 保证发送者和接收者属于同一用户
信号处理函数阻塞 处理函数执行时间过长影响系统响应 尽量简短,设置标志位延迟处理
多线程信号处理 信号默认投递给主线程 使用sigaction配合线程掩码

信号处理函数中的安全函数列表

异步信号安全函数(可在信号处理函数中调用):

  • _exit()_Exit()
  • read()write()
  • sigaction()sigprocmask()
  • kill()raise()
  • alarm()pause()sleep()
  • getpid()getuid()
  • pthread_sigmask()(多线程)

不安全函数(避免在信号处理函数中调用):

  • printf()fprintf()malloc()free()
  • exit()abort()
  • 任何使用全局变量的函数

6.5 面试要点

Q1: signal()和sigaction()的区别?

  1. signal()是简单API,sigaction()是更强大的替代方案
  2. signal()在某些系统中handler会被重置为默认,sigaction()不会
  3. sigaction()可以精确控制信号屏蔽、自动重启行为等
  4. sigaction()支持SA_SIGINFO获取信号详细信息
  5. 生产环境推荐使用sigaction()

Q2: 什么是EINTR?如何处理?

  • EINTR:系统调用被信号中断,返回-1并设置errno=EINTR
  • 产生场景:read/write/select/poll等阻塞调用被信号打断
  • 处理方式
    • 使用SA_RESTART标志自动重启
    • 手动检查errno并重试
    • 在循环中处理中断(推荐)

Q3: 为什么信号处理函数中要避免使用printf?

  1. printf()使用了全局的stdio缓冲区,在信号处理函数中调用可能导致死锁
  2. printf()内部使用了malloc,malloc不是异步信号安全的
  3. 多线程环境下,printf()需要获取锁,可能造成竞争
  4. 安全替代:使用write()系统调用

Q4: SIGCHLD的处理方式有哪些?

  1. 忽略signal(SIGCHLD, SIG_IGN) 或设置SA_NOCLDWAIT(不产生僵尸进程)
  2. 自定义处理:使用sigaction()注册处理函数,在函数中调用waitpid()回收
  3. 处理多个子进程:使用while(waitpid(-1, &status, WNOHANG) > 0)循环回收

Q5: 实时信号和非实时信号的区别?

特性 非实时信号(1-31) 实时信号(34-64)
排队 不支持,多次发送只保留一次 支持,按发送顺序排队
优先级 优先级从34递增
携带数据 不能 可通过sigqueue传递联合体数据
典型用途 传统Unix信号 用户自定义、实时系统

Q6: 嵌入式开发中信号的典型应用场景?

  1. 定时器:SIGALRM实现软件定时器,用于超时检测、周期任务
  2. 子进程管理:SIGCHLD处理守护进程、监控进程的退出
  3. 用户输入:SIGINT处理Ctrl+C优雅退出
  4. 看门狗:SIGALRM定期喂狗,超时则复位
  5. 进程间同步:SIGUSR1/SIGUSR2实现简单的父子进程同步