目录

Linux内核信号处理

Linux内核信号处理

在Linux内核中,信号处理是一个复杂的过程,涉及用户态和内核态的交互。以下是信号处理的详细流程,结合代码和注释进行说明。


1. 信号处理的整体流程

信号处理的流程可以分为以下几个步骤:

  1. 信号产生 :内核或用户程序发送信号。

  2. 信号传递 :内核将信号添加到目标进程的信号队列中。

  3. 信号检查 :在进程从内核态返回到用户态时,内核检查是否有未处理的信号。

  4. 信号处理

    • 如果进程注册了信号处理函数,内核会调用该函数。
    • 如果没有注册处理函数,内核会执行默认行为(如终止进程、忽略信号等)。
  5. 信号处理完成 :进程返回到被信号打断的代码位置继续执行。


2. 信号处理的代码实现

以下是Linux内核中信号处理的核心代码片段,结合注释进行说明。

(1) 信号传递

当内核需要向进程发送信号时,会调用 send_signal() 函数。

// kernel/signal.c
static int send_signal(int sig, struct kernel_siginfo *info, struct task_struct *t, int group)
{
    struct sigpending *pending;
    struct sigqueue *q;

    // 获取目标进程的信号队列
    pending = group ? &t->signal->shared_pending : &t->pending;

    // 分配一个信号队列项
    q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit);

    // 将信号添加到队列中
    sigaddset(&pending->signal, sig);
    list_add_tail(&q->list, &pending->list);

    // 唤醒进程(如果进程处于可中断睡眠)
    signal_wake_up(t, sig == SIGKILL);
    return 0;
}
  • send_signal() :将信号添加到目标进程的信号队列中。
  • signal_wake_up() :如果进程处于可中断睡眠状态,唤醒进程。

(2) 信号检查

在进程从内核态返回到用户态时,内核会调用 do_signal() 函数检查是否有未处理的信号。

// arch/x86/kernel/signal.c
void do_signal(struct pt_regs *regs)
{
    struct ksignal ksig;

    // 检查是否有未处理的信号
    if (get_signal(&ksig)) {
        // 如果有信号,处理信号
        handle_signal(&ksig, regs);
        return;
    }

    // 如果没有信号,恢复执行
    restore_saved_sigmask();
}
  • get_signal() :从信号队列中获取一个未处理的信号。
  • handle_signal() :调用用户注册的信号处理函数。

(3) 信号处理

如果进程注册了信号处理函数,内核会调用 handle_signal() 函数。

// arch/x86/kernel/signal.c
static void handle_signal(struct ksignal *ksig, struct pt_regs *regs)
{
    // 设置用户态的信号处理函数
    regs->ip = (unsigned long) ksig->ka.sa.sa_handler;
    regs->sp = (unsigned long) ksig->ka.sa.sa_restorer;

    // 切换到用户态执行信号处理函数
    return;
}
  • regs->ip :设置指令指针(IP)为信号处理函数的地址。
  • regs->sp :设置栈指针(SP)为信号处理函数的栈。

(4) 信号处理完成

信号处理函数执行完毕后,进程会返回到被信号打断的代码位置继续执行。

// arch/x86/kernel/signal.c
void sys_rt_sigreturn(void)
{
    // 恢复被信号打断的上下文
    restore_sigcontext(&current->thread.regs);

    // 返回到用户态继续执行
    return;
}
  • restore_sigcontext() :恢复被信号打断的寄存器上下文。
  • sys_rt_sigreturn() :返回到用户态继续执行。

3. 信号处理的场景和影响

(1) 场景1:进程正在运行
  • 场景 :进程正在执行用户态代码,突然收到信号(如 SIGINT )。

  • 流程

    1. 内核将信号添加到进程的信号队列中。
    2. 进程从内核态返回到用户态时,检查到未处理的信号。
    3. 内核调用用户注册的信号处理函数。
    4. 信号处理函数执行完毕后,进程返回到被信号打断的代码位置继续执行。
  • 影响 :进程的执行被信号打断,但会继续执行。

(2) 场景2:进程处于可中断睡眠
  • 场景 :进程正在等待I/O操作(如 read() ),突然收到信号(如 SIGTERM )。

  • 流程

    1. 内核将信号添加到进程的信号队列中。
    2. 内核唤醒进程,并将其状态设置为 TASK_RUNNING
    3. 进程从内核态返回到用户态时,检查到未处理的信号。
    4. 内核调用用户注册的信号处理函数。
    5. 信号处理函数执行完毕后,进程返回到被信号打断的代码位置继续执行。
  • 影响 :进程被信号唤醒,I/O操作可能被中断。

(3) 场景3:进程处于不可中断睡眠
  • 场景 :进程正在等待硬件I/O操作(如磁盘读写),突然收到信号(如 SIGKILL )。

  • 流程

    1. 内核将信号添加到进程的信号队列中。
    2. 由于进程处于不可中断睡眠状态,信号不会唤醒进程。
    3. 进程继续等待硬件I/O操作完成。
    4. 硬件I/O操作完成后,进程被唤醒并处理信号。
  • 影响 :信号不会立即生效,进程必须等待硬件操作完成。


4. 总结

信号处理是Linux内核中一个重要的机制,涉及用户态和内核态的交互。通过 send_signal()do_signal()handle_signal() 等函数,内核实现了信号的传递、检查和处理。信号处理的具体行为和影响取决于进程的当前状态和信号类型。