信号
信号用来通知进程异步事件,可以把它理解为对中断的一种模拟。它是一个很小的消息,用来达到两个目的:
- 告知进程发生了一个特定的事件;
- 强迫进程执行自身所包含的信号处理程序。
linux预先定义了一些常规信号,并为它们定义了一些缺省操作。除此之外,还有一类实时信号,它们需要排队进行处理,我们也可以自己定义信号和信号处理方式。
既然信号是和进程相关的,那么task_struct
中就必然包含有与信号相关的域了。
1 2 3 4 5 6 7 8 9
|
task_struct{ ... struct signal_struct *signal; //进程信号描述符 struct sighand_struct *sighand; //进程信号处理程序描述符 sigset_t blocked; //被阻塞信号掩码 sigset_t real_bloced; //被阻塞信号临时掩码 struct sigpending pending; //存放私有挂起信号 ... }
|
信号的产生
信号是由内核函数产生的,它们完成信号处理的第一步,也即更新一个/多个进程的描述符。产生的信号并不直接传递,而是根据信号的类型、目标进程的状态唤醒进程,让它们来接收信号。内核提供了一组产生信号的函数,包括为进程、线程组产生信号等,但它们最终都会调用__send_signal()
。当然,在调用__send_signal()
之前,会检查这个信号是否应该被忽略(进程没有被跟踪、信号被阻塞,显示忽略信号)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
|
static int __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; int ret = 0, result; assert_spin_locked(&t->sighand->siglock); result = TRACE_SIGNAL_IGNORED; if (!prepare_signal(sig, t, from_ancestor_ns || (info == SEND_SIG_FORCED))) goto ret; //获取进程或线程组的私有挂起队列 pending = group ? &t->signal->shared_pending : &t->pending; //这个信号已经挂起了,忽略它 result = TRACE_SIGNAL_ALREADY_PENDING; if (legacy_queue(pending, sig)) goto ret; result = TRACE_SIGNAL_DELIVERED; //如果是kernel内部的某些强制信号,就立马执行 if (info == SEND_SIG_FORCED) goto out_set; //如果没有超过挂起信号的上限 if (sig < SIGRTMIN) override_rlimit = (is_si_special(info) || info->si_code >= 0); else override_rlimit = 0; //产生一个sigqueue对象,并把它加入到队列中去 q = __sigqueue_alloc(sig, 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 = from_kuid_munged(current_user_ns(), 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; } //...... }
|
在信号产生之后,linux会调用signal_wake_up()
通知进程,告知有新的挂起信号到来,如果当前进程占有了CPU,那么就可以立即执行;否则则要强制进行重新调度。
信号的传递
在信号产生之后,如何确保挂起的信号被正确的处理呢?进程在信号产生时,可能并不在CPU上运行。在进程恢复用户态执行时,会进行检查,如果存在非阻塞的挂起信号,就调用do_signal()
,这个函数会逐个助理挂起的非阻塞信号,而信号的处理则进一步调用handle_signal()
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
handle_signal(struct ksignal *ksig, struct pt_regs *regs) { bool stepping, failed; struct fpu *fpu = ¤t->thread.fpu; //是否处于系统调用中 if (syscall_get_nr(current, regs) >= 0) { //系统调用被打断了,没有执行完,需要重新执行 switch (syscall_get_error(current, regs)) { case -ERESTART_RESTARTBLOCK: case -ERESTARTNOHAND: regs->ax = -EINTR; break; case -ERESTARTSYS: if (!(ksig->ka.sa.sa_flags & SA_RESTART)) { regs->ax = -EINTR; break; } /* fallthrough */ case -ERESTARTNOINTR: regs->ax = regs->orig_ax; regs->ip -= 2; break; } } //设置栈帧 failed = (setup_rt_frame(ksig, regs) < 0); if (!failed) { regs->flags &= ~(X86_EFLAGS_DF|X86_EFLAGS_RF|X86_EFLAGS_TF); /* * Ensure the signal handler starts with the new fpu state. */ if (fpu->fpstate_active) fpu__clear(fpu); } signal_setup_done(failed, ksig, stepping); }
|
这里存在一个问题:handle_signal()
处于内核态中,但信号处理程序是在用户态定义的,因此这里存在着堆栈转换的问题。linux采用的方法是:把内核态堆栈中的硬件上下文,拷贝到当前进程的用户态堆栈中。而当信号处理程序完成时,会自动调用sigreturn()
把硬件上下文拷贝回内核态堆栈中,并且恢复用户态堆栈中的内容。这里需要构造一个用户态栈帧:
首先内核需要把内核栈中的内容复制到用户态堆栈中去,把内核态堆栈的返回地址修改为信号处理程序的入口。注意,为了让信号处理程序结束时,能够清除栈上的内容,用户态堆栈还应该放入一个信号处理程序的返回地址,它指向__kernel_sigreturn()
,把硬件上下文拷贝到内核态堆栈,然后把这个栈帧删除,随后从内核态返回到用户态继续执行。
信号的接口
kill
/tkill
/kgill
系统调用分别用来给某个进程、线程组发送信号。其中,kill(pid, sig)
分别接受一个进程的pid号,以及一个所发送的信号。
实时信号的发送则应该使用rt_sigqueueinfo()
来进行发送。如果用户需要为信号指定一个操作,那么则应该使用sigaction(sig, &act, &oact)
系统调用,act
为指定的操作,而old_act
用来记录以前的信号。
The link of this page is https://blog.nooa.tech/articles/1d60a972/ . Welcome to reproduce it!