[内核摘要]信号

  信号在第一版的Uinx系统中被提出,用来跟用户模式进程进行交互;内核也使用信号来通知进程系统事件的发生。信号机制在近30年只有很小的变动。

  11.1 信号的角色

  信号是很短的一个信息,可能发送给一个进程或者进程组。发给进程的唯一的信息就是一个用来标识信号的数字。

  以前缀SIG开始的宏的集合用来标识信号;比如,SIGCHLD宏,在Linux中的值是17,用来标识当子进程停止或者终止时发给父进程的信号。SIGSEGV宏,用来标识当一个进程访问无效内存时发给它的信号。

  信号服务于两种目的:

  • 使进程意识到某个指定事件的发生
  • 使得一个进程执行在它代码中的信号句柄(signal handler)

  当然,这两个目的不是互斥的,因为一个进程常常通过执行一段特点的代码来响应某个事件。

        表 11-1 Linux的前31个信号

    # 信号名字              默认动作           描述

    1 SIGHUP              终止                  挂断控制终端或进程

    2 SIGINT               终止                  来自键盘的中断

           3 SIGQUIT            打印(dump)        从键盘退出

    4 SIGILL               打印       非法指令  

    5 SIGTRAP           打印                  调试断点

    6 SIGABRT           打印                  异常终止

    7 SIGIOT             打印        跟SIGABRT相同

    8 SIGFPE             打印                   浮点异常

    9 SIGKILL        终止        强迫进程终止

    10 SIGUSR1         终止        对进程有效

    11 SIGSEGV        打印                   无效的内存访问

    12 SIGUSER2       终止        对进程有效

    13 SIGPIPE          终止        没有读者的时候写管道

    14 SIGALRM        终止        实时时钟RTC

    15 SIGTERM    终止       进程终止

    16 SIGSTKFLT    终止        协处理器栈错误

    17 SIGCHLD    忽视       子进程停止或终止,或者被追踪的时候收到该信号

    18 SIGCONT   继续        恢复被停止进程,使其继续执行

    19 SIGSTOP   停止        停止进程的执行  

    20 SIGTSTP    停止        从tty产生的进程停止信号

    21 SIGTTIN    停止        后台进程需要输入

    22 SIGTTOU   停止        后台进程需要输出

    23 SIGURG   忽视        套接字上的紧急情况

    24 SIGXCPU   打印        CPU时间超过限制

    25 SIGXFSZ   打印        文件大小超过限制

    26 SIGVTALRM 终止         虚拟定时器时钟 

    27 SIGPROF   终止        profile定时器时钟

    28 SIGWINCH   忽视         窗体尺寸调整 

    29 SIGIO     终止        IO可用

    30 SIGPWR    终止                    电源供给错误

    31 SIGSYS    打印        无效的系统调用

    31 SIGUNUSED  打印       跟SIGSYS相同

  

   11.3.2 捕捉信号

  如果接收的信号的SA_ONESHOT设置了,它必须(在保存原有用户注册句柄之后)复位句柄至默认,这样将来同一个信号再次产生就不会触发执行信号句柄。

  执行一个信号句柄是相当复杂的,因为在用户模式和内核模式之间切换时需要兼顾好两者的栈。

  信号句柄(signal handlers)是用户模式进程定义的函数,并且包含在用户代码段中。handle_signal()函数运行在内核态的同时信号句柄运行在用户态;这意味着当前进程必须首先执行处于用户态的信号句柄之后才能恢复它(在内核的)的正常执行。更多的是,当内核试图恢复该进程(在内核)的正常执行时,内核栈不再包含中断程序的硬件上下文(内核从中断程序中返回时会处理信号,那么跳到用户态信号处理完毕返回内核态时,内核栈的数据已空,所以说不再包含硬件的上下文),因为内核栈在每次从用户态切换到内核态的时候都会被清空。

  另一个额外的副作用就是信号句柄可能会执行系统调用。在这种情况下,在系统调用执行完以后,控制就要先转回到信号句柄而不是回到中断程序的正常执行流。

  Linux所采用的解决方案是拷贝内核栈中的硬件上下文到当前进程的用户栈中。用户栈在信号句柄结束时也会以相同的方式进行修改,sigreturn系统调用被自动调用用来将用户栈中的硬件上下文内容拷贝到内核栈中,并且把用户栈原先的内容(载入内核栈内容之前)载入。

  图11-2阐述了捕捉信号时所调用函数的执行流。一个非阻塞信号发送给了进程。当一个中断或异常产生时,进程切换到内核模式。在它返回到用户空间前,内核执行do_signal()函数通过调用handle_signal()来处理信号,并且配置好用户栈(通过调用setup_frame()或setup_rt_frame())。当进程切换到用户空间,它开始执行信号句柄,因为信号句柄的起始地址已被载入程序计数器PC。当该句柄函数执行完毕,通过调用setup_frame()或setup_rt_frame()放在用户栈中的返回代码开始执行,该代码调用sigreturn()或rt_sigreturn()系统调用,将栈中的硬件上下文拷贝到内核栈中,并把用户栈设置为执行句柄之前的状态(通过调用restore_sigcontext())。当该系统调用终止,进程也就恢复了执行。

  11.3.2.1 配置栈

  setup_frame()函数接收四个参数,含义如下:

  sig 信号的号码

  ka 与信号关联的k_sigaction表的地址

  oldset 阻塞信号位掩码的地址

  regs 内核栈中存放用户态寄存器内容的区域的地址

  setup_frame()函数把称作一帧的数据推入用户栈中,该帧数据包含了执行信号句柄所需要的信息,并且保证正确地返回到sys_sigreturn()函数。一帧是一个sigframe表包含如下成员:

  retcode 信号句柄返回函数地址

  ucontext

      uc_mcontext 在切换到内核模式之前保存用户态进程的硬件上下文,还包含了进              程阻塞的信号集

  setup_frame()函数始于调用get_sigframe()来计算栈中存放该帧的内存地址,该内存地址在用户栈中。

  因为栈是向地址低的方向生长的,该帧的初始地址是通过当前栈顶地址减去帧大小并且把结果8字节对齐得到的。

  一旦做完这些,setup_frame()函数修改内核栈中存放用户态寄存器的内容,来保证进程切在用户态的执行后能够执行信号句柄函数。

  setup_rt_frame()函数跟setup_frame()函数很类似,但是它在用户栈中存放了额外的帧(存放在rt_sigframe数据结构中),包含了与信号相关的siginfo_t表。

  11.3.2.2 评估信号标志

  设置用户栈后,handle_signal()函数检查与信号有关的标志,如果SA_NODEFER标志没有置位,该信号在信号句柄执行时需被阻塞。

  recalc_sigpending()函数检查进程是否有非阻塞的挂起信号,并据此设置TIF_SIGPENDING标志。

  11.3.2.3 开始执行信号句柄

  当do_signal()返回后,当前进程恢复它在用户模式的执行。因为setup_frame()所做的准备工作,程序计数器pc指向信号句柄函数的第一条指令,sp寄存器指向推入用户栈中的帧数据。结果是信号句柄开始被执行。

  11.3.2.4 信号句柄执行完毕

  sys_sigreturn()函数从sigframe的uc_mcontext中拷贝进程硬件上下文至内核栈中,并把帧数据从用户栈中删除;通过调用restore_sigcontext()函数实现这两步。

  (在setup_frame->setup_sigframe中把当前进程硬件上下文内容保存到帧数据中,即保存到uc_mcontext中,一并放入用户栈,信号句柄执行完毕再放回内核栈)

  11.3.3 系统调用再执行

  与系统调用相关的请求不会立即被内核满足;当这发生时,发出系统调用的进程状态被置为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE。

  如果进程被设置成TASK_INTERRUPTIBLE时有其他的进程向它发送了一个信号,内核就不会继续完成系统调用,立即把它置位TASK_RUNNING状态。信号在进程切换回用户态的时候被处理。那么系统调用服务程序就没有完成它的工作,返回一个错误代码,比如EINTR,ERESTARTNOHAND,ERESTART_RESTARTBLOCK,ERESTARTSYS或ERESTARTNOINTR。

  实际上,在这种情况下用户态进程能够获取到的唯一错误码是EINTR,这意味着系统调用没有被完成。应用层程序员可以检查这个返回值来决定是否要重新调用系统调用。剩余的错误码是在内核内部使用的,来表征该系统调用是否要在信号句柄执行完毕以后自动再执行。

  表11-11列出了与未完成系统调用有关的错误码和它们对信号的每个行为(执行默认行为,忽视信号,捕捉该信号)的影响。

  终止

    系统调用不会自动被执行;进程在int $80或sysenter指令的后一条恢复在用户态的执行。

  再执行

    内核强制用户态进程将系统调用号载入eax寄存器,并且重新执行int $80或sysenter指令,进程不会意识到再执行的过程,也不会收到错误码。

    (所以对用户态进程来说,如果系统调用返回EINTR,那么系统调用一定没有完成,如果内核执行了系统调用的reexecution,用户态进程不会收到错误码)

  看情况depends

    如果处理的信号的SA_RESTART标志置位了才再执行系统调用,否则,系统调用以EINTR错误码结束。

      表 11-11 系统调用再执行

  信号动作       EINTR   ERESTARTSYS    ERESTARTNOHAND和ERESTART_RESTARTBLOCK          ERESTARTNOINTR

   默认           终止    再执行        再执行                        再执行

   忽视    终止    再执行        再执行                        再执行

   捕捉           终止    看情况        终止                         再执行

  11.4.1 kill()系统调用

  kill(pid,sig)系统调用通常用来发送信号给常见的进程或者多线程应用;它对应的服务程序是sys_kill()函数。整型pid参数有多个意思,取决于它的数值:

  pid>0

    sig信号发送给PID为pid的进程(或线程组)。

    ( The sig signal is sent to the thread group of the process whose PID is equal to pid.这里的process就是thread group的意思,而不是light weight process的意思)

  pid = 0

    sig信号发送给与调用kill()进程处于同一个进程组(process group)内的所有进程(或线程组)。

  pid = -1

    信号发送给所有的进程(或线程组),除了交换进程(PID 0),初始进程(PID 1),和当前进程(current)。 

  pid < -1

    信号发送给标示符为-pid的进程组内所有进程(或线程组)。 

  kill()系统调用能够发送所有信号,即使是处于32到64的所谓的实时信号。然而,kill()系统调用并不保证会添加一个新到成员到目的进程的信号挂起队列,因此会丢失多个挂起信号。实时信号应通过系统调用rt_sigqueueinfo()来发送。

  11.4.2 tkill()和tgkill()系统调用

  tkill()和tgkill()系统调用用来发送信号给线程组中某个指定的进程(轻量级进程)。

  11.4.3 改变信号行为

  

                     

  

  

  

  

 

 

 

posted @ 2013-07-15 22:16  IrisZhou  阅读(719)  评论(0编辑  收藏  举报