您的位置:首页 > 运维架构 > Linux

Linux信号机制概述

2015-10-24 16:59 267 查看


Linux信号机制概述

还是先看看Linux中用户空间怎么运用的,用户空间编程实例如下:

#include<signal.h>

#include<stdio.h>

#include<unistd.h>

/*下面为两个新的信号操作函数*/

void handler(int sig)

{

printf("Receive signal :%u\n",sig);

}

void sigroutine(int num)

{

switch(num)

{

case 1:

printf("SIGUP signal\n");

break;

case 2:

printf("SIGINT signal\n");

break;

case 3:

printf("SIGQUIT signal\n");

break;

default:

break;

}

return;

}

int main(void)

{

struct sigaction sa;

int count;

sa.sa_handler=handler;

sigemptyset(&sa.sa_mask);

sa.sa_flags=0;

printf("task id is:%d\n",getpid());

/*下面四条语句为相应的信号设置新的处理方法*/

sigaction(SIGTERM,&sa,NULL);

signal(SIGHUP,sigroutine);

signal(SIGINT,sigroutine);

signal(SIGQUIT,sigroutine);

while(1)

{

sigsuspend(&sa.sa_mask);/*阻塞,一直等待信号到达*/

printf("loop\n");

}

return 0;

}

可见,用户空间调用了很多系统调用来实现信号的编程,为了弄清楚他的内在原理,决定将内核中的实现做一个大致的梳理。为了理清思路,我们由内核中实现信号操作涉及的关键数据结构关系画出下图,我们看到,内核中的数据结构实现较简单,主要分两部分,一部分用于信号操作(即handler),由进程的sighand字段开始;另一部分用于信号的挂起,由进程的signal和pending字段索引。



由关系图,我们大致观其实现原理如下:

1, 进程的所有信号(现为32个)由一个数组task->sighand->action[]保存,数组的下标即为信号的ID,比如SIGQUIT等,每个操作由一个数据结构sigaction实现,该字段的sa_handler即为实现的操作;

2, 进程对挂起的信号有两种队列,一种为所有进程共享的。该队列的每一项为一个sigqueue结构,通过该结构info字段的si_signo等属性可以定位到对应的信号ID。其中sigset_t结构为一个32位整型,用于定位到ID,即类似位图的表示。

我们看几个最基本的操作于内核中的实现。

1, 设置新的action;

系统调用signal用于实现这个功能,当然也可以用sigaction系统调用,

SYSCALL_DEFINE2(signal, int, sig, __sighandler_t, handler)

{

struct k_sigaction new_sa, old_sa;

int ret;

new_sa.sa.sa_handler = handler;

new_sa.sa.sa_flags = SA_ONESHOT | SA_NOMASK;

sigemptyset(&new_sa.sa.sa_mask);

ret = do_sigaction(sig, &new_sa, &old_sa);

return ret ? ret : (unsignedlong)old_sa.sa.sa_handler;

}

该系统调用分配一个新的action后,调用do_sigaction完成实际工作,最终返回旧的action的handler。

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

{

struct task_struct *t = current;

struct k_sigaction *k;

sigset_t mask;

……

k = &t->sighand->action[sig-1];

spin_lock_irq(¤t->sighand->siglock);

if (oact)

*oact = *k;/*保存旧的action*/

if (act) {

sigdelsetmask(&act->sa.sa_mask,

sigmask(SIGKILL) | sigmask(SIGSTOP));

*k = *act;/*设置新的action*/

/*对两种handler的特殊处理*/

if (sig_handler_ignored(sig_handler(t, sig), sig)) {

……

}

}

spin_unlock_irq(¤t->sighand->siglock);

return 0;

}

实现很简单,先保存旧的action,用于系统调用返回,然后设置新的action。

2, 发送信号

发送信号的系统调用有很多,最终都会调用__send_signal()函数。

staticint __send_signal(int sig,struct siginfo *info, struct task_struct
*t,

int group, int from_ancestor_ns)

{

struct sigpending *pending;

struct sigqueue *q;

int override_rlimit;

……

/*找到需要挂起的队列*/

pending = group ? &t->signal->shared_pending : &t->pending;

……

/*分配队列项结构*/

q = __sigqueue_alloc(t, GFP_ATOMIC | __GFP_NOTRACK_FALSE_POSITIVE,

override_rlimit);

if (q) {/*如果分配成功,将该结构添加到挂起队列,并进行初始化*/

list_add_tail(&q->list, &pending->list);

switch ((unsigned long) info) {

case (unsigned long) SEND_SIG_NOINFO:

q->info.si_signo = sig;

q->info.si_errno = 0;

q->info.si_code = SI_USER;

q->info.si_pid = task_tgid_nr_ns(current,

task_active_pid_ns(t));

q->info.si_uid = current_uid();

break;

case (unsigned long) SEND_SIG_PRIV:

q->info.si_signo = sig;

q->info.si_errno = 0;

q->info.si_code = SI_KERNEL;

q->info.si_pid = 0;

q->info.si_uid = 0;

break;

default:

copy_siginfo(&q->info, info);

if (from_ancestor_ns)

q->info.si_pid = 0;

break;

}

} else if (!is_si_special(info)) {

if (sig >= SIGRTMIN && info->si_code != SI_USER)

return -EAGAIN;

}

out_set:

signalfd_notify(t, sig);/*唤醒action中的等待队列*/

sigaddset(&pending->signal, sig);/*设置信号ID位掩码,即上面所说的那个位图*/

complete_signal(sig, t, group);/*试着唤醒执行该信号的进程*/

return 0;

}

发送信号,即将该信号链接到制定进程的信号挂起队列上,最后试着唤醒执行该信号的进程。

3, 信号捕获与执行

说了这么一堆,但我们还不明白内核怎样确保进程的挂起信号得到处理呢?内核在允许进程恢复用户态下的执行之前,检查进程TIF_SIGPENDING标志的值。每当内核处理玩一个中断或异常时,就检查是否存在挂起信号,即我们可以这样理解,在每次由内核态切换到用户态前,内核都会发起信号队列的处理,具体的信号发起和体系结构有关,但最终为了处理阻塞的挂起信号,内核都会调用do_signal()函数,为说明程序执行框架,下面的代码省略了大部分无关的代码。

staticvoid do_signal(struct pt_regs *regs)

{

struct k_sigaction ka;

siginfo_t info;

int signr;

sigset_t *oldset;

……

/*获取挂起信号*/

signr = get_signal_to_deliver(&info, &ka, regs, NULL);

if (signr > 0) {

……

/*实际的信号处理*/

if (handle_signal(signr, &info, &ka, oldset, regs) == 0) {

current_thread_info()->status &= ~TS_RESTORE_SIGMASK;

}

return;

}

/*如果从系统调用返回*/

if (syscall_get_nr(current, regs) >= 0) {

/* Restart the system call - no handlers present */

switch (syscall_get_error(current, regs)) {

case -ERESTARTNOHAND:

case -ERESTARTSYS:

case -ERESTARTNOINTR:

regs->ax = regs->orig_ax;

regs->ip -= 2;

break;

case -ERESTART_RESTARTBLOCK:

regs->ax = NR_restart_syscall;

regs->ip -= 2;

break;

}

}

……

}

该系统调用主要分为两部分执行,首先是冲挂起队列中查找信号,然后是执行信号处理函数。

1)查找指定信号由do_signal()->get_signal_to_deliver()完成

int get_signal_to_deliver(siginfo_t *info,struct k_sigaction *return_ka,

struct pt_regs *regs, void *cookie)

{

struct sighand_struct *sighand = current->sighand;

struct signal_struct *signal = current->signal;

int signr;

relock:

……

for (;;) {

struct k_sigaction *ka;

……

signr = tracehook_get_signal(current, regs, info, return_ka);

if (unlikely(signr < 0))

goto relock;

if (unlikely(signr != 0))

ka = return_ka;

else {

/*从挂起队列中获取信号*/

signr = dequeue_signal(current, ¤t->blocked,

info);

……

ka = &sighand->action[signr-1];/*找到对应的action*/

}

if (ka->sa.sa_handler == SIG_IGN) /* Do nothing. */

continue;

if (ka->sa.sa_handler != SIG_DFL) {

/*将找到的action设置为参数返回*/

*return_ka = *ka;

if (ka->sa.sa_flags & SA_ONESHOT)

ka->sa.sa_handler = SIG_DFL;

break; /* will return non-zero "signr" value */

}

……

}//end for

spin_unlock_irq(&sighand->siglock);

return signr;

}

int dequeue_signal(struct task_struct *tsk, sigset_t *mask, siginfo_t *info)

{

int signr;

/*先从私有挂起队列中寻找*/

signr = __dequeue_signal(&tsk->pending, mask, info);

if (!signr) {

/*当私有挂起队列中找不到时,从共享队列中找*/

signr = __dequeue_signal(&tsk->signal->shared_pending,

mask, info);

……

}

……

return signr;

}

内核先从自己私有的挂起信号队列中查找,当找不到时,再从共享信号队列中查找,以参数的形式返回找到的action。

2)信号处理do_signal()->handle_signal()

staticint

handle_signal(unsignedlong sig, siginfo_t *info, struct k_sigaction *ka,

sigset_t *oldset, struct pt_regs *regs)

{

int ret;

/* Are we from a system call? */

if (syscall_get_nr(current, regs) >= 0) {

/* If so, check system call restarting.. */

switch (syscall_get_error(current, regs)) {

case -ERESTART_RESTARTBLOCK:

case -ERESTARTNOHAND:

regs->ax = -EINTR;

break;

case -ERESTARTSYS:

if (!(ka->sa.sa_flags & SA_RESTART)) {

regs->ax = -EINTR;

break;

}

/* fallthrough */

case -ERESTARTNOINTR:

regs->ax = regs->orig_ax;

regs->ip -= 2;

break;

}

}

……

/*实际处理*/

ret = setup_rt_frame(sig, ka, info, oldset, regs);

if (ret)

return ret;

……

return 0;

}

上面的setup_rt_frame()在Intel32体系下会最终调用下面函数完成。

int ia32_setup_frame(int sig,struct k_sigaction *ka,

compat_sigset_t *set,struct pt_regs *regs)

{

struct sigframe_ia32 __user *frame;

void __user *restorer;

int err = 0;

void __user *fpstate = NULL;

/* copy_to_user optimizes that into a single 8 byte store */

/*设置ia32_sigreturn系统调用,用于返回*/

staticconst struct {

u16 poplmovl;

u32 val;

u16 int80;

} __attribute__((packed)) code = {

0xb858, /* popl %eax ; movl $...,%eax */

__NR_ia32_sigreturn,

0x80cd, /* int $0x80 */

};

/*返回为栈中的一部分数据*/

frame = get_sigframe(ka, regs,sizeof(*frame), &fpstate);

……

/*设置为用户空间的各种寄存器,完成设置后,由于ip设置为handler处,所以调到该处执行*/

/* Set up registers for signal handler */

regs->sp = (unsignedlong) frame;

regs->ip = (unsignedlong) ka->sa.sa_handler;

/* Make -mregparm=3 work */

regs->ax = sig;

regs->dx = 0;

regs->cx = 0;

loadsegment(ds, __USER32_DS);

loadsegment(es, __USER32_DS);

regs->cs = __USER32_CS;

regs->ss = __USER32_DS;

return 0;

}

终于看到内核怎么处理了吧,由于信号的handler为用户空间程序,系统需要返回到用户空间执行,所以我们看到,内核直接设置ip为handler,代码段cs为用户空间代码段,数据段ds、es以及栈ss等都设置为用户空间的,该函数完成后,程序将冲用户空间的handler处开始执行。上面程序设置ia32_sigreturn系统调用作为程序返回的调用。整体的运行原理图如下(该图参考《深入理解Linux内核框架》 http://www.linuxidc.com/linux/2011-08/41228.htm ):



Linux内核信号机制用到很多地方,最常见的是进程间通信,原理较简单,并且异步调用。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: