进程间通信-信号

如前面博客所述,信号(signal,亦称软中断)机制是软件层次上对中断机制的一种模拟。从概念上说,一个进程接收到一个信号与一个处理器接收到一个中断请求是一样的。而一个进程可以向另一个(或另一组)进程发送信号,也跟在多处理器系统中一个处理器可以向其他处理器发出中断请求一样。当然,对一个处理器的中断请求并不一定来自其他处理器,也可以是来自各种中断源,甚至来自处理器本身。相应地,信号也不一定都来自其他进程,也可以来自不同的来源,还可以来自本进程的执行。更重要的是,二者都是异步的。处理器在执行一段程序时并不需要停下来等待中断的发生,也不知道中断会在何时发生。信号也是一样,一个进程并不需要通过一个什么操作来等待信号的到来,也不知道什么时候会有信号到达。

事实上,在所有的进程间通信机制中只有信号是异步的。二者之间的这种相似和类比不仅仅是概念上的,也体现在它们的实现上。就像在中断机制中有一个中断向量表一样,在每个进程的task_struct结构中都有个指针sig,指向一个signal_struct结构,这个结构就不妨称为信号向量表。在中断机制中,对每种中断请求都可以加以屏蔽而不让处理器对之作出响应,在信号机制中也有类似的手段。当然,由于中断机制时通过硬件和软件的结合来实现的,而信号则纯粹由软件实现,所以在具体的细节上必然有所不同。但是如果将二者对照起来看,就可以看出信号机制中的有些数据结构和算法实际上就是对中断机制中一些硬件特征的模拟。同时,正是由于二者间的相似,在中断处理中可能碰到的问题和经验一般也适用于信号机制。例如,嵌套中断往往会给程序设计带来一些困难,而嵌套信号也会带来类似的问题。正因为这样,读者在阅读本博客时不妨多多回顾和对照中断与异常那个系列的有关段落。

人们对信号与中断的相似性(以及其他一些问题)并不是一开始就充分认识和深刻理解的。早期Unix系统中对的信号机制比较简单和原始,没有充分吸取在中断处理方面所积累的经验,后来在实践中暴露出一些问题而被称为不可靠信号。正因为这样,在各种Unix的变形版中就纷纷对辛哈机制加以扩充,以实现可靠信号。在这方面最主要的有BSD和AT&T分别在4.2BSD、4.3BSD和SRV3中所做的扩充。但是,这种分别进行的扩充使不同版本间的兼容性成了问题,所以后来又在POSIX.1和POSIX.4两种标准中对信号机制进行了标准化。其中POSIX.1规定了对信号机制的基本要求,而POSIX.4则规定了对信号机制对的扩充,后者是POSIX.1的一个超集。linux内核的信号机制符合POSIX.4的规定。不过POSIX值规定了信号机制的功能和应用界面,并没有规定如何实现。例如,同一种功能可以在操作系统内核中实现,也可以在库程序中实现,所以有些非Unix类的操作系统也可能支持POSIX。

既然信号机制与终端机制在概念上是一致的,我们就从与终端向量表相对应的信号向量表开始。如前所述,每个进程的task_struct结构中有一个指针sig,指向一个signal_struct 结构。这个数据结构类型定义如下:

struct signal_struct {
	atomic_t		count;
	struct k_sigaction	action[_NSIG];
	spinlock_t		siglock;
};

结构中的数组action就相当于是一个信号向量表,数组中的每个元素就相当于一个信号向量,确定了当进程接收到一个具体的信号时应该采取的行动,就好像一个中断向量指向一个中断服务程序一样。不过,信号向量有它的特殊之处,除指向一个信号处理程序以外,它还可以是两个特殊常数SIG_DFL和SIG_IGN之一,分别表示应该对信号采取默认(default)的反应或者忽略而不做任何反应。下面给出这两个常数的定义:

#define SIG_DFL	((__sighandler_t)0)	/* default signal handling */
#define SIG_IGN	((__sighandler_t)1)	/* ignore signal */
#define SIG_ERR	((__sighandler_t)-1)	/* error return from signal */

由于SIG_DFL的数值为0,当向量表为空白时所有的信号向量都视作SIG_DFL。

信号向量与中断向量还有一个重要的不同之处。大家知道,中断向量表在系统空间中,每个中断向量所指向的中断响应程序也在系统空间中。然而,虽然信号向量表也在系统空间中,可是这些向量所指向的处理程序却一般都是在用户空间中。

对信号的检测和响应总是发生在系统空间,通常发生在两种情况下:第一,当前进程由于系统调用、中断或异常而进入系统空间以后,由于信号的存在而提前返回到用户空间。当有信号要响应时,处理器执行路线的景象如下图:

从图中不难看出,信号处理程序(相当于中断服务程序)的启动、执行以及返回变得复杂了,读者在后面将会看到这些过程是如何实现的。

中断向量表中的每个向量基本上是个函数指针,早期Unix系统中的信号向量表也是一样。但是,经过扩充与改进,现在的信号向量已经不仅仅是函数指针了。每个信号向量都是一个sigaction数据结构,定义如下:

struct sigaction {
	union {
	  __sighandler_t _sa_handler;
	  void (*_sa_sigaction)(int, struct siginfo *, void *);
	} _u;
	sigset_t sa_mask;
	unsigned long sa_flags;
	void (*sa_restorer)(void);
};

这里的_sa_sigaction和_sa_handler都是函数指针。数据类型__sighandler_t也是在signal.h中定义的:

typedef void (*__sighandler_t)(int);

可见,_sa_sigaction和_sa_handler只是在调用时的参数表不同,具体将_u解释成哪一个指针取决于具体的约定。

另一个指针sa_restorer现在已经基本不用了,但是sa_mask和sa_flags两个字段却扮演着重要的角色。

先来看sa_mask。简单地说,sa_mask是一个位图,其中的每一位都对应着一种信号。如果位图中的某一位为1,就表示在执行当前信号的处理程序期间要将相应的信号暂时屏蔽,使得在执行的过程中不会嵌套地相应那种信号。特别地,不管维度中的相应位是否为1,当前信号本身总是自动屏蔽,使得对同一种信号的处理不会嵌套发生,除非sa_flags中的SA_NODEFER或SA_NOMASK标志为1。显然,这正是借鉴了在中断服务中关闭中断以防嵌套的经验,对于熟悉中断机制的读者来说似乎不是什么深奥的道理,可是这却是将不可靠信号改进成可靠信号的关键性的一步。在早期Unix系统的信号机制中,当时的设计人眼似乎认为异种信号处理的嵌套不是什么问题,而同种信号处理的嵌套可以通过一种简单的方法来避免。怎么避免呢?那就是:每当执行一个信号处理程序时,就由内核自动将信号向量表中相应的函数指针设置成SIG_DFL。从而,在执行一个信号处理程序的过程中如果又接收到同种信号的话,就会因为此时的信号向量已经改成SIG_DFL而不会嵌套进入同一个处理程序。这样,应用程序所设置的信号向量就是一次性的,所以信号处理程序中在完成了需要防止嵌套的部分以后就要再次设置信号向量,为下一次执行同一信号处理程序做好准备。这套方案看起来似乎可行,但是在实践中却碰到了问题。一种典型的情景就是对CTRL_C的处理。大家知道,在键盘上启动一个程序后,按一下CTRL_C通常会使正在运行的程序流产,这实际上就是通过信号机制来实现的。当在键盘上按CTRL_C时,内核会向相应的进程发出一个SIGINT信号,而对这个信号的默认反映就是通过do_exit结束运行。有些应用程序对CTRL_C的作用另有安排,所以就要为SIGINT另行设置一个响亮,使它指向应用程序中的一个函数,在那个函数中对CTRL_C这个事件作出响应,并在此设置(或曰恢复)该信号响亮,为下次CTRL_C事件做好准备。可是,在实践中却发现,两次CTRL_C事件往往过于密集,有时候刚进入信号处理程序,还没有来得及重新设置信号向量,第二个信号就到达了。由于此时向量表对应于SIGINT的向量已在启动其处理程序时自动改变成SIG_DFL,而对SIG_DFL信号的默认反映又是结束进程的运行,所以第二个SIGINT信号的到来就往往把进程杀了。正因这样,早期的信号机制被称为不可靠信号。从这里人们得出了一些教训。首先信号向量不应该是一次性的,也就是不应该在执行相应处理程序时将向量改成SIG_DFL。其次,在执行一个信号处理成的过程中应该将该种信号自动屏蔽掉,以防同意处理程序的嵌套。此外,还应该有一个手段,使应用程序可以在执行处理程序的期间有选择地将若干种其他信号屏蔽掉,这就是sa_mask的来历。所谓屏蔽,与将信号忽略丢弃是不同的。它只是将信号暂时遮盖一下,一旦屏蔽去掉,已经到达的信号仍旧还在。位图sa_mask的类型为sigset_t,定义如下:

#define _NSIG		64
#define _NSIG_BPW	32
#define _NSIG_WORDS	(_NSIG / _NSIG_BPW)

typedef unsigned long old_sigset_t;		/* at least 32 bits */

typedef struct {
	unsigned long sig[_NSIG_WORDS];
} sigset_t;

这种数据结构主要用于模拟中断控制器(如Intel8259)中的中断请求寄存器和中断屏蔽寄存器,task_struct结构中的blocked就相当于中断屏蔽寄存器。以前task_struct结构中还有个sigset_t数据结构signal,就是相当于中断请求寄存器,后来把它移入了sigpending数据结构中(现在task_struct结构中有个sigpending数据结构pending)。注意这里的_NSIG正是前述数组action的大小。早期Unix系统中只定义了32种信号,所以只要一个无符号长整数就可容纳了。而现在linux(以及POSIX.4)定义了64种信号,将来也许还会增加,所以才有以上的定义。

再来看sa_flags中的标志位,它们的定义也在signal.h中:

/*
 * SA_FLAGS values:
 *
 * SA_ONSTACK indicates that a registered stack_t will be used.
 * SA_INTERRUPT is a no-op, but left due to historical reasons. Use the
 * SA_RESTART flag to get restarting signals (which were the default long ago)
 * SA_NOCLDSTOP flag to turn off SIGCHLD when children stop.
 * SA_RESETHAND clears the handler when the signal is delivered.
 * SA_NOCLDWAIT flag on SIGCHLD to inhibit zombies.
 * SA_NODEFER prevents the current signal from being masked in the handler.
 *
 * SA_ONESHOT and SA_NOMASK are the historical Linux names for the Single
 * Unix names RESETHAND and NODEFER respectively.
 */
#define SA_NOCLDSTOP	0x00000001
#define SA_NOCLDWAIT	0x00000002 /* not supported yet */
#define SA_SIGINFO	0x00000004
#define SA_ONSTACK	0x08000000
#define SA_RESTART	0x10000000
#define SA_NODEFER	0x40000000

这些标志位的作用以后读了相关的代码就会清楚,但是有几个标志位特别值得一提。一个是

SA_SIGINFO。这个标志位为1表示信号处理程序有三个参数(否则只有一个,即所处理的信号本身的标识号,如SIGINT),其中之一为指向一个siginfo_t数据结构的指针。与siginfo_t有关的数据结构和常数定义如下:

typedef union sigval {
	int sival_int;
	void *sival_ptr;
} sigval_t;

#define SI_MAX_SIZE	128
#define SI_PAD_SIZE	((SI_MAX_SIZE/sizeof(int)) - 3)

typedef struct siginfo {
	int si_signo;
	int si_errno;
	int si_code;

	union {
		int _pad[SI_PAD_SIZE];

		/* kill() */
		struct {
			pid_t _pid;		/* sender's pid */
			uid_t _uid;		/* sender's uid */
		} _kill;

		/* POSIX.1b timers */
		struct {
			unsigned int _timer1;
			unsigned int _timer2;
		} _timer;

		/* POSIX.1b signals */
		struct {
			pid_t _pid;		/* sender's pid */
			uid_t _uid;		/* sender's uid */
			sigval_t _sigval;
		} _rt;

		/* SIGCHLD */
		struct {
			pid_t _pid;		/* which child */
			uid_t _uid;		/* sender's uid */
			int _status;		/* exit code */
			clock_t _utime;
			clock_t _stime;
		} _sigchld;

		/* SIGILL, SIGFPE, SIGSEGV, SIGBUS */
		struct {
			void *_addr; /* faulting insn/memory ref. */
		} _sigfault;

		/* SIGPOLL */
		struct {
			int _band;	/* POLL_IN, POLL_OUT, POLL_MSG */
			int _fd;
		} _sigpoll;
	} _sifields;
} siginfo_t;

如前所述,信号机制对中断机制的模拟。就像中断请求一样,(早期的)信号所载送的信息是二元的,也就是有或没有,仅此而已。在中断机制中,一般都是由中断服务程序来读相应外设的状态寄存器以获取进一步的信息,所以传统的信号也得要与其他的手段相结合才能完成一些信息量要求稍大的通信。针对这个缺点,改进后的信号机制通信的双档可以随信号一起传递一个siginfo_t数据结构以及一个void指针。其中siginfo_t结构的主体是一个union,根据信号类型si_signo的值而赋予不同的解释;而void指针所指的数据类型则由通信的双方自行约定。

此处先将linux信号的专用名、定义值以及默认反映等项信息列于下表。

linux信号专用名、定义值与默认反映
信号定义值用途或来源     默认的反应
SIGHUP1控制TTY断开连接进程终止
SIGINT2用户在键盘上按CTRL_C进程终止
SIGQUIT3TTY键盘上按CTRL_\进程流产(core)
SIGILL4非法指令(异常)进程流产(core)
SIGTRAP5遇到debug断电,用于调试进程流产
SIGABRT6使进程流产进程流产(core)
SIGIOT6同上同上
SIGBUS7访问内存失败进程流产
SIGFPE8算术运算或浮点处理出错进程流产(core)
SIGKILL9使进程终止(不可屏蔽)进程终止
SIGUSR110由应用软件自行定义和使用忽略
SIGSEGV11越界访问内存进程流产(core)
SIGUSR212由应用软件自行定义和使用忽略
SIGPIPE13管道断裂(管道的读端已关闭)进程流产(core)
SIGALRM14由settimer设置的定时器到点忽略
SIGTERM15使进程终止进程终止
SIGSTKFLT16用于堆栈出错(尚未使用)进程终止
SIGCHLD17用于子进程终止忽略
SIGCONT18进程继续运行,与SIGSTOP结合使用忽略
SIGSTOP19        进程暂停运行,转入TASK_STOPPED状态进程暂停
SIGTSTP20CTRL_Z,进程挂起(TASK_STOPPED)进程暂停
SIGTTIN21后台进程读控制终端时使用进程暂停
SIGTTOU22后台进程写控制终端时使用进程暂停
SIGURG23用于紧急I/O状况忽略
SIGXCPU24进程使用CPU已超出限制进程终止
SIGXFSZ25文件大小超出限制
SIGVTALRM26由settimer设置的虚拟定时器到点进程终止
SIGPROF27由settimer设置的统计定时器到点进程终止
SIGWINCH28控制终端窗口的大小被改变忽略
SIGIO29用于异步I/0进程终止
SIGPOLL29同上同上
SIGPWR30用于电源失效进程终止
SIGSYS31保留,未使用进程终止
SIGUNUSED32从SIGRTMIN至SIGRTMAX为实时信号进程终止
SIGRTMAX    (_NSIG-1)

以前,进程的task_struct结构中有两个位图(sigset_t)signal和blocked(现在signal在task_struct结构内部的sigpending结构pending中),分别用来模拟中断控制器硬件中的中断状态(或者说中断请求)寄存器和中断屏蔽寄存器。每当有一个中断请求到来时,中断状态寄存器中与之相应的某一位就被置成1,表示有相应的中断请求在等待处理,并且一直要到中断响应程序读出这个寄存器时才又被清0。如果连续有两次中断请求到来,则有可能因处理器来不及响应而被合并,因为中断装填寄存器中具体的状态位一旦被置成1以后(在清0之前)就反映不出到底被连续几次置1。中断机制中这并不是什么问题,因为通常中断响应程序在检查到某个中断通道中有中断请求就会轮询连接在该通道中所有的中断源,还是可以知道到底有几个中断源发生了请求。所以,在中断机制中这种合并效应只是形式上的,不是实质性的。

可是,在信号机制中就不同了。接收到信号的进程无法轮询所有可能的信号来源。因此,在信号机制中这种合并效应是实质性的。解决这个问题的出路在于为信号准备一个队列,没产生一个信号就把它挂入这个队列,这样就可以确保一个信号也不会丢失了。所以,就在task_struct结构中设置了一个信号队列,后来又把task_struct结构中的信号队列和信号位图合并成一个sigpending数据结构,定义如下:

struct sigpending {
	struct sigqueue *head, **tail;
	sigset_t signal;
};

顺便提一下,在task_struct中还有两个用于信号机制的成分。一个是sas_ss_sp,用于记录当前进程在用户空间执行信号处理程序时的堆栈位置。另一个是sas_ss_size,那就是堆栈的大小。下面我们列出task_struct数据结构中所有与信号有关的成分,以便查阅:


struct task_struct {
......
	int sigpending;
......
	int exit_code, exit_signal;
	int pdeath_signal;  /*  The signal sent when the parent dies  */
......
/* signal handlers */
	spinlock_t sigmask_lock;	/* Protects signal and blocked */
	struct signal_struct *sig;

	sigset_t blocked;
	struct sigpending pending;

	unsigned long sas_ss_sp;
	size_t sas_ss_size;
......
};

注意在task_struct结构中有个字段叫sigpending,是个整数,用来表示这个进程是否有信号在等待处理;而上述的sigpending数据结构则包括一个信号队列和一个信号位图,不要把两个sigpending搞混淆了。

上述的种种改进无疑是 很有必要的,但是可惜来迟了。在做出这些改进之前,人们已经在Unix上开发了很多使用信号的软件。就信号的使用而言,这些软件在相当程度上可以说是为早期欠成熟的信号机制量身定做的。现在对信号机制有了改进,但是对这些已经存在的软件还要保证它们能在新的环境中发挥余热,并且不改变其运行时的习性。显然,比较简单的办法是保留原先已经定义的(31个)信号不变(包括有关的系统调用以及使用方式),而另外再定义一些新的信号(和一些新的系统调用),对这些新的信号则实施改进了的信号机制。在实践中,这种对老编制实行老政策,新编制实行新政策的现象常常是不可避免的。另一方面,仔细想一下就可以明白,上述的这些改进其实大多是跟时间有关的,所以把新增的信号称为RT(实时)信号。从SIGRTMIN到SIGRTMAX全都是这些RT信号。不过,这里要注意不要被RT这两个字母迷惑了,这些信号与一般意义上的实时并没有关系。例如,这些信号的传递不比普通的信号快,也没有时间上的承若。

有了上面这些预备知识,我们就可以开始介绍代码了。

先看信号向量,也就是信号处理程序的安装,其作用类似于中断向量的设置。linux为此提供了三个系统调用。第一个是传统的、老的调用:

sighandler_t signal(int signum, sighandler_t handler);

这里的数据类型sighandler_t为指向信号处理程序的函数指针。调用的参数有两个,一个是信号的定义值signum(如SIGINT),另一个就是指向由用户定义的该信号处理程序的函数指针handler。不过handler也可以是两个特殊值之一,即SIG_IGN或SIG_DFL,分别表示忽略该信号或采取对此信号的默认反映。系统调用的返回值为该信号原先的handler。其他两个系统调用则是新的,但是在用户程序设计界面却作为相同的库函数出现:

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

这个库函数会根据信号的编号不同,确定应该落实系统调用sys_sigaction还是sys_rt_sigaction。

这里的signum还是一样,而act和oldact为两个指向sigaction数据结构的指针。其中act所指向的结构为新的、待设置的向量,而oldact所指则为返回老向量的数据结构。

像其它系统调用一样,它们在内核中的入口分别为sys_signal,sys_sigaction和sys_rt_sigaction。函数sys_signal的代码如下:

/*
 * For backwards compatibility.  Functionality superseded by sigaction.
 */
asmlinkage unsigned long
sys_signal(int sig, __sighandler_t handler)
{
	struct k_sigaction new_sa, old_sa;
	int ret;

	new_sa.sa.sa_handler = handler;
	new_sa.sa.sa_flags = SA_ONESHOT | SA_NOMASK;

	ret = do_sigaction(sig, &new_sa, &old_sa);

	return ret ? ret : (unsigned long)old_sa.sa.sa_handler;
}

函数sys_rt_sigaction的代码也在同一个文件中:

asmlinkage long
sys_rt_sigaction(int sig, const struct sigaction *act, struct sigaction *oact,
		 size_t sigsetsize)
{
	struct k_sigaction new_sa, old_sa;
	int ret = -EINVAL;

	/* XXX: Don't preclude handling different sized sigset_t's.  */
	if (sigsetsize != sizeof(sigset_t))
		goto out;

	if (act) {
		if (copy_from_user(&new_sa.sa, act, sizeof(new_sa.sa)))
			return -EFAULT;
	}

	ret = do_sigaction(sig, act ? &new_sa : NULL, oact ? &old_sa : NULL);

	if (!ret && oact) {
		if (copy_to_user(oact, &old_sa.sa, sizeof(old_sa.sa)))
			return -EFAULT;
	}
out:
	return ret;
}

比较一下就可以看出,这两个函数其实都调用do_sigaction完成具体的操作。所不同的是,sys_signal从应用程序得到的信息量较少(只有handler),同时又要保持早起Unix系统调用signal相同的性状,所以将new_sa.sa.sa_flags 固定设成 SA_ONESHOT | SA_NOMASK。标志位SA_ONESHOT标识所设置的向量是一次性的,也就是要按传统的方式,在使用了所设置的函数指针以后就将其改成SIG_DFL,而用户空间的信号处理程序则负有再次调用signal恢复该向量的责任,另一个标志位SA_NOMASK,则表示在执行信号处理程序时不使用任何信号屏蔽,因为最初的信号机制中是没有信号屏蔽这一说的。

另一个函数sys_sigaction实际上是与sys_rt_sigaction一样的,只是细节略有不同。首先,是在sys_sigaction中不存在第四个参数sigsetsize。其次,也许更重要的,是在sys_sigaction参数表中使用的是old_sigaction结构指针,而不是sigaction结构指针,这两种数据结构含有相同的成分,但是这些成分在结构中的次序不通。所以sys_sigaction中只好把这些成分逐项地从用户控件复制到系统空间,而不能像sys_rt_sigaction中那样把整个数据结构一次就复制过来。读者也许会好奇,为什么要改变这些成分在数据结构中的次序呢?这不是自找麻烦吗?其实这样做是有道理的。在old_sigaction树结构中,sa_mask挤在sa_handler与sa_flags中间,这样就限制了sa_mask进一步扩充其大小,也就是定义更多的信号的可能。所以,在sigaction数据结构中将sa_mask移到了末尾,这样当sa_mask的大小改变时,虽然sigaction的大小也要随之改变,但各个成分在数据结构中的位移却不会改变。考虑到这一点,明知这样做会带来不便也只好忍痛了。事实上,对于设计内核代码的人来说,可能带来的混淆还真是不小,因为在应用程序中所用的sigaction数据解耦股却又是对应于内核代码中的oldsigaction数据结构!这完全要靠不同的.h文件来把它们区分开来。不过,好在内核中用到这个数据结构的代码只是很小一段。

不管怎样,这三个函数最终都是调用do_sigaction来完成的,它才是这些系统调用的主题,代码如下:

sys_signal=>do_sigaction

int
do_sigaction(int sig, const struct k_sigaction *act, struct k_sigaction *oact)
{
	struct k_sigaction *k;

	if (sig < 1 || sig > _NSIG ||
	    (act && (sig == SIGKILL || sig == SIGSTOP)))
		return -EINVAL;

	k = &current->sig->action[sig-1];

	spin_lock(&current->sig->siglock);

	if (oact)
		*oact = *k;

	if (act) {
		*k = *act;
		sigdelsetmask(&k->sa.sa_mask, sigmask(SIGKILL) | sigmask(SIGSTOP));

		/*
		 * POSIX 3.3.1.3:
		 *  "Setting a signal action to SIG_IGN for a signal that is
		 *   pending shall cause the pending signal to be discarded,
		 *   whether or not it is blocked."
		 *
		 *  "Setting a signal action to SIG_DFL for a signal that is
		 *   pending and whose default action is to ignore the signal
		 *   (for example, SIGCHLD), shall cause the pending signal to
		 *   be discarded, whether or not it is blocked"
		 *
		 * Note the silly behaviour of SIGCHLD: SIG_IGN means that the
		 * signal isn't actually ignored, but does automatic child
		 * reaping, while SIG_DFL is explicitly said by POSIX to force
		 * the signal to be ignored.
		 */

		if (k->sa.sa_handler == SIG_IGN
		    || (k->sa.sa_handler == SIG_DFL
			&& (sig == SIGCONT ||
			    sig == SIGCHLD ||
			    sig == SIGURG ||
			    sig == SIGWINCH))) {
			spin_lock_irq(&current->sigmask_lock);
			if (rm_sig_from_queue(sig, current))
				recalc_sigpending(current);
			spin_unlock_irq(&current->sigmask_lock);
		}
	}

	spin_unlock(&current->sig->siglock);
	return 0;
}

系统对信号SIGKILL和SIGSTOP的响应是不允许改变的,所以要放在开头时加以检查。同时这两个信号也不允许被屏蔽掉,所以要在屏蔽位图k->sa.sa_mask中将这两个信号对应的屏蔽位清除(见1029行)。信号的数值是从1开始定义的,所以在用信号数值作为下表时要用sig-1而不是sig(见1020行)。注意1024行和1028行中的赋值都是整个数据结构的赋值。

当新设置的向量为SIG_IGN时,或者SIG_DFL而涉及的信号为SIGCONT、SIGCHLD和SIGWINCH之一时,如果已经有一个或者几个这样的信号在等待处理,则按POSIX标准的规定要将这些已经到达的信号丢弃,所以通过rm_sig_from_queue丢弃已经到达的信号。代码如下:

sys_signal=>do_sigaction=>rm_sig_from_queue

/*
 * Remove signal sig from t->pending.
 * Returns 1 if sig was found.
 *
 * All callers must be holding t->sigmask_lock.
 */
static int rm_sig_from_queue(int sig, struct task_struct *t)
{
	return rm_from_queue(sig, &t->pending);
}

函数rm_from_queue也在同一个文件中:

sys_signal=>do_sigaction=>rm_sig_from_queue=>rm_from_queue

static int rm_from_queue(int sig, struct sigpending *s)
{
	struct sigqueue *q, **pp;

	if (!sigismember(&s->signal, sig))
		return 0;

	sigdelset(&s->signal, sig);

	pp = &s->head;

	while ((q = *pp) != NULL) {
		if (q->info.si_signo == sig) {
			if ((*pp = q->next) == NULL)
				s->tail = pp;
			kmem_cache_free(sigqueue_cachep,q);
			atomic_dec(&nr_queued_signals);
			continue;
		}
		pp = &q->next;
	}
	return 1;
}

对于传统的老信号来说,一个进程是否有信号等待着处理,是由task_struct结构中的位图signal来反应的,位图中的某一位为1就标识一斤接收到了相应的信号尚未处理,但是却无从知道究竟接收到了几个同种的信号。在这种情况下只是简单地将位图种相应的标志为清0(件277行)。而对于新的实时信号,则信号的到达不光是定性的,也是定量的。到达的信号除了使位图中的相应标志位变成1以外还进入了本进程的信号队列,所以还要在队列中搜素并将其释放(见281行的while循环)。

回到do_sigaction的代码中,由于丢弃了若干已经到达的信号,当前进程的task_struct结构中表示是否有(任何)信号在等待处理的总标志sigpending也得要重新算一下(见1055行)。读者也许会问,为什么task_struct结构中要有那么个总标志?判断一下sigpending结构中的位图signal是否为0不就可以了吗?问题在于位图并不就是一个长整数。从前,当信号的数量少于32个时,那样确实是可以的,但现在不行了。

跟设置信号向量有关的系统调用还有一些:

sigprocmask--改变本进程的task_struct结构中的信号屏蔽位图blocked。注意这个屏蔽位图与具体的向量k_sigaction数据结构中的屏蔽位图sa_mask不同。位图sa_mask的屏蔽位图只在执行相应的处理程序时才起作用,而blocked中的屏蔽位图则一直都起作用。还要注意,所谓屏蔽的意思只是暂时阻止对已经到达的信号做出响应,一旦屏蔽取消,这些已经到达的信号还是会得到处理的。

sigpending--检查有哪些信号已经到达而尚未处理。

siguspend--暂时改变本进程的信号屏蔽位图,并使进程进入睡眠,等待任何一个未屏蔽的信号到达。

这些系统调用也都有相应的实时信号版本,如rt_siguspend等。所有这些系统调用的实现大都在arch/i386/kernel/signal.c和kernel/signal.c两个文件中。一来限于篇幅,而来也给读者留下举一反三的空间,这里就不深入到有关的代码中去了。

信号向量设置好了,就做好了接收和处理信号的准备,下一步就要看怎样向一个进程发送信号了。

同样,发送信号的系统调用也有新旧不同的版本。老的版本是kill:

int kill(pid_t pid, int sig);

参数pid为目标进程的pid,当pid为0时,表示发送给当前进程所在进程组中的所有的进程,为-1时则发送给系统中的所有进程。

新的版本为sigqueue;

int sigqueue(pid_t pid, int sig, const union sigval val);

与kill不同的是,sigqueue发送的除信号sig本身外还有附加的信息,就是val。此外,sigqueue只能将信号发送给一个特定的进程,而不像kill那样可以通过将参数pid设成0来发送给整个进程组。参数val是一个union,它可以是一个长整数,但实际上总是一个指向siginfo数据结构的指针。

在clib中还有个库函数raise(int sig),用来发送一个信号给自己,相当于kill(getpid(), sig)。

系统调用kill在内核中的主体为sys_kill,代码如下:


asmlinkage long
sys_kill(int pid, int sig)
{
	struct siginfo info;

	info.si_signo = sig;
	info.si_errno = 0;
	info.si_code = SI_USER;
	info.si_pid = current->pid;
	info.si_uid = current->uid;

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

这段代码很简单,先准备一个siginfo结构,然后调用kill_something_info:

sys_kill=>kill_something_info



/*
 * kill_something_info() interprets pid in interesting ways just like kill(2).
 *
 * POSIX specifies that kill(-1,sig) is unspecified, but what we have
 * is probably wrong.  Should make it like BSD or SYSV.
 */

static int kill_something_info(int sig, struct siginfo *info, int pid)
{
	if (!pid) {
		return kill_pg_info(sig, info, current->pgrp);
	} else if (pid == -1) {
		int retval = 0, count = 0;
		struct task_struct * p;

		read_lock(&tasklist_lock);
		for_each_task(p) {
			if (p->pid > 1 && p != current) {
				int err = send_sig_info(sig, info, p);
				++count;
				if (err != -EPERM)
					retval = err;
			}
		}
		read_unlock(&tasklist_lock);
		return count ? retval : -ESRCH;
	} else if (pid < 0) {
		return kill_pg_info(sig, info, -pid);
	} else {
		return kill_proc_info(sig, info, pid);
	}
}

可见,之所以需要kill_something_info这一层,是因为要根据pid的值来确定是要将信号发送给一个特定的进程(通过kill_proc_info),还是整个进程组(通过kill_pg_info),还是全部进程?

相比之下,另一个系统调用sigqueue只允许将信号发送一个特定的进程,并且随同信号发送的siginfo结构也是由用户进程自己设置的,所以它在内核中的实现sys_rt_sigqueueinfo要简单的多。


asmlinkage long
sys_rt_sigqueueinfo(int pid, int sig, siginfo_t *uinfo)
{
	siginfo_t info;

	if (copy_from_user(&info, uinfo, sizeof(siginfo_t)))
		return -EFAULT;

	/* Not even root can pretend to send signals from the kernel.
	   Nor can they impersonate a kill(), which adds source info.  */
	if (info.si_code >= 0)
		return -EPERM;
	info.si_signo = sig;

	/* POSIX.1b doesn't mention process groups.  */
	return kill_proc_info(sig, &info, pid);
}

这里的kill_proc_info根据pid扎到目标进程的task_struct结构,然后通过send_sig_info,将信号发送给它:

sys_kill=>kill_something_info=>kill_proc_info


inline int
kill_proc_info(int sig, struct siginfo *info, pid_t pid)
{
	int error;
	struct task_struct *p;

	read_lock(&tasklist_lock);
	p = find_task_by_pid(pid);
	error = -ESRCH;
	if (p)
		error = send_sig_info(sig, info, p);
	read_unlock(&tasklist_lock);
	return error;
}

而kill_pg_info则将同意信号发送给整个进程组:

sys_kill=>kill_something_info=>kill_pg_info


/*
 * kill_pg_info() sends a signal to a process group: this is what the tty
 * control characters do (^C, ^Z etc)
 */

int
kill_pg_info(int sig, struct siginfo *info, pid_t pgrp)
{
	int retval = -EINVAL;
	if (pgrp > 0) {
		struct task_struct *p;

		retval = -ESRCH;
		read_lock(&tasklist_lock);
		for_each_task(p) {
			if (p->pgrp == pgrp) {
				int err = send_sig_info(sig, info, p);
				if (retval)
					retval = err;
			}
		}
		read_unlock(&tasklist_lock);
	}
	return retval;
}

可见,kill_pg_info最终也是逐个地找到同一进程组中所有进程的task_struct结构,并调用send_sig_info将信号发送给它们,也就是说,最后都是通过send_sig_info来完成的。我们在系统调用exit博客中提到过这个函数,但当时没有深入到它的代码中。现在,让我们来看看到底是怎么发送的。这个函数比较大,所以还是分段来看:

sys_kill=>kill_something_info=>kill_proc_info=>send_sig_info

int
send_sig_info(int sig, struct siginfo *info, struct task_struct *t)
{
	unsigned long flags;
	int ret;


#if DEBUG_SIG
printk("SIG queue (%s:%d): %d ", t->comm, t->pid, sig);
#endif

	ret = -EINVAL;
	if (sig < 0 || sig > _NSIG)
		goto out_nolock;
	/* The somewhat baroque permissions check... */
	ret = -EPERM;
	if (bad_signal(sig, info, t))
		goto out_nolock;

	/* The null signal is a permissions and process existance probe.
	   No signal is actually delivered.  Same goes for zombies. */
	ret = 0;
	if (!sig || !t->sig)
		goto out_nolock;

首先是对输入参数的检查,即所谓健康检查,这是通过bad_signal进行的:

sys_kill=>kill_something_info=>kill_proc_info=>send_sig_info=>bad_signal


/*
 * Bad permissions for sending the signal
 */
int bad_signal(int sig, struct siginfo *info, struct task_struct *t)
{
	return (!info || ((unsigned long)info != 1 && SI_FROMUSER(info)))
	    && ((sig != SIGCONT) || (current->session != t->session))
	    && (current->euid ^ t->suid) && (current->euid ^ t->uid)
	    && (current->uid ^ t->suid) && (current->uid ^ t->uid)
	    && !capable(CAP_KILL);
}

这里的current指向当前进程(信号的发送者)的task_struct结构,t则指向目标进程(信号的接收者)的task_struct结构。宏操作SI_FROMUSER以及有关的一些定义如下:

/*
 * si_code values
 * Digital reserves positive values for kernel-generated signals.
 */
#define SI_USER		0		/* sent by kill, sigsend, raise */
#define SI_KERNEL	0x80		/* sent by the kernel from somewhere */
#define SI_QUEUE	-1		/* sent by sigqueue */
#define SI_TIMER __SI_CODE(__SI_TIMER,-2) /* sent by timer expiration */
#define SI_MESGQ	-3		/* sent by real time mesq state change */
#define SI_ASYNCIO	-4		/* sent by AIO completion */
#define SI_SIGIO	-5		/* sent by queued SIGIO */

#define SI_FROMUSER(siptr)	((siptr)->si_code <= 0)
#define SI_FROMKERNEL(siptr)	((siptr)->si_code > 0)

上例的7个常数用于siginfo结构中的si_code字段,用来区分7种不同的信号源,读者可以结合看一下前面sys_kill中的986行。在sys_rt_sigqueueinfo中,则随同siginfo结构一起来自进程的用户空间,其值必须为一负数。

信号一般只能发送给属于同一个session以及同一个用户(见文件系统系列博客)的进程,除非发送信号的进程可以通过suser暂时性地得到特权用户的权限。代码中的capable(CAP_KILL)正在试图取得这种特权,读者可参阅文件系统系列的有关内容。

这里要提醒一下,在上面的if语句中,capable(CAP_KILL)出现在一个与条件表达式中,所以只有在前面的所有各项均为true时才能执行,这是由c语言的语义规则决定的。还有,这里的异或运算,如(current->euid ^ t->suid),实际上就是检验两者是否不等。

我们假定通过了这些检验,继续往下看:

sys_kill=>kill_something_info=>kill_proc_info=>send_sig_info


	spin_lock_irqsave(&t->sigmask_lock, flags);
	handle_stop_signal(sig, t);

	/* Optimize away the signal, if it's a signal that can be
	   handled immediately (ie non-blocked and untraced) and
	   that is ignored (either explicitly or by default).  */

	if (ignored_signal(sig, t))
		goto out;

前面讲过,一个进程可以通过信号屏蔽位图来暂时扣压(或遮盖)所接收到的信号。但是,在接收到某些特定的信号以后,就不容许屏蔽另一些特定的后续信号,所以对这些信号要强行除去屏蔽,为后续信号的处理扫清道路。例如,在接收到SIGSTOP以后,其后续信号必然是SIGCONT,所以要将屏蔽位图中的SIGCONT屏蔽位强行清0.而SIGCONT的后续信号则可以是SIGSTOP、SIGTSTP、SIGCCONT来说,如果目标进程正在TASK_STOPPED状态(注意,不是睡眠状态),还要将其唤醒,也就是将进程的状态改成TASK_RUNNING。这些处理都是由handle_stop_signal完成的。其代码也在同一文件中:

sys_kill=>kill_something_info=>kill_proc_info=>send_sig_info=>handle_stop_signal


/*
 * Handle TASK_STOPPED cases etc implicit behaviour
 * of certain magical signals.
 *
 * SIGKILL gets spread out to every thread. 
 */
static void handle_stop_signal(int sig, struct task_struct *t)
{
	switch (sig) {
	case SIGKILL: case SIGCONT:
		/* Wake up the process if stopped.  */
		if (t->state == TASK_STOPPED)
			wake_up_process(t);
		t->exit_code = 0;
		rm_sig_from_queue(SIGSTOP, t);
		rm_sig_from_queue(SIGTSTP, t);
		rm_sig_from_queue(SIGTTOU, t);
		rm_sig_from_queue(SIGTTIN, t);
		break;

	case SIGSTOP: case SIGTSTP:
	case SIGTTIN: case SIGTTOU:
		/* If we're stopping again, cancel SIGCONT */
		rm_sig_from_queue(SIGCONT, t);
		break;
	}
}

回到send_sig_info的代码中,535行是一项优化。如果目标进程的信号向量表中对所投递信号的响应是忽略(SIG_IGN),并且不在跟踪模式中,也没有加以屏蔽,那就根本不用投递了(除SIGCHLD外)。函数ignored_signal的代码如下:

sys_kill=>kill_something_info=>kill_proc_info=>send_sig_info=>ignored_signal

		

/*
 * Determine whether a signal should be posted or not.
 *
 * Signals with SIG_IGN can be ignored, except for the
 * special case of a SIGCHLD. 
 *
 * Some signals with SIG_DFL default to a non-action.
 */
static int ignored_signal(int sig, struct task_struct *t)
{
	/* Don't ignore traced or blocked signals */
	if ((t->ptrace & PT_PTRACED) || sigismember(&t->blocked, sig))
		return 0;

	return signal_type(sig, t->sig) == 0;
}

sys_kill=>kill_something_info=>kill_proc_info=>send_sig_info=>ignored_signal=>signal_type


/*
 * Signal type:
 *    < 0 : global action (kill - spread to all non-blocked threads)
 *    = 0 : ignored
 *    > 0 : wake up.
 */
static int signal_type(int sig, struct signal_struct *signals)
{
	unsigned long handler;

	if (!signals)
		return 0;
	
	handler = (unsigned long) signals->action[sig-1].sa.sa_handler;
	if (handler > 1)
		return 1;

	/* "Ignore" handler.. Illogical, but that has an implicit handler for SIGCHLD */
	if (handler == 1)
		return sig == SIGCHLD;

	/* Default handler. Normally lethal, but.. */
	switch (sig) {

	/* Ignored */
	case SIGCONT: case SIGWINCH:
	case SIGCHLD: case SIGURG:
		return 0;

	/* Implicit behaviour */
	case SIGTSTP: case SIGTTIN: case SIGTTOU:
		return 1;

	/* Implicit actions (kill or do special stuff) */
	default:
		return -1;
	}
}

不然的话,就进入具体投递的过程了。我们继续往下看:

sys_kill=>kill_something_info=>kill_proc_info=>send_sig_info

	/* Support queueing exactly one non-rt signal, so that we
	   can get more detailed information about the cause of
	   the signal. */
	if (sig < SIGRTMIN && sigismember(&t->pending.signal, sig))
		goto out;

	ret = deliver_signal(sig, info, t);
out:
	spin_unlock_irqrestore(&t->sigmask_lock, flags);
	if ((t->state & TASK_INTERRUPTIBLE) && signal_pending(t))
		wake_up_process(t);
out_nolock:
#if DEBUG_SIG
printk(" %d -> %d\n", signal_pending(t), ret);
#endif

	return ret;
}

对于老编制的信号(sig<SIGRTMIN),所谓投递本来是很简单的,因为那只是将目标进程的接收信号位图signal中相应的标志位设成1,而无需将信号挂入队列。以前讲过,这样的机制有时候会将短时期中接收到的多个同种信号合并成一个。但是,内核汇总对这些老编制信号也套用了siginfo数据结构(见sys_kill的代码),尽管这个数据结构中的信息并非来自应用程序,也并不完整,但多少个总也载送着一些信息。所以,这里采用了一种折中的办法,就是对于老编制的信号也将其siginfo结构挂入队列,不过只挂入一次。以SIGINT为例,当第一个SIGINT到达时,接收位图中的SIGINT标志位为0,所以将其设置成1,并将伴随的siginfo结构挂入队列。然后,如果在第一个SIGINT信号尚未处理时第二个SIGINT又到来了,则此时接收到位图中相应的标志位已经为1,队列中已经有一个SIGINT的siginfo数据结构在等待处理,所以就不需要再投递了。这就是541行中sigismember所做的测试。所以,在来不及处理的情况下,相继到达的同种信号就合并了。这样的实现一来是多少也增加了一些信息量,二来读者以后会看到简化了对信号作出响应时的代码。同时,这样的实现也与传统的信号机制在语义上完全一致。

如果到达的信号属于新编制,即实时信号,或者虽属老编制,但接收位图中相对的标志位为0,那就要通过投递信号了。

sys_kill=>kill_something_info=>kill_proc_info=>send_sig_info=>deliver_signal


static int deliver_signal(int sig, struct siginfo *info, struct task_struct *t)
{
	int retval = send_signal(sig, info, &t->pending);

	if (!retval && !sigismember(&t->blocked, sig))
		signal_wake_up(t);

	return retval;
}

具体的操作主要是,其代码如下:

sys_kill=>kill_something_info=>kill_proc_info=>send_sig_info=>deliver_signal=>send_signal


static int send_signal(int sig, struct siginfo *info, struct sigpending *signals)
{
	struct sigqueue * q = NULL;

	/* 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 (atomic_read(&nr_queued_signals) < max_queued_signals) {
		q = kmem_cache_alloc(sigqueue_cachep, GFP_ATOMIC);
	}

	if (q) {
		atomic_inc(&nr_queued_signals);
		q->next = NULL;
		*signals->tail = q;
		signals->tail = &q->next;
		switch ((unsigned long) info) {
			case 0:
				q->info.si_signo = sig;
				q->info.si_errno = 0;
				q->info.si_code = SI_USER;
				q->info.si_pid = current->pid;
				q->info.si_uid = current->uid;
				break;
			case 1:
				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 (sig >= SIGRTMIN && info && (unsigned long)info != 1
		   && 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().
		 */
		return -EAGAIN;
	}

	sigaddset(&signals->signal, sig);
	return 0;
}

投递时将siginfo结构的内容复制到一个sigqueue数据结构中,并将这个结构挂入对了。sigqueue的数据结构的定义如下:

struct sigqueue {
	struct sigqueue *next;
	siginfo_t info;
};

读者也许会问,为什么不直接在siginfo_t数据结构中增加一个指针next,从而直接将此数据结构挂入队列中呢?这是因为并非每次调用sys_kill或sys_rt_sigqueueinfo时都一定会将这个数据结构挂入队列,而分配释放这样一个数据结构的系统开销实际上远超实际上当真必要时临时加以复制的开销。所以,在sys_kill和sys_rt_sigqueueinfo中,将这个siginfo_t结构作为局部变量安排在堆栈上,只在确有必要时才分配一个siginfo_t数据结构并加以复制。

不过,并非在所有的情况下都是将伴随着(或者说载送着)信号的siginfo_t结构复制到sigqueue结构中,有两种情况下是例外的,那就是当参数info的值为特殊值0或1的时候。在这两种情况下,info应别视作整数而不是指针,发生的信号来自系统空间(而不是由一个进程在用户空间中通过系统调用发出)的情况下。此时由send_sig_info补充产生出相应的内容(见424-437行)。

最后,还要通过sigaddset将接收位图中相应的标志位设置成1。

成功地投递了信号并不说明这个信号就是在等待着处理了,还要看目标进程是否屏蔽了这个信号,所以回到deliver_signal中要调用sigismember加以检查(497行)。如果目标进程正在睡眠中,并且没有屏蔽所投递的信号,就要将其唤醒并立即进行调度(498行)。

函数signal_wake_up的代码如下,可以结合进程调度博客自行阅读:


/*
 * Tell a process that it has a new active signal..
 *
 * NOTE! we rely on the previous spin_lock to
 * lock interrupts for us! We can only be called with
 * "sigmask_lock" held, and the local interrupt must
 * have been disabled when that got acquired!
 *
 * No need to set need_resched since signal event passing
 * goes through ->blocked
 */
static inline void signal_wake_up(struct task_struct *t)
{
	t->sigpending = 1;

	if (t->state & TASK_INTERRUPTIBLE) {
		wake_up_process(t);
		return;
	}

#ifdef CONFIG_SMP
	/*
	 * If the task is running on a different CPU 
	 * force a reschedule on the other CPU to make
	 * it notice the new signal quickly.
	 *
	 * The code below is a tad loose and might occasionally
	 * kick the wrong CPU if we catch the process in the
	 * process of changing - but no harm is done by that
	 * other than doing an extra (lightweight) IPI interrupt.
	 */
	spin_lock(&runqueue_lock);
	if (t->has_cpu && t->processor != smp_processor_id())
		smp_send_reschedule(t->processor);
	spin_unlock(&runqueue_lock);
#endif /* CONFIG_SMP */
}

函数wake_up_process的代码在之前的博客已经度过,读者不妨重温一下。治理只是提一下:将目标进程唤醒以后,如果目标进程的优先级别高于当前进程,那么在当前进程从系统调用返回之际就有可能进行一次调度,而目标进程是否被调度运行,则取决于其优先级别及其他各种因素。线面还要谈这个问题。

并非所有的信号都是由某个进程在用户空间通过系统调用发送的。例如,在页面异常处理程序do_page_fault里,当页面异常无法恢复时就会通过force_sig_info向当前进程发送一个SIGBUS信号。函数force_sig_info是内核发送信号的基本手段。代码如下:

do_page_fault=>force_sig_info

/*
 * Force a signal that the process can't ignore: if necessary
 * we unblock the signal and change any SIG_IGN to SIG_DFL.
 */

int
force_sig_info(int sig, struct siginfo *info, struct task_struct *t)
{
	unsigned long int flags;

	spin_lock_irqsave(&t->sigmask_lock, flags);
	if (t->sig == NULL) {
		spin_unlock_irqrestore(&t->sigmask_lock, flags);
		return -ESRCH;
	}

	if (t->sig->action[sig-1].sa.sa_handler == SIG_IGN)
		t->sig->action[sig-1].sa.sa_handler = SIG_DFL;
	sigdelset(&t->blocked, sig);
	recalc_sigpending(t);
	spin_unlock_irqrestore(&t->sigmask_lock, flags);

	return send_sig_info(sig, info, t);
}

至此,信号的投递已经完成,接下来就是目标进程如何发现信号的到来以及如何对此作出反应了。

在中断机制中,处理器的硬件在每条指令结束时都要检测是否有中断请求存在。信号机制时纯软件的,当然不能依靠硬件来检测信号的到来。同时,要在每条指令结束时都来检测显然也是不现实的,甚至是不可能的。那么,一个进程在什么情况下检测信号的存在呢?首先是每当从系统调用、中断处理或异常处理返回用户空间的前夕;还有就是当前进程被吃泡面个睡眠中唤醒(必定是在系统调用中)的时候,此时若发现有信号在等待就要提前从系统调用返回。总而言之,不管是正常返回还是提前返回,在返回到用户空间的前夕总是要检测信号的存在并做出反映,这一点我们在之前的博客中已经提到过。下面是arch/i386/kernel/entry.S中的一个片段:

ret_with_reschedule:
	cmpl $0,need_resched(%ebx)
	jne reschedule
	cmpl $0,sigpending(%ebx)
	jne signal_return
restore_all:
	RESTORE_ALL

	ALIGN
signal_return:
	sti				# we can get here from an interrupt handler
	testl $(VM_MASK),EFLAGS(%esp)
	movl %esp,%eax
	jne v86_signal_return
	xorl %edx,%edx
	call SYMBOL_NAME(do_signal)
	jmp restore_all

建议读者回过头去看一下有关的内容,搞清楚sigpending(%ebx)就是current->sigpending。这里还要指出一点,一般来说,当进程运行于用户空间时,即使信号到达了也不会引起进程立刻对信号作出反应,而要到当前进程因某种原因(包括时钟中断)进入内核并从内核返回时才会作出反应,所以通常在时间上都会有一段延迟。可是,当信号来源于异常处理(或中断服务、系统调用)时,则由于进程已经在内核中运行,在返回到用户空间之前就会作出反应,所以几乎可以认为是立即就作出反应。特别是在异常处理时,这种反应发生在返回用户空间重新执行引起异常的那条指令之前,所以从用户空间的程序执行角度来看就是立即的。

对信号作出反应的具体操作是通过do_signal完成的。这又是一个比较大的函数,我们还是一段一段往下看。代码如下:

ret_with_reschedule=>do_signal

/*
 * Note that 'init' is a special process: it doesn't get signals it doesn't
 * want to handle. Thus you cannot kill init even with a SIGKILL even by
 * mistake.
 */
int do_signal(struct pt_regs *regs, sigset_t *oldset)
{
	siginfo_t info;
	struct k_sigaction *ka;

	/*
	 * We want the common case to go fast, which
	 * is why we may in certain cases get here from
	 * kernel mode. Just return without doing anything
	 * if so.
	 */
	if ((regs->xcs & 3) != 3)
		return 1;

	if (!oldset)
		oldset = &current->blocked;

读者不妨先回顾一下有关pt_regs数据结构的内容。在中断处理、异常处理或系统调用中,处理器的系统空间堆栈保存着该处理器在内核之前的现场,也就是各个寄存器在进入内核前的内容。linux的内核将系统空间堆栈中这些寄存器的映像看作一个数据结构,这就是pt_regs。所以,这里的指针regs指向系统空间堆栈中的这些寄存器映像,其中regs->xcs为处理器进入这些程序前代码段寄存器CS的内容。如果处理器是从用户空间进入中断、异常或陷阱(系统调用),则当时CS的最低两位必定是3(表示用户空间)。反过来,如果regs->xcs的最低两位不等于3的话,就说明本次中断或异常发生于系统空间,所以处理器并不是处于返回用户空间的前夕,并不需要对信号作出反应。否则,就要继续往下跑了

ret_with_reschedule=>do_signal

	for (;;) {
		unsigned long signr;

		spin_lock_irq(&current->sigmask_lock);
		signr = dequeue_signal(&current->blocked, &info);
		spin_unlock_irq(&current->sigmask_lock);

		if (!signr)
			break;

		if ((current->ptrace & PT_PTRACED) && signr != SIGKILL) {
			/* Let the debugger run.  */
			current->exit_code = signr;
			current->state = TASK_STOPPED;
			notify_parent(current, SIGCHLD);
			schedule();

			/* We're back.  Did the debugger cancel the sig?  */
			if (!(signr = current->exit_code))
				continue;
			current->exit_code = 0;

			/* The debugger continued.  Ignore SIGSTOP.  */
			if (signr == SIGSTOP)
				continue;

			/* Update the siginfo structure.  Is this good?  */
			if (signr != info.si_signo) {
				info.si_signo = signr;
				info.si_errno = 0;
				info.si_code = SI_USER;
				info.si_pid = current->p_pptr->pid;
				info.si_uid = current->p_pptr->uid;
			}

			/* If the (new) signal is now blocked, requeue it.  */
			if (sigismember(&current->blocked, signr)) {
				send_sig_info(signr, &info, current);
				continue;
			}
		}

		ka = &current->sig->action[signr-1];
		if (ka->sa.sa_handler == SIG_IGN) {
			if (signr != SIGCHLD)
				continue;
			/* Check for SIGCHLD: it's special.  */
			while (sys_wait4(-1, NULL, WNOHANG, NULL) > 0)
				/* nothing */;
			continue;
		}

		if (ka->sa.sa_handler == SIG_DFL) {
			int exit_code = signr;

			/* Init gets no signals it doesn't want.  */
			if (current->pid == 1)
				continue;

			switch (signr) {
			case SIGCONT: case SIGCHLD: case SIGWINCH:
				continue;

			case SIGTSTP: case SIGTTIN: case SIGTTOU:
				if (is_orphaned_pgrp(current->pgrp))
					continue;
				/* FALLTHRU */

			case SIGSTOP:
				current->state = TASK_STOPPED;
				current->exit_code = signr;
				if (!(current->p_pptr->sig->action[SIGCHLD-1].sa.sa_flags & SA_NOCLDSTOP))
					notify_parent(current, SIGCHLD);
				schedule();
				continue;

			case SIGQUIT: case SIGILL: case SIGTRAP:
			case SIGABRT: case SIGFPE: case SIGSEGV:
			case SIGBUS: case SIGSYS: case SIGXCPU: case SIGXFSZ:
				if (do_coredump(signr, regs))
					exit_code |= 0x80;
				/* FALLTHRU */

			default:
				sigaddset(&current->pending.signal, signr);
				recalc_sigpending(current);
				current->flags |= PF_SIGNALED;
				do_exit(exit_code);
				/* NOTREACHED */
			}
		}

		/* Reenable any watchpoints before delivering the
		 * signal to user space. The processor register will
		 * have been cleared if the watchpoint triggered
		 * inside the kernel.
		 */
		__asm__("movl %0,%%db7"	: : "r" (current->thread.debugreg[7]));

		/* Whee!  Actually deliver the signal.  */
		handle_signal(signr, ka, &info, oldset, regs);
		return 1;
	}

这是一个比较大的for循环(601-703行)。在每一轮循环中都要从进程的信号队列中通过dequeue_signal取出一个未加屏蔽的信号加以处理,知道信号队列中不再存在这样的信号(见608行),或者相应的信号向量为SIG_DFL,即对该信号采取默认的反应方式为止。而这默认的反应又是让接收到信号的进程寿终正寝(见688行),或者执行了一个由用户设置的信号处理程序之后(见702行)。

函数dequeue_signal的代码也在同一文件中,其代码并不复杂却颇为繁琐,我们就不深入进去了。简单地说,它参照一个屏蔽位图,在这里是current->blocked,从当前进程的信号队列中找到一个未加屏蔽的信号,即sigqueue数据结构,将其脱链并把其内容复制到数据结构info中,释放该sigqueue数据结构,然后将进程的信号接收位图中相应的标志位清0,并重新计算一下进程的sigpending标志。

当一个进程受到其父进程的跟踪而处于debug模式时,对信号的反应有一些特殊的考虑(611-641行)。我们这里先跳过它,到讲述ptrace时再回过头来看这一段代码。

如前所述,对具体信号的反应取决于其信号向量的设置,所以要根据信号的数值在信号向量表中找到相应的向量,即k_sigaction数据结构,并让指针ka指向这个数据结构。

如果设置的反应方式是忽略(SIG_IGN),那么一般来说对这个信号的处理就完成了(见646行)。但有一个例外,那就是当信号SIGCHLD时。这个信号通常是在一个子进程通过exit系统调用结束其生命时向父进程发出的。所以此时接收到SIGCHLD信号的进程要通过sys_wait4来检查其所有的子进程(第一个参数为-1),只要找到一个已经结束生命的子进程就为其料理后事。注意,这里的第三个参数WNOHANG,表示如果没有发现已经结束生命的子进程也要立即返回(正常返回值为0),而不作等待。

另一个特殊的反应方式是SIG_DFL。当信号向量为SIG_DFL时,多数信号(包括SIG_KILL)都会落入684行的default部分,通过do_exit结束进程的运行。对于SIG_KILL来说,一拉它的信号向量不容许设置的;二来向量中的相应屏蔽位也在每次设置信号向量时自动清0(均见do_sigaction),而且在通过sys_rt_sigprocmask设置进程的信号屏蔽位图时也会自动将SIGKILL的 屏蔽位自动清0。所以对于目标进程,这个信号是头号杀手。注意代码中的667和682行,这些地方都没有break语句。不过,pid为1的init进程对所有这些信号都有免疫力而不受任何影响(见658行)。

由此可见,当信号向量为SIG_IGN或SIG_DFL时,对信号的反应都在系统空间完成,而无须回到用户空间。

如果信号向量指向某个由用户设置的信号处理程序,那就要调用handle_signal予以执行了。我们将在后面加以介绍。

当601行的for循环正常结束时,也就是说当执行到第703行的后面时,当前进程中肯定已经没有未加屏蔽的信号了。这是因为在这个for循环中只有一个出口,即break语句,那就是在第609行处。而且已经处理过的信号肯定全都不是通过用户定义的信号处理程序进程的,否则就在702行处反悔了。再进一步,这些信号中也没有一个使进程结束运行,否则就在688行通过不会返回的do_exit退出了。换言之,这些信号只可能是SIGCHLD、SIGCONT、SIGWINCH、SIGTSTP、SIGTTIN、SIGTOU和SIGSTOP,或者信号向量设置成SIG_IGN的其他信号。这些信号如果来自某个系统调用的过程中,则往往标志着该系统调用过程的失败。这种情况常常发生在设备驱动程序中,并且往往会要求自动重新执行失败的系统调用,就好像在执行指令的过程中发生异常时要求重新执行失败的指令一样。那么,这是怎么实现的呢?让我们继续往下看do_signal:

ret_with_reschedule=>do_signal

	/* Did we come from a system call? */
	if (regs->orig_eax >= 0) {
		/* Restart the system call - no handlers present */
		if (regs->eax == -ERESTARTNOHAND ||
		    regs->eax == -ERESTARTSYS ||
		    regs->eax == -ERESTARTNOINTR) {
			regs->eax = regs->orig_eax;
			regs->eip -= 2;
		}
	}
	return 0;
}

首先要明白,regs->orig_eax为处理器进入系统空间前寄存器EAX的内容,而regs->eax则为系统调用的返回值。处理器在因系统调用而进入系统空间之前,寄存器EAX中为系统调用号。而系统调用号不会使负数,所以首先要检查regs->orig_eax是否大于等于0。另一方面,失败的系统调用若要求自动重新执行就会将EAX中的返回值regs->eax设置成负的ERESTARTNOHAND、ERESTARTSYS和ERESTARTNOINTR之一。所以,当regs->eax为这三者之一时,就将regs->orig_eax写回regs->eax并且将regs->eip的数值减2。我们在前面的博客中讲过,系统调用通过一条int $0x80指令实现的。在正常的情况下,当处理器执行该指令进入系统空间时其指令指针EIP指向其下一条指令,这样当处理器返回到用户空间时就会继续往下执行。现在,将regs->eip的数值减2,就使得处理器返回到用户空间时其EIP又回过去指向该INT指令了(因为INT指令的大小时两个字节),所以就会重新执行一次该系统调用。

如果用户设置了信号处理程序(在用户空间中),就要通过函数handle_signal准备好对处理程序的执行,其代码如下:

ret_with_reschedule=>do_signal=>handle_signal


/*
 * OK, we're invoking a handler
 */	

static void
handle_signal(unsigned long sig, struct k_sigaction *ka,
	      siginfo_t *info, sigset_t *oldset, struct pt_regs * regs)
{
	/* Are we from a system call? */
	if (regs->orig_eax >= 0) {
		/* If so, check system call restarting.. */
		switch (regs->eax) {
			case -ERESTARTNOHAND:
				regs->eax = -EINTR;
				break;

			case -ERESTARTSYS:
				if (!(ka->sa.sa_flags & SA_RESTART)) {
					regs->eax = -EINTR;
					break;
				}
			/* fallthrough */
			case -ERESTARTNOINTR:
				regs->eax = regs->orig_eax;
				regs->eip -= 2;
		}
	}

	/* Set up the stack frame */
	if (ka->sa.sa_flags & SA_SIGINFO)
		setup_rt_frame(sig, ka, info, oldset, regs);
	else
		setup_frame(sig, ka, oldset, regs);

	if (ka->sa.sa_flags & SA_ONESHOT)
		ka->sa.sa_handler = SIG_DFL;

	if (!(ka->sa.sa_flags & SA_NODEFER)) {
		spin_lock_irq(&current->sigmask_lock);
		sigorsets(&current->blocked,&current->blocked,&ka->sa.sa_mask);
		sigaddset(&current->blocked,sig);
		recalc_sigpending(current);
		spin_unlock_irq(&current->sigmask_lock);
	}
}

由于在do_signal中执行完handle_signal以后接着就返回了,所以这里也要考虑系统调用失败后重新执行的问题。不过,此时(执行用户设置的信号处理程序前夕)的重新执行(实际上是为重新执行所做的准备)却是有区分并且有条件的了。

前面讲过,由用户提供的信号处理程序是在用户空间执行的,而且执行完毕以后还要回到系统空间,这是由setup_rt_frame或setup_frame作出安排的。二者的代码大同小异,所以我们在这里只看setup_frame。在深入到代码中去之前,先大致介绍一下所涉及的一些问题和解决方案。大家知道,在调用一个子程序时,堆栈要往下(逻辑意义上是往上)伸展,这是因为需要在堆栈中保存子程序的返回地址,还因为子程序往往会有局部变量,也要占用堆栈中的空间。此外,调用子程序时的参数也是在堆栈中。子程序调用嵌套越深,则堆栈伸展的层次也越多。而堆栈中的每一个这样的层次,就称为一个框架即frame。当子程序和调用它的程序都在同一空间中时,堆栈的伸展,也就是在堆栈中框架的建立时很自然的。因为首先call指令本身就会将返回地址自动压入堆栈,而调用参数则通过push指令压入堆栈。其次,在堆栈中为局部变量分配空间也很简单,只要在进入子程序之后适当调整堆栈指针就可以了。然而,当二者不在同一空间是,情况就比较复杂了。从某种意义上讲,中断处理、异常处理以及系统调用,都可以看做是子程序调用,只不过调用者在用户空间而子程序在系统空间。所以,在返回到用户空间前夕,系统空间堆栈的内容,也就是指针regs所指向的pt_regs数据结构,实际上就是一个框架。这个框架的内容决定了当处理器回到用户空间时从何处继续执行指令,用户空间堆栈在何处以及各个寄存器的内容。现在,既然要求处理器在回到用户空间时要执行另一段程序,就得在系统空间堆栈为之准备一个不同的框架。可是,最终还得要回到当初作出系统调用或被中断的地方去,所以原先的框架也不能丢掉,要保存起来。保存在哪里呢?一个进程的系统空间堆栈的大小是很有限的,所以最合理的就是把它作为信号处理程序的附加局部变量,也就是保存在进程的用户空间堆栈的因调用该处理程序而形成的框架里。这样,就有必要在进入用户空间执行信号处理程序之前,就准备号用户空间堆栈中的框架,只有如此才能先把原先的框架复制到用户空间的框架中作为局部变量保存起来,回到系统空间中以后再从那里复制回来。框架的形成在程序运行过程中,特别是在子程序调用的过程中自然形成的,但是框架的形成也有其规律可循。现在尚未执行对信号处理程序的调用,当然也不存在调用该处理程序的框架,所以实际上是按照形成框架的规律先做好准备,预先在用户空间堆栈中打下一些埋伏。

另一个问题是,在用户空间执行完信号处理程序以后,又怎样重返系统空间?我们知道,从用户空间进入系统空间的手段无非就是中断、异常以及陷阱,而系统调用正是陷阱的运用。显然,中断和异常都不如系统调用更为合适,所以内核中为了这个目的而专门设置了一个系统调用sigreturn。不过,要求用户在其信号处理程序中调用一个特别的库函数或系统调用时不大合适的。因为一来那样就对信号处理程序有了特殊的要求,二来更难以保证用户不会忘记在其程序中作出这样的调用,并且C编译也难保证正编译时加以检查(当然,也并非绝不可能)。所以,最好是由内核在启动信号处理程序时自动地插入这个调用。

这样,思路就渐渐清晰了。整个过程大致上可以归纳为以下这些步骤:

  1. 在用户空间堆栈中为信号处理程序的执行预先创建一个框架,框架中包括一个作为局部变量的数据结构,并把系统空间堆栈中的原始框架保存到这个数据结构中。
  2. 在信号处理程序中插入对系统调用sigreturn的调用。
  3. 将系统空间堆栈中原始框架修改成为执行信号处理程序所需的框架。
  4. 返回到用户空间,但是却执行信号处理程序。
  5. 信号处理程序执行完毕以后,通过系统调用sigreturn重返系统空间。
  6. 在系统调用sigreturn中从用户空间恢复原始框架。
  7. 再返回到用户空间,继续执行原先的用户程序。

知道了这个大致过程,有关的源代码就比较容易理解了。先来看文件arch/i386/kernel/signal.c中的函数setup_frame:

ret_with_reschedule=>do_signal=>handle_signal=>setup_frame


static void setup_frame(int sig, struct k_sigaction *ka,
			sigset_t *set, struct pt_regs * regs)
{
	struct sigframe *frame;
	int err = 0;

	frame = get_sigframe(ka, regs, sizeof(*frame));

	if (!access_ok(VERIFY_WRITE, frame, sizeof(*frame)))
		goto give_sigsegv;

	err |= __put_user((current->exec_domain
		           && current->exec_domain->signal_invmap
		           && sig < 32
		           ? current->exec_domain->signal_invmap[sig]
		           : sig),
		          &frame->sig);
	if (err)
		goto give_sigsegv;

	err |= setup_sigcontext(&frame->sc, &frame->fpstate, regs, set->sig[0]);
	if (err)
		goto give_sigsegv;

	if (_NSIG_WORDS > 1) {
		err |= __copy_to_user(frame->extramask, &set->sig[1],
				      sizeof(frame->extramask));
	}
	if (err)
		goto give_sigsegv;

	/* Set up to return from userspace.  If provided, use a stub
	   already in userspace.  */
	if (ka->sa.sa_flags & SA_RESTORER) {
		err |= __put_user(ka->sa.sa_restorer, &frame->pretcode);
	} else {
		err |= __put_user(frame->retcode, &frame->pretcode);
		/* This is popl %eax ; movl $,%eax ; int $0x80 */
		err |= __put_user(0xb858, (short *)(frame->retcode+0));
		err |= __put_user(__NR_sigreturn, (int *)(frame->retcode+2));
		err |= __put_user(0x80cd, (short *)(frame->retcode+6));
	}

	if (err)
		goto give_sigsegv;

	/* Set up registers for signal handler */
	regs->esp = (unsigned long) frame;
	regs->eip = (unsigned long) ka->sa.sa_handler;

	set_fs(USER_DS);
	regs->xds = __USER_DS;
	regs->xes = __USER_DS;
	regs->xss = __USER_DS;
	regs->xcs = __USER_CS;
	regs->eflags &= ~TF_MASK;

#if DEBUG_SIG
	printk("SIG deliver (%s:%d): sp=%p pc=%p ra=%p\n",
		current->comm, current->pid, frame, regs->eip, frame->pretcode);
#endif

	return;

give_sigsegv:
	if (sig == SIGSEGV)
		ka->sa.sa_handler = SIG_DFL;
	force_sig(SIGSEGV, current);
}

首先是用户空间中的框架,即sigframe数据结构,定义如下:

/*
 * Do a signal return; undo the signal stack.
 */

struct sigframe
{
	char *pretcode;
	int sig;
	struct sigcontext sc;
	struct _fpstate fpstate;
	unsigned long extramask[_NSIG_WORDS-1];
	char retcode[8];
};

这个数据结构实际上只是框架的一部分,因为具体的信号处理程序本身也会有其局部变量。所以,这个数据结构中的成分都是附加局部变量,而对于信号处理程序来说是不可见的(在信号处理程序中不能引用这些局部变量)。其中sigcontext数据结构定义如下:


struct sigcontext {
	unsigned short gs, __gsh;
	unsigned short fs, __fsh;
	unsigned short es, __esh;
	unsigned short ds, __dsh;
	unsigned long edi;
	unsigned long esi;
	unsigned long ebp;
	unsigned long esp;
	unsigned long ebx;
	unsigned long edx;
	unsigned long ecx;
	unsigned long eax;
	unsigned long trapno;
	unsigned long err;
	unsigned long eip;
	unsigned short cs, __csh;
	unsigned long eflags;
	unsigned long esp_at_signal;
	unsigned short ss, __ssh;
	struct _fpstate * fpstate;
	unsigned long oldmask;
	unsigned long cr2;
};

显然,这个数据结构就是用来保存系统空间堆栈的原始框架的。至于sigframe结构中其他成分的作用与用途,等一下就会清楚。框架的结构确定额,还要确定其在用户空间中的位置,这就是get_sigframe要做的是:

ret_with_reschedule=>do_signal=>handle_signal=>setup_frame=>get_sigframe


/*
 * Determine which stack to use..
 */
static inline void *
get_sigframe(struct k_sigaction *ka, struct pt_regs * regs, size_t frame_size)
{
	unsigned long esp;

	/* Default to using normal stack */
	esp = regs->esp;

	/* This is the X/Open sanctioned signal stack switching.  */
	if (ka->sa.sa_flags & SA_ONSTACK) {
		if (! on_sig_stack(esp))
			esp = current->sas_ss_sp + current->sas_ss_size;
	}

	/* This is the legacy signal stack switching. */
	else if ((regs->xss & 0xffff) != __USER_DS &&
		 !(ka->sa.sa_flags & SA_RESTORER) &&
		 ka->sa.sa_restorer) {
		esp = (unsigned long) ka->sa.sa_restorer;
	}

	return (void *)((esp - frame_size) & -8ul);
}

这里的regs->esp是用户空间中当前的堆栈指针,也就是进入系统空间之前的堆栈指针,所以在典型情况下执行信号处理程序的框架就要从这一点往下伸展。但是,有两个例外。第一个例外是用户进程已经通过系统调用sigaltstack为信号处理程序的执行设置了替换堆栈,并且在设置信号向量时将flags中的标志位SA_ONSTACK设成1。这种情况下当前进程的task_struct结构中sas_ss_sp和sas_ss_size分别为所设置的堆栈位置和大小,而current->sas_ss_sp + current->sas_ss_size则为该堆栈空间的顶点,堆栈从这一点开始向下伸展。不过,先要检查一下是否已经在这个替换堆栈上。这里的inline函数on_sig_stack的定义如下:

ret_with_reschedule=>do_signal=>handle_signal=>setup_frame=>get_sigframe=>on_sig_stack

/* True if we are on the alternate signal stack.  */

static inline int on_sig_stack(unsigned long sp)
{
	return (sp - current->sas_ss_sp < current->sas_ss_size);
}

第二个例外与在执行完信号处理程序后重返系统空间的过程有关。如前所述,最妥当的办法是让内核自动插入一些代码,通过系统调用sigreturn解决这个问题。也就是说,把这个问题交给操作系统,用户进程就不用操这个心了。可是在发展的过程中有过一段时期,曾经在系统调用sigaction的界面上提供了一个收单,让用户给定一段程序用户这个目的,这就是sigaction数据结构中的指针sa_restore。现在,系统调用sigaction的man页面中已经明确讲sa_restore已经过时并且不应使用,但是出于兼容的需要还得考虑其存在。所以,如果使用了sa_restore,就要把框架的顶点设置在这个位置上。

至此,框架的顶点已经确定了。由于堆栈时向下伸展的,所以这个框架(其实是框架的一部分)的起始地址在其下方相差一个frame_size的地方,而frame_size正是sigframe数据结构的大小。注意这里的无符号长整数-8实际上是0xffff fff8,用以对齐框架起始地址的边界。这样,对于信号处理程序,这个sigframe数据结构就相当于一个额外的调用参数。

用户空间框架的位置frame已经确定,让我们回到setup_frame中。接着检验一下用户空间中的这一部分是否可写,然后就是往用户空间的这个框架中复制信息了。这里的__put_user将其第一个参数复制到用户空间中由其第二个参数所指向的地址。首先是frame->sig,因为这个量有点特殊。在有的执行域,即Unix变种里,为信号定义的数值可能会有所不同。在这种情况下相应exec_demain数据结构中的指针signal_invmap会指向一个信号变换表,所以要把这个因素考虑进去。下面就是复制系统空间堆栈上的pt_regs结构以及一些有关的内容了,包括有关浮点处理器的内容和信号屏蔽位图:

ret_with_reschedule=>do_signal=>handle_signal=>setup_frame=>setup_sigcontext


/*
 * Set up a signal frame.
 */

static int
setup_sigcontext(struct sigcontext *sc, struct _fpstate *fpstate,
		 struct pt_regs *regs, unsigned long mask)
{
	int tmp, err = 0;

	tmp = 0;
	__asm__("movl %%gs,%0" : "=r"(tmp): "0"(tmp));
	err |= __put_user(tmp, (unsigned int *)&sc->gs);
	__asm__("movl %%fs,%0" : "=r"(tmp): "0"(tmp));
	err |= __put_user(tmp, (unsigned int *)&sc->fs);

	err |= __put_user(regs->xes, (unsigned int *)&sc->es);
	err |= __put_user(regs->xds, (unsigned int *)&sc->ds);
	err |= __put_user(regs->edi, &sc->edi);
	err |= __put_user(regs->esi, &sc->esi);
	err |= __put_user(regs->ebp, &sc->ebp);
	err |= __put_user(regs->esp, &sc->esp);
	err |= __put_user(regs->ebx, &sc->ebx);
	err |= __put_user(regs->edx, &sc->edx);
	err |= __put_user(regs->ecx, &sc->ecx);
	err |= __put_user(regs->eax, &sc->eax);
	err |= __put_user(current->thread.trap_no, &sc->trapno);
	err |= __put_user(current->thread.error_code, &sc->err);
	err |= __put_user(regs->eip, &sc->eip);
	err |= __put_user(regs->xcs, (unsigned int *)&sc->cs);
	err |= __put_user(regs->eflags, &sc->eflags);
	err |= __put_user(regs->esp, &sc->esp_at_signal);
	err |= __put_user(regs->xss, (unsigned int *)&sc->ss);

	tmp = save_i387(fpstate);
	if (tmp < 0)
	  err = 1;
	else
	  err |= __put_user(tmp ? fpstate : NULL, &sc->fpstate);

	/* non-iBCS2 extensions.. */
	err |= __put_user(mask, &sc->oldmask);
	err |= __put_user(current->thread.cr2, &sc->cr2);

	return err;
}

我们把这个函数的代码留给读者。完成以后,如果信号屏蔽位图的大小超过一个长整数的大小,则还要把超出的部分也复制过去。

下面是关键的部分,也就是对重返系统空间进行安排了。在sigframe数据结构中有个8字节的数字retcode,还有个指针precode。指针precode指向一段进程在执行完信号处理程序后重返系统空间的代码。如果用户提供了这么一个函数,就把该函数指针复制到precode;否则,在典型的情况下,就使这个指针指向retcode(见424行),并且在retcode中预先写入这样三条指令(见426-428行):

popl  %eax
movl $__NR_sigreturn, %exa;
int $0x80

这三条指令正好占8个字节。指令中的__NR_sigreturn为系统调用sigreturn的调用号。经过这样处理以后,用户空间中堆栈的构成如下图所示。

 

这里要指出,当前进程返回到用户空间时(下面就会看到),是返回而不是调用进入信号处理程序的,所以在pretcode的下方不会再压入一个返回地址。这样,pretcode就正好出在本来应该是信号处理程序运行框架中返回地址的位置上。在信号处理程序的末尾执行ret指令时,就会把它当成返回地址而转入预先埋伏在retcode中的三条指令,或者由用户另行提供的sa_restore函数。而位于pretcode上方的sig,则成为对信号处理程序的第一个调用参数。可见,sigframe数据结构的内容,包括各个字段的次序,是根据整个执行过程精心设计好的,不能随便更改。

读者也许要问:这里一共就是三条指令,进程在执行int $0x80执行进入sigreturn系统调用之后最终还要回到用户空间来,那时候按理应该回到它的下一条指令,可是这里没有指令了啊。答案是,在系统调用sigreturn中,要从用户空间的这个框架中恢复转入用户空间之前的原始框架,所以到那时候就会回到原先应该去的地方,也就是当初发生中断、异常或系统调用的地方。

安排好了用户空间中的框架,就是安排系统空间的框架了。这里最关键的是返回到用户空间时的堆栈指针regs->esp和取指令指针regs->eip。还有就是一些段寄存器,不过其实这些寄存器的值本来也就是__USER_DSHE __USER_CS,只有在特殊的情况下才有例外。至于一些通用寄存器,如%eax、%ebx等,则对于信号处理程序并无意义,所以不需要设置。最后,处理器在用户空间时有可能正处于硬件跟踪模式,而信号处理程序相应于中断处理,所以在及你如这段程序时要把硬件跟踪关闭,也就是在标志寄存器的映像regs->eflgas中将TF为清0。经过这样的安排以后,就为表面上按正常途径返回用户空间,但是实际上却转入信号处理程序做好了准备。最终,处理器经由setup_frame、handle_signa和do_signal逐层返回到entry.S中的signal_return,从而进入restore_all。从那以后的过程读者应该已经熟悉了。由于regs->esp和regs->eip的设置,处理器进入用户空间时从ka->sa.sa_handler所指向的地方开始执行,而堆栈指针则指向前面设置好了的框架,实际上是指向frame->pretcode,即信号处理程序的返回地址,正好跟在用户空间中通过call语句进入信号处理程序时一样。

信号处理程序执行完毕以后,处理器又通过系统调用sigreturn进入系统空间。内核中实现这个系统调用的主体是sys_sigreturn,其代码如下:

asmlinkage int sys_sigreturn(unsigned long __unused)
{
	struct pt_regs *regs = (struct pt_regs *) &__unused;
	struct sigframe *frame = (struct sigframe *)(regs->esp - 8);
	sigset_t set;
	int eax;

	if (verify_area(VERIFY_READ, frame, sizeof(*frame)))
		goto badframe;
	if (__get_user(set.sig[0], &frame->sc.oldmask)
	    || (_NSIG_WORDS > 1
		&& __copy_from_user(&set.sig[1], &frame->extramask,
				    sizeof(frame->extramask))))
		goto badframe;

	sigdelsetmask(&set, ~_BLOCKABLE);
	spin_lock_irq(&current->sigmask_lock);
	current->blocked = set;
	recalc_sigpending(current);
	spin_unlock_irq(&current->sigmask_lock);
	
	if (restore_sigcontext(regs, &frame->sc, &eax))
		goto badframe;
	return eax;

badframe:
	force_sig(SIGSEGV, current);
	return 0;
}	

显然,这段程序的作用,就是从用户空间执行信号处理程序的框架中恢复当初系统空间中的原始框架。我们把这段程序留给读者自己阅读,不过有两点要提示一下。

首先,系统调用的框架就是系统空间堆栈上的pt_regs数据结构。在sys_sigreturn中取第一个调用参数__unused的地址就是得到了这个结构的起始地址。读者不妨回顾一下前面的有关内容。在执行完宏操作SAVE_ALL以后,系统空间堆栈中的最后一项,也就是pt_regs结构中的第一项,是%ebx的内容。它的下面就是调用sys_sigreturn的第一个(也是唯一的)参数__unused。不贵这里并不需要用到这个参数的内容,而只是要只熬到它在堆栈中的地址,因为这就是pt_regs数据结构的起始地址。

还有,就是用户空间中的框架,也就是sigframe数据结构的起始地址frame。该结构中底部的第一项pretcode就是信号处理程序的返回地址,所以当处理器从信号处理程序返回时,堆栈指针就调整到了这一项上方,也就是起始地址加4个字节的地方。然后,前述三条指令中的第一条popl %eax又使堆栈指针往上调了4个字节。这样,当处理器在用户空间执行int指令进入系统空间时,其用户空间的堆栈指针指向该sigframe结构的起始地址再加上8个字节的地方,所以(regs->eps-8)就是这个结构的起始地址。

函数中其余的代码,以及处理器使用恢复后的原始框架返回用户空间的过程,读者应该不会有什么困难了。

读者也许要问,既然通过sigreturn重返系统空间以后实际上不干什么事,只是恢复了原始框架以后就从原先的系统调用(或中断)返回了,那么是否可以简化一点呢?例如,可以在用户空间堆栈中,从当前的系统调用框架向下调整,先将系统空间堆栈中的返回的孩子搬到用户空间堆栈中,而把系统空间堆栈中的返回地址,改成指向用户空间的信号处理程序。这样,从当前系统调用返回时就会返回到用户空间中的信号处理程序。而在执行完信号处理程序后碰到ret指令时,则又返回到原先进行系统调用或发生中断的地方。这样,整个过程简化了,代码也简单了,而系统调用sigreturn与不需要了,岂不很好?事实上,早期的Unix(如第六版)正是这样做的。但是在这样的解决方案中必须有个保证就是用户空间的信号处理程序对于处理器的工作现场(即内核中通过SAVE_ALL保存的所有寄存器的内容)完全透明,即不改变这些寄存器的内容。例如,如果能保证在进入信号处理程序时一定会调用一段类似于SAVE_ALL的程序,而在离开信号处理程序之前则调用一段类似于RESTORE_ALL的程序,那就没有问题了。然而,信号处理程序是由用户开发,且在用户空间中运行的,没有一个通用有效并且可靠的放吧可以保证用户开发的程序对寄存器内容的透明性。明白了这一点,就可以理解为什么要散费苦心地来设计一个sigreturn系统调用了。

通过对从设置信号向量、发送信号、到执行信号处理程序的全过程的了解,并且将此过程中与中断机制中设置中断向量、中断请求、到执行中断处理程序的过程加以类比,读者应该对信号机制有了比较深入的理解。当然,进程之间通过信号机制的互动要有用户程序的参与,而那已是属于应用程序设计的范畴了。有兴趣(或有需要)的读者可以参考有关专著。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值