贴一部分:
总结
信号分成两种:
regularsignal( 非实时信号 ), 对应的编码值为 [1,31]
real timesignal 对应的编码值为 [32,64]
编码为 0 的信号 不是有效信号,只用于检查是当前进程否有发送信号的 权限 ,并不真正发送。
线程会有自己的悬挂信号队列 , 并且线程组也有一个信号悬挂队列 . 信号悬挂队列保存 task 实例接收到的信号 , 只有当该信号被处理后它才会从悬挂队列中卸下 .
信号悬挂队列还有一个对应的阻塞信号集合 , 当一个信号在阻塞信号集合中时 ,task 不会处理该被阻塞的信号 ( 但是该信号依旧在悬挂队列中 ). 当阻塞取消时 , 它会被处理 .
对一个信号 , 要三种处理方式 :
忽略该信号 ;
采用默认方式处理 ( 调用系统指定的信号处理函数 );
使用用户指定的方式处理 ( 调用用户指定的信号处理函数 ).
对于某些信号只能采用默认的方式处理 (eg:SIGKILL,SIGSTOP).
信号处理可以分成两个阶段 : 信号产生并通知到接收方 (generation), 接收方进行处理 (deliver)
………
简介
Unix 为了允许用户态进程之间的通信而引入signal. 此外, 内核使用signal 给进程通知系统事件.近30 年来,signal 只有很小的变化 . 以下我们先介绍linuxkernel 如何处理signal, 然后讨论允许进程间 exchange 信号的系统调用.
The Role of Signals
signal 是一种可以发送给一个进程或一组进程的短消息( 或者说是信号 , 但是这么容易和信号量混淆). 这种消息通常只是一个整数 , 而不包含额外的参数 .
linux 提供了很多种signal, 这些signal 通过宏来标识( 这个宏作为这个信号的名字). 并且这些宏的名字的开头是SIG.eg: 宏SIGCHLD , 它对应的整数值为17, 用来表示子进程结束时给父进程发送的消息 ( 即当子进程结束时应该向父进程发送标识符为17 的signal/ 消息/ 信号) .宏SIGSEGV, 它对应的整数值为11, 当进程引用一个无效的物理地址时( 内核) 会向进程发送标识符为11 的signal/ 消息/ 信号 ( 参考linux 内存管理的页错误异常处理程序, 以及linux 中断处理).
信号有两个目的:
1. 使一个进程意识到一个特殊事件发生了( 不同的事件用不同的signal 标识)
2. 并使目标进程进行相应处理(eg: 执行的信号处理函数 , signalhandler). 相应的处理也可以是忽略它 .
当然 , 这两个目的不是互斥的 , 因为通常一个进程意识到一个事件发生后就会执行该事件相应的处理函数 .
下表是linux2.6 在80x86 上的前31 个signals 及其相关说明 . 这些信号中有些是体系结构相关的(eg:SIGCHLD,SIGSTOP), 有些则专门了某些体系结构才存在的(eg:SIGSTKFLT)( 可以参考中断处理 , 里面也列出了一些异常对应的signal).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
|
上述signal 称为regularsignal . 除此之外,POSIX 还引入了另外一类singal 即real-timesignal . real timesignal 的标识符的值从32 到64. 它们与reagularsignal 的区别在于每一次发送的real timesignal 都会被加入悬挂信号队列,所以多次发送的real timesignal 会被缓存起来( 而不会导致后面的被忽略掉) . 而同一种( 即标识符一样)regularsignal 不会被缓存,即如果同一个signal 被发送多次 , 它们只有一个会被放入接受进程的悬挂队列 .
虽然linux kernel 并没有使用real timesignal. 但是它也( 通过特殊的系统调用) 支持posix定义的realtime signal.
有很多系统调用可以给进程发送singal, 也有很多系统调可以指定进程在接收某一个signal 时应该如何响应( 即实行哪一个函数). 下表给出了这类系统调用:( 关于这些系统调用的更多信息参考下文)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
signal 可能在任意时候被发送给一个状态未知的进程 . 当信号被发送给一个当前并不正在执行的进程时, 内核必须把先把该信号保存直到该进程恢复执行.(to do ???????) 被阻塞的信号尽管会被加入进程的悬挂信号队列 , 但是在其被解除阻塞之前不会被处理(deliver),Blockinga signal (described later) requires that delivery of the signal beheld off until it is later unblocked,which acer s the problemof signals being raised before they can be delivered.
内核把信号传送分成两个阶段:
signalgeneration: 内核更新信号的目的进程的相关数据结构 , 这样该进程就能知道它接收到了一个信号. 觉得称为收到信号阶段更恰当. 这个generation 翻译成目的进程接收也不错 .
signaldelivery(): 内核强制目的进程处理接收到的信号,这主要是通过修改进程的执行状态或者在目的进程中执行信号处理函数来实现的 . 觉得称为处理收到的信号阶段更恰当 . diliver 这里翻译成处理更恰当 . deliver 的翻译: 有很多个 , 估计翻译成incomputing 比较合理
一个genearatedsignal 最多只能deliver 一次( 即一个信号最多只会被处理一次) . signal 是可消耗资源 , 一旦一个signal 被deliver, 那么所有进程对它的引用都会被取消 . 已经产生但是还未被处理(deliver) 的信号称为pendingsignal ( 悬挂信号). 对于regularsignal, 在某一个时刻 , 一种signal 在一个进程中只能有一个实例( 因为进程没有用队列缓存其收到的signal) . 因为有31 种regualarsignal , 所以一个进程某一个时刻可以有31 个各类signal 的实例. 此外因为linux 进程对realtimesignal 采用不同的处理方式, 它会保存接收到的realtimesignal 的实例 , 所以可以同时有很多同种signal 的实例 .
问题: 不同种类的信号的优先级( 从值较小的开始处理) .
一般而言 , 一个信号可能会被悬挂很长是时间( 即一个进程收到一个信号后 , 该信号有可能在该进程里很久 , 因为进程没空来处理它), 主要有如下因素:
1. 信号通常被当前进程处理 . Signalsare usually delivered only to the currently running process (thatis, to the current process).
2. 某种类型的信号可能被本进程阻塞. 只有当其被取消阻塞好才会被处理 .
3. 当一个进程执行某一种信号的处理函数时 , 一般会自动阻塞这种信号 , 等处理完毕后才会取消阻塞 . 这意味着一个信号处理函数不会被同种信号阻塞 .
尽管信号在概念上很直观 , 但是内核的实现却相当复杂. 内核必须:
1. 记录一个进程阻塞了哪些信号
2. 当从核心态切换到用户态时 , 检查进程是否接受到了signal.( 几乎每一次时钟中断都要干这样的事 , 费时吗?).
3. 检查信号是否可以被忽略. 当如下条件均满足时则可被忽略:
1). 目标进程未被其它进程traced( 即PT_PTRACED==0). 但一个被traced 的进程收到一个信号时 , 内核停止目标线程 , 并且给tracing 进程发送信号SIGCHLD.tracing 进程可能会通过SIGCONT来恢复traced 进程的执行
2). 目标进程未阻塞该信号 .
3). 信号正被目标进程忽略( 或者由于忽略是显式指定的或者由于忽略是默认操作).
4. 处理信号 . 这可能需要切换到信号处理函数
此外, linux 还需要处理BSD, SystemV 中signal 语义的差异性 . 另外 , 还需要遵守POSIX 的定义 .
处理信号的方式 (Actions Performed uponDelivering a Signal)
一个进程可以采用三中方式来响应它接收到的信号:
1.(ignore) 显示忽略该信号
2.(default) 调用默认的函数来响应该信号( 这些默认的函数由内核定义) , 一般这些默认的函数都分成如下几种( 采用哪一种取决于信号的类型 , 参考前面的表格):
1 2 3 4 5 |
|
3.(catch) 调用相应的信号处理函数 ( 这个信号处理函数通常是程序员在运行时指定的). 这意味着进程需要在执行时显式地指明它需要catch 哪一种信号. 并且指明其处理函数 . catch 是一种主动处理的措施 .
注意上述的三个处理方式被标识为:ignore, default,catch. 这三个处理方式以后会通过这三个标识符引用 .
注意阻塞一个信号和忽略一个信号是不同 , 一个信号被阻塞是就当前不会被处理 , 即一个信号只有在解除阻塞后才会被处理 . 忽略一个信号是指采用忽略的方式来处理该信号( 即对该信号的处理方式就是什么也不做) .
SIGKILL 和SIGSTOP 这两个信号不能忽略 , 不能阻塞 , 不能使用用户定义的函数(caught) . 所以总是执行它们的默认行为 . 所以 , 它们允许具有恰当特权级的用户杀死别的进程, 而不必在意被杀进程的防护措施 ( 这样就允许高特权级用户杀死低特权级的用户占用大量cpu 的时间) .
注: 有两个特殊情况. 第一 , 任意进程都不能给进程0( 即swapper 进程) 发信号 . 第二 , 发给进程1 的信号都会被丢弃(discarded), 除非它们被catch. 所以进程 0 不会死亡, 进程1 仅在int 程序结束时死亡 .
一个信号对一个进程而言是致命的(fatal) , 当前仅当该信号导致内核杀死该进程 . 所以,SIGKILL 总是致命的. 此外 , 如果一个进程对一个信号的默认行为是terminate 并且该进程没有catch 该信号 , 那么该信号对这个进程而言也是致命的 . 注意 , 在catch 情况下 , 如果一个进程的信号处理函数自己杀死了该进程 , 那么该信号对这个进程而言不是致命的 , 因为不是内核杀死该进程而是进程的信号处理函数自己杀死了该进程.
POSIX 信号以及多线程程序
POSIX1003.1 标准对多线程程序的信号处理有更加严格的要求:
( 由于linux 采用轻量级进程来实现线程 , 所以对linux 的实现也会有影响)
1. 多线程程序的所有线程应该共享信号处理函数 , 但是每一个线程必须有自己的maskof pending and blocked signals
2. POSIX 接口kill( ), sigqueue() 必须把信号发给线程组 , 而不是指定线程. 另外内核产生的SIGCHLD,SIGINT, or SIGQUIT 也必须发给线程组 .
3. 线程组中只有有一个线程来处理(deliver) 的共享的信号就可以了 . 下问介绍如何选择这个线程 .
4. 如果线程组收到一个致命的信号 , 内核要杀死线程组的所有线程, 而不是仅仅处理该信号的线程 .
为了遵从POSIX 标准,linux2.6 使用轻量级进程实现线程组.
下文中 , 线程组表示OS 概念中的进程, 而线程表示linux 的轻量级进程. 进程也( 更多地时候)表示linux 的轻量级进程 . 另外每一个线程有一个私有的悬挂信号列表 , 线程组共享一个悬挂信号列表 .