10深入理解Linux内核(第3版)

信号

信号处理

信号信号处理函数信号集

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

第10章:信号(Signals)

一、导读

10.1 本章学习目标

第10章从内核角度讲解信号的实现机制。信号是UNIX中最古老的进程间通信方式,用于通知进程发生了异步事件。从用户按下Ctrl+C发送SIGINT,到子进程退出时父进程收到SIGCHLD,再到定时器到期触发SIGALRM——信号无处不在。理解信号的内核实现,对于编写健壮的系统程序、理解进程间通信、以及诊断程序行为都至关重要。

本章的核心学习目标包括:

  • 理解信号在内核中的表示:task_struct中的信号相关字段
  • 掌握信号发送的内核流程:从kill系统调用到信号加入pending集合
  • 理解信号检查和处理时机:内核何时检查待处理信号
  • 掌握信号处理函数的栈帧构造:内核如何跳转到用户空间的信号处理函数
  • 了解实时信号与普通信号的区别:排队机制和可靠性差异
  • 理解信号阻塞和排队的实现:sigprocmask和信号队列

10.2 为什么本章如此重要

信号是进程间通信的基础机制,也是很多系统行为的底层支撑。进程退出、定时器、I/O事件、错误通知等,都通过信号机制传递。不理解信号的实现,就无法理解为什么某些系统调用会被中断(EINTR),为什么多线程程序中的信号处理如此复杂,为什么某些程序在退出前需要做清理工作。

从安全角度看,信号也是一个潜在的攻击面。信号处理函数运行在用户空间,但由内核触发,如果处理不当可能导致竞态条件、资源泄漏等问题。从性能角度看,频繁的发送和处理信号会带来额外的开销。

10.3 核心问题引导

在阅读本章时,请带着以下问题思考:

当执行kill -9 PID时,内核如何将SIGKILL信号传递给目标进程?

为什么SIGKILL和SIGSTOP不能被捕获或忽略?内核如何特殊处理这两个信号?

信号处理函数是如何被执行的?内核如何构造特殊的栈帧跳转到用户空间?

为什么普通信号不排队?如果发送多个相同的信号,为什么只处理一次?

sigreturn系统调用的作用是什么?为什么信号处理函数返回时需要它?

多线程程序中,信号是如何在线程之间分配的?


二、核心概念详解

2.1 信号的内核表示

每个进程在内核中的task_struct包含信号相关的信息:

c
/* 信号相关的数据结构 */
/* include/linux/sched.h */
struct task_struct {
    /* ... */

    /* 信号处理 */
    struct signal_struct *signal;     /* 线程组共享的信号信息 */
    struct sighand_struct *sighand;   /* 信号处理函数数组 */
    sigset_t blocked;                 /* 被阻塞的信号集 */
    sigset_t real_blocked;            /* 临时阻塞集 */
    sigset_t saved_sigmask;           /* 保存的信号掩码 */
    struct sigpending pending;        /* 线程私有的待处理信号 */

    /* ... */
};

/* 线程组共享的信号信息 */
struct signal_struct {
    atomic_t sigcnt;                  /* 引用计数 */
    atomic_t live;                    /* 线程数 */
    int nr_threads;                   /* 线程总数 */

    /* 进程组的信号 */
    struct sigpending shared_pending; /* 线程组共享的待处理信号 */

    /* POSIX 定时器 */
    struct list_head posix_timers;

    /* 进程统计 */
    cputime_t utime;
    cputime_t stime;

    /* 退出相关 */
    int exit_code;
    int exit_signal;

    /* ... */
};

/* 信号处理函数数组 */
struct sighand_struct {
    atomic_t count;                   /* 引用计数 */
    struct k_sigaction action[_NSIG]; /* 每个信号的处理函数 */
    spinlock_t siglock;               /* 保护锁 */
};

/* 信号动作 */
struct k_sigaction {
    __sighandler_t sa_handler;        /* 信号处理函数 */
    unsigned long sa_flags;           /* 标志 */
    __sigrestore_t sa_restorer;       /* 恢复函数 */
    sigset_t sa_mask;                 /* 执行处理函数时额外阻塞的信号 */
};

/* 待处理信号集 */
struct sigpending {
    struct list_head list;            /* sigqueue 链表 */
    sigset_t signal;                  /* 信号位图 */
};
bash
# 查看进程的信号信息
cat /proc/self/status | grep Sig
# SigQ:   0/63497        # 信号队列中当前/最大信号数
# SigPnd: 0000000000000000  # 待处理信号(位图)
# SigBlk: 0000000000000000  # 阻塞的信号
# SigIgn: 0000000000000000  # 忽略的信号
# SigCgt: 0000000000000000  # 捕获的信号

2.2 信号发送

信号发送通过kill系统调用实现。内核将信号添加到目标进程的pending集合中。

c
/* kill 系统调用 */
/* kernel/signal.c */
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig) {
    struct siginfo info;

    /* 填充 siginfo */
    info.si_signo = sig;
    info.si_errno = 0;
    info.si_code = SI_USER;     /* 来自用户空间 */
    info.si_pid = task_tgid_vnr(current);  /* 发送者PID */
    info.si_uid = from_kuid_munged(current_user_ns(), current_uid());

    return kill_something_info(sig, &info, pid);
}

/* 信号发送的核心流程 */
static int kill_something_info(int sig, struct siginfo *info, pid_t pid) {
    if (pid > 0) {
        /* 发送给特定进程 */
        struct task_struct *p = find_task_by_vnr(pid);
        if (!p)
            return -ESRCH;
        return send_signal(sig, info, p, PIDTYPE_PID);
    }
    /* pid == 0: 发送给同进程组的所有进程 */
    /* pid == -1: 发送给所有有权限发送的进程 */
    /* pid < -1: 发送给进程组 |pid| 的所有进程 */
}

/* 发送信号到进程 */
static int send_signal(int sig, struct siginfo *info,
                        struct task_struct *t, enum pid_type type) {
    struct sigpending *pending;
    struct sigqueue *q;

    /* 确定使用哪个 pending 集合 */
    /* 对于线程组共享的信号,使用 shared_pending */
    /* 对于线程私有的信号,使用 pending */
    pending = (type == PIDTYPE_PID) ? &t->pending : &t->signal->shared_pending;

    /* 检查信号是否被忽略 */
    if (sig_ignored(t, sig))
        return 0;

    /* 分配 sigqueue 结构 */
    q = __sigqueue_alloc(sig, t->signal, GFP_ATOMIC, 0);
    if (q) {
        list_add_tail(&q->list, &pending->list);
        *siginfo_from_sigqueue(q, info);
    }

    /* 在信号位图中标记 */
    sigaddset(&pending->signal, sig);

    /* 唤醒目标进程(如果它在睡眠) */
    signal_wake_up(t, sig == SIGKILL);

    return 0;
}

2.3 信号检查和传递

内核在从内核态返回用户态之前检查待处理信号。如果有未被阻塞的信号,内核会传递信号。

c
/* 信号检查(内核返回用户态前) */
/* arch/x86/kernel/signal.c */
void do_notify_resume(struct pt_regs *regs, unsigned long thread_info_flags) {
    /* 检查是否有待处理信号 */
    if (thread_info_flags & _TIF_SIGPENDING)
        do_signal(regs);

    /* 检查是否需要通知跟踪器 */
    if (thread_info_flags & _TIF_NOTIFY_RESUME) {
        clear_thread_flag(TIF_NOTIFY_RESUME);
        tracehook_notify_resume(regs);
    }
}

/* 信号传递的核心函数 */
static void do_signal(struct pt_regs *regs) {
    struct ksignal ksig;
    bool stepping;

    /* 获取下一个要处理的信号 */
    if (get_signal(&ksig)) {
        /* 有信号需要处理 */

        /* 检查是否是停止/继续信号 */
        if (ksig->sig == SIGSTOP || ksig->sig == SIGTSTP ||
            ksig->sig == SIGTTIN || ksig->sig == SIGTTOU) {
            /* 停止进程 */
            do_signal_stop(ksig->sig);
            return;
        }

        /* 检查信号动作 */
        switch (ksig->ka.sa.sa_handler) {
        case SIG_DFL:
            /* 默认动作 */
            switch (sig_default_action(ksig->sig)) {
            case ACTION_TERM:
                /* 终止进程 */
                do_exit(ksig->sig | 0x80);  /* core dump */
                break;
            case ACTION_STOP:
                /* 停止进程 */
                do_signal_stop(ksig->sig);
                break;
            case ACTION_CONT:
                /* 继续进程 */
                break;
            case ACTION_IGN:
            case ACTION_NONE:
                break;
            }
            break;

        case SIG_IGN:
            /* 忽略信号 */
            break;

        default:
            /* 用户定义的信号处理函数 */
            /* 构造特殊的用户态栈帧 */
            handle_signal(&ksig, regs);
            break;
        }
    }
}

2.4 信号处理函数的执行

当信号的动作是用户定义的处理函数时,内核需要构造特殊的用户态栈帧,使进程跳转到信号处理函数执行。

c
/* 信号处理函数的栈帧构造(x86-64,简化) */
/* arch/x86/kernel/signal.c */
static int handle_signal(struct ksignal *ksig, struct pt_regs *regs) {
    sigset_t *oldset = sigmask_to_save();
    struct pt_regs *regs_copy;
    int ret;

    /* 1. 在用户栈上分配信号帧 */
    /* 信号帧包含:
     * - 保存的寄存器上下文
     * - 信号编号
     * - siginfo 结构
     * - ucontext 结构
     * - 信号掩码
     */

    /* 2. 设置信号处理函数的参数 */
    /* rdi = 信号编号 */
    /* rsi = siginfo 指针 */
    /* rdx = ucontext 指针 */

    /* 3. 修改返回地址为信号处理函数 */
    regs->ip = (unsigned long)ksig->ka.sa.sa_handler;

    /* 4. 设置信号帧的返回地址为 sigreturn trampoline */
    /* 信号处理函数执行完后,ret 指令跳转到 sigreturn trampoline */
    /* sigreturn trampoline 调用 sigreturn 系统调用 */

    /* 5. 更新信号掩码 */
    sigact_restore_blocked(oldset, &ksig->ka);

    /* 6. 清除 TIF_SIGPENDING 标志 */
    clear_thread_flag(TIF_SIGPENDING);

    return 0;
}

/*
 * 信号处理的完整流程:
 *
 * 1. 内核检测到待处理信号
 * 2. 在用户栈上构造信号帧(保存当前上下文)
 * 3. 修改 RIP 为信号处理函数地址
 * 4. 返回用户态 → CPU 执行信号处理函数
 * 5. 信号处理函数执行 ret → 跳转到 sigreturn trampoline
 * 6. trampoline 调用 sigreturn 系统调用
 * 7. 内核恢复保存的上下文
 * 8. 继续执行原来的代码
 */
c
/* sigreturn 系统调用 */
/* 从信号处理函数返回 */
SYSCALL_DEFINE0(sigreturn) {
    struct pt_regs *regs = current_pt_regs();
    struct sigframe __user *frame;
    sigset_t set;

    /* 获取信号帧地址 */
    frame = (struct sigframe __user *)(regs->sp - 8);

    /* 验证帧地址 */
    if (!access_ok(frame, sizeof(*frame)))
        goto badframe;

    /* 从信号帧恢复信号掩码 */
    if (__copy_from_user(&set, &frame->uc.uc_sigmask, sizeof(set)))
        goto badframe;

    set_current_blocked(&set);

    /* 恢复寄存器上下文 */
    if (restore_sigcontext(regs, &frame->uc.uc_mcontext))
        goto badframe;

    /* 恢复栈指针 */
    /* 返回到原来的执行点 */
    return regs->ax;

badframe:
    force_sig(SIGSEGV);
    return 0;
}

2.5 实时信号

Linux支持两种信号:普通信号(1-31)和实时信号(SIGRTMIN-SIGRTMAX,通常34-64)。

c
/* 普通信号 vs 实时信号 */
/*
 * 普通信号(1-31):
 * - 不排队:多个相同信号只保留一个
 * - 不保证传递顺序
 * - 不携带额外信息(除了信号编号)
 * - 例如:SIGINT, SIGTERM, SIGKILL
 *
 * 实时信号(SIGRTMIN-SIGRTMAX):
 * - 排队:多个相同信号都会保留
 * - 按发送顺序传递
 * - 可以携带额外数据(sigval)
 * - 更可靠
 * - 例如:SIGRTMIN, SIGRTMIN+1, ...
 */

/* 实时信号的发送 */
#include <signal.h>
#include <stdio.h>

int main() {
    sigval_t value;
    value.sival_int = 42;

    /* 发送实时信号,携带数据 */
    sigqueue(getpid(), SIGRTMIN, value);

    return 0;
}

/* 实时信号的处理 */
void rt_handler(int sig, siginfo_t *info, void *context) {
    printf("Received signal %d, data = %d\n",
           sig, info->si_value.sival_int);
}

int main() {
    struct sigaction sa;
    sa.sa_sigaction = rt_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO;  /* 必须设置,以获取 siginfo */

    sigaction(SIGRTMIN, &sa, NULL);

    /* 发送多个相同的实时信号 */
    for (int i = 0; i < 5; i++) {
        sigval_t val;
        val.sival_int = i;
        sigqueue(getpid(), SIGRTMIN, val);
    }

    /* 输出:
     * Received signal 34, data = 0
     * Received signal 34, data = 1
     * Received signal 34, data = 2
     * Received signal 34, data = 3
     * Received signal 34, data = 4
     * 所有5个信号都被接收!
     */

    return 0;
}

2.6 信号的阻塞

进程可以阻塞某些信号,被阻塞的信号不会立即传递,而是留在pending集合中,直到解除阻塞。

c
/* 信号阻塞的内核实现 */
/* kernel/signal.c */

/* sigprocmask 系统调用 */
SYSCALL_DEFINE3(sigprocmask, int, how, old_sigset_t, nset,
                old_sigset_t __user *, oset) {
    sigset_t old_set;
    int error;

    /* 保存旧的信号掩码 */
    if (oset) {
        old_set = current->blocked;
        /* 复制到用户空间... */
    }

    if (nset) {
        sigset_t new_set;
        /* 从用户空间复制... */

        spin_lock_irq(&current->sighand->siglock);

        switch (how) {
        case SIG_BLOCK:
            /* 阻塞指定信号 */
            sigorsets(&current->blocked, &current->blocked, &new_set);
            break;
        case SIG_UNBLOCK:
            /* 解除阻塞 */
            signotset(&new_set, &new_set, NULL);
            sigandsets(&current->blocked, &current->blocked, &new_set);
            break;
        case SIG_SETMASK:
            /* 设置信号掩码 */
            current->blocked = new_set;
            break;
        default:
            error = -EINVAL;
        }

        /* 检查是否有待处理的信号需要传递 */
        recalc_sigpending();

        spin_unlock_irq(&current->sighand->siglock);
    }

    return error;
}

/*
 * 信号阻塞的效果:
 *
 * 1. 信号被阻塞时:
 *    - 信号仍然被添加到 pending 集合
 *    - 但不会被传递(不执行处理函数)
 *    - 信号在 pending 中等待
 *
 * 2. 解除阻塞时:
 *    - 内核检查 pending 集合
 *    - 如果有未阻塞的待处理信号,立即传递
 *
 * 3. 特殊情况:
 *    - SIGKILL 和 SIGSTOP 不能被阻塞
 *    - 内核在 sigprocmask 中自动清除这两个信号
 */
c
/* 用户空间使用信号阻塞 */
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    sigset_t set, old_set, pending;

    /* 阻塞 SIGINT (Ctrl+C) */
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigprocmask(SIG_BLOCK, &set, &old_set);

    printf("SIGINT is blocked. Press Ctrl+C...\n");
    sleep(5);

    /* 检查 pending 信号 */
    sigpending(&pending);
    if (sigismember(&pending, SIGINT)) {
        printf("SIGINT is pending!\n");
    }

    /* 解除阻塞 */
    printf("Unblocking SIGINT...\n");
    sigprocmask(SIG_SETMASK, &old_set, NULL);
    /* 此时 SIGINT 会被立即传递 */

    printf("Done.\n");
    return 0;
}

三、实战案例

3.1 观察信号的排队行为

c
/* signal_queue.c - 测试信号的排队行为 */
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

static volatile int sigint_count = 0;
static volatile int sigrt_count = 0;

void sigint_handler(int sig) {
    sigint_count++;
    printf("SIGINT received (count: %d)\n", sigint_count);
}

void sigrt_handler(int sig, siginfo_t *info, void *context) {
    sigrt_count++;
    printf("SIGRTMIN received (count: %d, data: %d)\n",
           sigrt_count, info->si_value.sival_int);
}

int main() {
    struct sigaction sa;

    /* 设置 SIGINT 处理函数 */
    sa.sa_handler = sigint_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);

    /* 设置 SIGRTMIN 处理函数 */
    sa.sa_sigaction = sigrt_handler;
    sa.sa_flags = SA_SIGINFO;
    sigaction(SIGRTMIN, &sa, NULL);

    /* 阻塞信号 */
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGRTMIN);
    sigprocmask(SIG_BLOCK, &set, NULL);

    printf("PID: %d\n", getpid());
    printf("Sending signals while blocked...\n");

    /* 发送多个 SIGINT(不排队) */
    for (int i = 0; i < 5; i++) {
        kill(getpid(), SIGINT);
    }

    /* 发送多个 SIGRTMIN(排队) */
    for (int i = 0; i < 5; i++) {
        sigval_t val;
        val.sival_int = i * 10;
        sigqueue(getpid(), SIGRTMIN, val);
    }

    printf("Unblocking signals...\n");

    /* 解除阻塞 */
    sigprocmask(SIG_UNBLOCK, &set, NULL);

    printf("\nFinal counts:\n");
    printf("SIGINT: %d (expected: 1, not queued)\n", sigint_count);
    printf("SIGRTMIN: %d (expected: 5, queued)\n", sigrt_count);

    return 0;
}

运行结果:

PID: 12345
Sending signals while blocked...
Unblocking signals...
SIGINT received (count: 1)
SIGRTMIN received (count: 1, data: 0)
SIGRTMIN received (count: 2, data: 10)
SIGRTMIN received (count: 3, data: 20)
SIGRTMIN received (count: 4, data: 30)
SIGRTMIN received (count: 5, data: 40)

Final counts:
SIGINT: 1 (expected: 1, not queued)
SIGRTMIN: 5 (expected: 5, queued)

3.2 信号与系统调用的交互

c
/* signal_syscall.c - 信号中断系统调用 */
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

void handler(int sig) {
    printf("Caught signal %d (%s)\n", sig, strsignal(sig));
}

int main() {
    struct sigaction sa;

    /* 设置 SIGINT 处理函数,不使用 SA_RESTART */
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;  /* 不使用 SA_RESTART */
    sigaction(SIGINT, &sa, NULL);

    char buf[100];
    printf("Reading from stdin (press Ctrl+C to interrupt)...\n");

    ssize_t n = read(STDIN_FILENO, buf, sizeof(buf));

    if (n < 0) {
        if (errno == EINTR) {
            printf("read() was interrupted by signal! (EINTR)\n");
            printf("This is because SA_RESTART was not set.\n");
        } else {
            perror("read");
        }
    } else {
        printf("Read %zd bytes\n", n);
    }

    return 0;
}

3.3 使用 /proc 分析信号状态

bash
# 查看进程的信号信息
cat /proc/self/status | grep -i sig
# SigQ:   0/63497
# SigPnd: 0000000000000000
# SigBlk: 0000000000000000
# SigIgn: 0000000000000004  # SIGINT 被忽略?
# SigCgt: 0000000000000000

# 解码信号位图
python3 -c "
mask = 0x0000000000000004
for i in range(1, 65):
    if mask & (1 << (i-1)):
        print(f'Signal {i} is set')
"
# Signal 3 is set  (SIGQUIT)

# 查看信号定义
kill -l
# 1) SIGHUP   2) SIGINT   3) SIGQUIT  4) SIGILL  ...

四、本章小结

本章从内核角度深入讲解了信号的实现机制。信号在内核中通过task_struct的pending集合和blocked掩码管理。信号发送将信号添加到目标进程的pending集合,内核在返回用户态前检查pending集合,传递未被阻塞的信号。

信号处理函数的执行需要内核构造特殊的用户态栈帧:保存当前上下文,修改指令指针跳转到处理函数,处理函数返回后通过sigreturn系统调用恢复原始上下文。普通信号不排队(多个相同信号只保留一个),实时信号支持排队和携带数据。

信号的阻塞机制允许进程临时延迟信号的传递,被阻塞的信号留在pending集合中,直到解除阻塞后立即传递。SIGKILL和SIGSTOP不能被阻塞、捕获或忽略,这是内核的强制规定。


五、思考与练习

思考题

为什么SIGKILL不能被捕获或忽略?如果允许捕获SIGKILL,会带来什么问题?

信号处理函数为什么应该是"可重入安全"的?哪些函数是信号安全的?

在多线程程序中,信号是如何在线程之间分配的?线程组共享的信号和线程私有的信号有什么区别?

为什么信号处理函数中不能调用printf?应该使用什么替代?

编程练习

编写一个程序,验证普通信号不排队的行为:阻塞SIGINT,发送多个SIGINT,然后解除阻塞,观察只处理一次。

编写一个程序,使用实时信号(SIGRTMIN)实现简单的进程间消息传递,验证实时信号的排队和数据携带功能。

编写一个程序,演示信号中断系统调用的行为(EINTR),然后使用SA_RESTART标志重新测试,对比两种行为。


10/10信号
0%