学习方法论
写作原则

标题括号中的数字代表完成度与完善度
0.0-1.0 代表完成度,1.1-1.5 代表完善度
0.0 :还没开始写
0.1 :写了一个简介
0.3 :写了一小部分内容
0.5 :写了一半内容
0.9 :还有个别内容没写
1.0 :内容都写完了,但是不一定完善
1.1 :内容比较完善
1.3 :内容很完善
1.5 :内容非常完善,接近完美



推荐阅读: 操作系统导论
推荐阅读: 深入理解Linux进程间通信

一、信号机制概览

相信大家对信号并不陌生,很多人都用过kill命令或者Ctrl+C组合键杀死过进程,或者遇到过程序因为收到SIGSEGV信号而崩溃的。而对信号的基本原理,估计很多人都不太了解,今天我们就来详细讲解一下。

1.1 信号基本原理

信号机制是UNIX系统最古老的机制之一,它不仅是内核处理程序在运行时发生错误的方式,还是终端管理进程的方式,并且还是一种进程间通信机制。信号机制由三部分构成,首先是信号是怎么产生的,或者说是谁发送的,然后是信号是怎么投递到进程或者线程的,最后是信号是怎么处理的。下面我们先看一张图:

信号机制框架
从图中我们可以看到信号的产生方式也就是发送方有三种。首先是终端发送,比如我们在终端里输入Ctrl+C快捷键时,终端会给当前进程发送SIGINT信号。其次是内核发送,这里的内核发送是指内核里的异常处理的信号发送,比如进程非法访问内存,在异常处理中就会给当前线程发送SIGSEGV信号。最后是进程发送,也就是一个进程给另一个进程发送或者是进程自己给自己发送。这里有很多接口函数可以选择,有的可以发给线程,有的可以发给进程,有的可以发给进程组甚至会话组。

下一个过程就是信号是如何从发送方发送到目标进程或者线程的信号队列里的,这个过程叫做投递。不同的发送方,其发送方式和投递过程是不同的,这个后面会展开讲。

最后是信号的处理过程,这个最复杂牵涉问题最多。信号发送可以发送给进程或者线程,但是信号的处理是在线程中进行的,因为线程是代码执行的单元。线程首先处理自己队列里的信号,自己的处理完了再去处理进程队列里的信号。处理的时候要考虑信号掩码(mask),被掩码阻塞的信号暂时不处理,还放回原队列中去。信号处理方式有三种,如果程序什么也没设置的话,走默认处理(default)方式。默认处理有五种情况,不同的信号,其默认处理方式不同。这五种情况分别是ignore(忽略)、term(终结进程也就是杀死进程)、core(coredump内存转储并杀死进程)、stop(暂停进程)、cont(continue恢复执行进程)。还有两种方式是进程提前通过接口函数signal或者sigaction设置了处理方式,设置IGN来忽略信号,或者设置一个信号处理函数handler来处理信号。大家注意,默认处理中的忽略和进程主动设置的忽略,两者的逻辑是不同的,一个是默认处理是忽略,一个是进程主动要求要忽略。你想要忽略一个默认处理不是忽略的信号,就必须要主动设置忽略。

1.2 信号类型简介

我们明白了信号的基本原理之后,就要进一步追问,系统都有哪些信号呢,这些信号有什么不同呢?刚开始的时候,UNIX系统只有1-31总共31个信号,这些信号每个都有特殊的含义和特定的用法。这些信号的实现有一个特点,它们是用bit flag实现的。这就会导致当一个信号还在待决的时候,又来了一个同样的信号,再次设置bit位是没有意义的,所以就会丢失一次信号。为了解决这个问题,后来POSIX规定增加32-64这33个信号作为实时信号,并规定实时信号不能丢失,要用队列来实现。我们把之前的信号1-31叫做标准信号,由于标准信号会丢失,所以标准信号也叫做不可靠信号,由于标准信号是用bit flag实现的,所以标准信号也叫做标记信号(flag signal)。由于实时信号不会丢失,所以实时信号也叫作可靠信号,由于实时信号是用队列实现的,所以实时信号也叫做排队信号(queue signal)。我们平常遇到的SIGSEGV、SIGABRT等信都是标准信号。

1.3 同步信号与异步信号

其实信号本身没有同步异步之分,而信号的发送有同步异步之分。如果一个信号的发送和线程当前的执行有关,则这个信号是同步的。比如程序执行遇到了异常,内核给当前线程发送信号,这个就是同步发送;一个线程自己给自己发送信号也是同步发送;终端由于后台进程读写终端而给进程发送的SIGTTIN和SIGTTOU也是同步发送。所以当我们说同步信号的时候实际上指的是信号的发送是同步的。所有信号都有默认的使用和发送方式,所以我们把在默认情况下是同步发送的信号叫做同步信号。还有一个更狭义的同步信号的概念,内核把异常处理所发送的信号叫做同步信号。不过要注意的是,不是说同步信号只能同步发送,你可以用kill命令发送任意信号。但是用kill发送特定含义的信号和它的默认发送,在很多方面是不同的。我们后文提到同步信号异步信号的时候,都是指的默认情况。

1.4 信号的处理时机

信号是在什么时候被处理的呢?是在被投递的时候处理的吗,不是的。信号是在线程将要返回用户空间之前进行处理的。线程返回用户空间有两种情况,一是从系统调用返回,二是从中断返回。返回之前,线程会检查队列里有没有信号要处理,有的话就处理。

1.5 信号与多线程

信号是单线程时代的产物。在单线程时代,一个进程就只有一个线程(就是主线程),所以进程就是线程,线程就是进程。信号所有的属性既是进程全局的又是线程私有的,因为这两者没有区别。但是到了多线程时代,这两者就有区别了,进程是资源分配与管理的单元,线程是程序执行的单元。一个进程往往有多个线程,那么信号的这些属性究竟应该是进程全局的还是线程私有的呢?这还真不好处理的。经过一番慎重的分析与思考,UNIX系统做出了如下的决定。

信号的发送既可以发送给进程,也可以发送给线程,但是同步信号(也就是和当前线程执行相关而产生的信号)应当发送给当前线程。进程发送信号可以选择不同的接口函数,有的接口是发给进程的,有的接口是发给线程的。线程信号队列中的信号只能由线程自己处理,进程信号队列中的信号由进程中的线程处理,具体是由哪个线程处理是不确定的。

信号掩码(mask)的设置是线程私有的,每个线程都可以设置不同的信号掩码。

信号处理方式的设置是进程全局的,后面线程设置的方式会覆盖前面线程的设置。

信号处理的效果是进程全局的。

我们先说默认处理的几种情况:忽略一个信号是指整个进程忽略这个信号,而不是说某个线程忽略了其它线程还可以去处理。终结是终结的整个进程,而不只终结一个线程。内存转储是整个进程进行内存转储并终结整个进程。Stop是暂停整个进程而不是只暂停一个线程。Cont是恢复执行整个进程而不是只恢复执行一个线程。

非默认处理有两种情况:如果进程设置了忽略某个信号,则是整个进程都忽略这个信号,而不是某个线程忽略这个信号。如果进程设置了信号处理函数handler,则handler的执行效果是进程全局的。这点怎么理解呢?可以从两方面来理解,一是如果信号是发送给进程的,则每个线程都有可能来执行这个handler;二是handler虽然是在某个线程中执行的,但是对于线程来说,只有线程栈是线程私有的,其它内存是整个进程共享的,handler对线程栈的影响是线程私有的,handler返回之后它的栈帧就销毁了,handler只有对全局内存的影响才会留下来,所以它的影响是进程全局的。

我们再来总结一下:信号可以发送给进程也可以发送给线程。发送给线程的信号只能由线程处理,如果线程阻塞了信号则信号会一直pending,直到线程解除阻塞然后就会去处理该信号。发送给进程的信号可以由该进程中的任意一个未阻塞该信号的线程来处理,具体哪个线程是不确定的,如果所有线程都阻塞该信号,则该信号一直pending,直到任一线程解除阻塞。信号无论是怎么发送和处理的,信号的处理效果都是进程全局的。

二、信号类型详解

前面我们已经对信号机制有了全面的了解,这一章我们就来详细说说所有的信号类型。

2.1 标准信号与实时信号的区别

我们知道信号分为标准信号和实时信号,它们之间最大的区别就是在信号处于待决的状态下又来了同样的信号会怎么处理。除此之外,它们还有以下三点不同。

1.实时信号如果使用接口sigqueue发送的话,可以携带一个额外的整数信息或者指针信息。

2.实时信号有优先级,数值越小优先级越高,优先级高的优先处理,同等优先级的按照先来后到的顺序处理。

3.标准信号都是预定义信号,每个信号都有特定的含义,而实时信号则没有预定义的含义。

根据特点3,两个进程可以使用实时信号来达到进程间通信的目的。因为实时信号没有特定的含义,所以系统不会使用实时信号,进程之间可以自行约定某个信号的含义。而且不同的进程之间可以约定不同的含义而不会相互影响。不过glibc的pthread实现使用了32、33这两个实时信号,所以大家不要用这两个实时信号。

2.2 信号的属性特征

在进入下一节之前我们先来解释一下信号属性的含义。

可阻塞:
我们可以通过某些接口来阻塞(暂时屏蔽)一个信号。但是有的信号可以阻塞,有的信号无法阻塞。有的信号虽然可以成功设置阻塞,但是其信号会被强制发送,所以最终还是阻塞不了。比如内核在异常处理时会强制发送信号,所以是阻塞不了的。但是同样的信号你用kill来发,阻塞还是生效的,因为kill不是强制发送。信号阻塞,有很多地方会叫做信号屏蔽,两者都是一样的。但是屏蔽容易被人和忽略理解混了,所以本文里用阻塞。阻塞,含义明确,就是阻塞住了,后面不阻塞了信号还是会到来的。

可忽略:
有些信号默认处理就是忽略的,但是有些信号默认处理不是忽略。如果我们想忽略这些信号的话,可以通过一些接口设置来忽略它。有些信号是可以设置忽略的,但是有些接口无法设置忽略。有的信号虽然可以设置忽略成功,但是内核在异常处理时会强制发送信号,这时忽略是无效的。不过同样的信号用kill来发,忽略就是有效的,因为kill不是强制发送。大家注意忽略和阻塞不同,阻塞是暂时不处理,而忽略其实也是一种处理,相当于是空处理。

可捕获:
我们可以通过一些接口来设置信号处理函数handler来处理信号,这个行为叫做捕获。有些信号是能捕获的,有些信号是不能捕获的。与可阻塞和可忽略不同的是,强制发送的信号也是可捕获的。但是可捕获存在一个特殊情况,有些时候是不能二次捕获的。有两个信号SIGSEGV、SIGABRT是不能二次捕获的,后面会进行讲解。

默认处理:
默认处理是当我们没有设置忽略和捕获函数时,内核对信号的默认处理方式。前面已经介绍过有五种处理方式,这里就不再赘述了。由于大部分的信号处理是terminate或者coredump,都是会导致进程死亡的,所以信号发送命令叫做kill。其实kill并不会杀死进程,它只是给进程送了个信号而已。

发送者:
这里指的是信号在一般情况是从哪里发送的,表明了信号使用的场景。

发给:
这里是指信号一般情况下是发给进程还是线程,表明了信号是和整个进程相关还是和某个线程相关。一般由某个线程自己触发的信号会发送给这个线程自己,让它自己来处理,但是这个信号的含义如果是进程全局的就会发送给进程来处理,进程里的任何一个线程都有可能会被选择来处理。无论是发送给进程还是线程,信号的处理效果都是进程全局的。

含义:
这个信号的含义,代表什么时候该使用它,如果收到了它就意味着遇到了什么情况。

2.3 标准信号详解

下面让我们通过一张图来看看所有信号的相关信息:
信号类型概览
我们先来解释一下信号0,其实0不算是一个信号,但是也可以算作是半个信号。因为发送信号0给一个进程或者线程,它会走发送检测过程,但是并不会真的投递给进程或者线程。检测流程会检测发送者是否有权限发送、进程是否存在,如果遇到问题就返回错误值。所以发送信号0可以用作检测进程是否存在的方法。

我们再来看一下实时信号,因为实时信号没有特定的含义,所以比较简单。实时信号的默认处理是终结进程,相关属性是可阻塞,可忽略,可捕获。它的一般使用方法都是进程发给其它进程或者线程来作为进程间通信的方法。其中32-33被glibc的pthread使用了。

标准信号一共有1-31共31个,我们按照它们的特点不同分类进行讲解:

首先说一下SIGKILL和一些暂停、继续相关的信号。其中SIGKILL和SIGSTOP是POSIX标准规定的不可阻塞、不可忽略、不可捕获的信号,它们的语义一定会得到执行。SIGCONT信号官方没有特别规定,它的实现上是不可阻塞、不可忽略的,虽然能捕获,但是相当于没捕获。因为捕获的意思是执行其信号处理函数就不再执行其默认处理了,但是SIGCONT的默认语义一定会得到执行。其它三个暂停信号SIGTSTP、SIGTTIN、SIGTTOU是不能阻塞的,但是可以忽略可以捕获,忽略或者捕获之后,它们的默认语义暂停程序就不会得到执行。SIGSTOP、SIGCONT,进程在想要暂停、恢复执行其它进程的时候可以发送这两个信号,内核里面再需要暂停、恢复执行进程的时候也会发送这两个信号。SIGTSTP是当在终端输入Ctrl+Z快捷键时,终端驱动会给当前进程发送这个信号。SIGTTIN是当后台进程读取终端的时候,终端会向进程发送的。SIGTTOU是在后台进程想要向终端输出的时候,终端会向进程发送的。这几个信号都是直接发送给进程的,因为它们的语义就是要操作整个进程。

下面我们再来看6个标记紫色的信号,这几个信号都是和当前线程正在执行时发生异常有关。内核里单独把这6个信号放在一起成为同步信号。因为它们都是强制发送的,会忽略阻塞和忽略设置,所以图中把它们都看做是不可忽略不可阻塞的。但是它们是可以捕获的,让它们可以捕获的原因是因为这样可以让进程知道自己出错的原因,让进程可以在临死之前可以做一些记录工作,为程序员解BUG多提供一些信息。捕获了之后,原先默认的语义就不会执行,所以信号函数执行完之后它们还会继续执行。但是一般情况下这么做是没有意义的,所以一般都会在信号函数里退出进程。SIGSEGV的可捕获前面加了个[不],代表的是不能二次捕获,也就是说如果在信号处理函数里面又发生了SIGSEGV,则这个SIGSEGV就不可捕获了,会走默认语义发生coredump并杀死进程。这些信号的发送方都是内核里异常处理相关的代码,信号都会发送给线程,因为是这些线程引起的这些问题,放到原线程里去处理比较好。

我们再接着看SIGABRT信号,这个信号比较特殊。它的目的是给库程序来用的。当库程序发现程序出现了不可挽回的错误,就会调用函数abort,这个函数会给当前线程发送信号SIGABRT。SIGABRT信号本身没什么特殊的,但是abort函数比较特殊。POSIX规范要求abort函数执行完成之后,进程一定要被杀死。于是abort函数的实现就是这样的,先取消阻塞SIGABRT信号,然后给当前线程发信号SIGABRT。无论SIGABRT信号是被忽略还是被捕获了,最后还是要返回到abort函数里面,然后abort函数就把SIGABRT信号的处理方式设置为默认,然后再发一个SIGABRT,这下进程就一定会死了。也就是说你可以捕获SIGABRT信号,但是进程最后还是一定会死。所以上图里说SIGABRT是不可阻塞、不可忽略、不可二次捕获的([不]可捕获代表的是不可二次捕获)。SIGABRT的不可二次捕获和SIGSEGV的不可二次捕获情形不太一样。如果是手工发送的SIGABRT信号,它就是一个普通的信号,没有前面说的逻辑。不过手工发送SIGABRT信号没有意义,一般都是使用abort函数来发送。其实遇到abort函数的SIGABRT信号也不是必死,有一种不规范的做法可以避免一死,那就是在信号处理函数中使用longjmp。但是这种做法没有意义,因为程序现在已经处于不一致状态了,coredump之后结束进程,然后好好地解bug才是最好的选择。

下面我们再看一下与终端相关的4个信号,SIGINT、SIGHUP、SIGQUIT、SIGTERM。你在终端上输入Ctrl+C,终端驱动就会给当前进程发送SIGINT,默认处理是杀死进程。你用kill命令给一个进程发信号,默认发的就是SIGTERM信号,默认处理也是杀死进程。当终端脱离进程的时候会给进程发SIGHUP,默认处理也是杀死进程。脱离终端有三种情况:一是物理终端与大型机断开了连接,现在已经没有物理终端了,所以这种情况不会有了;二是终端模拟器(也就是命令行窗口)被关闭了;三是我们通过ssh等工具连接到了网络终端,如果此时网络断了或者客户端程序死了。这三种情况终端驱动都会给关联的进程发送SIGHUP信号。最后一个信号是SIGTERM,当你在终端输入Ctrl+\的时候,终端驱动就会给当前进程发送SIGTERM信号,默认处理是coredump并杀死进程。

与定时器相关的几个信号(SIGALRM、SIGVTALRM、SIGPROF)是什么情况下发送的,请参看《深入理解Linux时间子系统》3.2节。

还有一些信号这里就不再过多介绍了。

三、信号的发送

现在我们来看一下信号发送,主要是看发送场景。具体的发送过程在下一章信号的投递里面讲解。信号发送场景比较典型的有三种,一是终端发送,也就是我们在命令行运行程序时会遇到的情况;二是内核发送,内核也很庞大,里面的情况也很多,我们这里主要讲的是异常处理发送信号;三是进程发送,就是一个进程给另一个进程发。

3.1 终端发送

我们看一下伪终端是如何发送信号的

linux-src/drivers/tty/pty.c

/* Send a signal to the slave */
static int pty_signal(struct tty_struct *tty, int sig)
{
	struct pid *pgrp;

	if (sig != SIGINT && sig != SIGQUIT && sig != SIGTSTP)
		return -EINVAL;

	if (tty->link) {
		pgrp = tty_get_pgrp(tty->link);
		if (pgrp)
			kill_pgrp(pgrp, sig, 1);
		put_pid(pgrp);
	}
	return 0;
}

linux-src/drivers/tty/sysrq.c

static void sysrq_handle_term(int key)
{
	send_sig_all(SIGTERM);
	console_loglevel = CONSOLE_LOGLEVEL_DEBUG;
}

/*
 * Signal sysrq helper function.  Sends a signal to all user processes.
 */
static void send_sig_all(int sig)
{
	struct task_struct *p;

	read_lock(&tasklist_lock);
	for_each_process(p) {
		if (p->flags & PF_KTHREAD)
			continue;
		if (is_global_init(p))
			continue;

		do_send_sig_info(sig, SEND_SIG_PRIV, p, PIDTYPE_MAX);
	}
	read_unlock(&tasklist_lock);
}

linux-src/drivers/tty/tty_io.c

static void __tty_hangup(struct tty_struct *tty, int exit_session)
{
	refs = tty_signal_session_leader(tty, exit_session);
}

linux-src/drivers/tty/tty_jobctrl.c

int tty_signal_session_leader(struct tty_struct *tty, int exit_session)
{
	struct task_struct *p;
	int refs = 0;
	struct pid *tty_pgrp = NULL;

	read_lock(&tasklist_lock);
	if (tty->ctrl.session) {
		do_each_pid_task(tty->ctrl.session, PIDTYPE_SID, p) {
			spin_lock_irq(&p->sighand->siglock);
			if (p->signal->tty == tty) {
				p->signal->tty = NULL;
				/*
				 * We defer the dereferences outside of
				 * the tasklist lock.
				 */
				refs++;
			}
			if (!p->signal->leader) {
				spin_unlock_irq(&p->sighand->siglock);
				continue;
			}
			__group_send_sig_info(SIGHUP, SEND_SIG_PRIV, p);
			__group_send_sig_info(SIGCONT, SEND_SIG_PRIV, p);
			put_pid(p->signal->tty_old_pgrp);  /* A noop */
			spin_lock(&tty->ctrl.lock);
			tty_pgrp = get_pid(tty->ctrl.pgrp);
			if (tty->ctrl.pgrp)
				p->signal->tty_old_pgrp =
					get_pid(tty->ctrl.pgrp);
			spin_unlock(&tty->ctrl.lock);
			spin_unlock_irq(&p->sighand->siglock);
		} while_each_pid_task(tty->ctrl.session, PIDTYPE_SID, p);
	}
	read_unlock(&tasklist_lock);

	if (tty_pgrp) {
		if (exit_session)
			kill_pgrp(tty_pgrp, SIGHUP, exit_session);
		put_pid(tty_pgrp);
	}

	return refs;
}

这是终端驱动发送信号的几个场景,代码就不具体分析了。

3.2 内核发送

我们最常遇到的信号SIGSEGV,一般都是在缺页异常里,如果我们访问的虚拟内存是未分配的虚拟内存,则会发生SIGSEGV。下面我们看一下代码。

X86的缺页异常的代码如下:
linux-src/arch/x86/mm/fault.c

DEFINE_IDTENTRY_RAW_ERRORCODE(exc_page_fault)
{
	unsigned long address = read_cr2();
	irqentry_state_t state;

	prefetchw(&current->mm->mmap_lock);

	if (kvm_handle_async_pf(regs, (u32)address))
		return;

	state = irqentry_enter(regs);

	instrumentation_begin();
	handle_page_fault(regs, error_code, address);
	instrumentation_end();

	irqentry_exit(regs, state);
}

static __always_inline void
handle_page_fault(struct pt_regs *regs, unsigned long error_code,
			      unsigned long address)
{
	trace_page_fault_entries(regs, error_code, address);

	if (unlikely(kmmio_fault(regs, address)))
		return;

	if (unlikely(fault_in_kernel_space(address))) {
		do_kern_addr_fault(regs, error_code, address);
	} else {
		do_user_addr_fault(regs, error_code, address);
		local_irq_disable();
	}
}
static inline
void do_user_addr_fault(struct pt_regs *regs,
			unsigned long error_code,
			unsigned long address)
{
	struct vm_area_struct *vma;
	struct task_struct *tsk;
	struct mm_struct *mm;
	vm_fault_t fault;
	unsigned int flags = FAULT_FLAG_DEFAULT;

	tsk = current;
	mm = tsk->mm;

	if (unlikely((error_code & (X86_PF_USER | X86_PF_INSTR)) == X86_PF_INSTR)) {
		/*
		 * Whoops, this is kernel mode code trying to execute from
		 * user memory.  Unless this is AMD erratum #93, which
		 * corrupts RIP such that it looks like a user address,
		 * this is unrecoverable.  Don't even try to look up the
		 * VMA or look for extable entries.
		 */
		if (is_errata93(regs, address))
			return;

		page_fault_oops(regs, error_code, address);
		return;
	}

	/* kprobes don't want to hook the spurious faults: */
	if (WARN_ON_ONCE(kprobe_page_fault(regs, X86_TRAP_PF)))
		return;

	/*
	 * Reserved bits are never expected to be set on
	 * entries in the user portion of the page tables.
	 */
	if (unlikely(error_code & X86_PF_RSVD))
		pgtable_bad(regs, error_code, address);

	/*
	 * If SMAP is on, check for invalid kernel (supervisor) access to user
	 * pages in the user address space.  The odd case here is WRUSS,
	 * which, according to the preliminary documentation, does not respect
	 * SMAP and will have the USER bit set so, in all cases, SMAP
	 * enforcement appears to be consistent with the USER bit.
	 */
	if (unlikely(cpu_feature_enabled(X86_FEATURE_SMAP) &&
		     !(error_code & X86_PF_USER) &&
		     !(regs->flags & X86_EFLAGS_AC))) {
		/*
		 * No extable entry here.  This was a kernel access to an
		 * invalid pointer.  get_kernel_nofault() will not get here.
		 */
		page_fault_oops(regs, error_code, address);
		return;
	}

	/*
	 * If we're in an interrupt, have no user context or are running
	 * in a region with pagefaults disabled then we must not take the fault
	 */
	if (unlikely(faulthandler_disabled() || !mm)) {
		bad_area_nosemaphore(regs, error_code, address);
		return;
	}

	/*
	 * It's safe to allow irq's after cr2 has been saved and the
	 * vmalloc fault has been handled.
	 *
	 * User-mode registers count as a user access even for any
	 * potential system fault or CPU buglet:
	 */
	if (user_mode(regs)) {
		local_irq_enable();
		flags |= FAULT_FLAG_USER;
	} else {
		if (regs->flags & X86_EFLAGS_IF)
			local_irq_enable();
	}

	perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, address);

	if (error_code & X86_PF_WRITE)
		flags |= FAULT_FLAG_WRITE;
	if (error_code & X86_PF_INSTR)
		flags |= FAULT_FLAG_INSTRUCTION;

#ifdef CONFIG_X86_64
	/*
	 * Faults in the vsyscall page might need emulation.  The
	 * vsyscall page is at a high address (>PAGE_OFFSET), but is
	 * considered to be part of the user address space.
	 *
	 * The vsyscall page does not have a "real" VMA, so do this
	 * emulation before we go searching for VMAs.
	 *
	 * PKRU never rejects instruction fetches, so we don't need
	 * to consider the PF_PK bit.
	 */
	if (is_vsyscall_vaddr(address)) {
		if (emulate_vsyscall(error_code, regs, address))
			return;
	}
#endif

	/*
	 * Kernel-mode access to the user address space should only occur
	 * on well-defined single instructions listed in the exception
	 * tables.  But, an erroneous kernel fault occurring outside one of
	 * those areas which also holds mmap_lock might deadlock attempting
	 * to validate the fault against the address space.
	 *
	 * Only do the expensive exception table search when we might be at
	 * risk of a deadlock.  This happens if we
	 * 1. Failed to acquire mmap_lock, and
	 * 2. The access did not originate in userspace.
	 */
	if (unlikely(!mmap_read_trylock(mm))) {
		if (!user_mode(regs) && !search_exception_tables(regs->ip)) {
			/*
			 * Fault from code in kernel from
			 * which we do not expect faults.
			 */
			bad_area_nosemaphore(regs, error_code, address);
			return;
		}
retry:
		mmap_read_lock(mm);
	} else {
		/*
		 * The above down_read_trylock() might have succeeded in
		 * which case we'll have missed the might_sleep() from
		 * down_read():
		 */
		might_sleep();
	}

	vma = find_vma(mm, address);
	if (unlikely(!vma)) {
		bad_area(regs, error_code, address);
		return;
	}
	if (likely(vma->vm_start <= address))
		goto good_area;
	if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
		bad_area(regs, error_code, address);
		return;
	}
	if (unlikely(expand_stack(vma, address))) {
		bad_area(regs, error_code, address);
		return;
	}

	/*
	 * Ok, we have a good vm_area for this memory access, so
	 * we can handle it..
	 */
good_area:
	if (unlikely(access_error(error_code, vma))) {
		bad_area_access_error(regs, error_code, address, vma);
		return;
	}

	/*
	 * If for any reason at all we couldn't handle the fault,
	 * make sure we exit gracefully rather than endlessly redo
	 * the fault.  Since we never set FAULT_FLAG_RETRY_NOWAIT, if
	 * we get VM_FAULT_RETRY back, the mmap_lock has been unlocked.
	 *
	 * Note that handle_userfault() may also release and reacquire mmap_lock
	 * (and not return with VM_FAULT_RETRY), when returning to userland to
	 * repeat the page fault later with a VM_FAULT_NOPAGE retval
	 * (potentially after handling any pending signal during the return to
	 * userland). The return to userland is identified whenever
	 * FAULT_FLAG_USER|FAULT_FLAG_KILLABLE are both set in flags.
	 */
	fault = handle_mm_fault(vma, address, flags, regs);

	if (fault_signal_pending(fault, regs)) {
		/*
		 * Quick path to respond to signals.  The core mm code
		 * has unlocked the mm for us if we get here.
		 */
		if (!user_mode(regs))
			kernelmode_fixup_or_oops(regs, error_code, address,
						 SIGBUS, BUS_ADRERR,
						 ARCH_DEFAULT_PKEY);
		return;
	}

	/*
	 * If we need to retry the mmap_lock has already been released,
	 * and if there is a fatal signal pending there is no guarantee
	 * that we made any progress. Handle this case first.
	 */
	if (unlikely((fault & VM_FAULT_RETRY) &&
		     (flags & FAULT_FLAG_ALLOW_RETRY))) {
		flags |= FAULT_FLAG_TRIED;
		goto retry;
	}

	mmap_read_unlock(mm);
	if (likely(!(fault & VM_FAULT_ERROR)))
		return;

	if (fatal_signal_pending(current) && !user_mode(regs)) {
		kernelmode_fixup_or_oops(regs, error_code, address,
					 0, 0, ARCH_DEFAULT_PKEY);
		return;
	}

	if (fault & VM_FAULT_OOM) {
		/* Kernel mode? Handle exceptions or die: */
		if (!user_mode(regs)) {
			kernelmode_fixup_or_oops(regs, error_code, address,
						 SIGSEGV, SEGV_MAPERR,
						 ARCH_DEFAULT_PKEY);
			return;
		}

		/*
		 * We ran out of memory, call the OOM killer, and return the
		 * userspace (which will retry the fault, or kill us if we got
		 * oom-killed):
		 */
		pagefault_out_of_memory();
	} else {
		if (fault & (VM_FAULT_SIGBUS|VM_FAULT_HWPOISON|
			     VM_FAULT_HWPOISON_LARGE))
			do_sigbus(regs, error_code, address, fault);
		else if (fault & VM_FAULT_SIGSEGV)
			bad_area_nosemaphore(regs, error_code, address);
		else
			BUG();
	}
}
static void
__bad_area_nosemaphore(struct pt_regs *regs, unsigned long error_code,
		       unsigned long address, u32 pkey, int si_code)
{
	struct task_struct *tsk = current;
	if (likely(show_unhandled_signals))
		show_signal_msg(regs, error_code, address, tsk);

	set_signal_archinfo(address, error_code);

	if (si_code == SEGV_PKUERR)
		force_sig_pkuerr((void __user *)address, pkey);
	else
		force_sig_fault(SIGSEGV, si_code, (void __user *)address);

	local_irq_disable();
}

处理用户空间缺页异常的函数是do_user_addr_fault,在这个函数里面会检测各种错误情况并最终调用函数__bad_area_nosemaphore给当前线程发送信号SIGSEGV。

还有一些其它异常处理发送信号的场景,这里就不再举例了。

3.3 进程发送

进程如果想要向另外一个进程\线程或发送信号的话,可以使用系统提供的一些接口函数。如下所示:

函数描述
int kill(pid_t pid, int sig);Sends a signal to a specified process, to all members of a specified process group, or to all processes on the system.
int raise(int sig);Sends a signal to the calling thread.
int killpg(int pgrp, int sig);Sends a signal to all of the members of a specified process group.
int pthread_kill(pthread_t thread, int sig);Sends a signal to a specified POSIX thread in the same process as the caller.
int tgkill(pid_t tgid, pid_t tid, int sig);Sends a signal to a specified thread within a specific process.(This is the system call used to implement pthread_kill(3).)
int sigqueue(pid_t pid, int sig, const union sigval value);Sends a real-time signal with accompanying data to a specified process.

我们最常用的接口函数就是kill,它有两个参数,一个是进程标识符pid,一个是信号的值sig,就是把信号sig发给进程pid。raise函数给自己也就是当前线程发信号,它只有一个参数sig。killpg是给整个进程组发信号,在实现上是给进程组的每个进程都发信号。pthread_kill是给同一个进程中的某个线程发信号。tgkill可以给其它进程中的某个线程发信号。sigqueue是用来发实时信号的,实时信号可以多带一个附加数据,当然可以用来发普通信号,但是这样附加数据就会被忽略。

四、信号的投递

上一章我们讲了信号的发送场景,这一章我们讲一讲信号是怎么发送到进程的。我们先讲一下进程的接收装置和信号来了放哪里、怎么放,然后再具体讲投递流程。

4.1 信号待决队列

每个进程都有一个信号队列,每个线程也有一个信号队列。信号队列的数据结构如下所示:

linux-src/include/linux/signal_types.h

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

可以看到信号队列非常简单,sigset是个bit flag,代表当前队列里有哪些信号,list是信号列表的头指针。下面我们来看一下信号队列里的条目。

struct sigqueue {
	struct list_head list;
	int flags;
	kernel_siginfo_t info;
	struct ucounts *ucounts;
};

每发送一次信号都会生成一个sigqueue,sigqueue里面包含了很多和信号相关的信息。

在Linux里面,每个task_struct都代表一个线程,里面包含了一个sigpending 。Linux里面没有直接代表进程的结构体,但是一个进程的所有线程都共享同一个signal_struct。signal_struct里面也包含了一个sigpending,这个sigpending代表进程的信号队列。

4.2 信号投递流程

我们前面说了很多发送信号的方法,总体上可以分为两类,普通发送和强制发送。异常处理发送信号都是用的强制发送,其它的基本上都是用的普通发送,但也有一些其它情况用的是强制发送。这两类方法方法最终都会调用同一个函数来发送信号,我们来看一下:

linux-src/kernel/signal.c

static int send_signal(int sig, struct kernel_siginfo *info, struct task_struct *t,
			enum pid_type type)
{
	/* Should SIGKILL or SIGSTOP be received by a pid namespace init? */
	bool force = false;

	if (info == SEND_SIG_NOINFO) {
		/* Force if sent from an ancestor pid namespace */
		force = !task_pid_nr_ns(current, task_active_pid_ns(t));
	} else if (info == SEND_SIG_PRIV) {
		/* Don't ignore kernel generated signals */
		force = true;
	} else if (has_si_pid_and_uid(info)) {
		/* SIGKILL and SIGSTOP is special or has ids */
		struct user_namespace *t_user_ns;

		rcu_read_lock();
		t_user_ns = task_cred_xxx(t, user_ns);
		if (current_user_ns() != t_user_ns) {
			kuid_t uid = make_kuid(current_user_ns(), info->si_uid);
			info->si_uid = from_kuid_munged(t_user_ns, uid);
		}
		rcu_read_unlock();

		/* A kernel generated signal? */
		force = (info->si_code == SI_KERNEL);

		/* From an ancestor pid namespace? */
		if (!task_pid_nr_ns(current, task_active_pid_ns(t))) {
			info->si_pid = 0;
			force = true;
		}
	}
	return __send_signal(sig, info, t, type, force);
}

static int __send_signal(int sig, struct kernel_siginfo *info, struct task_struct *t,
			enum pid_type type, bool force)
{
	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, force))
		goto ret;

	pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending;
	/*
	 * Short-circuit ignored signals and support queuing
	 * exactly one non-rt signal, so that we can get more
	 * detailed information about the cause of the signal.
	 */
	result = TRACE_SIGNAL_ALREADY_PENDING;
	if (legacy_queue(pending, sig))
		goto ret;

	result = TRACE_SIGNAL_DELIVERED;
	/*
	 * Skip useless siginfo allocation for SIGKILL and kernel threads.
	 */
	if ((sig == SIGKILL) || (t->flags & PF_KTHREAD))
		goto out_set;

	/*
	 * Real-time signals must be queued if sent by sigqueue, or
	 * some other real-time mechanism.  It is implementation
	 * defined whether kill() does so.  We attempt to do so, on
	 * the principle of least surprise, but since kill is not
	 * allowed to fail with EAGAIN when low on memory we just
	 * make sure at least one signal gets delivered and don't
	 * pass on the info struct.
	 */
	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, 0);

	if (q) {
		list_add_tail(&q->list, &pending->list);
		switch ((unsigned long) info) {
		case (unsigned long) SEND_SIG_NOINFO:
			clear_siginfo(&q->info);
			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));
			rcu_read_lock();
			q->info.si_uid =
				from_kuid_munged(task_cred_xxx(t, user_ns),
						 current_uid());
			rcu_read_unlock();
			break;
		case (unsigned long) SEND_SIG_PRIV:
			clear_siginfo(&q->info);
			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 (!is_si_special(info) &&
		   sig >= SIGRTMIN && info->si_code != SI_USER) {
		/*
		 * Queue overflow, abort.  We may abort if the
		 * signal was rt and sent by user using something
		 * other than kill().
		 */
		result = TRACE_SIGNAL_OVERFLOW_FAIL;
		ret = -EAGAIN;
		goto ret;
	} else {
		/*
		 * This is a silent loss of information.  We still
		 * send the signal, but the *info bits are lost.
		 */
		result = TRACE_SIGNAL_LOSE_INFO;
	}

out_set:
	signalfd_notify(t, sig);
	sigaddset(&pending->signal, sig);

	/* Let multiprocess signals appear after on-going forks */
	if (type > PIDTYPE_TGID) {
		struct multiprocess_signals *delayed;
		hlist_for_each_entry(delayed, &t->signal->multiprocess, node) {
			sigset_t *signal = &delayed->signal;
			/* Can't queue both a stop and a continue signal */
			if (sig == SIGCONT)
				sigdelsetmask(signal, SIG_KERNEL_STOP_MASK);
			else if (sig_kernel_stop(sig))
				sigdelset(signal, SIGCONT);
			sigaddset(signal, sig);
		}
	}

	complete_signal(sig, t, type);
ret:
	trace_signal_generate(sig, info, t, type != PIDTYPE_PID, result);
	return ret;
}

send_signal做了一些简单的处理,然后直接调用__send_signal。__send_signal先调用prepare_signal,prepare_signal对暂停恢复类的信号先做了一下预处理,然后查看信号是否被忽略。然后根据PID类型决定是把信号放到进程队列里还是线程队列里。然后会判断信号是不是传统信号(也就是标准信号),对于传统信号,如果信号队列里已经有一个了,就不再接收了,这么做是为了兼容过去。然后调用__sigqueue_alloc分配一个信号条目sigqueue,分配好之后填充各种数据,然后把它加入到队列中去。最后调用complete_signal,此函数会选择一个合适的线程来唤醒,一般会唤醒当前线程。唤醒的线程很可能醒来就去进行信号处理。

强制发送:
强制发送的入口函数是force_sig_info_to_task,它会先把信号的阻塞和忽略取消掉,然后再调用函数send_signal进行发送。代码如下:

linux-src/kernel/signal.c

static int
force_sig_info_to_task(struct kernel_siginfo *info, struct task_struct *t,
	enum sig_handler handler)
{
	unsigned long int flags;
	int ret, blocked, ignored;
	struct k_sigaction *action;
	int sig = info->si_signo;

	spin_lock_irqsave(&t->sighand->siglock, flags);
	action = &t->sighand->action[sig-1];
	ignored = action->sa.sa_handler == SIG_IGN;
	blocked = sigismember(&t->blocked, sig);
	if (blocked || ignored || (handler != HANDLER_CURRENT)) {
		action->sa.sa_handler = SIG_DFL;
		if (handler == HANDLER_EXIT)
			action->sa.sa_flags |= SA_IMMUTABLE;
		if (blocked) {
			sigdelset(&t->blocked, sig);
			recalc_sigpending_and_wake(t);
		}
	}
	/*
	 * Don't clear SIGNAL_UNKILLABLE for traced tasks, users won't expect
	 * debugging to leave init killable. But HANDLER_EXIT is always fatal.
	 */
	if (action->sa.sa_handler == SIG_DFL &&
	    (!t->ptrace || (handler == HANDLER_EXIT)))
		t->signal->flags &= ~SIGNAL_UNKILLABLE;
	ret = send_signal(sig, info, t, PIDTYPE_PID);
	spin_unlock_irqrestore(&t->sighand->siglock, flags);

	return ret;
}

内核又封装了几个函数来辅助强制发送,分别是force_sig_info、force_sig、force_fatal_sig、force_exit_sig、force_sigsegv、force_sig_fault_to_task、force_sig_fault,它们的代码就不再具体介绍了。

普通发送:
do_send_sig_info先对send_signal进行了一次封装,然后do_send_specific、group_send_sig_info又分别对其进行了封装。do_send_specific代表发送到线程,group_send_sig_info代表发送到进程。给线程发信号的接口函数最终都是调用的do_send_specific。给进程发信号的接口函数最终都是调用的group_send_sig_info。下面我们看一下kill和tgkill的调用流程。

先看kill接口函数的流程:
linux-src/kernel/signal.c

SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
	struct kernel_siginfo info;

	prepare_kill_siginfo(sig, &info);

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

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

	if (pid > 0)
		return kill_proc_info(sig, info, pid);

	/* -INT_MIN is undefined.  Exclude this case to avoid a UBSAN warning */
	if (pid == INT_MIN)
		return -ESRCH;

	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,
							      PIDTYPE_MAX);
				++count;
				if (err != -EPERM)
					retval = err;
			}
		}
		ret = count ? retval : -ESRCH;
	}
	read_unlock(&tasklist_lock);

	return ret;
}

static int kill_proc_info(int sig, struct kernel_siginfo *info, pid_t pid)
{
	int error;
	rcu_read_lock();
	error = kill_pid_info(sig, info, find_vpid(pid));
	rcu_read_unlock();
	return error;
}

int kill_pid_info(int sig, struct kernel_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, PIDTYPE_TGID);
		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.
		 */
	}
}

下面再来看一下tgkill函数的流程:
linux-src/kernel/signal.c

SYSCALL_DEFINE3(tgkill, pid_t, tgid, pid_t, pid, int, sig)
{
	/* This is only valid for single tasks */
	if (pid <= 0 || tgid <= 0)
		return -EINVAL;

	return do_tkill(tgid, pid, sig);
}


static int do_tkill(pid_t tgid, pid_t pid, int sig)
{
	struct kernel_siginfo info;

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

	return do_send_specific(tgid, pid, sig, &info);
}

五、信号的处理

下面我们来看一下信号的处理。在处理之前我们要先看看信号的阻塞,阻塞的信号是暂时不被处理的。然后看看信号的忽略与捕获,这是设置信号如何处理。最后再看信号的处理流程。

5.1 信号的阻塞

当一个线程暂时不想接收信号的时候,比如说正在处理一个关键的任务,就可以暂时屏蔽信号,等任务完成之后就可以解决屏蔽了。信号屏蔽是线程局部的,每个线程都可以有自己私有信号屏蔽设置。屏蔽信号的接口函数有两个,sigprocmask和pthread_sigmask。前者是单线程时代的接口函数,后者是多线程时代POSIX规定的接口函数。两者实际上没有区别,下面我们以pthread_sigmask为例来讲解一下。

int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);

sigset_t是一个bit flag,参数set可以用来指定想屏蔽的信号,oldset用来返回线程之前的数据。how用来指定如何设置屏蔽参数,有三个值:SIG_BLOCK,在线程原来blocked的基础上再增加blocked信号;SIG_UNBLOCK,从线程原来的blocked信号中移除一些blocked信号;SIG_SETMASK,直接把blocked信号设置为set,不考虑原有的设置。

如果你要阻塞SIGKILL和SIGSTOP,此函数实现并不会返回错误值,而是会默默地忽略,返回设置成功。

5.2 信号的忽略与捕获

信号的忽略与捕获的设置方法是一样,所以放在一起讲。其实忽略可以看成是一种特殊的捕获,相当于是信号处理函数是空函数。

设置信号处理方式的接口函数有两个,signal和sigaction。signal是早期的设置函数,适用于标准信号,比较简单。sigaction是后来新增的接口函数,功能比较强大,适用于实时信号,当然也可以用于标准信号。

我们先来看signal函数接口:

sighandler_t signal(int signum, sighandler_t handler);

signal有两个参数,第一个是信号数值,第二个是信号处理函数。信号处理函数的接口如下所示:

typedef void (*sighandler_t)(int);

第二个参数可以传递特殊值SIG_IGN,代表忽略这个信号,还可以传递特殊值SIG_DFL,代表恢复信号的默认处理方式。

下面再来看sigaction函数的接口:

 int sigaction(int signum, const struct sigaction *restrict act, struct sigaction *restrict oldact);

有三个参数,第一个参数是信号数值,第二个参数是要设置的情况,第三个参数会返回旧的设置情况,可以为NULL。下面我们看一下struct sigaction结构体的定义:

struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};

这里面有两个函数指针,接口不一样,sa_handler是用来处理标准信号的,sa_sigaction是用来处理实时信号的,到底用哪个函数呢?在字段sa_flags里面设置SA_SIGINFO的话就用用后者,不设置的话就用前者。sa_handler也可以设置特殊值SIG_IGN、SIG_DFL,含义和前面说的一样。关于此接口更多的信息就不再赘述了,大家可以看下面的官方文档:https://man7.org/linux/man-pages/man2/sigaction.2.html

5.3 异步信号安全

我们可以通过设置信号处理函数来捕获信号,那信号处理函数能像普通函数一样什么接口函数都能调用吗?不能,我们只能调用异步信号安全的函数。很多常用的函数都不是信号安全函数,不能在信号处理函数里面调用,比如printf。那要是想在信号处理函数里面输出数据该咋办呢?可以使用write接口函数,这个函数是异步信号安全的。具体都有哪些函数是异步信号安全的呢,请看 https://man7.org/linux/man-pages/man7/signal-safety.7.html

5.4 信号处理流程

信号处理是在线程从内核空间返回用户空间的时候处理的。而从内核空间返回用户空间是和架构相关的,所以这一部分的代码是在架构代码里面的。下面我们以x86为例讲解一下(代码进行了删减)。

linux-src/kernel/entry/common.c

static unsigned long exit_to_user_mode_loop(struct pt_regs *regs, unsigned long ti_work)
{
	while (ti_work & EXIT_TO_USER_MODE_WORK) {
		if (ti_work & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL))
			handle_signal_work(regs, ti_work);
	}
	return ti_work;
}

static void handle_signal_work(struct pt_regs *regs, unsigned long ti_work)
{
	if (ti_work & _TIF_NOTIFY_SIGNAL)
		tracehook_notify_signal();

	arch_do_signal_or_restart(regs, ti_work & _TIF_SIGPENDING);
}

linux-src/arch/x86/kernel/signal.c

void arch_do_signal_or_restart(struct pt_regs *regs, bool has_signal)
{
	struct ksignal ksig;

	if (has_signal && get_signal(&ksig)) {
		handle_signal(&ksig, regs);
		return;
	}

	restore_saved_sigmask();
}

可以看出线程在返回到用户空间之前不断地检查有没有信号要处理。如果有的话就使用函数get_signal取出一个信号,然后在函数handle_signal里面去执行。get_signal的代码我们就不贴出来了,在这里讲一下它的大概逻辑。get_signal会先看有没有STOP相关的信号,如果有的话执行处理。然后去取一个信号出来,先取同步信号,同步信号只从当前线程的信号队列里去取,这里的同步信号是指前面讲的异常处理的6个信号。如果没有同步信号的话就去取其它信号,其它信号先从线程的信号队列里面去取,如果没有的话就再去进程的信号里面去取。如果取到的信号的处理设置是忽略,或者是默认处理但默认处理方式也是忽略,则继续取下一个信号。如果取到的信号没有设置信号处理函数,则在这里执行其默认处理,终结进程或者coredump之后再终结进程。如果没有取到信号则get_signal返回值为0,如果取到了信号,且信号设置了信号处理函数则返回值为1,且输出参数ksig会包含相应信号的相关的信息。然后把ksig传递给函数handle_signal来处理。下面我们看一下handle_signal函数的实现。

linux-src/arch/x86/kernel/signal.c

static void
handle_signal(struct ksignal *ksig, struct pt_regs *regs)
{
	bool stepping, failed;
	struct fpu *fpu = &current->thread.fpu;

	if (v8086_mode(regs))
		save_v86_state((struct kernel_vm86_regs *) regs, VM86_SIGNAL);

	/* Are we from a system call? */
	if (syscall_get_nr(current, regs) != -1) {
		/* 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;
		}
	}

	/*
	 * If TF is set due to a debugger (TIF_FORCED_TF), clear TF now
	 * so that register information in the sigcontext is correct and
	 * then notify the tracer before entering the signal handler.
	 */
	stepping = test_thread_flag(TIF_SINGLESTEP);
	if (stepping)
		user_disable_single_step(current);

	failed = (setup_rt_frame(ksig, regs) < 0);
	if (!failed) {
		/*
		 * Clear the direction flag as per the ABI for function entry.
		 *
		 * Clear RF when entering the signal handler, because
		 * it might disable possible debug exception from the
		 * signal handler.
		 *
		 * Clear TF for the case when it wasn't set by debugger to
		 * avoid the recursive send_sigtrap() in SIGTRAP handler.
		 */
		regs->flags &= ~(X86_EFLAGS_DF|X86_EFLAGS_RF|X86_EFLAGS_TF);
		/*
		 * Ensure the signal handler starts with the new fpu state.
		 */
		fpu__clear_user_states(fpu);
	}
	signal_setup_done(failed, ksig, stepping);
}

这段代码虽然看起来不太复杂,但是实际上却非常难以理解。setup_rt_frame为了使线程返回用户空间后能执行信号处理函数便开始伪造用户线程栈帧。栈帧首先保存一些线程当前的状态到栈上,然后再伪造出仿佛是一个蹦床函数调用了信号处理函数一样。然后再伪造出仿佛是信号处理函数通过系统调用进入了内核一样。然后线程从内核返回用户空间就会执行信号处理函数,信号处理函数执行完返回的时候时候会返回到蹦床函数。蹦床函数会调用sigreturn系统调用进入内核,sigreturn会读取蹦床函数的栈帧,因为这上面保持的是之前的线程执行信息。然后把这些信息进行恢复,这样线程再回到用户空间的时候就又回到了线程之前执行的地方。

六、信号处理的同步化

对于异步信号来说,有很多的问题,比如你不确定你正在干啥的时候它来了,还有就是在异步信号的处理函数里面有很多的函数不能调用。为此我们可以把异步信号转化为同步信号。我们前面说过,同步信号、异步信号是指信号的发送是同步的还是异步的,那异步信号肯定不可能转化为同步信号啊。我们此处所说的转化是指把信号的处理从异步转化为同步。转化的方法就是用一个函数来等信号,这样信号和线程执行的相对性就是固定的了,就相当于是同步信号了。等的方式有两种,一种是等待信号被处理,信号还是走前面所说的处理流程,另一种是等待信号并截获信号,信号被我们偷走了,不会再走前面所说的信号处理流程了。

6.1 信号等待

信号等待的接口函数有两个pause和sigsuspend,它们的接口是:

int pause(void);
int sigsuspend(const sigset_t *mask);

pause会使当前的线程进入休眠状态,直到有信号到来并且被处理完成之后,函数才会返回。sigsuspend接口更为灵活,可以指定一个屏蔽信号集,线程在休眠的同时还能屏蔽不想等待的信号,只等自己想等的信号。

6.2 信号截获

除了等待信号被处理之外,我们还可以等待并截获信号,信号就不会走正常的处理流程,我们可以对截获到的信号进行相应的处理。信号截获一共有四个接口函数,我们先来讲三个。

int sigwait(const sigset_t *restrict set, int *restrict sig);
int sigwaitinfo(const sigset_t *restrict set, siginfo_t *restrict info);
int sigtimedwait(const sigset_t *restrict set, siginfo_t *restrict info, const struct timespec *restrict timeout);

接口函数sigwait有两个参数,第一个参数是要等待的信号集,第二个参数是输出参数,是等待并截获到的信号。函数返回之后,我们就可以根据sig的值进行相应的处理。接口函数sigwaitinfo也有两个参数,第一个参数和前面的是一样的,第二个参数是输出参数,类型是siginfo_t,能获得更多信号相关的信息。接口函数sigtimedwait和sigwaitinfo差不多,只是多个了时间参数,如果等了这么长时间之后还没有等来信号就会直接返回。

还有一个接口函数,它把要等待的信号信息转化为了fd,等信号直接变成了read fd的操作。其接口如下:

int signalfd(int fd, const sigset_t *mask, int flags);

第二个参数代表要等待的信号集。第一个参数如果是-1,代表要创建一个新的fd,如果是一个已有的signalfd,代表修改已经fd的信号集。然后我们就可以对这个fd进行read操作了,read的缓存区至少要有 sizeof(struct signalfd_siginfo)个字节。Read每次返回都会读取若干个struct signalfd_siginfo结构体。最关键的是我们还可以对这个fd进行select、poll操作。关于此接口的详细情况请看https://man7.org/linux/man-pages/man2/signalfd.2.html

七、总结回顾

现在我们对信号有了全面的了解,对信号的发送、投递、处理流程都比较熟悉了。下面让我们再看一下信号机制的总体框架图再来回顾一下。

信号机制框架
相信我们以后再遇到了信号相关的问题,一定能更好地处理。


参考文献:
《Linux Kernel Development》
《Understanding the Linux Kernel》
《Professional Linux Kernel Architecture》
《The Linux Programming Interface》

https://man7.org/linux/man-pages/man7/signal.7.html
https://man7.org/linux/man-pages/man7/signal-safety.7.html
https://man7.org/linux/man-pages/man7/sigevent.7.html

http://akaedu.github.io/book/ch33.html
http://gityuan.com/2015/12/20/signal/
https://zhuanlan.zhihu.com/p/77598393
https://zhuanlan.zhihu.com/p/79062142
https://zhuanlan.zhihu.com/p/77627175
https://zhuanlan.zhihu.com/p/78653866

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐