Linux Kernel: Signal

Table of Contents

1. Linux Kernel: Signal

1.1. Overview

信号 (signal) 是一个很短的消息, 发送者可以是 kernel 本身或某个进程, 接收者可以是单个进程或一组进程(线程), 通常这个消息只会包含一个数字来标识具体是哪个信号, 但新的 sigaction API 可以使 kernel 在消息中保存更多的更多的内容, 例如发送者的 pid, uid 等.

传统的信号一个 31 个, 后来 POSIX 又引入了 real-time signal 的概念, real-time signal 使用的信号范围是 32 .. 64, 所以信号的总数不超过 64 个.

task_struct 中针对每个信号都有一个处理函数 (sa_handler)), 用户可以通过 signal 或 sigaction 来设置这个 sa_handler. sa_handler 可以是一段用户空间的代码, 也可以是一些特殊值表示特殊的处理:

  1. SIG_DFL 表示处理这个信号采用 kernel 默认的处理函数
  2. SIG_IGN 表示忽略这个信号, 不做任何处理

用户进程 A 可以通过 kill, tkill, tgkill 三个系统调用向另一个进程 B "发送" 一个特定的信号, 这个过程称为 signal generating, 其中:

  1. tkill 是仅仅向某个线程发送信号, 和线程所在的线程组 (thread group) 中的某它线程无关
  2. tgkill 功能和 tkill 是一样的…只不过它会在 tkill 基础上做一些检查
  3. kill 是向整个 thread group 发送信号, kernel 负责从 thread group 中选择某个 thread 来响应这个信号, 另外, 如果这个信号是 fatal 的, 则 kernel 还会向 thread group 的所有线程发送 SIG_KILL 使得整个 thread group 都结束执行

B 进程事先可以通过 sigprocmask 将特定信息标记为 BLOCKED, A 进程发送过来的信号会被记入 B 进程的 pending signal 列表中, 当 B 后面再次通过 sigprocmask 解除对这个信号的 block 时, B 会立即收到这个信号. 这一点与 SIG_IGN 是不同的: 如果某个信号的处理函数通过 sigal 或 sigaction 设置为 SIG_IGN, 则这个信号并不会进入 pending signal 列表中: 它会被直接丢弃

A 发送信号给 B 只是将信号记在 B 的 pending signal 中, B 真正响应这个信号的时机是从 kernel mode 返回 user mode 时, 参考 ret_from_intr 中关于 work_pending 的描述. B 响应信号的过程称为 signal delivering

当 B 事先注册的 sa_handler 被调用时, 因为 sa_handler 是一段用户空间的代码, 而 ret_from_intr 时还在 kernel mode, 所以 kernel 需要一种手段能在 kernel mode 调用 user mode 的代码

1.2. 数据结构

kernel_signal.png

  1. p->pending 类型为 sigpending, 其中的 pending->signal 是一个 64 big 的 mask, 标识有哪个 pending 的信号. pending->list 是一个 sigqueue 的链表, sigqueue 描述了每一个 pending 的信号的具体信息
  2. p->signal 中的许多信息和 signal 的处理关系并不紧密, 例如 signal->rlim 是进程的 rlimit 信息, rlimit 中的大部分信息和信号没有一点关系. p->signal->share_pending 和 p->pending 类似, 如果一个信号是发给整个 thread group 的, 则需要使用 share_pending 做一些记录
  3. p->sighand 是针对每个信号的 sa_handler 信息, signal 和 sigaction 系统调用会直接修改它的内容.

1.3. 信号产生

根据信号的来源 (进程通过 kill 发起或 kernel 本身发起的), kernel 通过许多函数来发送信号 (signal generating):

  1. 发送给单个线程
    • sys_tkill
    • sys_tgkill
    • send_sig{_info}
    • force_sig{_info}
  2. 发送给整个线程组
    • send_group_sig_info
    • kill_pg{_info}
    • kill_proc{_info}
    • sys_kill

不论上面提到的哪个函数, 最终都会调用到 specific_send_sig_info 或 group_send_sig_info

1.3.1. 给单个线程发送信号

1.3.1.1. specific_send_sig_info
specific_send_sig_info(sig, info, t):
  if (sig_ignored(t, sig)):
        // task_struct->block 是一个 64 bit 的 mask, 标识哪些 sig 被
        // block. 被 block 的 sig 不能被 ignore, 因为它们需要被放在 pending
        // signal 中以便 unblock 时进程能再次收到这个 sig
        if (sigismember(&t->blocked, sig)):
          return 0;
        // 若 t->sighand 中 sig 对应的 handler 为 SIG_IGN 或 handler 为
        // SIG_DFL 但 sig 的默认 handler 也为 ignore, 则忽略这个 sig
        handler = t->sighand->action[sig-1].sa.sa_handler;
        return   handler == SIG_IGN || (handler == SIG_DFL && sig_kernel_ignore(sig));
    goto out;
  // 没有 ignore 的 sig 最终需在放在 t->pending 中, sigpending->signal 是
  // 一个 bitmask, 标识 pending 列表中是否已经存在相同 sig 的 signal.
  //
  // 对于普通的 (非 real time) 的 signal, 对于同一个 sig, pending 列表中
  // 最多只会保存一个对应的 signal, 即多个普通 signal 不会累积在 pending
  // 中.
  //
  // 但对于 real time signal, 多个 real time signal 是可以在 pending 中累
  // 积的
  if (LEGACY_QUEUE(&t->pending, sig)):
        (((sig) < SIGRTMIN) && sigismember(&(pending)->signal, (sig)))
    goto out;

  send_signal(sig, info, t, &t->pending);
  // 如果 sig 没有 block, 则唤醒进程以便它能及时响应这个 signal
  if (!ret && !sigismember(&t->blocked, sig)):
    signal_wake_up(t, sig == SIGKILL);
      // 设置 TIF_SIGPENDING, 以便 ret_from_intr 时能发现有 pending signal
      set_tsk_thread_flag(t, TIF_SIGPENDING);
      // 正常情况下只有处于 TASK_INTERRUPTIBLE 状态的进程才会被唤醒, 但若
      // sig 为 SIG_KILL, 则 TASK_STOPPED 状态和 TASK_TRACED 状态的进程也
      // 会被唤醒.
      //
      // 所以正在 sleep 或阻塞在 read 上的进程会被唤醒, 这也是 signal 导致系统调
      // 用提前返回的原因. 另外, 处于 D 状态 (TASK_UNINTERRUPTIBLE) 的进
      // 程不会被唤醒, 所以 D 状态的进程无法及时响应 signal
      mask = TASK_INTERRUPTIBLE;
      if (resume):
        mask |= TASK_STOPPED | TASK_TRACED;
      wake_up_state(t, mask)
1.3.1.2. send_signal
send_signal(sig, info, t, pending):
  // 分配一个 sigqueue, 后面需要将它插入 pending 链表中
  q = __sigqueue_alloc(t, GFP_ATOMIC);
  list_add_tail(&q->list, &signals->list);
  switch ((unsigned long) info):
    // send_signal 时传入的 info (siginfo) 一般是由上层填充的, 包含
    // signo, errno, code, pid, uid, addr 等数据. 然后 send_signal 时通过
    // copy_siginfo 将 info 复制到 q->info 中.
    //
    // 但有时 kernel 有时会通过 force_sig, send_sig 发送一些信号, 而且这
    // 些信号并不需要通过 siginfo 传递大部分信息, 为了避免 copy_siginfo,
    // kernel 使用特定的 0, 1 做为 info 并在这里进行特殊的处理.
    case 0:
      q->info.si_signo = sig;
      q->info.si_errno = 0;
      q->info.si_code = SI_USER;
      q->info.si_pid = current->pid;
      q->info.si_uid = current->uid;
      break;
    case 1:
      q->info.si_signo = sig;
      q->info.si_errno = 0;
      q->info.si_code = SI_KERNEL;
      q->info.si_pid = 0;
      q->info.si_uid = 0;
      break;
    default:
      copy_siginfo(&q->info, info);
      break;

    sigaddset(&pending->signal, sig);

1.3.2. 给线程组发送信号

1.3.2.1. group_send_sig_info
group_send_sig_info:
  __group_send_sig_info(sig, info, p);
    // 同一个 thread group 的 t->signal 和 t->sighand 是共享的 (t->blocked
    // 和 t->pending 是单独的), 所以 sig_ignored 对 p 或者 p 这个线程组中
    // 的所有线程都会返回相同的结果
    if (sig_ignored(p, sig)):
      return ret;
    // sig 将被加入 p->shared_pending 而不是 t->pending
    if (LEGACY_QUEUE(&p->signal->shared_pending, sig)):
      return ret;
    // 与 specific_send_sig_info 相同, 只不过是加入到 p->shared_pending 而不
    // 是 t->pending
    send_signal(sig, info, p, &p->signal->shared_pending);
    // 对于 specific_send_sig_info 来说, 这里会通过 signal_wake_up 唤
    // 醒 p 来响应这个信号. 但对于 group_send_sig_info 来说, 这里会通
    // 过__group_complete_signal 来选择 p 所在的 thread group 中的某一
    // 个 "合适" 的线程来响应这个信号
    __group_complete_signal(sig, p);
1.3.2.2. __group_complete_signal
// __group_complete_signal 的目的是从 p 所在的 thread group 中选择一个
// 线程来唤醒, 以响应之前已经加入到 p->shared_pending 中的信号
__group_complete_signal(sig, p):
  // wants_signal 用来检查 thread p 是否 "应该" 响应 sig, 响应 sig 的条件
  // 为:
  // 1. sig 没有被 p block
  // 2. p->state 不是 mask 所指定的 state
  // 3. p 正占用着 cpu 或 p 没有任何 pending 信号 (p->pending 及
  //    p->shared_pending 为空)
  if (wants_signal(sig, p, mask)):
        #define wants_signal(sig, p, mask)                      \
          (!sigismember(&(p)->blocked, sig)             \
           && !((p)->state & mask)                      \
           && !((p)->flags & PF_EXITING)                 \
           && (task_curr(p) || !signal_pending(p)))
    // 优先选择 kill syscall 最初指定的线程来响应信号
    t = p;
  else:
    // 从 thread group 中选择一个 wants_signal 为真的
    // p->signal->curr_target 记录了上次响应信号的 thread, 通过这个可以
    // 在一定程度上实现信号处理的 load balancing
    t = p->signal->curr_target;
    if (t == NULL):
      t = p->signal->curr_target = p;
    // 遍历所有的 thread
    while (!wants_signal(sig, t, mask)):
      t = next_thread(t);
      // 没有任何一个 thread wants_signal, 那这里无法唤醒任何线程: 只能等
      // 着某个线程 "自然" 唤醒后来处理这个信号了
      if (t == p->signal->curr_target)
        return;
    p->signal->curr_target = t;

  if (sig_fatal(p, sig) ....):
    // 如果是 fatal 信号, 直接 kill 掉 thread group 中所有线程, 这一点
    // 与 get_signal_to_deliver 里的逻辑差不多
    ...
  // 最后, 唤醒 t 来响应这个信号
  signal_wake_up(t, sig == SIGKILL);

1.3.3. 举例

1.3.3.1. 应用发起的 kill
sys_kill(int pid, int sig):
  struct siginfo info;

  info.si_signo = sig;
  info.si_errno = 0;
  info.si_code = SI_USER;
  info.si_pid = current->tgid;
  info.si_uid = current->uid;

  kill_something_info(sig, &info, pid);
  // 若 pid 为 0, 则表示要给当前进程所在进程组的所有进程发送信号. 关于进
  // 程组, 参考 [[file:process.org::*Processes%20Relationship][Processes Relationship]]
  if (!pid):
    return kill_pg_info(sig, info, process_group(current));
      // pg 是指 process group, 即进程组
      do_each_task_pid(pgrp, PIDTYPE_PGID, p) {
        group_send_sig_info(sig, info, p);
      } while_each_task_pid(pgrp, PIDTYPE_PGID, p);

  // 若 pid 为负数, 则给 -pid 所指示的进程组的每个进程发送信号
  else if (pid < 0):
    return kill_pg_info(sig, info, -pid);
  // 给 pid 指示的进程发送信号
  else:
    return kill_proc_info(sig, info, pid);
      p = find_task_by_pid(pid);
      group_send_sig_info(sig, info, p);
1.3.3.2. page fault 导致的 SIGSEGV
do_page_fault:
  info.si_signo = SIGSEGV;
  info.si_errno = 0;
  info.si_addr = (void __user *)address;
  force_sig_info(SIGSEGV, &info, tsk);
    // 若 sig 被 blocked, 则 force_sig_info 强制把它 unblock; 若
    // sa_handler 被修改为 SIG_IGN, 则 force_sig_info 会强制把
    // sa_handler 变为 SIG_DFL...
    //
    // 所以看起来应用把 sa_handler 写成一个空函数和直接设置为 SIG_IGN 还是
    // 有些差别的
    if (sigismember(&t->blocked, sig) ||
        t->sighand->action[sig-1].sa.sa_handler == SIG_IGN):
      t->sighand->action[sig-1].sa.sa_handler = SIG_DFL;
      sigdelset(&t->blocked, sig);
      recalc_sigpending_tsk(t);
    specific_send_sig_info(sig, info, t);

1.4. 信号传递

信号传递 (signal delivering) 是指 pending 信号对应的 sa_handler 真正被执行的过程. 一般情况下, 中断处理的 resume_userspace 会检查是否有需要处理的信号(resume_kernel 并不会做这种检查)

resume_userspace 被调用可以分为两种情况:

  1. 中断或 syscall 正常完成, 调用 resume_userspace
  2. syscall 阻塞, 但被信号唤醒 (通过 signal_wake_up), 从而调用 resume_userspace

其中后者是 kernel 处理 "syscall 被中断并重启" 问题的关键.

resume_userspace:
  movl TI_flags(%ebp), %ecx
  andl $_TIF_WORK_MASK, %ecx

  jne work_pending

work_pending:
  testb $_TIF_NEED_RESCHED, %cl
  jz work_notifysig

work_notifysig:
  call do_notify_resume
    // signal_wake_up 时会置位.
    //
    // 另外更一般的情况下, kernel 每次对 pending 的改变后都会调用
    // recalc_sigpending_tsk 根据 pending->signal 来对 TIF_SIGPENDING 置位
    // 或复位
    if (thread_info_flags & _TIF_SIGPENDING):
      do_signal(regs,oldset);

1.4.1. do_signal

do_signal 是信号处理最主要的部分, 它需要完成信号处理中最复杂的部分: 在 kernel space 调用 user space 下的 sa_handler

do_signal:
  signr = get_signal_to_deliver(&info, &ka, regs, NULL);
  if (signr > 0):
    handle_signal(signr, &info, &ka, oldset, regs);
    return 1
1.4.1.1. get_signal_to_deliver

找一个 pending 的 signal, 并交给 handle_signal 去处理. 如果 pending signal 对应的 handler 是 SIG_DFL, 则执行默认的操作: ignore, coredump 或 exit

get_signal_to_deliver:
  sigset_t *mask = &current->blocked;
  signr = dequeue_signal(current, mask, info);
    int signr = __dequeue_signal(&tsk->pending, mask, info);
      // 从 pending->signal 这个 bitmask 找到 mask 之外的第一个 pending
      // 的 signr
      sig = next_signal(pending, mask);
      // 从 pending 列表中找到对应于 sig 的 siginfo
      collect_signal(sig, pending, info)
    // 若 pending 中没有, 则查找 shared_pending. group_send_sig_info 时
    // 会将信号加入到 shared_pending 中
    if (!signr):
      signr = __dequeue_signal(&tsk->signal->shared_pending, mask, info);
  // 没有任何 pending signal
  if (!signr):
    break;

  // 找到一个 pending signal
  ka = &current->sighand->action[signr-1];
  if (ka->sa.sa_handler == SIG_IGN):
    continue;
  if (ka->sa.sa_handler != SIG_DFL):
    // 找到一个有 sa_handler 的信号, 返回到上层, 后续会调用 handle_signal
    // 来处理它
    *return_ka = *ka;
    if (ka->sa.sa_flags & SA_ONESHOT):
      ka->sa.sa_handler = SIG_DFL;
    break;
  // 前两个判断都不满足, 走 SIG_DFL 的处理路径
  if (sig_kernel_ignore(signr)):
    continue;
  if (current->pid == 1):
    continue;
  // sig 是 SIGSTOP 一类的信号
  if (sig_kernel_stop(signr)):
    do_signal_stop(signr)
    continue
  // 剩下的全是 fatal 信号
  if (sig_kernel_coredump(signr)):
    // 需要生成 coredump
    do_coredump((long)signr, signr, regs);
  // 对于 fatal 信号, 不仅仅是当前线程退出, 而是整个 thread group 中的所
  // 有线程都会退出
  do_group_exit(signr);
    zap_other_threads(current);
      for (t = next_thread(p); t != p; t = next_thread(t)):
        // 相当于给其它每个子线程都发送一个 SIGKILL
        sigaddset(&t->pending.signal, SIGKILL);
        rm_from_queue(SIG_KERNEL_STOP_MASK, &t->pending);
        signal_wake_up(t, 1);
    // do_exit 不会返回, 所以后面的 handle_signal 并不会执行
    do_exit(exit_code);
1.4.1.2. handle_signal
handle_signal(sig, info, ka, ...):
  // 最初应用通过 sigaction 时使用了 SA_SIGINFO 这个 flag, 表示上层的
  // handler 执行时需要拿到 siginfo 信息, kernel 需要调用 sigaction 时注
  // 册的 sa_sigaction 回调, 而不是 sa_handler.
  //
  // sa_sigaction 回调的原型为:
  //
  // void (sa_action *) (int signr, siginfo_t * info, void *)
  if (ka->sa.sa_flags & SA_SIGINFO):
    setup_rt_frame(sig, ka, info, oldset, regs);
  else:
    setup_frame(sig, ka, oldset, regs);
1.4.1.3. setup_frame

kernel_signal_frame.png

setup_frame:
  struct sigframe __user *frame;
  frame = get_sigframe(ka, regs, sizeof(*frame));
    // 在 user mode stack 上向下扩展出一块区域, 存放 sigframe
    esp = regs->esp;
    return (void __user *)((esp - frame_size) & -8ul);

  __put_user(sig, &frame->sig);
  setup_sigcontext(&frame->sc, &frame->fpstate, regs, set->sig[0]);
    // 将 regs 中原始内容保存在 sigframe 的 sigcontext 中, 因为后面 regs
    // 中的内容会被替换为 sa_handler 对应的部分
    __put_user(regs->edi, &sc->edi);
    __put_user(regs->esi, &sc->esi);
    __put_user(regs->ebp, &sc->ebp);
    __put_user(regs->esp, &sc->esp);
    __put_user(regs->ebx, &sc->ebx);
    __put_user(regs->edx, &sc->edx);
    __put_user(regs->ecx, &sc->ecx);
    __put_user(regs->eax, &sc->eax);
    __put_user(regs->eip, &sc->eip);
    __put_user(regs->xcs, (unsigned int __user *)&sc->cs);
    __put_user(regs->eflags, &sc->eflags);
    __put_user(regs->esp, &sc->esp_at_signal);

  restorer = &__kernel_sigreturn;
  __put_user(restorer, &frame->pretcode);

  // 这里对 regs 重新赋值, 当 do_signal 返回后 iret 会负责用 regs 恢复
  // user space 的上下文, 导致 user space 会跳转到 sa_handler, 并且参数为
  // sig, esp 为 frame.
  //
  // 所以, sa_handler 在 user mode 执行时 stack 的布局 (从高到低) 为:
  //
  // ..
  // sigcontext
  // sig
  // pretcode (__kernel_sigreturn)  <-- esp
  //
  // 因为 x86 默认使用栈来传递参数, 所以 sa_handler 执行时第一个参数 signr
  // 与栈上的 sig 是对应的 // 这里对 regs 重新赋值, 当 do_signal 返回后
  // iret 会负责用 regs 恢复 user space 的上下文, 导致 user space 会跳转到
  // sa_handler, 并且参数为 sig, esp 为 frame.
  //
  // 所以, 参考 sigframe 的定义, sa_handler 在 user mode 执行时 stack 的
  // 布局 (从高到低) 为:
  //
  // --high
  // ..
  // sigcontext
  // sig
  // pretcode (__kernel_sigreturn)  <-- esp
  // --low
  //
  regs->esp = (unsigned long) frame;
  regs->eip = (unsigned long) ka->sa.sa_handler;
  // x86 默认使用栈来传递参数, 这里并不明确设置 eax 的目的...
  regs->eax = (unsigned long) sig;
  regs->edx = (unsigned long) 0;
  regs->ecx = (unsigned long) 0;
  regs->xcs = __USER_CS;

setup_rt_frame 和 setup_frame 基本相同, 只不过它会在栈上额外设置一个 siginfo 的指针.

1.4.1.4. __kernel_sigreturn

sa_handler 执行结束后, 按照 c 默认的调用约定和前面描述的栈布局, 会跳转到 __kernel_sigreturn 函数, __kernel_sigreturn 函数是保存在 vsyscall page 中的一个函数, 所以应用可以直接访问, 关于 vsyscall page, 参考

通过将 vsyscall page dump 到本地并反汇编, 能看到 __kernel_sigreturn 的代码如下:

pop %eax
mov $0x77,%eax
int $0x80

可见它会调用 sys_sigreturn 这个系统调用

显然 sys_sigreturn 需要完成以下的任务: 从 sigframe->sc 中恢复旧的 regs 以便 user mode 能返回到被 sa_handler 打断的位置.

sys_sigreturn:
  // 为什么是 regs->esp - 8?
  // 根据前面描述的 sa_handler 的栈布局:
  //
  // --high
  // sigcontext
  // sig
  // pretcode (__kernel_sigreturn)  <-- esp
  // --low
  //
  // 当 sa_handler 结束时调用 ret 之前, esp 是指向 __kernel_sigreturn 的,
  // 也就是 sigframe. ret 指令会导致 esp + 4, 然后 __kernel_sigreturn 的
  // 第一句是 pop %eax, 导致 esp 再次 + 4, 此后通过 int 0x80 进入 syscall
  // 导致当前的 esp 已经偏离 sigframe 8 字节, 所以这里需要 -8 ... 至于
  // __kernel_sigreturn 为啥会 pop %eax 不清楚...
  //
  // 对应于 sys_rt_sigreturn 的 __kernel_rt_sigreturn 并没有这个 pop 操
  // 作, 所以 sys_rt_sigreturn 获得 sigframe 时只需要 - 4
  struct sigframe __user *frame = (struct sigframe __user *)(regs->esp - 8);
  // 使用 frame->sc 恢复旧的 regs
  restore_sigcontext(regs, &frame->sc, &eax)
1.4.1.5. 观察 sigframe

测试程序:

#include <signal.h>

void * foo(int signr) {

}

int main(int argc, char *argv[]) {
    signal(10, foo);
    sleep(1000);
    return 0;
}

使用 gdb 分析的过程:

$> gcc -O0 -g test.c -m32
$> gdb ./a.out
(gdb) b main
(gdb) b foo
(gdb) r
Starting program: /home/sunway/a.out

Breakpoint 1, main (argc=1, argv=0xffffc454) at test.c:8
8           signal(10, foo);
(gdb) n
9           sleep(1000);
(gdb) n

# 在这里使用 kill -10 <pid> 给 a.out 发送一个信号, 导致 sleep 系统调用
# 被打断而提前返回, 但这里的 "提前返回" 并不是指此时控制已经返回到
# sleep 之后的代码 (0xf7fd8d49), 因为在返回 sleep 之后的代码前,
# sa_handler 需要先被执行

Program received signal SIGUSR1, User defined signal 1.
0xf7fd8d49 in __kernel_vsyscall ()

(gdb) n
Single stepping until exit from function __kernel_vsyscall,
which has no line number information.
foo (signr=10) at test.c:3
3       void * foo(int signr) {

(gdb) x /20x $esp
0xffffbd6c:     0xf7fd8d20      0x0000000a      0x00000063      0x00000000
0xffffbd7c:     0x0000002b      0x0000002b      0xffffc368      0xf7fd4240
0xffffbd8c:     0x00000000      0xffffc32c      0xffffc368      0x7fffffff
0xffffbd9c:     0xffffc368      0xfffffffc      0x00000001      0x00000000
0xffffbdac:     0xf7fd8d49      0x00000023      0x00000246      0xffffc32c

# 按照 setup_frame 设定的栈布局:
#
# 1. 栈顶的 0xf7fd8d20 即是位于 vsyscall page 的 __kernel_sigreturn
# 2. 0x0000000a 是 signr
# 3. 从 0x00000063 开始后面对应的数据是 sigcontext 中的内容, 其中
#    0xffffbdac 处保存的 0xf7fd8d49 对应于 sigcontext->eip, 它和前面
#    sleep 被提前打断时显示的 eip 是一致的, 都对应于 __kernel_vsyscall

(gdb) disass 0xf7fd8d20
Dump of assembler code for function __kernel_sigreturn:
   0xf7fd8d20 <+0>:     pop    %eax
   0xf7fd8d21 <+1>:     mov    $0x77,%eax
   0xf7fd8d26 <+6>:     int    $0x80
   0xf7fd8d28 <+8>:     nop
End of assembler dump.

(gdb) disass 0xf7fd8d49
Dump of assembler code for function __kernel_vsyscall:
   0xf7fd8d40 <+0>:     push   %ecx
   0xf7fd8d41 <+1>:     push   %edx
   0xf7fd8d42 <+2>:     push   %ebp
   0xf7fd8d43 <+3>:     mov    %esp,%ebp
   0xf7fd8d45 <+5>:     sysenter
   // 这里的 int $0x80 是什么意思? 和
   // [[file:process.org::*sysenter,%20syscall,%20vsyscall][sysenter,
   // syscall, vsyscall]] 中 dump 的 __kernel_vsyscall 不一样...
   0xf7fd8d47 <+7>:     int    $0x80
   0xf7fd8d49 <+9>:     pop    %ebp
   0xf7fd8d4a <+10>:    pop    %edx
   0xf7fd8d4b <+11>:    pop    %ecx
   0xf7fd8d4c <+12>:    ret
End of assembler dump.
1.4.1.6. sigaltstack

一般情况下 sigframe 都是在 user stack, 但由于 user stack 一般较小, 若 user stack 已满怎么办?

可以通过 sigaltstack 通知 kernel 在另一块区域上建立 sigframe, 凡是 sa_flags 中包含 SA_ONSTACK 的 signal 都会使用 altstack (alternative stack)

1.5. 相关的系统调用

1.5.1. kill, tkill, tgkill

1.5.2. signal, sigaction

1.5.3. sigsuspend

1.5.4. sigpending

1.5.5. sigprocmask

1.6. Appendix

1.6.1. 系统调用的中断与重启

https://github.com/mozilla/rr/wiki/Linux-signals

有一些系统调用会导致 kernel 进入 TASK_INTERRUPTIBLE 状态, 例如:

  1. nanosleep
  2. 针对 pipe 的 read, write
  3. wait 类的调用

但发生信号时, kernel 会通过 signal_wake_up 强制将处理 TASK_INTERRUPTIBLE 状态的进程唤醒, 但此时它们等待的事件并没有完成, 却直接走到了之前造成阻塞的指令的下一条指令, 即结果 syscall 就被打断了.

以 pipe read 为例:

  1. 用户发起 pipe read 并被信号打断

    pipe_readv:
      for (;;):
        // pipe 的 read 操作阻塞, 等待 writer 向 pipe 写数据
        PIPE_WAITING_WRITERS(*inode)
        // 假设阻塞过程中有信号发生, 当前进程被唤醒, 则 PIPE_WAITING_WRITERS
        // 会直接返回, 执行下一条指令 (signal_pending)
        //
        // signal_pending 为真, 说明当前进程有 pending signal, 则说明是信号唤
        // 醒了当前进程, 则 syscall 提前返回 -ERESTARTSYS
        //
        // 由于 syscall 返回后 (但在调用 do_signal 之前) 会将返回值 (eax) 放
        // 在 pt_regs 中, 后续 do_signal 可以根据 pt_regs->eax 知道被打断的
        // syscall 关于重启的意图: 返回 EINTR 由上层重启 syscall, 或由 kernel
        // 直接通过设置 ip 自动重启 syscall
        if (signal_pending(current)):
          ret = -ERESTARTSYS;
          break;
    
  2. handle_signal

    handle_signal:
      // 当前是一个 syscall
      if (regs->orig_eax >= 0):
        switch (regs->eax):
          // 若被打断的 syscall 返回 ERESTART_RESTARTBLOCK 或 ERESTARTNOHAND,
          // 则直接对上层返回 EINTR, 由上层决定是否重启 syscall
          //
          // 若 syscall 返回 ERESTARTSYS, 则根据 sigaction 是否指定了
          // SA_RESTART 决定是返回 EINTR 还是通过修改 eip 和 eax 直接由
          // kernel 来重启 syscall
          case -ERESTART_RESTARTBLOCK:
          case -ERESTARTNOHAND:
            regs->eax = -EINTR;
            break;
    
          case -ERESTARTSYS:
            if (!(ka->sa.sa_flags & SA_RESTART)):
              regs->eax = -EINTR;
              break;
    
          /* fallthrough */
          case -ERESTARTNOINTR:
            regs->eax = regs->orig_eax;
            // 不论 int 0x80 或 sysenter, 指令长度都是 2 bytes
            regs->eip -= 2;
      // ...
      setup_frame
    

编程时需要考虑 syscall 被信号打断的情况时, 一般使用如下的代码:

do {
    rc = (int)the_sys_call();
} while(rc < 0 && errno == EINTR);

1.6.2. 信号与 longjmp

#include <setjmp.h>

jmp_buf buff;

void foo(int sig) {
    printf("foo\n");
    longjmp(buff, 1);
}

int main(int argc, char *argv[]) {
    int x = setjmp(buff);
    signal(11, foo);
    printf("hello %d\n", x);
    sleep(20);
    return 0;
}

由于 longjmp 不会返回, 所以栈上的 __kernel_sigreturn 并不会被调用, 所以 longjmp 后不会再返回到被信号打断的位置.

1.6.3. 信号与线程

通过 fork 时的 copy_process 代码, 可以看到:

  1. p->sighand 是共享的, 任何一个线程里修改了 sighand 都会影响到其它线程
  2. p->blocked 在各个线程中是独立的, 所以 sigprocmask 只会影响当前的线程
  3. p->pending 是独立的
  4. p->shared_pending 是共享的

Backlinks

RISU (RISU > Implementation details > risu.c): 1. master 和 apprentice 通信 2. 通过 sighandler 触发通信和检查, 并且用 sighandler 的 ucontext 读写寄存器 (参 考 kernel signal, signal handling in DBT) 3. 检查寄存器, 内存是否一致

Author: [email protected]
Date: 2016-08-10 Wed 00:00
Last updated: 2024-08-19 Mon 21:25

知识共享许可协议