大家好,又见面了,我是全栈君,祝每个程序员都可以多学几门语言。
linux signal 处理
说明:
本文主要翻译自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 在80×86 上的前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 a signal (described later) requires that delivery of the signal be held off until it is later unblocked, which acer s the problem of signals being raised before they can be delivered.
内核把信号传送分成两个阶段:
signal generation: 内核更新信号的目的进程的相关数据结构 , 这样该进程就能知道它接收到了一个信号. 认为称为收到信号阶段更恰当. 这个generation 翻译成目的进程接收也不错 .
signal delivery(): 内核强制目的进程处理接收到的信号,这主要是通过改动进程的运行状态或者在目的进程中运行信号处理函数来实现的 . 认为称为处理收到的信号阶段更恰当 . diliver 这里翻译成处理更恰当 .
deliver 的翻译: 有非常多个 , 预计翻译成in computing 比較合理
一个genearated signal 最多仅仅能deliver 一次( 即一个信号最多仅仅会被处理一次) . signal 是可消耗资源 , 一旦一个signal 被deliver, 那么全部进程对它的引用都会被取消 .
已经产生可是还未被处理(deliver) 的信号称为pending signal ( 悬挂信号). 对于regular signal, 在某一个时刻 , 一种signal 在一个进程中仅仅能有一个实例( 由于进程没实用队列缓存其收到的signal) . 由于有31 种regualar signal , 所以一个进程某一个时刻能够有31 个各类signal 的实例. 此外由于linux 进程对real time signal 採用不同的处理方式, 它会保存接收到的real time signal 的实例 , 所以能够同一时候有非常多同种signal 的实例 .
问题: 不同种类的信号的优先级( 从值较小的開始处理) .
一般而言 , 一个信号可能会被悬挂非常长是时间( 即一个进程收到一个信号后 , 该信号有可能在该进程里非常久 , 由于进程没空来处理它), 主要有例如以下因素:
1. 信号通常被当前进程处理 . Signals are usually delivered only to the currently running 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: The process is terminated (killed)
Dump: The process is terminated (killed) and a core file containing its execution context is created, if possible; this file may be used for debug purposes.
Ignore:The signal is ignored.
Stop:The process is stopped, i.e., put in the TASK_STOPPED state.
Continue:If the 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 and blocked signals
2. POSIX 接口kill( ), sigqueue( ) 必须把信号发给线程组 , 而不是指定线程. 另外内核产生的SIGCHLD, SIGINT, or SIGQUIT 也必须发给线程组 .
3. 线程组中仅仅有有一个线程来处理(deliver) 的共享的信号就能够了 . 下问介绍怎样选择这个线程 .
4. 假设线程组收到一个致命的信号 , 内核要杀死线程组的全部线程, 而不是只处理该信号的线程 .
为了遵从POSIX 标准, linux2.6 使用轻量级进程实现线程组.
下文中 , 线程组表示OS 概念中的进程, 而线程表示linux 的轻量级进程. 进程也( 很多其它地时候) 表示linux 的轻量级进程 . 另外每个线程有一个私有的悬挂信号列表 , 线程组共享一个悬挂信号列表 .
与信号有关的数据结构
注:pending/ 悬挂信号, 表示进程收到信号 , 可是还没有来得及处理 , 或者正在处理可是还没有处理完毕 .
对于每个进程, 内核必须知道它当前悬挂(pending) 着哪些信号或者屏蔽(mask) 着哪些信号 . 还要知道线程组怎样处理信号. 为此内核使用了几个重要的数据结构( 它们可通过task 实例訪问), 例如以下图:
The most significant 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 time signal. 每一种相应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 functions reduce 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 process group).
非孤儿进程组 指假设进程组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’t be considered until the next invocation of do_signal( ) . This approach ensures 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 返回后, 进程由核心态切换到用户态, 于是运行了信号处理函数.
Terminating the signal 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.
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)
其用来给一个线程组( 传统意义上的进程) 发信息. 其相应的系统服务例程(service routine) 是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 BSD Unix 还有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(int signum, 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 : (unsigned long)old_sa.sa.sa_handler;
获得被堵塞的悬挂信号
系统调用 sigpending () 同意 用户获得当前线程被堵塞的悬挂信号. 函数原型为:
int sigpending(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 能够用来改动当前线程的堵塞信号集合. 可是它仅适用于非实时信号. 函数原型为:
int sigprocmask(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;
悬挂( 暂停) 进程
int sigsuspend(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
……………
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/118358.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...