首页 > 代码库 > linux signal 处理
linux signal 处理
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是一种能够发送给一个进程或一组进程的短消息(或者说是信号,可是这么easy和信号量混淆). 这样的消息通常仅仅是一个整数,而不包括额外的參数.
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的成员列在下表中:
Process descriptor fields related to signal handling | ||
Type | Name | Description |
struct signal_struct * | signal | Pointer to the process‘s signal descriptor(线程组共用的信号) |
struct sighand_struct * | sighand | Pointer to the process‘s signal handler descriptor(线程组共用) |
sigset_t | blocked | Mask of blocked signals(线程私有) |
sigset_t | real_blocked | Temporary mask of blocked signals (used by the rt_sigtimedwait( ) system call) (线程私有) |
struct sigpending | pending | Data structure storing the private pending signals |
unsigned long | sas_ss_sp | Address of alternative signal handler stack.(能够不提供) |
size_t | sas_ss_size | Size of alternative signal handler stack(能够不提供) |
int (*) (void *) | Notifier | Pointer to a function used by a device driver to block some signals of the process |
void * | notifier_data | Pointer to data that might be used by the notifier function (previous field of table) |
sigset_t * | notifier_mask | Bit mask of signals blocked by a device driver through a notifier function |
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中与信号处理有关的成员:
The fields of the signal descriptor related to signal handling | ||
Type | Name | Description |
atomic_t | count | Usage counter of the signal descriptor |
atomic_t | live | Number of live processes in the thread group |
wait_queue_head_t | wait_chldexit | Wait queue for the processes sleeping in a wait4( ) system call |
struct task_struct * | curr_target | Descriptor of the last process in the thread group that received a signal |
struct sigpending | shared_pending | Data structure storing the shared pending signals |
int | group_exit_code | Process termination code for the thread group |
struct task_struct * | group_exit_task | Used when killing a whole thread group |
int | notify_count | Used when killing a whole thread group |
int | group_stop_count | Used when stopping a whole thread group |
unsigned int | flags | Flags used when delivering signals that modify the status of the process |
除了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表示不是由于错误才发出信号的.
si_code:标识谁发出了这个信号.參考下表:
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( ) | System call handler of 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.
注意:信号有三种处理方式,仅仅有使用自己定义处理函数才须要这样麻烦啊.
接下来我们须要细致瞧瞧这一切怎么发生的.
Settingup the frame
为了能恰当地为信号处理函数设置栈,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则用于特征标志,所以调试器可以知道这是一个信号处理函数的栈.
Frame on the UserMode stack
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.
Evaluating thesignal flags
设置好栈后,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也马上返回.
Starting thesignal handler
do_signal返回后, 进程由核心态切换到用户态,于是运行了信号处理函数.
Terminating thesignal handler
信号处理函数结束后,由于其返回值的地址(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.
Reexecution of system calls | ||||
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.
kill( ) 系统调用
原型为: 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均以库函数提供.
tkill( ) &tgkill( ) 系统调用
这两个函数给指定线程发信号. 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;
悬挂(暂停)进程
系统调用sigsuspend的原型例如以下:
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通过这个系统调用实现.
todo
...............
linux signal 处理