2.并使目标进程进行相应处理(eg: 执行的信号处理函数,signal handler).相应的处理也可以是忽略它.
信号分成两种:
regular signal(非实时信号),对应的编码值为[1,31]
real time signal对应的编码值为[32,64]
对一个信号,要三种处理方式:
忽略该信号;
采用默认方式处理(调用系统指定的信号处理函数);
使用用户指定的方式处理(调用用户指定的信号处理函数).
对于某些信号只能采用默认的方式处理(eg:SIGKILL,SIGSTOP).
信号处理可以分成两个阶段:信号产生并通知到接收方(generation), 接收方进行处理(deliver)
Unix为了允许用户态进程之间的通信而引入signal.此外, 内核使用signal给进程通知系统事件. 近30年来, signal只有很小的变化.
The Role of Signals
signal是一种可以发送给一个进程或一组进程的短消息(或者说是信号,但是这么容易和信号量混淆). 这种消息通常只是一个整数,而不包含额外的参数.
linux提供了很多种signal, 这些signal通过宏来标识(这个宏作为这个信号的名字). 并且这些宏的名字的开头是SIG
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.
内核把信号传送分成两个阶段:
signalgeneration: 内核更新信号的目的进程的相关数据结构,这样该进程就能知道它接收到了一个信号. 觉得称为收到信号阶段更恰当. 这个generation翻译成目的进程接收也不错.
signal delivery():内核强制目的进程处理接收到的信号,这主要是通过修改进程的执行状态或者在目的进程中执行信号处理函数来实现的. 觉得称为处理收到的信号阶段更恰当. diliver这里翻译成处理更恰当.
一个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. 记录一个进程阻塞了哪些信号
2. 当从核心态切换到用户态时,检查进程是否接受到了signal.(几乎每一次时钟中断都要干这样的事,费时吗?).
3. 检查信号是否可以被忽略. 当如下条件均满足时则可被忽略:
1). 目标进程未被其它进程traced(即PT_PTRACED==0).但一个被traced的进程收到一个信号时,内核停止目标线程,并且给tracing 进程发送信号SIGCHLD. tracing进程可能会通过SIGCONT来恢复traced进程的执行
2). 目标进程未阻塞该信号.
3). 信号正被目标进程忽略(或者由于忽略是显式指定的或者由于忽略是默认操作).
4. 处理信号.这可能需要切换到信号处理函数
处理信号的方式 (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程序结束时死亡.
---------------------------------------------我是分割线--------------------------------------------
与信号处理相关的系统调用
因为当进程在用户态时,允许发送和接受信号. 这意味着必须定义一些系统调用来允许这类操作. 不幸的是,由于历史的原因这些操作的语义有可能会重合,也意味着某些系统调用可能很少被用到.比如,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通过这个系统调用实现.