加入收藏 | 设为首页 |

恶作剧之吻2-Linux信号处理

海外新闻 时间: 浏览:226 次

什么是信号

信号本质上是在软件层次上对中止机制的一种模仿,其首要有以下几种来历:

  • 程序过错:除零,不合法内存拜访等。
  • 外部信号:终端 Ctrl-C 发作 SGINT 信号,定时器到期发作SIGALRM等。
  • 显式恳求:kill函数答应进程发送任何信号给其他进程或进程组。

现在 Linux 支撑64种信号。信号分为非实时信号(不牢靠信号)和实时信号(牢靠信号)两种类型,对应于 Linux 的信号值为 1-31 和 34-64。

信号是异步的,一个进程不用经过任何操作来等候信号的抵达。现实上,进程也不知道信号究竟什么时分抵达。一般来说,咱们只需求在进程中设置信号相应的处理函数,当有信号抵达的时分,由体系异步触发相应的处理函数即可。如下代码:

#include 
#include
#include
void sigcb(int signo) {
switch (signo) {
case SIGHUP:
printf("Get a signal -- SIGHUP\n");
break;
case SIGINT:
printf("Get a signal -- SIGINT\n");
break;
case SIGQUIT:
printf("Get a signal -- SIGQUIT\n");
break;
}
return;
}
int main() {
signal(SIGHUP, sigcb);
signal(SIGINT, sigcb);
signal(SIGQUIT, sigcb);
for (;;) {
sleep(1);
}
}

运转程序后,当咱们按下 Ctrl+C 后,屏幕大将会打印 Get a signal -- SIGINT。当然咱们能够运用 kill -s SIGINT pid指令来发送一个信号给进程,屏幕相同打印出 Get a signal -- SIGINT 的信息。

信号完结原理

接下来咱们剖析一下Linux对信号处理机制的完结原理。

信号处理相关的数据结构

在进程办理结构 task_struct 中有几个与信号处理相关的字段,如下:

struct task_struct {
...
int sigpending;
...
struct signal_struct *sig;
sigset_t blocked;
struct sigpending pending;
...
}

成员 sigpending 表明进程是否有信号需求处理(1表明有,0表明没有)。成员 blocked 表明被屏蔽的信息,每个位代表一个被屏蔽的信号。成员 sig 表明信号相应的处理办法,其类型是 struct signal_struct,界说如下:

#define _NSIG 64
struct signal_struct {
atomic_t count;
struct k_sigaction action[_NSIG];
spinlock_t siglock;
};
typedef void (*__sighandler_t)(int);
struct sigaction {
__sighandler_t sa_handler;
unsigned long sa_flags;
void (*sa_restorer)(void);
sigset_t sa_mask;
};
struct k_sigaction {
struct sigaction sa;
};

能够看出,struct signal_struct 是个比较复杂的结构,其 action 成员是个 struct k_sigaction 结构的数组,数组中的每个成员代表着相应信号的处理信息,而 struct k_sigaction 结构其实是 struct sigaction 的简略封装。

咱们再来看看 struct sigaction 这个结构,其间 sa_handler 成员是类型为 __sighandler_t 的函数指针,代表着信号处理的办法。

最终咱们恶作剧之吻2-Linux信号处理来看看 struct task_struct 结构的 pending 成员,其类型为 struct sigpending,存储着进程接纳到的信号行列,struct sigpending 的界说如下:

struct sigqueue {
struct sigqueue *next;
siginfo_t info;
};
struct sigpending {
struct sigqueue *head, **tail;
sigset_t signal;
};

当进程接纳到一个信号时,就需求把接纳到的信号增加 pending 这个行列中。

发送信号

能够经过 kill() 体系调用发送一个信号给指定的进程,其原型如下:

int kill(pid_t pid, int sig);

参数 pid 指定要接纳信号进程的ID,而参数 sig 是要发送的信号。kill() 体系调用最终会进入内核态,而且调用内核函数 sys_kill(),代码如下:

asmlinkage long
sys_kill(int pid, int sig)
{
struct siginfo info;
info.si_signo = sig;
info.si_errno = 0;
info.si_code = SI_USER;
info.si_pid = current->pid;
info.si_uid = current->uid;
return kill_something_info(sig, &info, pid);
}

sys_kill() 的代码比较简略,首要初始化 info 变量的成员,接着调用 kill_something_info() 函数来处理发送信号的操作。kill_something_info() 函数的代码如下:

static int kill_something_info(int sig, struct siginfo *info, int pid)
{
if (!pid) {
return kill_pg_info(sig, info, current->pgrp);
} else if (pid == -1) {
int retval = 0, count = 0;
struct task_struct * p;
read_lock(&tasklist_lock);
for_each_task(p) {
if (p->pid > 1 && p != current) {
int err = send_sig_info(sig, info, p);
++count;
if (err != -EPERM)
retval = err;
}
}
read_unlock(&tasklist_lock);
return count ? retval : -ESRCH;
} else if (pid < 0) {
return kill_pg_info(sig, info, -pid);
} else {
return kill_proc_info(sig, info, pid);
}
}

kill_something_info() 函数依据传入pid 的不同来进行不同的操作,有如下4中或许:

  • pid 等于0时,表明信号将送往一切与调用 kill() 的那个进程属同一个运用组的进程。
  • pid 大于零时,pid 是信号要送往的进程I恶作剧之吻2-Linux信号处理D。
  • pid 等于-1时,信号将送往调用进程有权给其发送信号的一切进程,除了进程1(init)。
  • pid 小于-1时,信号将送往以-pid为组标识的进程。

咱们这儿只剖析 恶作剧之吻2-Linux信号处理pid 大于0的状况,从上面的代码能够知道,当 pid 大于0时,会调用 kill_proc_info() 函数来处理信号发送操作,其代码如下:

inline int
kill_proc_info(int sig, struct siginfo *info, pid_t pid)
{
int error;
struct task_struct *p;
read_lock(&tasklist_lock);
p = find_task_by_pid(pid);
error = -ESRCH;
if (p)
error = send_sig_info(sig, info, p);
read_unlock(&tasklist_lock);
return error;
}

kill_proc_info() 首要经过调用 find_task_by_pid() 函数来取得 pid 对应的进程办理结构,然后经过 send_sig_info()函数来发送信号给此进程,send_sig_info() 函数代码如下:

int
send_sig_info(int sig, struct siginfo *info, struct task_struct *t)
{
unsigned long flags;
int ret;
ret = -EINVAL;
if (sig < 0 || sig > _NSIG)
goto out_nolock;
ret = -EPERM;
if (bad_signal(sig, info, t))
goto out_nolock;
ret = 0;
if (!sig || !t->sig)
goto out_nolock;
spin_lock_irqsave(&t->sigmask_lock, flags);
handle_stop_signal(sig, t);
if (ignored_signal(sig, t))
goto out;
if (sig < SIGRTMIN && sigismember(&t->pending.signal, sig))
goto out;
ret = deliver_signal(sig, info, t);
out:
spin_unlock_irqrestore(&t->sigmask_lock, flags);
if ((t->state & TASK_INTERRUPTIBLE) && signal_pending(t))
wake_up_process(t);
out_nolock:
return ret;
}

send_sig_info() 首要调用 bad恶作剧之吻2-Linux信号处理_signal() 函数来查看是否有权发送信号给进程,然后调用 ignored_signal() 函数来查看信号是否被疏忽,接着调用 deliver_signal() 函数开端发送信号,最终假如进程是睡觉状况就唤醒进程。咱们接着来剖析 deliver_signal() 函数:

static int deliver_signal(int sig, struct siginfo *info, struct task_struct *t)
{
int retval = send_signal(sig, info, &t->pending);
if (!retval && !sigismember(&t->blocked, sig))
signal_wake_up(t);
return retval;
}

deliver_signal() 首要调用 send_signal() 函数进行信号的发送,然后调用 signal_wake_up() 函数唤醒进程。咱们来剖析一下最重要的函数 send_signal():

static int send_signal(int sig, struct siginfo *info, struct sigpending *signals)
{
struct sigqueue * q = NULL;
if (atomic_read(&nr_queued_signals) < max_queued_signals) {
q = kmem_cache_alloc(sigqueue_cachep, GFP_ATOMIC);
}
if (q) {
atomic_inc(&nr_queued_signals);
q->next = NULL;
*signals->tail = q;
signals->tail = &q->next;
switch ((unsigned long) info) {
case 0:
q->info.si_signo = sig;
q->info.si_errno = 0;
q->info.si_code = SI_USER;
q->info.si_pid = current->pid;
q->info.si_uid = current->uid;
break;
case 1:
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);
break;
}
} else if (sig >= SIGRTMIN && info && (unsigned long)info != 1
&& info->si_code != SI_USER) {
return -EAGAIN;
}
sigaddset(&signals->signal, sig);
return 0;
}

send_signal() 函数尽管比较长,但逻辑仍是比较简略的。在 信号处理相关的数据结构 一节咱们介绍过进程办理结构 task_struct 有个 pending 的成员变量,其用于保存接纳到的信号行列。send_signal() 函数的第三个参数便是进程办理结构的 pending 成员变量。

send_signal() 首要调用 kmem_cache_alloc() 函数来请求一个类型为 struct sigqueue 的行列节点,然后把节点增加到 pending 行列中,接着依据参数 info 的值来进行不同的操作,最终经过 sigaddset() 函数来设置信号对应的标志位,表明进程接纳到该信号。

signal_wake_up() 函数会把进程的 sigpending 成员变量设置为1,表明有信号需求处理,假如进程是睡觉可中止状况还会唤醒进程。

至此,发送信号的流程现已完结,咱们能够经过下面的调用链来愈加直观的了解此进程:

kill() 
| User Space
---------------------------------------------------------------------------------------
| Kernel Space
sys_kill()
|---> kill_something_info()
|---> kill_proc_info()
|---> find_task_by_pid()
|---> send_sig_info()
|---> bad_signal()
|---> handle_stop_signal()
|---> ignored_signal()
|---> deliver_signal()
|---> send_signal()
| |---> kmem_cache_alloc()
| |---> sigaddset()
|---> signal_wake_up()

内核触发信号处理函数

上面介绍了怎样发作一个信号给指定的进程,可是什么时分会触发信号相应的处理函数呢?为了尽快让信号得到处理,Linux把信号处理进程放置在进程从内核态回来到用户态前,也便是在 ret_from_sys_call 处:

// arch/i386/kernel/entry.S
ENTRY(ret_from_sys_call)
...
ret_with_reschedule:
...
cmpl $0, sigpending(%ebx) // 查看进程的sigpending成员是否等于1
jne signal_return // 假如是就跳转到 signal_return 处履行
restore_all:
RESTORE_ALL
ALIGN
signal_return:
sti // 敞开硬件中止
testl $(VM_MASK),EFLAGS(%esp)
movl %esp,%eax
jne v86_signal_return
xorl %edx,%edx
call SYMBOL_NAME(do_signal) // 调用do_signal()函数进行处理
jmp restore_all

因为这是一段汇编代码,有点不太直观(大约知道意思就能够了),所以我在代码中进行了注释。首要的逻辑便是首要查看进程的 sigpending 成员是否等于1,假如是调用 do_signal() 函数进行处理,因为 do_signal() 函数代码比较长,所以咱们分段来阐明,如下:

int do_signal(struct pt_regs *regs, sigset_t *oldset)
{
siginfo_t info;
struct k_sigaction *ka;
if ((regs->xcs & 3) != 3)
return 1;
if (!oldset)
oldset = ¤t->blocked;
for (;;) {
unsigned long signr;
spin_lock_irq(¤t->sigmask_lock);
signr = dequeue_signal(¤t->blocked, &info);
spin_unlock_irq(¤t->sigmask_lock);
if (!signr)
break;

上面这段代码的首要逻辑是经过 dequeue_signal() 函数获取到进程接纳行列中的一个信号,假如没有信号,那么就跳出循环。咱们接着来剖析:

  ka = ¤t->sig->action[signr-1];
if (ka->sa.sa_handler == SIG_IGN) {
if (signr != SIGCHLD)
continue;
/* Check for SIGCHLD: it's special. */
while (sys_wait4(-1, NULL, WNOHANG, NULL) > 0)
/* nothing */;
continue;
}

上面这段代码首要获取到信号对应的处理办法,假如对此信号的处理是疏忽的话,那么就直接越过。

  if (ka->sa.sa_handler == SIG_DFL) {
int exit_code = signr;
/* Init gets no signals it doesn't want. */
if (current->pid == 1)
continue;
switch (signr) {
case SIGCONT: case SIGCHLD: case SIGWINCH:
continue;
case SIGTSTP: case SIGTTIN: case SIGTTOU:
if (is_orphaned_pgrp(current->pgrp))
continue;
/* FALLTHRU */
case SIGSTOP:
current->state = TASK_STOPPED;
current->exit_code = signr;
if (!(current->p_pptr->sig->action[SIGCHLD-1].sa.sa_flags & SA_NOCLDSTOP))
notify_parent(current, SIGCHLD);
schedule();
continue;
case SIGQUIT: case SIGILL: case SIGTRAP:
case SIGABRT: case SIGFPE: case SIGSEGV:
case SIGBUS: case SIGSYS: case SIGXCPU: case SIGXFSZ:
if (do_coredump(signr, regs))
exit_code |= 0x80;
/* FALLTHRU */
default:
sigaddset(¤t->pending.signal, signr);
recalc_sigpending(current);
current->flags |= PF_SIGNALED;
do_exit(exit_code);
/* NOTREACHED */
}
}
...
handle_signal(signr, ka, &info, oldset, regs);
return 1;
}
...
return 0;
}

上面的代码表明,假如指定为默许的处理办法,那么就运用体系的默许处理办法去处理信号,比方 SIGSEGV 信号的默许处理办法便是运用 do_coredump() 函数来生成一个 core dump 文件,而且经过调用 do_exit() 函数退出进程。

假如指定了自界说的处理办法,那么就经过 handle_signal() 函数去进行处理,handle_signal() 函数代码如下:

static void
handle_signal(unsigned long sig, struct k_sigaction *ka,
siginfo_t *info, sigset_t *oldset, struct pt_regs * regs)
{
...
if (ka->sa.sa_flags & SA_SIGINFO)
setup_rt_frame(sig胃胀, ka, info, oldset, regs);
else
setup_frame(sig, ka, oldset, regs);
if (ka->sa.sa_flags & SA_ONESHOT)
ka->sa.sa_handler = SIG_DFL;
if (!(ka->sa.sa_flags & SA_NODEFER)) {
spin_lock_irq(¤t->sigmask_lock);
sigorsets(¤t->blocked,¤t->bl恶作剧之吻2-Linux信号处理ocked,&ka->sa.sa_mask);
sigaddset(¤t->blocked,sig);
recalc_sigpending(current);
spin_unlock_irq(¤t->sigmask_lock);
}
}

因为信号处理程序是由用户供给的,所以信号处理程序的代码是在用户态的。而从体系调用回来到用户态前仍是归于内核态,CPU是制止内核态履行用户态代码的,那么怎样办?

答案先回来到用户态履行信号处理程序,履行完信号处理程序后再回来到内核态,再在内核态完结收尾作业。听起来有点绕,现实也的确是这样。下面经过一副图片来直观的展现这个进程(图片来历网络):

为了到达这个意图,Linux阅历了一个非常高低的进程。咱们知道,从内核态回来到用户态时,CPU要从内核栈中找到回来到用户态的地址(便是调用体系调用的下一条代码指令地址),Linux为了先让信号处理程序履行,所以就需求把这个回来地址修正为信号处理程序的进口,这样当从体系调用回来到用户态时,就能够履行信号处理程序了。

所以,handle_signal() 调用了 setup_frame() 函数来构建这个进程的运转环境(其实便是修正内核栈和用户栈相应的数据来完结)。咱们先来看看内核栈的内存布局图:

图中的 eip 便是内核态回来到用户态后开端履行的第一条指令地址,所以把 eip 改成信号处理程序的地址就能够在内核态回来到用户态的时分主动履行信号处理程序了。咱们看看 setup_frame() 函数其间有一行代码便是修正 eip 的值,如下:

static void setup_frame(int sig, struct k_sigaction *ka,
sigset_t *set, struct pt_regs * regs)
{
...
regs->eip = (unsigned long) ka->sa.sa_handler; // regs是内核栈中保存的寄存器调集
...
}

现在能够在内核态回来到用户态时主动履行信号处理程序了,可是当信号处理程序履行完怎样回来到内核态呢?Linux的做法便是在用户态栈空间构建一个 Frame(帧)(我也不知道为什么要这样叫),构建这个帧的意图便是为了履行完信号处理程序后回来到内核态,并康复本来内核栈的内容。回来到内核态的方法是调用一个名为 sigreturn() 体系调用,然后再 sigreturn() 中康复本来内核栈的内容。

怎样能在履行完信号处理程序后调用 sigreturn() 体系调用呢?其实跟前面修正内核栈 eip 的值相同,这儿修正的是用户栈 eip 的值,修正后跳转到一个履行下面代码的当地(用户栈的某一处):

popl %eax 
movl $__NR_sigreturn,%eax
int $0x80

从上面的汇编代码能够知道,这儿便是调用了 sigreturn() 体系调用。修正用户栈的代码在 setup_frame() 中,代码如下:

static void setup_frame(int sig, struct k_sigaction *ka,
sigset_t *set, struct pt_regs * regs)
{
...
err |= __put_user(frame->retcode, &frame->pretcode);
/* This is popl %eax ; movl $,%eax ; int $0x80 */
err |= __put_user(0xb858, (short *)(frame->retcode+0));
err |= __put_user(__NR_sigreturn, (int *)(frame->retcode+2));
err |= __put_user(0x80cd, (short *)(frame->retcode+6));
...
}

这几行代码比较难明,其实便是修正信号程序程序回来后要履行代码的地址。修正后如下图:

这样履行完信号处理程序后就会调用 sigreturn(),而 sigreturn() 要做的作业便是康复本来内核栈的内容了,咱们来看看 sigreturn() 的代码:

asmlinkage int sys_sigreturn(unsigned long __unused)
{
struct pt_regs *regs = (struct pt_regs *) &__unused;
struct sigframe *frame = (struct sigframe *)(regs->esp - 8);
sigset_t set;
int eax;
if (verify_area(VERIFY_READ, frame, sizeof(*frame)))
goto badframe;
if (__get_user(set.sig[0], &frame->sc.oldmask)
|| (_NSIG_WORDS > 1
&& __copy_from_user(&set.sig[1], &frame->extramask,
sizeof(frame->extramask))))
goto badframe;
sigdelsetmask(&set, ~_BLOCKABLE);
spin_lock_irq(¤t->sigmask_lock);
current->blocked = set;
recalc_sigpending(current);
spin_unlock_irq(¤t->sigmask_lock);
if (restore_sigcontext(regs, &frame->sc, &eax))
goto badframe;
return eax;
badframe:
force_sig(SIGSEGV, current);
return 0;
}

其间最重要的是调用 restore_sigcontext() 康复本来内核栈的内容,要康复本来内核栈的内容首要是要指定本来内核栈的内容,所以先要保存本来内核栈的内容。保存本来内核栈的内容也是在 setup_frame() 函数中,setup_frame() 函数把本来内核栈的内容保存到用户栈中(也便是上面所说的 帧 中)。restore_sigcontext() 函数便是从用户栈中读取本来内核栈的数据,然后康复之。保存内核栈内容首要由 setup_sigcontext() 函数完结,有爱好能够查阅代码,这儿就不做具体阐明晰。

这样,当从 sigreturn() 体系调用回来时,就能够按本来的途径回来到用户程序的下一个履行点(比方调用体系调用的下一行代码)。

设置信号处理程序

最终咱们来剖析一下怎样设置一个信号处理程序。

用户能够经过 signal() 体系调用设置一个信号处理程序,咱们来看看 signal() 体系调用的代码:

asmlinkage unsigned long
sys_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;
ret = do_sigaction(sig, &new_sa, &old_sa);
return ret ? ret : (unsigned long)old_sa.sa.sa_handler;
}

代码比较简略,便是先设置一个新的 struct k_sigaction 结构,把其 sa.sa_handler 字段设置为用户自界说的处理程序。然后经过 do_sigaction() 函数进行设置,代码如下:

int
do_sigaction(int sig, const struct k_sigaction *act, struct k_sigaction *oact)
{
struct k_sigaction *k;
if (sig < 1 || sig > _NSIG ||
(act && (sig == SIGKILL || sig == SIGSTOP)))
return -EINVAL;
k = ¤t->sig->action[sig-1];
spin_lock(¤t->sig->siglock);
if (oact)
*oact = *k;
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))) {
spin_lock_irq(¤t->sigmask_lock);
if (rm_sig_from_queue(sig, current))
recalc_sigpending(current);
spin_unlock_irq(¤t->sigmask_lock);
}
}
spin_unlock(¤t->sig->siglock);
return 0;
}

这个函数也不难,咱们上面介绍过,进程办理结构中有个 sig 的字段,它是一个 struct k_sigaction 结构的数组,每个元素保存着对应信号的处理程序,所以 do_sigaction() 函数便是修正这个信号处理程序。代码 k = ¤t->sig->action[sig-1] 便是获取对应信号的处理程序,然后把其设置为新的信号处理程序即可。