Linux进程间通讯(二)信号(下)

Linux进程间通讯

Linux进程间通讯(一)信号(上)

Linux进程间通讯(二)信号(下)

Linux进程间通讯(三)管道

Linux进程间通讯(四)共享内存

Linux进程间通讯(五)信号量

Linux进程间通讯(二)信号(下)


上一篇文章讲解了信号的注册,这篇文件讲解信号的发送和信号的处理

一、信号的发送

我们可以直接通过 kill 或者 sigqueu 系统调用,给某个进程发送信号,也可以通过 tkill 或者 tgkill 给某个线程发送信号。虽然方法很多,但是最终调用的都是 do_send_sig_info 函数

do_send_sig_info 函数会调用 send_signal,进而调用 __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;
......
	pending = group ? &t->signal->shared_pending : &t->pending;
......
	if (legacy_queue(pending, sig))
		goto ret;

	if (sig < SIGRTMIN)
		override_rlimit = (is_si_special(info) || info->si_code >= 0);
	else
		override_rlimit = 0;

	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;
		}

		userns_fixup_signal_uid(&q->info, t);

	} 
......
out_set:
	signalfd_notify(t, sig);
	sigaddset(&pending->signal, sig);
	complete_signal(sig, t, group);
ret:
	return ret;
}

进程的 task_struct 中,有两个 sigpending,表示进程收到的信号。一个表示整个进程收到的信号,所有线程共享,叫做 shared_pending;一个表示线程收到的信号,叫做 pending

__send_signal 首先决定使用哪个 sigpending,决定是发送给整个进程的,还是发送给某个线程的

struct sigpending 里面有两个成员,其定义如下

struct sigpending {
	struct list_head list;
	sigset_t signal;
};

  • sigset_t:是一个bitmap,它表示接收的哪些信号
  • list_head :是一个链表,它也表示接收的哪些信号

这两个有什么区别呢?我们接着往下看 __send_signal 的代码来寻找答案

接下来调用的是 legacy_queue,如果满足条件的话就退出,满足什么条件呢?看一下 legacy_queue 的定义

static inline int legacy_queue(struct sigpending *signals, int sig)
{
	return (sig < SIGRTMIN) && sigismember(&signals->signal, sig);
}


#define SIGRTMIN	32
#define SIGRTMAX	_NSIG
#define _NSIG		64

当信号小于 SIGRTMIN(即32)的时,并且信号当前在集合中,那么就退出

这样会导致什么现象呢?会导致信号的丢失,如果我们频繁地发送小于32的信号给同一个进程,可能在某一次发送的过程中,由于上一次的发送,信号还存在与 signal 中,此时该信号就会被丢失,因此我们将小于32的信号称为不可靠信号

如果信号大于32会怎么样呢?

我们继续看 __send_signal,接下来会分配一个 struct sigqueue 对象,然后将其加到 struct sigpending 的链表中,由于使用链表来保存发送给进程的信号,所以一般情况下,信号不会丢失,我们称大于 32 的信号为可靠信号

最后调用 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.
	 *
	 * If the main thread wants the signal, it gets first crack.
	 * Probably the least surprising to the average bear.
	 */
	if (wants_signal(sig, p))
		t = p;
	else if (!group || thread_group_empty(p))
		/*
		 * There is just one thread and it does not need to be woken.
		 * It will dequeue unblocked signals before it runs again.
		 */
		return;
	else {
		/*
		 * Otherwise try to find a suitable thread.
		 */
		t = signal->curr_target;
		while (!wants_signal(sig, t)) {
			t = next_thread(t);
			if (t == signal->curr_target)
				return;
		}
		signal->curr_target = t;
	}
......
	/*
	 * 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;
}

这里面的逻辑是,赶紧找一个线程来处理信号

首先找到一个进程或线程的 task_struct,然后调用 signal_wake_up 来试图唤醒它

signal_wake_up 会调用 signal_wake_up_state,其定义如下

void signal_wake_up_state(struct task_struct *t, unsigned int state)
{
	set_tsk_thread_flag(t, TIF_SIGPENDING);


	if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
		kick_process(t);
}

signal_wake_up_state 做了两件事

  • 第一件事是给这个 task_struct 设置 TIF_SIGPENDING。这一点向进程调度相似,当要发生系统调度的时候,并不是直接将现在运行的进程换下来,而是设置 TIF_SIGPENDING 标志,然后在系统调用结束或中断返回的时候,再调用 schedule 来调度进程。信号也是类似的,先设置一个 TIF_SIGPENDING 标志,表示这个进程有信号需要处理,然后再系统调用结束或者中断处理结束,从内核态返回用户态的时候,再来处理信号
  • 第二件事是试图唤醒这个进程。wake_up_state 会设置该进程的状态为 TASK_RUNNING,然后放到运行队列中,随着时钟不断地滴答,迟早会被调用

至此信号的发送过程已经结束了,接下来看如何处理它

二、信号的处理

从系统调用或者中断返回的时候,最终都会调用 exit_to_usermode_loop,其定义如下

static void exit_to_usermode_loop(struct pt_regs *regs, u32 cached_flags)
{
	while (true) {
......
		if (cached_flags & _TIF_NEED_RESCHED)
			schedule();
......
		/* deal with pending signal delivery */
		if (cached_flags & _TIF_SIGPENDING)
			do_signal(regs);
......
		if (!(cached_flags & EXIT_TO_USERMODE_LOOP_FLAGS))
			break;
	}
}

首先,会判断 _TIF_NEED_RESCHED 是否需要调度进程,如果需要就调度进程,然后会从 schedule 函数中返回

然后再通过 _TIF_SIGPENDING 判断是否需要处理信号,如果需要处理信号,那么就调用 do_signal 处理,下面看看它的定义

void do_signal(struct pt_regs *regs)
{
	struct ksignal ksig;

	if (get_signal(&ksig)) {
		/* Whee! Actually deliver the signal.  */
		handle_signal(&ksig, regs);
		return;
	}

	/* Did we come from a system call? */
	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 = get_nr_restart_syscall(regs);
			regs->ip -= 2;
			break;
		}
	}
	restore_saved_sigmask();
}

do_signal 会调用 handle_signal函数,按说,信号的处理就是调用信号的处理函数,但是这事并没有那么简单,因为信号的处理函数是在用户空间的,内核态下面是不能直接调用的,那可怎么办呢?

我们先回忆系统调用的过程。这个进程当时进行系统调用,在进入内核态的时候,会将该进程在用户态下面运行到某一行代码 Line A 保存进程在内核的 pt_regs 里面。在系统调用放回的时候,本应该从 pt_regs 中拿出 Line A,接着继续执行下去。但是现在要处理信号,我们不能直接返回 Line A,而应该返回信号处理函数的起始地址

handle_signal 的定义如下

static void
handle_signal(struct ksignal *ksig, struct pt_regs *regs)
{
	bool stepping, failed;
......
	/* 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 (!(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);
......
	signal_setup_done(failed, ksig, stepping);
}

所以这个时候,我们就需要来修改 pt_regs 了。这个时候,我们要看是否从系统调用中返回。如果是从系统调用中返回,还需要区分我们从系统调用中正常返回,还是在一个非运行状态的系统调用中,由于信号的唤醒而从系统调用中返回

下面我们来分析一个场景,假设从网卡读取数据,会发生系统调用,然后最后会调用到下面这个函数

static ssize_t tap_do_read(struct tap_queue *q,
			   struct iov_iter *to,
			   int noblock, struct sk_buff *skb)
{
......
	while (1) {
		if (!noblock)
			prepare_to_wait(sk_sleep(&q->sk), &wait,
					TASK_INTERRUPTIBLE);

		/* Read frames from the queue */
		skb = skb_array_consume(&q->skb_array);
		if (skb)
			break;
		if (noblock) {
			ret = -EAGAIN;
			break;
		}
		if (signal_pending(current)) {
			ret = -ERESTARTSYS;
			break;
		}
		/* Nothing to read, let's sleep */
		schedule();
	}
......
}

如果网卡没有数据,那么就调用 schedule,进入睡眠状态

当该进程被唤醒的时候,会从 schedule 中返回,然后重新执行循环,这个时候因为还没有数据,所以接着往下运行。当遇到 signal_pending 的时候,检测到 _TIF_SIGPENDING 被设置,这说明,这个系统调用调用在没有完成的情况下,被信号中断了,此时直接返回 -ERESTARTSYS

然后准备返回到用户空间,会调用到 exit_to_usermode_loop,最终又调用到了 handle_signal

然后在 handle_signal 中发现错误 -ERESTARTSYS 的时候,知道这是一个未完成的系统调用,设置错误码 -EINTR

接下来就开始折腾 pt_regs 了,主要通过调用 setup_rt_frame -> __set_rt_frame,其定义如下

static int __setup_rt_frame(int sig, struct ksignal *ksig,
			    sigset_t *set, struct pt_regs *regs)
{
	struct rt_sigframe __user *frame;
	void __user *fp = NULL;
	int err = 0;

	frame = get_sigframe(&ksig->ka, regs, sizeof(struct rt_sigframe), &fp);
......
	put_user_try {
......
		/* Set up to return from userspace.  If provided, use a stub
		   already in userspace.  */
		/* x86-64 should always use SA_RESTORER. */
		if (ksig->ka.sa.sa_flags & SA_RESTORER) {
			put_user_ex(ksig->ka.sa.sa_restorer, &frame->pretcode);
		} 
	} put_user_catch(err);

	err |= setup_sigcontext(&frame->uc.uc_mcontext, fp, regs, set->sig[0]);
	err |= __copy_to_user(&frame->uc.uc_sigmask, set, sizeof(*set));

	/* Set up registers for signal handler */
	regs->di = sig;
	/* In case the signal handler was declared without prototypes */
	regs->ax = 0;

	regs->si = (unsigned long)&frame->info;
	regs->dx = (unsigned long)&frame->uc;
	regs->ip = (unsigned long) ksig->ka.sa.sa_handler;

	regs->sp = (unsigned long)frame;
	regs->cs = __USER_CS;
......
	return 0;
}

frame 的类型是 rt_sigframe,是栈帧的意思

get_sigframe 会通过 pt_regs 的 sp 变量,获取用户栈,然后在用户栈中塞入一个栈帧,将 pt_regs中的值保存在这个结构中,然后又返回栈顶位置

然后又重新设置了 pt_regs,将 pt_regs 的 sp 指针设置为 frame,ip 设置为信号处理函数的地址,再修改 pt_regs 中其它的值。通过这个操作,就是将原来的 pt_regs 塞入用户栈中,然后栈指针往下生长,指令指针指向信号处理函数的地址

这样子,当系统调用返回的时候,执行的就是信号处理函数了

那当信号处理函数运行往后,又怎么回到进程原来的运行代码处呢?

其实栈中存放的不仅仅只有原先 pt_regs 的内容,还有 sa_restorer 函数的地址,当信号处理函数返回的时候,就会从栈中弹出这个函数地址,然后跳转到这个函数运行,sa_restorer 函数具体是什么呢?

它其实是一个系统调用,对应的定义如下

asmlinkage long sys_rt_sigreturn(void)
{
	struct pt_regs *regs = current_pt_regs();
	struct rt_sigframe __user *frame;
	sigset_t set;
	unsigned long uc_flags;

	frame = (struct rt_sigframe __user *)(regs->sp - sizeof(long));
	if (__copy_from_user(&set, &frame->uc.uc_sigmask, sizeof(set)))
		goto badframe;
	if (__get_user(uc_flags, &frame->uc.uc_flags))
		goto badframe;

	set_current_blocked(&set);

	if (restore_sigcontext(regs, &frame->uc.uc_mcontext, uc_flags))
		goto badframe;
......
	return regs->ax;
......
}

这里,会从用户进程的栈中,把 pt_regs 的信息恢复,之后系统调用返回就会返回到进程原理来运行地方了

对于进程来说,还误以为从上一次系统调用中返回,并不知道这其中的一系列操作

三、总结

信号的发送和处理是一个非常复杂的过程,下面来总结一下

  • 1.假设进程A发生系统调用进入内核
  • 2.按照系统调用的处理,会将用户栈的信息保存到 pt_regs 中,也即记住了原来用户态运行到了 Line A 的位置
  • 3.当在内核中读取数据的时候,发现没有数据读,此时会调用 schedule 发生调度
  • 4.当发送信号给进程A,会找到进程A的 task_struct ,将信号加入信号集合或者信号链表中,然后唤醒进程A
  • 5.当系统调用返回或者中断返回的时候,会切换到进程A,进程A被唤醒,然后判断是否又数据可读,发现没有,然后发现是被信号唤醒,于是返回一个错误
  • 6.在系统调用返回时,会进程信号的处理
  • 7.会将 pt_regs 还有 sa_restorer 的函数地址保存到用户栈中
  • 8.修改 pt_regs,栈指针向下生长,设置指令指针为信号处理函数地址,然后系统调用返回,返回后会执行信号处理函数
  • 9.当信号处理函数执行完之后,会从栈中弹处 sa_restorer 的函数地址,跳转到 sa_restorer 运行
  • 10.sa_restorer 是一个系统调用,它会从栈中获取进程原来 pt_regs 的值,然后恢复 pt_regs,系统调用返回后,就回到了进程原先运行的地方
发布了107 篇原创文章 · 获赞 197 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/weixin_42462202/article/details/102632425
今日推荐