源地址:http://blog.csdn.net/zhuixundelang/article/details/5979465
linuxsignal 处理
说明:
本文主要翻译自ULK 3rd chapter 11.
主要受 http://blog.csdn.net/yunsongice 影响,故发表在csdn.
另外,本文是最初版本,估计以后会有一个改进版本. 文中还有很多todo的地方.
另外,如果有版权问题,通知我,我马上删掉.
总结
信号分成两种:
regular signal(非实时信号),对应的编码值为[1,31]
real time signal对应的编码值为[32,64]
编码为0的信号不是有效信号,只用于检查是当前进程否有发送信号的权限,并不真正发送。
线程会有自己的悬挂信号队列, 并且线程组也有一个信号悬挂队列.
信号悬挂队列保存task实例接收到的信号,只有当该信号被处理后它才会从悬挂队列中卸下.
信号悬挂队列还有一个对应的阻塞信号集合,当一个信号在阻塞信号集合中时,task不会处理该被阻塞的信号(但是该信号依旧在悬挂队列中). 当阻塞取消时,它会被处理.
对一个信号,要三种处理方式:
忽略该信号;
采用默认方式处理(调用系统指定的信号处理函数);
使用用户指定的方式处理(调用用户指定的信号处理函数).
对于某些信号只能采用默认的方式处理(eg:SIGKILL,SIGSTOP).
信号处理可以分成两个阶段:信号产生并通知到接收方(generation), 接收方进行处理(deliver)
.........
简介
Unix为了允许用户态进程之间的通信而引入signal.此外, 内核使用signal给进程通知系统事件. 近30年来, signal只有很小的变化.
以下我们先介绍linux kernel如何处理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: 执行的信号处理函数,signal handler).相应的处理也可以是忽略它.
当然,这两个目的不是互斥的,因为通常一个进程意识到一个事件发生后就会执行该事件相应的处理函数.
下表是linux2.6在80x86上的前31个signals及其相关说明.这些信号中有些是体系结构相关的(eg:SIGCHLD,SIGSTOP),有些则专门了某些体系结构才存在的(eg:SIGSTKFLT) (可以参考中断处理,里面也列出了一些异常对应的signal).
The first 31 signals in Linux/i386 | ||||
# | Signal name | Default action | Comment | POSIX |
1 | SIGHUP | Terminate | Hang up controlling terminal or process | Yes |
2 | SIGINT | Terminate | Interrupt from keyboard | Yes |
3 | SIGQUIT | Dump | Quit from keyboard | Yes |
4 | SIGILL | Dump | Illegal instruction | Yes |
5 | SIGTRAP | Dump | Breakpoint for debugging | No |
6 | SIGABRT | Dump | Abnormal termination | Yes |
6 | SIGIOT | Dump | Equivalent to SIGABRT | No |
7 | SIGBUS | Dump | Bus error | No |
8 | SIGFPE | Dump | Floating-point exception | Yes |
9 | SIGKILL | Terminate | Forced-process termination | Yes |
10 | SIGUSR1 | Terminate | Available to processes | Yes |
11 | SIGSEGV | Dump | Invalid memory reference | Yes |
12 | SIGUSR2 | Terminate | Available to processes | Yes |
13 | SIGPIPE | Terminate | Write to pipe with no readers | Yes |
14 | SIGALRM | Terminate | Real-timerclock | Yes |
15 | SIGTERM | Terminate | Process termination | Yes |
16 | SIGSTKFLT | Terminate | Coprocessor stack error | No |
17 | SIGCHLD | Ignore | Child process stopped or terminated, or got signal if traced | Yes |
18 | SIGCONT | Continue | Resume execution, if stopped | Yes |
19 | SIGSTOP | Stop | Stop process execution | Yes |
20 | SIGTSTP | Stop | Stop process issued from tty | Yes |
21 | SIGTTIN | Stop | Background process requires input | Yes |
22 | SIGTTOU | Stop | Background process requires output | Yes |
23 | SIGURG | Ignore | Urgent condition on socket | No |
24 | SIGXCPU | Dump | CPU time limit exceeded | No |
25 | SIGXFSZ | Dump | File size limit exceeded | No |
26 | SIGVTALRM | Terminate | Virtual timer clock | No |
27 | SIGPROF | Terminate | Profile timer clock | No |
28 | SIGWINCH | Ignore | Window resizing | No |
29 | SIGIO | Terminate | I/O now possible | No |
29 | SIGPOLL | Terminate | Equivalent to SIGIO | No |
30 | SIGPWR | Terminate | Power supply failure | No |
31 | SIGSYS | Dump | Bad system call | No |
31 | SIGUNUSED | Dump | Equivalent to SIGSYS | No |
上述signal称为regular signal. 除此之外, POSIX还引入了另外一类singal即real-time signal. real time signal的标识符的值从32到64.它们与reagular signal的区别在于每一次发送的real time signal都会被加入悬挂信号队列,所以多次发送的real time signal会被缓存起来(而不会导致后面的被忽略掉). 而同一种(即标识符一样) regular signal不会被缓存, 即如果同一个signal被发送多次,它们只有一个会被放入接受进程的悬挂队列.
虽然linux kernel并没有使用real time signal. 但是它也(通过特殊的系统调用)支持posix定义的real time signal.
有很多系统调用可以给进程发送singal, 也有很多系统调可以指定进程在接收某一个signal时应该如何响应(即实行哪一个函数).下表给出了这类系统调用: (关于这些系统调用的更多信息参考下文)
System call | Description |
kill( ) | Send a signal to a thread group |
tkill( ) | Send a signal to a process |
tgkill( ) | Send a signal to a process in a specific thread group |
sigaction( ) | Change the action associated with a signal |
signal( ) | Similar to sigaction( ) |
sigpending( ) | Check whether there are pending signals |
sigprocmask( ) | Modify the set of blocked signals |
sigsuspend( ) | Wait for a signal |
rt_sigaction( ) | Change the action associated with a real-time signal |
rt_sigpending( ) | Check whether there are pending real-time signals |
rt_sigprocmask( ) | Modify the set of blocked real-time signals |
rt_sigqueueinfo( ) | Send a real-time signal to a thread group |
rt_sigsuspend( ) | Wait for a real-time signal |
rt_sigtimedwait( ) | Similar to rt_sigsuspend( ) |
signal可能在任意时候被发送给一个状态未知的进程.当信号被发送给一个当前并不正在执行的进程时, 内核必须把先把该信号保存直到该进程恢复执行. (to do ???????)
被阻塞的信号尽管会被加入进程的悬挂信号队列,但是在其被解除阻塞之前不会被处理(deliver),Blocking asignal (described later) requires that delivery of the signal be held off untilit is later unblocked, which acer s the problem ofsignals being raised before they can be delivered.
内核把信号传送分成两个阶段:
signalgeneration: 内核更新信号的目的进程的相关数据结构,这样该进程就能知道它接收到了一个信号. 觉得称为收到信号阶段更恰当. 这个generation翻译成目的进程接收也不错.
signal delivery():内核强制目的进程处理接收到的信号,这主要是通过修改进程的执行状态或者在目的进程中执行信号处理函数来实现的. 觉得称为处理收到的信号阶段更恰当. diliver这里翻译成处理更恰当.
deliver的翻译:有很多个,估计翻译成in computing比较合理
一个genearated signal最多只能deliver一次(即一个信号最多只会被处理一次). signal是可消耗资源,一旦一个signal被deliver,那么所有进程对它的引用都会被取消.
已经产生但是还未被处理(deliver)的信号称为pending signal(悬挂信号).对于regularsignal, 在某一个时刻,一种signal在一个进程中只能有一个实例(因为进程没有用队列缓存其收到的signal).因为有31种regualar signal,所以一个进程某一个时刻可以有31个各类signal的实例. 此外因为linux进程对real time signal采用不同的处理方式, 它会保存接收到的real time signal的实例,所以可以同时有很多同种signal的实例.
问题:不同种类的信号的优先级(从值较小的开始处理).
一般而言,一个信号可能会被悬挂很长是时间(即一个进程收到一个信号后,该信号有可能在该进程里很久,因为进程没空来处理它),主要有如下因素:
1. 信号通常被当前进程处理.Signals are usually delivered only to the currentlyrunning process (that is, 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, System V中signal语义的差异性.另外,还需要遵守POSIX的定义.
处理信号的方式 (Actions Performed upon Delivering a Signal)
一个进程可以采用三中方式来响应它接收到的信号:
1.(ignore)显示忽略该信号
2.(default)调用默认的函数来响应该信号(这些默认的函数由内核定义),一般这些默认的函数都分成如下几种(采用哪一种取决于信号的类型, 参考前面的表格):
Terminate: Theprocess is terminated (killed)
Dump: Theprocess is terminated (killed) and a core file containing its execution contextis created, if possible; this file may be used for debug purposes.
Ignore:Thesignal is ignored.
Stop:Theprocess is stopped, i.e., put in the TASK_STOPPED state.
Continue:Ifthe process was stopped (TASK_STOPPED), it is put into the TASK_RUNNING state.
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 信号以及多线程程序
POSIX 1003.1标准对多线程程序的信号处理有更加严格的要求:
(由于linux采用轻量级进程来实现线程,所以对linux的实现也会有影响)
1. 多线程程序的所有线程应该共享信号处理函数,但是每一个线程必须有自己的mask of pending andblocked signals
2. POSIX接口kill( ), sigqueue( ) 必须把信号发给线程组,而不是指定线程. 另外内核产生的SIGCHLD, SIGINT, or SIGQUIT也必须发给线程组.
3. 线程组中只有有一个线程来处理(deliver)的共享的信号就可以了.下问介绍如何选择这个线程.
4. 如果线程组收到一个致命的信号,内核要杀死线程组的所有线程, 而不是仅仅处理该信号的线程.
为了遵从POSIX标准, linux2.6使用轻量级进程实现线程组.
下文中,线程组表示OS概念中的进程, 而线程表示linux的轻量级进程. 进程也(更多地时候)表示linux的轻量级进程. 另外每一个线程有一个私有的悬挂信号列表,线程组共享一个悬挂信号列表.
与信号有关的数据结构
注:pending/悬挂信号, 表示进程收到信号,但是还没有来得及处理,或者正在处理但是还没有处理完成.
对于每一个进程, 内核必须知道它当前悬挂(pending)着哪些信号或者屏蔽(mask)着哪些信号.还要知道线程组如何处理信号. 为此内核使用了几个重要的数据结构(它们可通过task实例访问),如下图:
The mostsignificant data structures related to signal handling
(注意task中的一些关于signal的成员在上图中没有表现出来)
task中关于signal的成员列在下表中:
blocked成员保存进程masked out的signal.其类型为sigset_t,定义如下:
typedef struct {
unsigned long sig[2];
} sigset_t;
sizeof(long)==32, sigset_t被当成了bit array使用. 正如前文提到的,linux有64种信号,[1,31]为regular signal, [32,64]为real timesignal. 每一种对应sigset_t中一个bit.
信号描述符&信号处理函数描述符
task的signal, sighand成员分别是信号描述符与信号处理函数描述符.
signal成员是一个指针,它指向结构体signal_struct的实例,该实例保存了线程组悬挂着的信号. 也就是说线程组中的所有进程(这里称为task更合理)共用同一个signal_struct实例. signal_struct中的shared_pending成员保存了所有悬挂的信号(以双向链表组织).此外signal_struct中还保存了许多其它的信息(eg:进程资源限制信息, pgrp, session 信息).
下表列出了signal_struct中与信号处理有关的成员:
除了signal成员外,还有一个sighand成员用来指明相应的信号处理函数.
sighand成员是一个指针,指向一个sighand_struct变量,该变量为线程组共享.它描述了一个信号对应的信号处理函数.
sighand_struct成员如下:
The fields of the signal handler descriptor | ||
Type | Name | Description |
atomic_t | count | Usage counter of the signal handler descriptor |
struct k_sigaction [64] | action | Array of structures specifying the actions to be performed upon delivering the signals |
spinlock_t | siglock | Spin lock protecting both the signal descriptor and the signal handler descriptor |
sighand_struct中的重要成员是action, 它是一个数组,描述了每一种信号对应的信号处理函数.
sigaction数据结构
某一些平台上, 会赋予一个signal一些只能内核才可见的属性. 这些属性与sigaction(它在用户态也可见) 构成了结构体k_sigaction. 在x86上,k_sigaction就是sigaction.
注:用户使用的sigaction和内核使用的sigaction结构体有些不同但是,它们存储了相同的信息(自己参考一下用户态使用的sigaction结构体吧).
内核的sigaction的结构体的成员如下:
1)sa_handler:类型为 void (*)(int):
这个字段指示如何处理信号.它可以是指向处理函数的指针,也可以是SIG_DFL(==0)表示使用默认的处理函数,还可以是SIG_IGN(==1)表示忽略该信号
2)sa_flags:类型为unsigned long:
指定信号如何被处理的标志,参考下表 (指定信号如何处理的标志).
3)sa_mask:类型为sigset_t:
指定当该信号处理函数执行时,sa_mask中指定的信号必须屏蔽.
指定信号如何处理的标志
注:由于历史的原因,这些标志的前缀为SA_, 这和irqaction的flag类似,但其实它们没有关系.
Flags specifying how to handle a signal | |
Flag Name | Description |
SA_NOCLDSTOP | Applies only to SIGCHLD; do not send SIGCHLD to the parent when the process is stopped |
SA_NOCLDWAIT | Applies only to SIGCHLD; do not create a zombie when the process terminates |
SA_SIGINFO | Provide additional information to the signal handler |
SA_ONSTACK | Use an alternative stack for the signal handler |
SA_RESTART | Interrupted system calls are automatically restarted |
SA_NODEFER, SA_NOMASK | Do not mask the signal while executing the signal handler |
SA_RESETHAND, SA_ONESHOT | Reset to default action after executing the signal handler |
悬挂的信号队列 (sigpending)
通过前文我们知道有些系统调用能够给线程组发信号(eg:kill, rt_sigqueueinfo), 有些操作给指定的进程发信号(eg:tkill,tgkill).
为了区分这两类, task中其实有两种悬挂信号列表:
1.task的pending字段表示了本task上私有的悬挂信号(列表)
2.task的signal字段中的shared_pending字段则保存了线程组共享的悬挂信号(列表).
悬挂信号列表用数据结构sigpending表示,其定义如下:
struct sigpending {
struct list_head list;
sigset_t signal;
}
其signal成员指明当前悬挂队列悬挂了哪些信号.
其list字段其实是一个双向链表的头,链表的元素的类型是sigqueue. sigqueue的成员如下:
The fields of the sigqueue data structure | ||
Type | Name | Description |
struct list_head | list | Links for the pending signal queue's list |
spinlock_t * | lock | Pointer to the siglock field in the signal handler descriptor corresponding to the pending signal |
Int | flags | Flags of the sigqueue data structure |
siginfo_t | info | Describes the event that raised the signal |
struct user_struct * | user | Pointer to the per-user data structure of the process's owner |
(注:sigqueue的名字有queue,但它其实只是悬挂队列的一个元素.它会记录一个被悬挂的信号的信息)
siginfo_t是一个包含128 byte的数据结构,用来描述一个指定信号的发生,其成员如下:
si_signo:信号ID
si_errno:导致这个信号被发出的错误码. 0表示不是因为错误才发出信号的.
The most significant signal sender codes | |
Code Name | Sender |
SI_USER | kill( ) and raise( ) |
SI_KERNEL | Generic kernel function |
SI_QUEUE | sigqueue( ) |
SI_TIMER | Timer expiration |
SI_ASYNCIO | Asynchronous I/O completion |
SI_TKILL | tkill()and tgkill() |
_sifields: 这个字段是一个union,它有不少成员,哪一个成员有效取决于信号. 比如对于SIGKILL,则它会记录信号发送者的PID,UID;对于SIGSEGV,它会存储导致访问出错的内存地址.
操作信号数据结构的函数
一些宏和函数会使用信号数据结构.在下文的解说中, set表示指向sigset_t变量的指针, nsig表示信号的标识符(信号的整数值).mask是一个unsign long bit mask.
sigemptyset(set) and sigfillset(set)
把set所有bit设置为0或者1.
sigaddset(set,nsig) and sigdelset(set,nsig)
把set中对应与nsig的bit设置为1或者0. In practice, sigaddset( ) reduces to:
set->sig[(nsig - 1) / 32] |= 1UL<< ((nsig - 1) % 32);
and sigdelset( ) to:
set->sig[(nsig - 1) / 32] &= ~(1UL<< ((nsig - 1) % 32));
sigaddsetmask(set,mask) and sigdelsetmask(set,mask)
根据mask的值设置set.仅能设置1-32个signal. The corresponding functionsreduce to:
set->sig[0] |= mask;
and to:
set->sig[0]&= ~mask;
sigismember(set,nsig)
返回set中对应nsig的bit的值. In practice, this function reduces to:
return 1 & (set->sig[(nsig-1) / 32]>> ((nsig-1) % 32));
sigmask(nsig)
根据信号标志码nsig等到它的在sigset_t中的bit位的index.
sigandsets(d,s1,s2), sigorsets(d,s1,s2), and signandsets(d,s1,s2)
伪代码如下:d=s1 & s2; d=s1|s2, d=s1& (~s2)
sigtestsetmask(set,mask)
如果mask中的为1的位在set中的相应位也为1,那么返回1.否则返回0.只适用于1-32个信号.
siginitset(set,mask)
用mask设置set的1-32个信号,并把set的33-63个信号清空.
siginitsetinv(set,mask)
用(!mask)设置set的1-32个信号,并把set的33-63个信号设置为1.
signal_pending(p)
检查p的t->thread_info->flags是否为TIF_SIGPENDING.即检查p是否有悬挂的非阻塞信号.
recalc_sigpending_tsk(t) and recalc_sigpending( )
第一个函数检查t->pending->signal或者t->signal->shared_pending->signal 上是否有悬挂的非阻塞信号.若有设置t->thread_info->flags为TIF_SIGPENDING.
recalc_sigpending( )等价于 recalc_sigpending_tsk(current).
rm_from_queue(mask,q)
清掉悬挂信号队列q中的由mask指定的信号.
flush_sigqueue(q)
清掉悬挂信号队列q中的信号.
flush_signals(t)
删除t收到的所有信号.它会清掉t->thread_info->flags中的TIF_SIGPENDING标志,并且调用flush_sigqueue把t->pending和 t->signal->shared_pending清掉.
Generating a Signal
很多内核函数会产生signal, 它完成处理处理的第一个阶段(generate a signal),即更新信号的目标进程的相应字段. 但是它们并不直接完成信号处理的第二阶段(deliver the signal), 但是它们会根据目标进程的状态或者唤醒目标进程或者强制目标进程receive the signal.
注:generating a signal这个阶段是从源进程发起一个信号,然后源进程在内核态下修改目标进程的相应状态, 然后可能源进程还会唤醒目的进程.
无论一个信号从内核还是从另外一个进程被发送给另一个线程(目标进程), 内核都会执行如下的函数之一来发送信号:
Kernel functions that generate a signal for a process | |
Name | Description |
send_sig( ) | Sends a signal to a single process |
send_sig_info( ) | Like send_sig( ), with extended information in a siginfo_t structure |
force_sig( ) | Sends a signal that cannot be explicitly ignored or blocked by the process |
force_sig_info( ) | Like force_sig( ), with extended information in a siginfo_t structure |
force_sig_specific( ) | Like force_sig( ), but optimized for SIGSTOP and SIGKILL signals |
sys_tkill( ) | System call handler of tkill( ) |
sys_tgkill( ) |
所有这些函数最终都会调用specific_send_sig_info( ).
无论一个信号从内核还是从另外一个进程被发送给另一个线程组(目标进程), 内核都会执行如下的函数之一来发送信号:
Kernel functions that generate a signal for a thread group | |
Name | Description |
send_group_sig_info( ) | Sends a signal to a single thread group identified by the process descriptor of one of its members |
kill_pg( ) | Sends a signal to all thread groups in a process group |
kill_pg_info( ) | Like kill_pg( ), with extended information in a siginfo_t structure |
kill_proc( ) | Sends a signal to a single thread group identified by the PID of one of its members |
kill_proc_info( ) | Like kill_proc( ), with extended information in a siginfo_t structure |
sys_kill( ) | System call handler of kill( ) |
sys_rt_sigqueueinfo( ) | System call handler of rt_sigqueueinfo( ) |
这些函数最终都调用group_send_sig_info( ).
specific_send_sig_info函数说明
这个函数给指定的目标线程(目标进程)发送一个信号.它有三个参数:
参数sig:信号(即某一个信号).
参数info:或者是siginfo_t 变量地址或者如下三个特殊值:
0 :表示信号由用户态进程发送;
1 :表示信号由核心态(进程)发送;
2 :表示信号由核心态(进程)发送,并且信号是SIGKILL或者SIGSTOP.
参数t: 目标进程的task实例指针
specific_send_sig_info调用时必须禁止本cpu的中断,并且获得t->sighand->siglock spin lock. 它会执行如下操作:
1. 检查目标线程是否忽略该信号,若是返回0. 当如下三个条件均满足时则可认为忽略该信号:
1).目标线程未被traced(即t->ptrace不含PT_PTRACED标志).
2).该信号未被目标线程阻塞(即sigismember(&t->blocked, sig) == 0).
3).该信号被目标线程显式地忽略(即t->sighand->action[sig-1].sa_handler== SIG_IGN)或者隐式忽略(即handler==SIG_DFT并且信号为SIGCONT, SIGCHLD, SIGWINCH, or SIGURG.).
2. 检查信号是否是非实时信号(sig<32)并且同样的信号是否已经在线程的私有悬挂信号队列中了, 若是则返回0.
3. 调用send_signal(sig, info, t, &t->pending)把信号加入目标线程的私有悬挂信号队列中.下文会详述.
4. 如果send_signal成功并且信号未被目标线程阻塞,则调用signal_wake_up( )来通知目标进程有新的信号达到.这个函数执行如下步骤:
1).把标志TIF_SIGPENDING加到t->tHRead_info->flags中
2).调用try_to_wake_up().如果目标线程处于TASK_INTERRUPTIBLE或者TASK_STOPPED并且信号是SIGKILL则唤醒目标线程.
3).如果try_to_wake_up返回0,则目标线程处于runnable状态,之后检查目标线程是否在别的CPU上执行,如果是则向该CPU发送处理器中断以强制该cpu重调度目标线程(注:目前我们并未考虑多处理器的情况).因为每一个线程在从schedule()返回时都会检查是否存在悬挂的信号, 所以这个处理器中断将会使目标线程很快就看到这个新的悬挂信号.
5. 返回1(表示信号已经成功generated.)
send_signal函数
这个函数接受四个参数:sig, info,t, signals.其中sig, info,t在specific_send_sig_info中已经介绍过了. signals则是t的pending queue的首地址. 它的执行流程如:
1. 若info==2,那么这个信号是SIGKILL或是SIGSTOP,并且由kernel通过force_sig_specific产生.此时直接跳到9. 因为这种情况下,内核会立即执行信号处理,所以不用把该信号加入信号悬挂队列中.
2.如果目标进程的用户当前的悬挂信号数目(t->user->sigpending)小于目标进程的最大悬挂信号数目(t->signal->rlim[RLIMIT_SIGPENDING].rlim_cur),则为当前信号分配一个sigqueue变量,标识为q
3. 如果目标进程的用户当前的悬挂信号数目太大,或者上一步中分配sigqueue变量失败,则跳到9.
4. 增加目标进程的用户当前的悬挂信号数目(t->user->sigpending)以及t-user的引用数.
5. 把信号q加入目标线程的悬挂队列:
list_add_tail(&q->list,&signals->list);
6. 填充q,如下
if ((unsigned long)info == 0) {
q->info.si_signo = sig;
q->info.si_errno = 0;
q->info.si_code = SI_USER;
q->info._sifields._kill._pid =current->pid;
q->info._sifields._kill._uid =current->uid;
} else if ((unsigned long)info == 1){
q->info.si_signo = sig;
q->info.si_errno = 0;
q->info.si_code = SI_KERNEL;
q->info._sifields._kill._pid =0;
q->info._sifields._kill._uid =0;
} else
copy_siginfo(&q->info,info);
函数copy_siginfo用caller传进来的info填充q->info
7. 设置悬挂信号队列中的mask成员的与sig相应的位(以表示该信号在悬挂信号队列中)
sigaddset(&signals->signal,sig);
7. 返回0以表示信号被成功加入悬挂信号队列.
9. 如果执行这一步,则该信号不会被加入信号悬挂队列,原因有如下三个:1)有太多的悬挂信号了,或者2)没有空闲的空间来分配sigqueue变量了,或者3)该信号的处理由内核立即执行. 如果信号是实时信号并且通过内核函数发送并且显式要求加入队列,那么返回错误代码-EAGAIN(代码类似如下):
if (sig>=32 && info&& (unsigned long) info != 1 &&
info->si_code !=SI_USER)
return -EAGAIN;
10. 设置悬挂信号队列中的mask成员的与sig相应的位(以表示该信号在悬挂信号队列中)
sigaddset(&signals->signal,sig);
11. 返回0. 尽管该信号没有放到悬挂信号队列中, 但是相应的signals->signal中已经设置了
即使没有空间为信号分配sigqueue变量,也应该让目标信号知道相应的信号已经发生,这一点很重要. 考虑如下情形: 目标进程使用了很多内存以致于无法再分配sigqueue变量了, 但是内核必须保证对目标进程依的kill依然能够成功,否则管理员就没有机会杀死目标进程了.
group_send_sig_info函数
函数group_send_sig_info把一个信号发给一个线程组. 这个函数有三个参数:sig, info, p. (和specific_send_sig_info类似).
这个函数的执行流程如下:
1.检查参数sig的正确性:
if (sig < 0 || sig > 64)
return -EINVAL;
2. 如果信号的发送进程处于用户态,则检查这个发送操作是否允许. 仅当满足如下条件之一(才视为允许):
1).发送者进程有恰当的权限(通常发送者进程应该是system administrator).
2).信号为SIGCONT,并且目标进程和发送者进程在同一个login session.
3).目标进程和发送者进程属于同一个用户
3. 如果用户态的进程不能发送此信号,则返回-EPERM. 如果sig==0,则立即返回.(因为0是无效的信号). 如果sighand==0,也立即返回,因为此时目标进程正在被杀死,从而sighand被释放.
if (!sig || !p->sighand)
return 0;
4. 获得锁p->sighand->siglock,并且关闭本cpu中断.
5. 调用handle_stop_signal函数, 这个函数检查sig是否会和现有的悬挂的信号冲突,会的话解决冲突. 这个函数的步骤如下:
1).如果线程组正在被杀死(SIGNAL_GROUP_EXIT),则返回.
2).如果sig是IGSTOP,SIGTSTP, SIGTTIN, SIGTTOU中的一种, 则调用rm_from_queue,把线程组中所有悬挂的SIGCONT删除.注意:包含线程组共享的悬挂信号队列中的(p->signal->shared_pending)以及每一个线程私有悬挂队列中的.
3).如果sig是SIGCONT,则调用rm_from_queue,把线程组中所有悬挂的SIGSTOP,SIGTSTP, SIGTTIN, SIGTTOU删除.注意:包含线程组共享的悬挂信号队列中的(p->signal->shared_pending)以及每一个线程私有悬挂队列中的.之后为每一个线程调用try_to_wake_up.
6. 检查线程组是否忽略该信号,如果忽略返回0.
7.如果是非实时信号,并且该线程组已经有这种悬挂的信号了,那么返回0:
if (sig<32 &&sigismember(&p->signal->shared_pending.signal,sig))
return 0;
8.调用send_signal( )把信号加到线程组的共享悬挂信号队列中, 如果send_signal返回非0值,则group_send_sig_info退出并把该非零值返回.
9.调用__group_complete_signal( ) 来唤醒线程组中的一个轻量级进程.参考下文.
10.释放p->sighand->siglock并且打开本地中断.
11.返回 0 (success).
函数_ _group_complete_signal( )扫描目标线程组,并且返回一个能够处理(receive)该新信号的进程. 这样的进程必须同时具备如下的条件:
1)该进程不阻塞新信号.
2)进程的状态不是EXIT_ZOMBIE,EXIT_DEAD, TASK_TRACED, or TASK_STOPPED.但是当信号是SIGKILL是, 进程的状态允许是TASK_TRACED or TASK_STOPPED.
3)进程不处于正在被杀死的状态,即状态不是PF_EXITING.
4)或者进程正在某一个cpu上执行,或者进程的TIF_SIGPENDING 的标志未被设置.
一个线程组中满足上诉条件的线程(进程)可能很多,根据如下原则选择一个:
1)如果group_send_sig_info中的参数p指定的进程满足上述条件,则选择p.
2)否则从最后一个接收线程组信号的线程(p->signal->curr_target)开始查找满足上述条件的线程,找到为止.
(如果线程组中没有一个线程满足上述条件怎么办?)
如__group_complete_signal( ) 成功找到一个进程(表示为selected_p), 那么:
1.检查该信号是否是致命的,若是,通过给线程组中的每一个线程发送SIGKILL来杀死线程组
2.若不是,调用signal_wake_up来唤醒selected_p并告知它有新的悬挂信号,
Delivering a Signal
通过上面的介绍,内核通过修改目标进程的状态,告知目标进程有新的信号到达.但是目标进程对到达的新信号的处理(deliver signal)我们还没有介绍. 下面介绍目标进程如何在内核的帮助下处理达到的新信号.
注意当内核(代码)要把进程从核心态恢复成用户态时(当进程从异常/中断处理返回时), 内核会检查该进程的TIF_SIGPENDING标识,如果存在悬挂的信号,那么将先处理该信号.
这里需要介绍一下背景:当进程在用户态(用U1表示)下由于中断/异常而进入核心态,那么需要把U1的上下文记录到该进程的内核堆栈中.
为了处理非阻塞的信号,内核调用do_signal函数.这个函数接受两个参数:
regs: 指向U1上下文在内核堆栈的首地址 (参考进程管理).
oldest: 保存了一个变量的地址, 该变量保存了被阻塞的信号的信息(集合).如果该参数为NULL,那么这个地址就是¤t->blocked (如下文). 注意当自定义信号处理函数结束后,会把oldest设置为当前task的阻塞信号集合.(参考源代码,以及rt_frame函数).
我们这里描述的do_signal流程将会关注信号delivery(处理),而忽略很多细节,eg:竞争条件,产生core dump,停止和杀死线程组等等.
一般,do_signal一般仅在进程即将返回用户态时执行. 因此,如果一个中断处理函数调用do_signal, 那么do_signal只要按如下方式放回:
if ((regs->xcs & 3) != 3)
return 1;
如果oldest为NULL,那么do_signal会把它设置为当前进程阻塞的信号:
if (!oldset)
oldset = ¤t->blocked;
do_signal的核心是一个循环,该循环调用dequeue_signal从进程的私有悬挂信号队列和共享悬挂队列获取未被阻塞的信号. 如果成功获得这样的信号, 则通过handle_signal调用相应的信号处理函数, 否则退出do_signal.
(这个循环不是用C的循环语句来实现,而是通过修改核心栈的regs来实现.大概的流程可以认为如下:当由核心态时切换向用户态时,检查是否有非阻塞的悬挂信号,有则处理(包含:准备信号处理函数的帧,切换到用户态以执行信号处理函数,信号处理函数返回又进入核心态),无则返回原始的用户态上下文)
dequeue_signal先从私有悬挂信号列表中按照信号值从小到大取信号,取完后再从共享悬挂信号列表中取. (注意取后要更新相应的信息)
接着我们考虑, do_signal如何处理获得的信号(假设用signr表示).
首先,它会检查是否有别的进程在监控(monitoring)本进程,如果有,调用do_notify_parent_cldstop和schedule来让监控进程意识到本进程开始信号处理了.
接着,do_signal获得相应的信号处理描述符(通过current->sig->action[signr-1]),从而获得信号处理方式的信息.总共有三种处理方式:忽略,默认处理,使用用户定义的处理函数.
如果是忽略,那么什么也不做:
if(ka->sa.sa_handler == SIG_IGN)
continue;
执行默认的信号处理函数
如果指定的是默认的处理方式. 那么do_signal使用默认的处理方式来处理信号.(进程0不会涉及,参考前文)
对于init进程除外,则它要丢弃信号:
if (current->pid == 1)
continue;
对于其它进程, 默认的处理方式取决于信号.
第一类:这类信号的默认处理方式就是不处理
if (signr==SIGCONT || signr==SIGCHLD ||
signr==SIGWINCH || signr==SIGURG)
continue;//
第二类:这类信号的默认处理方式如下:
if (signr==SIGSTOP || signr==SIGTSTP ||
signr==SIGTTIN || signr==SIGTTOU) {
if (signr != SIGSTOP &&
is_orphaned_pgrp(current->signal->pgrp))
continue;
do_signal_stop(signr);
}
这里,SIGSTOP与其他的信号有些微的区别.
SIGSTOP停止整个线程组. 而其它信号只会停止不在孤儿进程组中的进程(线程组).
孤儿进程组(orphand processgroup).
非孤儿进程组指如果进程组A中有一个进程有父亲,并且该父进程在另外一个进程组B中,并且这两个进程组A,B都在用一个会话(session)中,那么进程组A就是非孤儿进程组.因此如果父进程死了,但是启动在进程的session依旧在,那么进程组A都不是孤儿.
注:这两个概念让我迷糊.
do_signal_stop 检查当前进程是否是线程组中的第一个正在被停止的进程,如果是,它就激活一个组停(group stop)。本质上,它会把信号描述符的group_stop_count 字段设置为正值,并且唤醒线程组中的每一个进程。每一个进程都会查看这个字段从而认识到正在停止整个线程组,并把自己的状态改为TASK_STOPPED,然后调用schedule. do_signal_stop也会给线程组的父进程发送SIGCHLD, 除非父进程已经被设置为SA_NOCLDSTOP flag of SIGCHLD.
默认行为是dump的信号处理可能会进程工作目录下创建一个core文件.这个文件列出了进程的地址空间和cpu寄存器的值. do_signal创建这个文件后,就会杀死整个线程组. 剩下18个信号的默认处理是terminate, 这仅仅是简单地杀死整个线程组. 为此,do_signal调用了do_group_exit。
使用指定的函数来处理信号(catching the signal)
如果程序为信号设置了处理函数,那么do_signal将会通过调用handle_signal 来强制该信号函数被执行:
handle_signal(signr, &info, &ka,oldset, regs);
if (ka->sa.sa_flags & SA_ONESHOT)
ka->sa.sa_handler = SIG_DFL;
return 1;
如果用户在为信号设置信号处理函数时指定了SA_ONESHOT,那么当该信号处理函数第一次执行后,其将会被reset.即以后来的这样的信号将会使用默认的处理函数.
Notice how do_signal( ) returns after having handled a single signal. Other pending signals won'tbe considered until the next invocation of do_signal( ). This approachensures that real-time signals will be dealt with in the proper order.
执行一个信号处理函数相当复杂,因为需要内核小心处理用户信号处理函数的调用栈, 然后把控制权交给用户处理函数(注意这里涉及内核态到用户态的转换).
用户的信号处理函数定义在用户态中并且包含在用户代码段中,它需要在用户态(U2)下执行. hande_signal函数在核心态下执行. 此外,由于当前的核心态是在前一个用户态(U1)转过来, 这意味着当信号处理函数(U2)结束,回到内核态,然后内核态还需要回到U1,而当从U2进入核心态后,内核栈存放的已经不再是U1的上下文了(而是U2), 此外一般信号处理函数中还会发生系统调用(用户态到核心态的转换),而系统调用结束后要回到信号处理函数.
注意:每一个内核态切换到用户态,进程的内核堆栈都会被清空.
那么handle_signal如何调用信号处理函数呢??
Linux采用的方法如下: 每次调用信号处理函数之前,把U1的上下文拷贝到信号处理函数的栈中(一般信号处理函数的栈也是当前进程的用户态的栈,但是程序员也可以在设置信号处理函数时指定一个自己定义的栈,但是这里不影响这个方法,所以我们只描述信号处理函数使用进程用户态的栈的情况). 然后再执行信号处理函数.而当信号处理函数结束之后,会调用sigreturn()从U2的栈中把U1的上下文拷贝到内核栈中.
下图描述了信号处理函数的执行流程. 一个非阻塞的信号发给目标进程.当一个中断或异常发生后,目标进程从用户态(U1)进入核心态. 在它切换回用户态(U1)之前, 内核调用do_signal.这个函数逐一处理悬挂的非阻塞信号.而如果目标进程设置了对信号的处理函数,那么它会调用handle_signal来调用自定义的信号处理函数(这期间需要使用setup_frame或setup_rt_frame来为信号处理函数设置栈), 此时当切换到用户态时, 目标进程执行的是信号处理函数而不是U1.当信号处理函数结束后,位于setup_frame或setup_rt_frame栈之上的返回代码(return code)被执行,这返回代码会执行sigreturn或者rt_sigreturn从而把U1的上下文从setup_frame或setup_rt_frame栈中拷贝到核心栈.而这结束后,内核可以切换回U1.
注意:信号有三种处理方式,只有使用自定义处理函数才需要这样麻烦啊.
接下来我们需要仔细瞧瞧这一切怎么发生的.
为了能恰当地为信号处理函数设置栈,handle_signal调用setup_frame(当信号没有相应的siginfo_t时)或者setup_rt_frame(当信号有相应的siginfo_t时).为了判断采用哪一种, 需要参考sigaction中的sa_flag是否包含SA_SIGINO.
setup_frame接受四个参数,如下:
sig:信号标识
ka: 与信号相关的k_sigaction实例
oldest:进程阻塞的信号
regs: U1上下为在核心栈的地址.
setup_frame函数会在用户栈中分配一个sigframe变量,该变量包含了能够正确调用信号处理函数的信息(这些信息会被sys_sigreturn使用).sigframe的成员如下(其示意图如下):
pretcode :信号处理函数的返回地址.其指向标记为kernel_sigreturn的代码
sig :信号标识.
sc : sigcontext变量.它包含了U1的上下文信息,以及被进程阻塞的非实时信号的信息.
fpstate : _fpstate实例,用来存放U1的浮点运算有关的寄存器.
extramask : 被进程阻塞的实时信号的信息.
retcode :8字节的返回代码,用于发射sigreturn系统调用.早期版本的linux用于信号处理函数返回后的善后处理.linux2.6则用于特征标志,所以调试器能够知道这是一个信号处理函数的栈.
setup_frame函数首先获得sigframe变量的地址,如下:
frame =(regs->esp - sizeof(struct sigframe)) &0xfffffff8
注意:默认地信号处理函数使用得到栈是进程在用户态下的栈,但是用户在设置信号处理函数时可以指定.这里只讨论默认情况. 对于用户指定其实也一样.
另外由于栈从大地址到小地址增长,所以上面的代码要看明白了.此外还需要8字节对齐.
之后使用access_ok来验证frame是否可用,之后用__put_user来填充frame各个成员. 填充好之后,需要修改核心栈,这样从核心态切换到用户态时就能执行信号处理函数了,如下:
regs->esp = (unsigned long) frame;
regs->eip= (unsigned long) ka->sa.sa_handler;
regs->eax = (unsigned long) sig;
regs->edx = regs->ecx = 0;
regs->xds = regs->xes = regs->xss= _ _USER_DS;
regs->xcs = _ _USER_CS;
setup_rt_frame和setup_frame类似, 但是它在用户栈房的是一个rt_sigframe的实例, rt_sigframe除了sigframe外还包含了siginfo_t(它描述了信号的信息).另外它使用_ _kernel_rt_sigreturn.
设置好栈后,handle_signal检查和信号有关的flags. 如果没有设置SA_NODEFER , 那么在执行信号处理函数时,就要阻塞sigaction.sa_mask中指定的所有信号以及sig本身. 如下:
if (!(ka->sa.sa_flags & SA_NODEFER)){
spin_lock_irq(¤t->sighand->siglock);
sigorsets(¤t->blocked,¤t->blocked, &ka->sa.sa_mask);
sigaddset(¤t->blocked,sig);
recalc_sigpending(current);
spin_unlock_irq(¤t->sighand->siglock);
}
如前文所述,recalc_sigpending会重新检查进程是否还有未被阻塞的悬挂信号,并依此设置进程的TIF_SIGPENDING标志.
注意: sigorsets(¤t->blocked,¤t->blocked, &ka->sa.sa_mask)等价于current->blocked|= ka->sa.sa_mask. 而current->blocked原来的值已经存放在frame中了.
handle_signal返回到do_signal后,do_signal也立即返回.
do_signal返回后, 进程由核心态切换到用户态,于是执行了信号处理函数.
信号处理函数结束后,因为其返回值的地址(pretcode指定的)是_ _kernel_sigreturn指向的代码段,所以就会执行_ _kernel_sigreturn指向的代码.如下:
_ _kernel_sigreturn:
popl %eax
movl $_ _NR_sigreturn, %eax
int $0x80
这会导致sigreturn被执行 (会导致从用户态切换到核心态).
sys_sigreturn函数可以计算得到sigframe的地址.如下:
frame = (struct sigframe *)(regs.esp - 8);
if (verify_area(VERIFY_READ, frame,sizeof(*frame)) {
force_sig(SIGSEGV, current);
return 0;
}
接着,它要从frame中把进程真正阻塞的信号信息拷贝到current->blocked中.结果那些在sigaction中悬挂的信号解除了阻塞.之后调用recalc_sigpending.
接着sys_sigreturn需要调用restore_sigcontext把frame的sc(即U1的上下文)拷贝到内核栈中并把frame从用户栈中删除.
__kernel_sigreturn的处理与这类似.
重新执行系统调用(被信号处理掐断的系统调用)
注:当用核心态转向用户态时,该核心态可能是系统调用的核心态.
小小总结:当内核使用用户指定的处理方式时,因为是从用户态转向内核态再转向用户态,所以其处理比较复杂. 如下描述: 当从用户态(U1)转入内核态后,在内核态试图回到U1时,会先判断是否有非阻塞的悬挂信号,如果有就会先调用用户的处理函数(即进入用户态,这里是用户态2),处理完后,再回到内核态,然后再回到U1. 注意在U2中也有可能发生系统调用从而再次进入内核态. (注意在U2过程中,系统处于关中断状态,所以信号处理应该尽可能地快), 我们知道当用户态进入核心态时会把用户态的信息保存在核心态的栈中, 为了避免在从U2因系统调用再进入核心态是破坏U1在核心态中的信息, 在进入U2之前,要不U1在核心栈中的信息拷贝到U1的栈中,并在U2返回后,再把U2栈中保存U1的信息拷贝会核心栈.
注:U2使用的栈可以和U1是同一个栈, 也可以是用户在设置信号处理函数时指定的一段内存.
当一个进程调用某些并不能马上满足的系统调用(eg:写文件)时,内核会把该进程的状态设置为TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE.
当一个进程(表示为wp)处于TASK_INTERRUPTIBLE状态,而另外一个进程又给它发信号,那么内核会把wp的状态的进程设置为TASK_RUNNING(但是此时wp的系统调用仍未完成).而当wp切换会用户态时,这个信号会被deliver. 如果这种情况真的发生了,则系统调用服务例程并没有成功完成任务,但是会返回错误码EINTR, ERESTARTNOHAND, ERESTART_RESTARTBLOCK, ERESTARTSYS, 或 ERESTARTNOINTR. (参考中断处理的从中断返回部分).
从实践上看,用户获得的错误代码是是EINTR, 这意味着系统调用没有成功完成.程序员可以决定是否再次发起该系统调用.其余的错误代码由内核使用来判断是否在信号处理之后自动重新执行该系统调用.
下表列出了这些错误代码在每一种可能的中断行为下对未完成系统调用的影响.表中用的词定义如下:
Terminate:该系统调用不会被内核自动重新执行.而用户得到的该系统调用的返回值是-EINTER.对程序员而言该系统调用失败.
Reexecute:内核会强制进程在用户态下自动重新执行该系统调用(通过把中断号放到eax,执行int 0x80或者sysenter指令).但是这对程序员透明.
Depends:如果当被deliver的信号设置了SA_RESTART标志,那么自动重新执行该系统调用.否则中止系统调用并返回-EINTER.
Error codes and their impact on system call execution | ||||
Signal Action | EINTR | ERESTARTSYS | ERESTARTNOHAND ERESTART_RESTARTBLOCK | ERESTARTNOINTR |
Default | Terminate | Reexecute | Reexecute | Reexecute |
Ignore | Terminate | Reexecute | Reexecute | Reexecute |
Catch | Terminate | Depends | Terminate | Reexecute |
注:ERESTARTNOHAND, ERESTART_RESTARTBLOCK使用不同的机制来重新自动执行系统调用(参下文).
当delivering一个信号时,内核必须确信进程正在执行系统调用中,这样它才能reexecute该系统调用, 而regs中的成员orig_eax就是干这个事情的. 回想一下这个成员在中断/异常时如何被初始化的:
Interrupt:它等于 IRQ数值 - 256.
0x80 exception (或者 sysenter):它等于系统调用的编号.
Other exceptions:它等于-1.
所以如果该值>=0,那么可确定进程是在处于系统调用中被信号处理唤醒的(即信号处理唤醒一个等待系统调用完成(状态为TASK_INTERRUPTIBLE)的进程).所以内核在delivering信号时,能够返回上述的错误代码,并作出恰当的挽救.
重启被非自定义信号处理函数中断的系统调用
注:上面语句的中断不是OS中的中断,而是日常生活中的中断的含义.
如果系统调用因为信号的默认处理函数或者信号的忽略处理而中断(即由系统调用把task的状态改为可中断状态,但是却被信号的默认处理函数或者忽略信号操作把该task的状态改为running,如前文所述),那么do_signal函数需要分析系统调用的错误码来决定是否自动重新执行被停止的系统调用. 如果需要重启该系统调用,那么必须修改regs中的内容,从而在切换到用户态后,在用户态下再次执行该系统调用(即再次在用户态下让eax存放系统调用的编号,然后执行int 0x80或者sysenter).如下代码:
if (regs->orig_eax >= 0) {
if (regs->eax == -ERESTARTNOHAND ||regs->eax == -ERESTARTSYS ||
regs->eax == -ERESTARTNOINTR){
regs->eax = regs->orig_eax;
regs->eip -= 2;
}
if (regs->eax ==-ERESTART_RESTARTBLOCK) {
regs->eax = __NR_restart_syscall;
regs->eip -= 2;
}
}
regs->eax存放系统调用的编号. 此外,int 0x80或者sysreturn均为2字节. 所以regs->eip-=2等价于切换到用户态后重新执行int 0x80或者sysretrun指令.
对于错误码ERESTART_RESTARTBLOCK,它需要使用restart_syscall系统调用,而不是使用原来的系统调用. 这个错误码只用在与时间有关的系统调用.一个典型的例子是nanosleep( ): 想象一下,一个进程调用这个函数来暂停20ms, 10ms后由于一个信号处理发生(从而激活这个进程),如果这信号处理后重新启动这个系统调用,那么它在重启的时候不能直接再次调用nanosleep,否则将会导致该进程睡觉30ms. 事实上,nanosleep会在当前进程的thread_info的restart_block中填写下如果需要重启nanosleep,那么需要调用哪一个函数,而如果其被信号处理中断,那么它会返回-ERESTART_RESTARTBLOCK, 而在重启该系统调用时,sys_restart_syscall会根据restart_block中的信息调用相应的函数.通常这个函数会计算出首次调用与再次调用的时间间距,然后再次暂停剩余的时间段.
在这种情况下,handle_signal会分析错误码以及sigaction中的标志是否包含了SA_RESTART,从而决定是否重启未完成的系统调用.代码如下:
if (regs->orig_eax >= 0) {
switch (regs->eax) {
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;
regs->eip -= 2;
}
}
如果需要重启系统调用,其处理与do_signal类似.否则向用户态返回-EINTR.
问题:
在信号处理函数中可以发生中断吗, 可以再发出系统调用吗,可以发出异常吗?
如果不行会有什么影响??
与信号处理相关的系统调用
因为当进程在用户态时,允许发送和接受信号. 这意味着必须定义一些系统调用来允许这类操作. 不幸的是,由于历史的原因这些操作的语义有可能会重合,也意味着某些系统调用可能很少被用到.比如,sys_sigaction, sys_rt_sigaction几乎相同,所以C的接口sigaction只调用了sys_rt_siaction.我们将会描述一些重要的系统调用.
进程组:Shell 上的一条命令行形成一个进程组.注意一条命令其实可以启动多个程序.进程组的ID为其领头进程的ID.
原型为: int kill(pid_t pid, int sig)
其用来给一个线程组(传统意义上的进程)发信息.其对应的系统服务例程(serviceroutine)是sys_kill. sig参数表示待发送的信号,pid根据其值有不同的含义,如下:
pid > 0:表示信号sig发送到由pid标识的线程组(即线程组的PID==pid).
pid = 0:表示信号sig发送到发送进程所在的进程组中的所有线程组.
pid = -1:表示信号sig发送到除进程0,进程1,当前进程外的所有进程
pid < -1:表示信号sig发送到进程组-pid中的所有线程组.
服务例程sys_kill会初始化一个siginfo_t变量,然后调用kill_something_info.如下:
info.si_signo = sig;
info.si_errno = 0;
info.si_code = SI_USER;
info._sifields._kill._pid =current->tgid;
info._sifields._kill._uid =current->uid;
return kill_something_info(sig, &info,pid);
kill_something_info会调用kill_proc_info(这个函数调用group_send_sig_info把信号发给线程组)或者kill_pg_info(这个会扫描目标进程组然后逐一调用send_sig_info)或者为系统中的每一个进程调用group_send_sig_info(当pid=-1时).
系统调用kill可以发送任意信号,然而它不保证该信号被加到目标进程的悬挂信号队列中. (这个是指对于非实时信号 它也有可能会丢弃该信号吗????) 对于实时信号,可以使用rt_sigqueueinfo.
System V and BSDUnix还有killpg系统调用, 它可以给一组进程发信号.在linux中,它通过kill来实现. 另外还有一个raise系统调用,它可以给当前进程发信号.在linux中,killpg, raise均以库函数提供.
这两个函数给指定线程发信号. pthread_kill使用它们之一来实现.函数原型为:
int tkill(int tid, int sig);
long sys_tgkill (int tgid, int pid, int sig);
tkill对应的服务例程是sys_tkill,它也会填充一个siginfo_t变量,进程权限检查,然后掉用specific_send_sig_info.
tgkill与tkill的差别在于它多了一个tgid的参数,它要求pid必须是tgid中的线程.其对应的服务例程是sys_tgkill,它做的事情和sys_tkill类似,但它还检查了pid是否在tgid中.这种检查在某些情况下可以避免race condition.比如:一个信号被发给了线程组A中的一个正在被杀死的线程(killing_id),如果另外一个线程组B很快地创建一个新的线程并且其PID= killing_id,那么信号有可能会发送到线程组B中的新建的线程. tgkill可以避免这种情况,因为线程组A,B的ID不一样.
程序员可以通过系统调用sigaction(sig,act,oact)来为信号sig设置用户自己的信号处理函数act. 当然如果用户没有设置,那么系统会使用默认的信号处理函数.其函数原型为:
int sigaction(intsignum, const struct sigaction *act, struct sigaction *oldact);
oldact用来保存信号signum的旧的信号处理函数(因为signum的新的信号处理函数是act,保存旧的是希望能够恢复使用旧的信号处理函数).
其对应的服务例程是sys_sigaction,它首先检查act地址的有效性,然后act的内容拷贝到一个类型为k_sigaction的本地变量new_ka,如下:
_ _get_user(new_ka.sa.sa_handler,&act->sa_handler);
_ _get_user(new_ka.sa.sa_flags,&act->sa_flags);
_ _get_user(mask, &act->sa_mask);
siginitset(&new_ka.sa.sa_mask, mask);
接着调用do_sigaction把new_ka拷贝到current->sig->action[sig-1]中的.类似如下:
k = ¤t->sig->action[sig-1];
if (act) {
*k = *act;
sigdelsetmask(&k->sa.sa_mask,sigmask(SIGKILL) | sigmask(SIGSTOP));
if (k->sa.sa_handler == SIG_IGN ||(k->sa.sa_handler == SIG_DFL &&
(sig==SIGCONT || sig==SIGCHLD ||sig==SIGWINCH || sig==SIGURG))) {
rm_from_queue(sigmask(sig),¤t->signal->shared_pending);
t = current;
do {
rm_from_queue(sigmask(sig),¤t->pending);
recalc_sigpending_tsk(t);
t = next_thread(t);
} while (t != current);
}
}
POSIX规定当默认行为是忽略时,把信号处理函数设置为SIG_IGN或者SIG_DFT会导致悬挂的信号被丢弃. 此外, SIKKILL和SIGSTOP永远不会被屏蔽 (参考上述代码).
此外, sigaction系统调用还允许程序员初始化sigaction中的sa_flags.
System V也提供signal系统调用. C库的signal使用rt_sigaction来实现. 但是linux仍然有相应的服务例程sys_signal.如下:
new_sa.sa.sa_handler = handler;
new_sa.sa.sa_flags = SA_ONESHOT |SA_NOMASK;
ret = do_sigaction(sig, &new_sa,&old_sa);
return ret ? ret : (unsignedlong)old_sa.sa.sa_handler;
获得被阻塞的悬挂信号
系统调用sigpending()允许用户获得当前线程被阻塞的悬挂信号.函数原型为:
intsigpending(sigset_t *set);
set用来接收被阻塞的悬挂信号的信息.
其对应的服务例程是sys_sigpending,其实现代码如下:
sigorsets(&pending,¤t->pending.signal,
¤t->signal->shared_pending.signal);
sigandsets(&pending,¤t->blocked, &pending);
copy_to_user(set, &pending, 4);
修改被阻塞的信号的集合
系统函数sigprocmask可以用来修改当前线程的阻塞信号集合.但是它仅适用于非实时信号.函数原型为:
intsigprocmask(int how, const sigset_t *set, sigset_t *oldset);
假设在执行这个函数之前线程的阻塞信号的集合为bs.执行这个函数之后线程的阻塞信号的集合为nbs.
oldsett:用于返回(返回)线程当前阻塞的信号的集合(*oldest=bs)
set:用于存储信号集合.怎么用它还取决于how参数.
how:执行线程的新的阻塞信号集合如果通过set参数获得.其可能的值及其含义如下:
SIG_BLOCK: nbs=bs|set
SIG_UNBLOCK:nbs=bs-set
SIG_SETMASK:nbs=set
其对应的服务例程是sys_sigprocmask().它调用copy_from_user把set值拷贝到本地变量new_set,并把bs拷贝到oldset中.其执行的代码类似如下:
if (copy_from_user(&new_set, set,sizeof(*set)))
return -EFAULT;
new_set &=~(sigmask(SIGKILL)|sigmask(SIGSTOP));
old_set = current->blocked.sig[0];
if (how == SIG_BLOCK)
sigaddsetmask(¤t->blocked,new_set);
else if (how == SIG_UNBLOCK)
sigdelsetmask(¤t->blocked,new_set);
else if (how == SIG_SETMASK)
current->blocked.sig[0] = new_set;
else
return -EINVAL;
recalc_sigpending(current);
if (oset && copy_to_user(oset,&old_set, sizeof(*oset)))
return -EFAULT;
return 0;
悬挂(暂停)进程
intsigsuspend(const sigset_t *mask);
其含义是:把本线程的阻塞信号设置为mask并把线程状态设置为TASK_INTERRUPTIBLE.并且只有当一个nonignored, nonblocked的信号发到本线程后才会把本线程唤醒(deliver该信号,系统调用返回).
其相应的服务例程为sys_sigsuspend,执行的代码为:
mask &= ~(sigmask(SIGKILL) | sigmask(SIGSTOP));
saveset = current->blocked;// saveset本地局部变量
siginitset(¤t->blocked, mask);
recalc_sigpending(current);
regs->eax = -EINTR;
while (1) {
current->state = TASK_INTERRUPTIBLE;
schedule( );
if (do_signal(regs, &saveset))//把阻塞信号集合恢复为saveset
return -EINTR;
}
(注意,本系统调用本身期望它被信号处理函数中断.)
函数schedule会导致执行别的进程(线程),而当本进程再次执行时(即上面的schedule返回了),它会调用do_signal来处理其未被阻塞的悬挂的信号,然后恢复线程的阻塞信号集合(saveset). 如果do_signal返回非0(do_signal中调用用户自定义信号处理函数或者杀死本线程时返回非0),那么该系统调用返回.
即只有当本线程处理完不被阻塞的信号( ==(!mask)|SIGKILL| SIGSTOP)后,它才会返回.
实时信号的系统调用
前面所述的系统调用仅适用于非实时信号,linux还引入了支持实时信号的系统调用.
一些实时系统调用(如: rt_sigaction, rt_sigpending,rt_sigprocmask, rt_sigsuspend)与它们的非实时的版本类似(只是在名字加了rt_).下面仅简单描述两个实时信号的系统调用.
rt_sigqueueinfo():把一个实时信号发给线程组(放到线程组的共享悬挂信号列表中).库函数sigqueue利用这个系统调用来实现.
rt_sigtimedwait():把阻塞的悬挂信号从悬挂信号队列中删除,如果在调用这个系统调用时还没有相应的阻塞悬挂信号,那么它会把本进程(task)阻塞一段时间. 库函数sigwaitinfo,sigtimedwait通过这个系统调用实现.