【进程通信】信号处理机制二——内核实现

十二月 27, 2023 [Linux 与系统] #Linux #Shell 与命令行 #obsidian #操作系统原理

1 信号处理的完整流程

  1. 用户输入命令,在Shell下启动一个前台进程。
  2. 用户按下Ctrl-C,这个键盘输入产生一个硬件中断。
  3. 如果CPU当前正在执行这个进程的代码,则该进程的用户空间代码暂停执行,CPU从用户态切换到内核态处理硬件中断。
  4. 终端驱动程序将Ctrl-C解释成一个SIGINT信号,记在该进程的PCB中(也可以说发送了一个SIGINT信号给该进程)。
  5. 当某个时刻要从内核返回到该进程的用户空间代码继续执行之前,首先处理PCB中记录的信号,发现有一个SIGINT信号待处理,而这个信号的默认处理动作是终止进程,所以直接终止进程而不再返回它的用户空间代码执行。

2 信号相关数据结构

struct task_struct {
	/* Signal handlers: */
	struct signal_struct		*signal;
	struct sighand_struct		*sighand;
	sigset_t			blocked;
	sigset_t			real_blocked;
	/* Restored if set_restore_sigmask() was used: */
	sigset_t			saved_sigmask;
	struct sigpending		pending;
}

其中成员 pending 表示进程是否有信号需要处理(1表示有,0表示没有)。成员 blocked 表示被屏蔽的信息,每个位代表一个被屏蔽的信号。成员 sighand 表示信号相应的处理方法。

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

sigaction

typedef void __signalfn_t(int);
typedef __signalfn_t __user *__sighandler_t;

struct sigaction {
#ifndef __ARCH_HAS_IRIX_SIGACTION
	__sighandler_t	sa_handler;
	unsigned long	sa_flags;
#else
	unsigned int	sa_flags;
	__sighandler_t	sa_handler;
#endif
#ifdef __ARCH_HAS_SA_RESTORER
	__sigrestore_t sa_restorer;
#endif
	sigset_t	sa_mask;	/* mask last for extensibility */
};

struct k_sigaction {
	struct sigaction sa;
#ifdef __ARCH_HAS_KA_RESTORER
	__sigrestore_t ka_restorer;
#endif
};

3.1 kill系统调用

/**
 *  sys_kill - send a signal to a process
 *  @pid: the PID of the process
 *  @sig: signal to be sent
 */
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
	struct siginfo info;

	info.si_signo = sig;
	info.si_errno = 0;
	info.si_code = SI_USER;
	info.si_pid = task_tgid_vnr(current);
	info.si_uid = from_kuid_munged(current_user_ns(), current_uid());

	return kill_something_info(sig, &info, pid);
}

3.2 kill_something_info

kill_something_info() 函数根据传入pid 的不同来进行不同的操作,有如下4种可能:

static int kill_something_info(int sig, struct siginfo *info, pid_t pid)
{
	int ret;

	if (pid > 0) {
		rcu_read_lock();
		ret = kill_pid_info(sig, info, find_vpid(pid));
		rcu_read_unlock();
		return ret;
	}

	read_lock(&tasklist_lock);
	if (pid != -1) {
		ret = __kill_pgrp_info(sig, info, pid ? find_vpid(-pid) : task_pgrp(current));
	} else {
		int retval = 0, count = 0;
		struct task_struct * p;

		for_each_process(p) {
			if (task_pid_vnr(p) > 1 &&!same_thread_group(p, current)) {
				int err = group_send_sig_info(sig, info, p);
				++count;
			}
		}
		ret = count ? retval : -ESRCH;
	}
	read_unlock(&tasklist_lock);

	return ret;
}

3.2.1 kill_pid_info

向某个进程发送信号

int kill_pid_info(int sig, struct siginfo *info, struct pid *pid)
{
	int error = -ESRCH;
	struct task_struct *p;

	for (;;) {
		rcu_read_lock();
		p = pid_task(pid, PIDTYPE_PID);
		if (p)
			error = group_send_sig_info(sig, info, p);
		rcu_read_unlock();
		if (likely(!p || error != -ESRCH))
			return error;

		/*
		 * The task was unhashed in between, try again.  If it
		 * is dead, pid_task() will return NULL, if we race with
		 * de_thread() it will find the new leader.
		 */
	}
}

3.2.2 __kill_pgrp_info

向某个进程组发送信号,遍历进程组里的所有进程都发送信号。

int __kill_pgrp_info(int sig, struct siginfo *info, struct pid *pgrp)
{
	struct task_struct *p = NULL;
	int retval, success;

	success = 0;
	retval = -ESRCH;
	do_each_pid_task(pgrp, PIDTYPE_PGID, p) {
		int err = group_send_sig_info(sig, info, p);
		success |= !err;
		retval = err;
	} while_each_pid_task(pgrp, PIDTYPE_PGID, p);
	return success ? 0 : retval;
}

3.3 __send_signal

group_send_sig_info
->do_send_sig_info
	->send_signal
		->__send_signal

static int __send_signal(int sig, struct siginfo *info, struct task_struct *t, int group, int from_ancestor_ns)
{
	//如果目标线程忽略信号,那么没必要发送信号。
	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;

	//判断分配信号队列节点时是否可以忽略信号队列长度的限制:
	//对于传统信号,如果是特殊的信号信息,或者信号的编码大于0,那么允许忽略;
	//如果是实时信号,那么不允许忽略。
	if (sig < SIGRTMIN)
		override_rlimit = (is_si_special(info) || info->si_code >= 0);
	else
		override_rlimit = 0;
	//分配一个信号队列节点。
	q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit);
	//如果分配信号队列节点成功,那么把它添加到信号队列中。
	list_add_tail(&q->list, &pending->list);

	//如果某个进程正在通过信号文件描述符(signalfd)监听信号,那么通知进程。
	//signalfd是进程创建用来接收信号的文件描述符,进程可以使用select或poll监听信号文件描述符。
	signalfd_notify(t, sig);

	//把信号添加到信号集合中
	sigaddset(&pending->signal, sig);

	//如果向线程组发送信号,那么需要在线程组中查找一个没有屏蔽信号的线程,
	//唤醒它,让它处理信号。
	complete_signal(sig, t, group);
}

sigqueue_cachep = KMEM_CACHE(sigqueue, SLAB_PANIC);
struct sigqueue *__sigqueue_alloc(int sig, struct task_struct *t, gfp_t flags, int override_rlimit)
{
	struct user_struct *user = get_uid(__task_cred(t)->user);
	atomic_inc(&user->sigpending);;

	struct sigqueue *q = kmem_cache_alloc(sigqueue_cachep, flags);
	INIT_LIST_HEAD(&q->list);
	q->flags = 0;
	q->user = user;

	return q;
}

3.4 complete_signal

如果向线程组发送信号,那么需要在线程组中查找一个没有屏蔽信号的线程, 唤醒它,让它处理信号。

static void complete_signal(int sig, struct task_struct *p, int group)
{
	struct signal_struct *signal = p->signal;
	struct task_struct *t;

	/* Now find a thread we can wake up to take the signal off the queue.*/
	t = find_wants_signal_task((sig, p));

	/* Found a killable thread.  If the signal will be fatal,
	 * then start taking the whole group down immediately. */
	if (sig_killable(p, sig)) {
		task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK);
		//加入到pending signal
		sigaddset(&t->pending.signal, SIGKILL);
		//唤醒task
		signal_wake_up(t, 1);
		return;
	}

	/*
	 * The signal is already in the shared-pending queue.
	 * Tell the chosen thread to wake up and dequeue it.
	 */
	signal_wake_up(t, sig == SIGKILL);
	return;
}

4 内核触发信号处理函数

为了尽快让信号得到处理,Linux把信号处理过程放置在进程从内核态返回到用户态前,也就是在 ret_to_user 处:

ret_to_user:
	/* Make sure need_resched and sigpending don't change between
		sampling and the rti.  */
	lda	$16, 7
	call_pal PAL_swpipl
	ldl	$17, TI_FLAGS($8)
	and	$17, _TIF_WORK_MASK, $2
	bne	$2, work_pending

work_pending

work_pending:
	mov	x0, sp				// 'regs'
	bl	do_notify_resume
#ifdef CONFIG_TRACE_IRQFLAGS
	bl	trace_hardirqs_on		// enabled while in userspace
#endif
	ldr	x1, [tsk, #TSK_TI_FLAGS]	// re-check for single-step
	b	finish_ret_to_user

4. 1 do_notify_resume

void do_notify_resume(struct pt_regs *regs, unsigned int thread_flags)
{
	do {
		if (thread_flags & _TIF_NEED_RESCHED) {
			/* Unmask Debug and SError for the next task */
			local_daif_restore(DAIF_PROCCTX_NOIRQ);

			schedule();
		} else {
			local_daif_restore(DAIF_PROCCTX);

			if (thread_flags & _TIF_UPROBE)
				uprobe_notify_resume(regs);

			if (thread_flags & _TIF_SIGPENDING)
				do_signal(regs);

			if (thread_flags & _TIF_NOTIFY_RESUME) {
				clear_thread_flag(TIF_NOTIFY_RESUME);
				tracehook_notify_resume(regs);
			}

			if (thread_flags & _TIF_FOREIGN_FPSTATE)
				fpsimd_restore_current_state();
		}

		local_daif_mask();
		thread_flags = READ_ONCE(current_thread_info()->flags);
	} while (thread_flags & _TIF_WORK_MASK);
}

4.2 do_signal

static void do_signal(struct pt_regs *regs)
{
	if (get_signal(&ksig)) {
		/*
		 * Depending on the signal settings, we may need to revert the
		 * decision to restart the system call, but skip this if a
		 * debugger has chosen to restart at a different PC.
		 */
		if (regs->pc == restart_addr &&
		    (retval == -ERESTARTNOHAND ||
		     retval == -ERESTART_RESTARTBLOCK ||
		     (retval == -ERESTARTSYS &&
		      !(ksig.ka.sa.sa_flags & SA_RESTART)))) {
			regs->regs[0] = -EINTR;
			regs->pc = continue_addr;
		}

		handle_signal(&ksig, regs);
		return;
	}
}

4.3 get_signal

int get_signal(struct ksignal *ksig)
{
	struct sighand_struct *sighand = current->sighand;
	struct signal_struct *signal = current->signal;
	int signr;

	for (;;) {
		struct k_sigaction *ka;
		//获取到进程接收队列中的一个信号,如果没有信号,那么就跳出循环
		signr = dequeue_signal(current, &current->blocked, &ksig->info);
		ka = &sighand->action[signr-1];
		if (ka->sa.sa_handler == SIG_IGN) /* Do nothing.  */
			continue;
		if (ka->sa.sa_handler != SIG_DFL) {
			/* Run the handler.  */
			ksig->ka = *ka;
			break; /* will return non-zero "signr" value */
		}
		/* Now we are doing the default action for this signal.*/
		if (sig_kernel_ignore(signr)) /* Default is nothing. */
			continue;

		if (sig_kernel_stop(signr)) {
			continue;
		}

		/*Anything else is fatal, maybe with a core dump.*/
		current->flags |= PF_SIGNALED;
		if (sig_kernel_coredump(signr))
			do_coredump(&ksig->info);

		/* Death signals, no core dump.*/
		do_group_exit(ksig->info.si_signo);
		/* NOTREACHED */
	}
	ksig->sig = signr;
	return ksig->sig > 0;
}

4.4 handle_signal

/* OK, we're invoking a handler */
static void handle_signal(struct ksignal *ksig, struct pt_regs *regs)
{
	struct task_struct *tsk = current;
	sigset_t *oldset = sigmask_to_save();
	int usig = ksig->sig;
	int ret;

	/* Set up the stack frame */
	ret = setup_rt_frame(usig, ksig, oldset, regs);

	/* Check that the resulting registers are actually sane. */
	ret |= !valid_user_regs(&regs->user_regs, current);

	/* Fast forward the stepping logic so we step into the signal handler. */
	if (!ret)
		user_fastforward_single_step(tsk);

	signal_setup_done(ret, ksig, 0);
}

void signal_setup_done(int failed, struct ksignal *ksig, int stepping)
{
	if (failed)
		force_sigsegv(ksig->sig, current);
	else
		signal_delivered(ksig, stepping);
}

setup_frame() 函数来构建这个过程的运行环境(其实就是修改内核栈和用户栈相应的数据来完成)

static int setup_rt_frame(int usig, struct ksignal *ksig, sigset_t *set,
			  struct pt_regs *regs)
{
	struct rt_sigframe_user_layout user;
	struct rt_sigframe __user *frame;
	int err = 0;

	fpsimd_signal_preserve_current_state();

	frame = user.sigframe;

	__put_user_error(0, &frame->uc.uc_flags, err);
	__put_user_error(NULL, &frame->uc.uc_link, err);

	err |= __save_altstack(&frame->uc.uc_stack, regs->sp);
	err |= setup_sigframe(&user, regs, set);
	if (err == 0) {
		setup_return(regs, &ksig->ka, &user, usig);
		if (ksig->ka.sa.sa_flags & SA_SIGINFO) {
			err |= copy_siginfo_to_user(&frame->info, &ksig->info);
			regs->regs[1] = (unsigned long)&frame->info;
			regs->regs[2] = (unsigned long)&frame->uc;
		}
	}

	return err;
}

现在可以在内核态返回到用户态时自动执行信号处理程序了,但是当信号处理程序执行完怎么返回到内核态呢?Linux的做法就是在用户态栈空间构建一个 Frame(帧)(我也不知道为什么要这样叫),构建这个帧的目的就是为了执行完信号处理程序后返回到内核态,并恢复原来内核栈的内容。

返回到内核态的方式是调用一个名为 sigreturn() 系统调用,然后再 sigreturn() 中恢复原来内核栈的内容。

ref:

Tiffany的世界:【进程通信】信号处理机制一

一文完全看懂 | Linux 信号处理原理与实现