Linux 信号浅谈

来源:互联网 发布:it技术支持工程师简历 编辑:程序博客网 时间:2024/06/05 20:34

信号机制是在软件层次上对中断机制的一种模拟,从概念上来说,一个进程接受到一个信号与一个处理器接收到的中断请求是一样的。实际上,信号是一种软中断。

 

既然是中断,那么就得有一个处理中断方法的中断向量表。在task_struct里,有一个指针sig,指向一个sig_struct结构。这个结构我们可以成为“信号向量表”。

 

struct signal_struct

{
atomic_t count;
struct k_sigaction action[_NSIG];
spinlock_t siglock;
};

 

其中action数组就可以理解“信号向量表”。 信号向量表除了指向一个信号的处理程序之外,还有两个特殊的宏SIG_DEF和SIG_IGN,分别采取的是对信号采取默认反应或者忽略。除此之外,信号向量表还有一个与中断向量表最大的不同:信号向量表存在于系统空间,向量所指向的处理程序在用户空间。而中断向量表存在于系统空间,但是中断向量所指向的中断响应程序也在系统空间里。

 

对于信号的检测和相应一般是在下面两种情况:当前进程由于系统调用、中断或者异常进入系统空间后,从系统空间返回到用户空间前夕;当前进程在内核空间中进入睡眠后刚被唤醒(如系统调用sleep)

 

下面接着讨论sigaction

 

struct sigaction

{
union

{
__sighandler_t _sa_handler;
void (*_sa_sigaction)(int, struct siginfo *, void *);
} _u;
sigset_t sa_mask;
unsigned long sa_flags;
void (*sa_restorer)(void);
}; 

 

第一个联合体就是这个信号的具体操作,两个都是函数指针。接下来的sa_mask是保证可靠信号的关键。这是一个位图,在多次收到同一个信号后会把位图上的该位置为1,也就是屏蔽掉它,使得在执行的过程中不会嵌套的响应该信号。除非第三个参数sa_flags中的SA_NODEFER或者SA_NOMASK标志位为1.当然,不管位图中的响应位是否为1,当前信号本身总是自动屏蔽。

 

既然我们是mask掉了,那么mask总有拿掉的一刻,拿掉了mask,那些又该从哪里找到原有的信号呢?在task_struct里有一个指向数据结构sigpending的指针,被mask掉的信号和信号位图组成了一个队列,放在sigpending里

 

struct sigpending

{

struct sigqueque *head,**tail;

sigset_t signal;

};

 

顺便提一下,task_struct还有两个用于信号机制的成分一个是sas__ss_sp,记录当前进程在用户空间执行信号处理程序的堆栈位置,还有一个是sas_ss_size,那是堆栈的大小.不过这两个不是重点,就不多讨论了。

 

接下来讨论下信号的安装。

 

信号的安装,作用类似中断向量的设置,linux提供了两种系统调用

 

sighandler_t signal(  int signum, //信号值

sighandler_t handler//用户定义的该信号处理程序的函数指针);

int sigaction (  int signum,//信号值

const struct sigaction *newact,//待设置的向量

struct sigaction *oldact//老向量);

 

signal()的底层是sys_signal(),signalaction会根据信号的编号不同,确定落实系统调用sys_sigaction()还是sys_rt_sigaction()。

 

最终,会调用do_sigaction()。

 

do_sigaction函数:

do_sigaction(int sig,const struct k_sigaction *act,struct k_sigaction *oact);

 

由于SIGKILL和SIGSTOP的处理程序是不能够被修改的,也是不能够被屏蔽的。所以do_sigaction会在一开始就对其进行检测,并且在屏蔽位图上处理。接下来从信号向量表里获得老的处理程序,以防万一。最后就是对数据结构的赋值了。

 

当新设置的向量为SIG_IGN或者SIG_DFL,涉及到了SIGCHLD,SIGCONT和SIGWINCH这种默认反应为忽略的信号,并且已经有这些信号等待处理的时候,就直接把这些信号全部丢弃掉。

 

信号安装好了,就来看看怎么样向一个进程发送信号。

 

 

发送信号的系统调用也是分为老版本和新版本。

 

老版本:int kill(pid_t pid,int sig);

新版本: int sigqueue(pid_t pid,int sig,const union sigval val);

 

Kill的底层是sys_kill(),代码很简单,准备一个siginfo结构然后调用kill_something_info(),调用它的目的是根据pid的值来确定将信号发送给谁,是一个特定进程(pid的进程),还是整个进程组,还是全部进程。

 

新版本的底层调用sys_rt_sigqueque就简单得多,因为系统调用只允许发送给一个特定进程。它通过pid找到目标进程的task_struct结构,然后通过send_sig_fifo()将信号发送过去。

 

发送完了,再来看看怎么发现信号并且如何反应。

 

在中断机制中,处理器的硬件在每条指令结束的时候都会检测是否有中断请求存在,信号是纯软件的,当然不能依靠硬件来检测信号。同时每条指令接受都来检测也是不现实的。那么进程在什么时候检测呢?首先是从系统调用、中断或者异常处理结束返回到用户空间前夕,要么就是进程从睡眠中被唤醒的时候(这会肯定在系统调用中)。如果发现有信号在等待就要提前从系统调用返回。总而言之,在返回到用户空间前夕总会检测信号的存在并且做出反应。

 

检测方式很简单,一个很大的for循环,从进程的信号队列里通过系统调用sigreturn()取出一个未加屏蔽的信号进行处理,直道信号队列中不再存在这样的信号,或者相应的信号向量为SIG_DFL.

 

处理过程比较繁琐,大概的过程是这样的:

1、先在用户空间堆栈里为信号处理程序的执行预先拓展堆栈,放一个作为局部的数据结构。将系统空间堆栈中的方法保存到这个数据结构里。

2、在信号处理程序里插入返回系统空间的系统调用sigreturn()

3、将系统空间堆栈中的方法改为执行信号处理程序所需的方法

4、返回用户空间,执行信号处理程序

5、信号处理程序执行完毕后,通过sigreturn返回系统空间

6、恢复用户空间的堆栈

7、返回用户空间继续执行程序。

 

原创粉丝点击