信号

信号

信号用来通知进程异步事件,可以把它理解为对中断的一种模拟。它是一个很小的消息,用来达到两个目的:

  • 告知进程发生了一个特定的事件;
  • 强迫进程执行自身所包含的信号处理程序。

linux预先定义了一些常规信号,并为它们定义了一些缺省操作。除此之外,还有一类实时信号,它们需要排队进行处理,我们也可以自己定义信号和信号处理方式。

既然信号是和进程相关的,那么task_struct中就必然包含有与信号相关的域了。

task_struct{
	...
	struct signal_struct *signal;		//进程信号描述符
	struct sighand_struct *sighand;		//进程信号处理程序描述符
	sigset_t blocked;					//被阻塞信号掩码
	sigset_t real_bloced;				//被阻塞信号临时掩码
	struct sigpending pending;			//存放私有挂起信号
	...
}

信号的产生

信号是由内核函数产生的,它们完成信号处理的第一步,也即更新一个/多个进程的描述符。产生的信号并不直接传递,而是根据信号的类型、目标进程的状态唤醒进程,让它们来接收信号。内核提供了一组产生信号的函数,包括为进程、线程组产生信号等,但它们最终都会调用__send_signal()。当然,在调用__send_signal()之前,会检查这个信号是否应该被忽略(进程没有被跟踪、信号被阻塞,显示忽略信号)

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()

handle_signal(struct ksignal *ksig, struct pt_regs *regs)
{
	bool stepping, failed;
	struct fpu *fpu = &current->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用来记录以前的信号。



本文链接: http://home.meng.uno/articles/1d60a972/ 欢迎转载!

© 2018.02.08 - 2020.10.14 Mengmeng Kuang  保留所有权利!

UV : | PV :

:D 获取中...

Creative Commons License