signal是一类比较特殊的存在,需要kernel和userspace通力合作才能将signal处理流程走通。

Android 平台 Native 代码的崩溃捕获机制及实现对signal的描述比较完整,建议读读。


signal处理流程

简单的流程描述如下:

userspace:

  • 利用系统调用sigaction(…)或signal(…)设置感兴趣的signal的属性,包括处理函数、flags等。
  • 已经不鼓励使用signal系统调用,在libc中被封装成sigaction的调用。

kernel:

  • 在处理sigaction时,把userspace的signal处理函数地址以及flags保存在当前进程的控制块中:

    task_struct->sighand->action[]
    
  • 当有signal触发后,会将相应signal number在如下成员中置位,并置目标进程的状态为TIF_SIGPENDING,然后唤醒目标进程;如果目标进程处于running状态且运行于其他CPU,则发送reschedule IPI中断以便目标进程可以进入kernel并在退出kernel时处理signal;如果目标进程就是当前进程,则在退出kernel时处理signal;如果目标进程从睡眠状态被唤醒,检测到有signal pending,也会退出睡眠状态,从而返回user space。

    task_struct->pending
    task_struct->signal->shared_pending
    
  • 在进程被唤醒并返回userspace前夕, 发现TIF_SIGPENDING置位,从而执行信号处理函数do_signal(…)。

  • do_signal: 根据signal number,从 task_struct->sighand->action[]中得到sigaction后,在进程的user stack或user专门的处理信号stack上建立 struct rt_sigframe栈帧。当前kernel stack中的数据会复制到struct rt_sigframe的成员uc.uc_mcontext中。以signal的user处理函数为返回地址,然后返回用户空间。

    • SA_RESTORER:为了保证执行完user signal handler后能够立刻返回kernel,返回地址指向一个特殊的user函数__restore_rt,实现在libc中。这个地址是在sigaction时传递给kernel的。__restore_rt如下(以x86_64为例)。可见就是系统调用rt_sigreturn,直接进入kernel。

       ENTRY_PRIVATE(__restore_rt)
      .L__restore_rt_START:
        mov $__NR_rt_sigreturn, %rax
        syscall
      .L__restore_rt_END:
      END(__restore_rt)
      

userpsace:

  • 进入userspace时,直接进入signal handler函数。执行完毕后,函数返回。根据上面所述,返回的地址就是__restore_rt,从而进入kernel。

kernel:

  • 进入stub_rt_sigreturn -> sys_rt_sigreturn,恢复信号处理之前的现场,包括恢复uc.uc_mcontext中的数据至kernel stack中、signal handler stack等,以便下次返回userspace时可进入正常流程。

以上就是signal处理的简化流程。


栈的问题

这里再说明一下处理signal时所用的栈的问题。可以用进程的用户栈或用专用于处理signal的栈,在Android上使用的是后者。

在Bionic的文件pthread_create.cpp中,有如下的函数实现:

pthread_create
   -> __pthread_start
      ->__init_alternate_signal_stack

在线程的入口函数__pthread_start中,会分配一段user空间作为信号处理栈空间,并利用系统调用sigaltstack将此栈的地址和长度告诉kernel,kernel会将此地址和长度保存在如下地方:

  task_struct -> sas_ss_sp
  task_struct -> sas_ss_size

后期在处理此线程的signal时,将此栈作为signal处理函数的栈。


signal的作用域

signal的作用域是进程,跨进程是无效的。在实现signal时,充分利用此限制,从而简单化了处理流程。

从上述的流程分析可见,kernel会保存user signal handler的地址,待处理signal时直接跳转至此地址。由于各个进程的userspace是不一样的,此进程的handler地址在其他进程内是无效或不是期望的函数地址。

那么作用域是线程还是进程的线程组昵?如果signal是给线程的,则pending在 task_struct->pending上;如果是给线程组的,则pending在 task_struct->signal->shared_pending。 由fork时的copy_signal(…)可见,当创建线程时所有的线程共享同一个对象task_struct->signal。

可参见kill, tgkill。kill是发送signal给进程,而tgkill则是发送signal给线程。

Logo

更多推荐