[隐藏]

一、概念

信号是什么?信号本质上是在软件层次上对中断的一种模拟,即软件中断。提供了一种处理异步事件的方法。例如,在终端运行一个程序然后输入中断键,则会通过信号机制终止这个进程。

每个信号都有一个名字,这些名字都以SIG开头。在终端运行 kill -l 可以查看系统支持的所有信号。下面是在Linux中查看的结果:

可以看到,Linux支持62个信号。比较奇怪的是,紧接着31号的是34号,而且从34号开始,名字都是SIGRTMIN加或SIGRTMAX减去一个整数。

实际上,过去Linux系统是只支持前31个信号的,也就是所谓了经典信号,SIGRTMIN和SIGRTMAX之间是针对实时进程引入的信号。

经典信号都是不可靠信号,也就是说,信号可能丢失,比如发送多次相同的信号,进程只能收到一次,因为当信号发送到程序时并不是立即执行处理函数而是等待到某个时机再执行,在这个时机还没有到来之前你无论发送一个信号多少次都只会记录一个;另一方面,信号不支持排队,信号的响应不保证顺序,发送信号的顺序和信号响应的顺序根本没有关系。

实时信号都是可靠信号,信号不会丢失,发多少次就能收到多少次,也支持排队。

这些符号都是被定义在<signal.h>中的正整数(实际上,Linux将它们定义在<bits/signum.h>中,并被<signal.h>引入)。

产生信号的事件对进程是随机出现的(显然,进程无法预先知晓你何时会输入中断键)。进程不能简单地测试一个变量(例如errno)来判断是否出现了一个信号,而是告诉内核“先此信号出现时,请执行这个操作”。

为了做到这一点,用户需要定义一个函数(信号处理程序),然后通知内核在某信号发生时执行它。这种方式叫做“捕捉信号”。

但是我们写程序的时候并没有定义一个函数捕捉中断信号啊,为什么它可以终止进程呢?

事实上,在用户没有定义信号处理函数时,内核根据信号类型执行默认动作:

忽略 什么都不做。你可以把大多数信号的处理方式都设为忽略,但有两个信号却不能被忽略。SIGKILL和SIGSTOP。原因是:它们向超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号(例如非法内存引用或除以0),则进程的运行行为是未定义的。
终止 终止进程或进程组
停止 将进程置于TASK_STOPPED状态
core 终止进程,并在进程当前工作目录创建地址空间的内存转储文件,复制进程的存储映像供进一步处理(例如,由GDB查看调试)

下表列出了Linux经典信号的详细说明

名字 说明 默认动作 详细说明
SIGHUP 连接断开 终止 如果终端接口检测到一个连接断开,则将此信号发送给与该终端相关的控制进程(会话首进程)。注意,接到此信号的会话首进程可能在后台,这有别于由终端正常产生的几个信号(中断、退出、挂起),这些信号总是传递给前台进程组。

如果会话首进程终止,则也产生此信号,此信号将被发送给前台进程组中的每一个进程。

SIGINT 终端中断符 终止 当用户按中断键(Ctrl+C)时,终端驱动程序产生此信号并发送至前台进程组的每个进程。
SIGQUIT 终端退出符 core 当用户按退出键(Ctrl+\)时,终端驱动程序产生此信号并发送至前台进程组的每个进程。
SIGILL 非法硬件指令 core 此信号指示进程执行了一条非法硬件指令
SIGTRAP 硬件故障 core 指示一个实现定义的硬件故障(当执行断点指令时,实现常用此信号将控制转移至调试程序)
SIGABRT 异常终止(abort) core 调用 abort 函数时产生此信号,进程异常终止
SIGBUS 硬件故障 core 指示一个实现定义的硬件故障。当出现某些类型的内存故障时,实现常常产生此信号
SIGFPE 算术异常 core 表示一个算术运算异常,例如除以0
SIGKILL 终止 终止 两个不能被捕获或忽略的信号之一。可用 kill -9 命令发送
SIGUSR1 用户定义的信号 终止 用户定义的信号,可用于应用程序
SIGSEGV 无效内存引用 core 指示进程进行了一次无效内存引用
SIGUSR2 用户定义的信号 终止 与 SIGUSR1 类似,用户定义的信号,可用于应用程序
SIGPIPE 写至无读进程的管道 终止 如果写在管道时读进程已终止,则产生此信号
SIGALRM 超时(alarm) 终止 在用 alarm 函数设置的定时器超时时,产生此信号。若由 setitimer 函数设置的间隔时间超时时,也会产生此信号
SIGTERM 终止 终止 kill 命令发送的系统默认终止信号
SIGSTKFLT 协处理器栈故障 终止 出现在Linux早期版本,旨在用于数学协处理器的栈故障。该信号并非由内核产生,但仍保留以向后兼容
SIGCHLD 子进程状态改变 忽略 在一个进程终止或停止时,将此信号发送给其父进程
SIGCONT 使暂停进程继续 继续/忽略 如果接收此信号的进程处于停止状态,则系统默认的动作是使该进程继续运行,否则默认动作是忽略此信号
SIGSTOP 停止 停止 两个不能被捕捉或忽略的信号之一,用于暂停一个进程的运行
SIGTSTP 终端停止符 停止 交互式停止信号,当用户在终端上按挂起键(Ctrl+Z) 时,终端驱动程序产生此信号。该信号发送至前台进程组中的所有进程
SIGTTIN 后台读控制tty 停止 当一个后台进程组中的进程试图读其控制终端时,终端驱动程序产生此信号。在下列情况下不产生此信号:a.读进程忽略或阻塞此信号;b.读进程所属的进程组是孤儿进程组,此时读操作返回出错,并将errno设置为EIO
SIGTTOU 后台写控制tty 停止 当一个后台进程组中的进程试图写到其控制终端时产生此信号。与 SIGTTIN 不同,一个进程可以选择允许后台进程写到控制终端。如果不允许后台进程写,在下列情况下不产生此信号:a.写进程忽略或阻塞此信号;b.写进程所属的进程组是孤儿进程组,此时写操作返回出错,并将 errno 设置为 EIO
SIGURG 紧急情况(套接字) 忽略 此信号通知进程已经发生一个紧急情况。在网络连接上接收到带外数据时,可选择产生此信号
SIGXCPU 超过CPU限制(setrlimit) core 如果进程超过了其软CPU时间限制,则产生此信号
SIGXFSZ 超过文件长度限制(setrlimit) core 如果进程超过了其软文件长度限制,则产生此信号
SIGVTALRM 虚拟时间闹钟(setitimer) 终止 由 setitimer 函数设置的虚拟间隔时间超时时产生此信号
SIGPROF 梗概时间超时(setitimer) 终止 由 setitimer 函数设置的梗概统计间隔时间超时时产生此信号
SIGWINCH 终端窗口大小改变 忽略 内核维持与每个终端或伪终端相关联的窗口大小。进程可以用 ioctl 函数 得到或设置窗口的大小。如果进程用 ioctl 设置窗口大小命令更改了窗口大小,则内核产生此信号并将其发送至前台进程组
SIGIO 异步I/O 终止 指示一个异步I/O事件
SIGPWR 电源失效/重启动 终止 接到蓄电池电压过低信息的进程将信号 SIGPWR 发送给 init 进程,由 init 处理停机操作
SIGSYS 无效系统调用 core 指示一个无效的系统调用

注意,所有实时信号的默认操作都是终止进程。

二、信号处理机制的实现

内核中与信号相关的数据结构如下图所示:

sighand_struct 结构用于管理设置的信号处理程序的信息。其中 count 保存了共享该实例的进程数目;设置的信号处理函数保存在 action 数组中,共有_NSIG项,_NSIG指定了可以处理的不同信号的数目,不出所料,大多数平台上其值为64。

k_sigaction 结构中有一个类型为 sigaction 的成员。sigaction 包含了用于描述处理函数的字段:

  • sa_handler 是一个指针,指向我们所说的信号处理函数,如果没有为信号设置用户定义的处理函数,则其值为 SIG_DFL
  • sa_mask 包含了一个位掩码,每个 bit 对应于系统中的一个信号。它用于在处理函数执行期间阻塞其他信号。在函数执行结束后,内核会重置其值,恢复到信号处理之前的原值
  • sa_flag 包含了额外的标志,用于指定信号处理方式的一些约束。

所有阻塞信号由 task_struct 的 blocked 成员指定。可以看到,sigset_t 类型里有 _NSIG_WORD 项的 unsigned long 类型的数组,实际上它是一个位掩码(也就是我们下面要提到的“信号集”),所包含的比特位数至少与所支持的信号数目相同。内核中 _NSIG_WORD 是这样计算的:

pending 用于建立一个链表,包含了所有引发、仍然有待内核处理的信号。signal 指定了仍然有待处理的信号集。sigpending 结构使用了内核中通用的链表结构 list_head 实现双链表。链表元素的类型是 sigqueue,其中的 siginfo_t 类型的成员包含了有关待决信号的详细信息 。

内核提供了 kill 和 tkill 系统调用分别向进程组或单个进程发送信号。当用户调用它们给某一进程发送信号时,内核进行过权限检查,确认该进程有向目标发送的权限后,就会产生一个 sigqueue 实例,填充数据后添加到目标进程的 sigpending 链表。

但是该系统调用并不会触发信号队列的处理,而是在每次由核心态切换到用户态时,内核发起信号队列的处理。

下图说明了大致的流程:

内核在进行一些处理后,将调用 do_signal 函数,do_signal 又调用了 handle_signal 操作进程在用户态的栈,使得在从核心态切换到用户态之后运行信号处理程序。

处理程序结束后将调用 sigreturn 系统调用,该函数负责恢复进程上下文,使得下一次切换到用户态时,应用程序可以继续运行。

三、发送和捕获信号

#include <signal.h>

void (*signal(int signo, void (*func)(int)))(int);

返回值:若成功返回信号以前的处理函数,若出错则返回SIG_ERR

int kill(pid_t pid, int signo);

int raise(int signo);

两个函数的返回值:若成功则返回0,若出错则返回-1

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

返回值:0或以前设置的闹钟时间的剩余秒数

int pause(void);

返回值:-1,并将 errno 设置为 EINTR

signal 函数接受两个参数:signo 代表需要被捕捉的特定 signal 的整数值;func 是指向信号处理函数的指针,该函数接受一个代表特定 signal 的整数值,返回值类型为void。func 的值也可以是 SIG_IGN 或 SIG_DEL 表示忽略该信号或执行默认动作,这些值也定义在<signal.h>中(实际上也是在<bits/signum.h>中定义并被引入<signal.h>的):

该函数的原型实在是太复杂了,让我们来分析一下吧。

首先我们知道,信号处理函数的原型:

void sigfunc(int);

现在假定我们希望声明一个指向 sigfunc 的函数指针,不妨命名为 sfp, 所以*sfp 就等于 sigfunc ,因此我们可以这样声明 sfp:

void (*sfp)(int);

因为 signal 函数的返回值类型与 sfp 的类型一样,我们可以如下声明 signal 函数:

void (*signal(something))(int);

我们已经知道,signal 的两个参数:一个整数,一个函数指针。sfp 的类型(void (*)(int))即符合第二个参数。

因此我们得到 signal 函数的声明:

void (*signal(int, void (*)(int)))(int);

当然,使用 typedef 可使其简单一些:

typedef  void  sigfunc(int);

sigfunc*  signal(int,  sigfunc *);

kill 函数将信号发送给进程或进程组。raise 则允许进程向自身发送信号。

调用

raise(signo);

等价于调用

kill(getpid(), signo);

kill 的 pid 参数有四种不同的情况:

pid > 0 将该信号发送给进程ID为 pid 的进程
pid == 0 将信号发送给与发送进程属于同一进程组的所有进程。“所有进程”不包括某些系统进程
pid < 0 将信号发送给其进程组ID等于 pid 的绝对值的所有进程。“所有进程”不包括某些系统进程
pid == -1 将信号发送给发送进程有权限向它们发送信号的系统上的所有进程。“所有进程”不包括某些系统进程

如上所述,进程发送信号时,内核将进行权限检查。超级用户可以给任一进程发送信号。对于普通用户,其基本规则是发送者的实际或有效用户ID等于接收者的实际或有效用户ID。但也有一个特例:如果被发送的信号是 SIGCONT,则进程可以将它发送给属于同一会话的任何进程。

如果 signo 参数为 0(POSIX.1将其定义为空信号),kill 仍执行正常的错误检查,但不发送信号。这常被用来检测某一进程是否存在。如果向一个不存在的进程发送信号,则 kill 返回 -1,并将 errno 设置为 ESRCH。但是,UNIX系统在经过一段时间后会重新使用进程ID,所以一个现有的具有所给定进程ID的进程可能并不是你真正想检测的进程。

使用 alarm 函数可以设置一个定时器,在 seconds 秒后会超时,产生 SIGALRM 信号。

每个进程只能有一个闹钟。如果在调用 alarm 时,已经为该进程设置过闹钟,而且它还没有超时,则将该闹钟的剩余时间值作为本次 alarm 调用的返回值。以前的闹钟则被新值代替。

pause 函数使调用进程挂起直到捕捉到一个信号。只有执行一个信号处理程序并从中返回时,pause 才返回。

四、信号集

在前面讲信号处理机制的实现的时候提到了信号集,现在就来看看如何在用户应用程序中使用信号集。

信号集的数据类型是 sigset_t,在Linux内核中使用 unsigned long 类型的数组来表示,但是因为数组的长度会根据系统可处理信号的数量的不同而不同,所以为了提高程序的可移植性,不能直接操作数组成员来处理信号集。实际上系统提供了处理信号集的函数:

#include <signal.h>

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set);

int sigdelset(sigset_t *set);

四个函数的返回值:若成功则返回0,若出错则返回-1

int sigismember(const sigset_t *set, int signo);

返回值:若真则返回1,若假则返回0,若出错则返回-1

函数 sigemptyset 初始化由 set 指向的信号集,清除其中所有信号。

函数 sigfillset 初始化由 set 指向的信号集,使其包含所有信号。

一旦初始化一个信号集后,函数 sigaddset 和函数 sigdelset 分别添加或删除一个信号。

函数 sigismember 测试某一信号是否在信号集中。

#include <signal.h>

int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);

int sigpending(sigset_t *set);

int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);

三个函数的返回值:若成功则返回0,若出错则返回-1

int sigsuspend(const sigset_t *sigmask);

返回值:-1,并将 errno 设置为 EINTR

在前面我们提到,task_struct 结构有一个 blocked 成员(我们称之为“信号屏蔽字”),它指定了进程阻塞的信号,被阻塞的信号将不能被递送给进程,直到进程解除阻塞。在信号被阻塞时,内核将其放置到待决列表上。如果同一个信号在阻塞期间被发送了多次,则在待决列表中只放置一次。也就是说,不管发送了多少相同的信号,在进程删除阻塞后,都只会接收到一个信号。

调用函数 sigprocmask 可以检测或更改其信号屏蔽字。

oset 是非空指针,那么进程的当前信号屏蔽字通过 oset 返回。

set 是非空指针,则 how 指定如何修改当前信号屏蔽字:

how 说明
SIG_BLOCK  将 set 指定的信号集和当前信号屏蔽字做并集后的结果作为新的信号屏蔽字,set 包含我们希望阻塞的附加信号
SIG_UNBLOCK   将 set 指定的信号集和当前信号屏蔽字做补集的交集后的结果作为新的信号屏蔽字,set 包含我们希望解除阻塞的信号
SIG_SETMASK  将新的信号屏蔽字设置为 set 所指向的信号集

如果 set 是空指针,则不改变信号屏蔽字,how 的值也没有意义。

在调用 sigprocmask 后如果有任何未决的、不再阻塞的信号,则在 sigprocmask 返回前,至少会将其中一个信号递送给该进程。

现在考虑一种情况。假定希望对一个已被阻塞的信号解除阻塞,然后调用 pause 以等待这个信号发生。假定信号是SIGINT,这该如何实现呢?直观上,我们可能这样实现:

如果在信号被阻塞时给进程发送该信号,那么该信号的递送就被推迟到对它解除阻塞。对应用程序而言,该信号好像发生在解除信号阻塞和 pause 调用之间。这样在调用 pause 的时候信号就已经被处理,pause 就将永远阻塞。

为了纠正此问题,需要在一个原子操作中先恢复信号屏蔽字,然后使进程休眠。这种功能就是 sigsuspend 函数提供的。

该函数将进程的信号屏蔽字设置为由 sigmask 指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号之前,将该进程挂起。如果捕捉到一个信号而且从该信号处理程序返回,则 sigsuspend 返回,并且将该进程的信号屏蔽字设置为调用该函数之前的值。

函数 sigpending 获得当前被阻塞的信号集。该信号集通过 set 参数返回。

sigaction 函数的功能与 signal 类似,用于检查和修改与指定信号相关联的处理动作。由于 signal 函数是由 ISO C 定义的,其语义与实现有关,所以最好使用 sigaction 函数代替 signal。

参数 signo 是要检测或修改其处理程序的信号编号。若 act 非空,则修改其动作为 act。若 oact 非空,则将原动作通过 oact 返回。

我们已经在上文中见过 sigaction 在内核中的定义了,用户空间的定义与此一样:

实际上,在Linux的实现中,sa_handler 和 sa_sigaction 在一个联合中,所以不能同时给它们两个都赋值。另外,不应该去使用 sa_restorer 成员,因为在 POSIX 中并没有定义该成员。

sa_handler 即信号处理程序,sa_mask 指定了一个信号集,在调用该信号处理程序之前,这一信号集会被添加到信号屏蔽字中,在信号处理程序返回时再将信号屏蔽字设为原值。sa_flags 字段指定对信号进行处理的各个选项。下标列出这些选项的意义:

选项 说明
 SA_INTERRUPT 由此信号中断的系统调用不会自动重启
 SA_NOCLDSTOP signo 是 SIGCHLD,当子进程停止时,不产生此信号。当子进程终止时,仍然产生此信号。当停止的进程继续运行时,也不发送此信号
 SA_NOCLDWAIT signo 是 SIGCHLD,当子进程终止时,不创建僵尸进程。若调用进程在后面调用 wait,则调用进程阻塞,直到其所有子进程都终止,此时返回 -1,并将 errno 设置为 ECHILD
 SA_NODEFER 当捕获到此信号时,在执行其信号处理程序时,系统不自动阻塞此信号(除非 sa_mask 包括了此信号)
 SA_ONSTACK 若用 sigaltstack() 声明了一替换栈,则将此信号递送非替换栈上的进程
 SA_RESETHAND 在此信号处理程序的入口处,将此信号的处理方式复位为 SIG_DFL,并清除 SA_SIGINFO 标志。
 SA_RESTART 由此信号中断的系统调用将自动重启
 SA_SIGINFO 此选项对信号处理程序提供附加信息:一个指向 siginfo 结构的指针以及一个指向进程上下文标识符的指针

当设置了 SIG_SIGINFO 标志时,将使用 sa_sigaction 字段,所以此时就不能使用 sa_handler 字段。此时将按下列方式调用信号处理程序:

void handler(int signc, siginfo_t *info, void *context);

context 参数是无类型指针,可被强制转换为 ucntext_t 结构类型,用于标识信号传递时进程的上下文。

我们已经在前文见过 siginfo_t 在内核中的定义了,下面来看一下它在用户空间的详细定义:

可以看到,除了 si_signo,si_errno,si_code 外(在Linux中,si_errno 通常并不使用),其他都在一个联合中。这是因为系统会根据不同的信号类型使用不同的字段。下面列出了 si_code 的值:

信号 代码 原因
 SIGILL ILL_ILLOPC

ILL_ILLOPN

ILL_ILLADR

ILL_ILLTRP

ILL_PRVOPC

ILL_PRVREG

ILL_COPROC

ILL_BADSTK

非法操作码

非法操作数

非法地址模式

非法陷入

特权操作码

特权寄存器

协处理器出错

内部栈出错

 SIGFPE FPE_INTDIV

FPE_INTOVF

FPE_FLTDIV

FPE_FLTOVF

FPE_FLTUND

FPE_FLTRES

FPE_FLTINV

FPE_FLTSUB

整数除以0

整数溢出

浮点除以0

浮点上溢

浮点下溢

浮点不精确结果

无效的浮点运算

下标越界

 SIGSEGV SEGV_MAPERR

SEGV_ACCERR

地址未映射到对象

对于映射对象的无效权限

 SIGBUS BUS_ADRALN

BUS_ADRERR

BUS_OBJERR

无效的地址对齐

不存在的物理地址

对象特有的硬件出错

 SIGTRAP TRAP_BPKPT

TRAP_TRACE

进程断点陷入

进程跟踪陷入

 SIGCHLD CLD_EXITED

CLD_KILLED

CLD_DUMPED

CLD_TRAPPED

CLD_STOPPED

CLD_CONTINUED

子进程已终止

子进程已异常终止(无core)

子进程已异常终止(有core)

被跟踪的子进程已陷入

子进程已停止

停止的子进程已继续

 SIGPOLL POLL_IN

POLL_OUT

POLL_MSG

POLL_ERR

POLL_PRI

POLL_HUP

数据可读

数据可写

输入消息可用

I/O出错

高优先级消息可用

设备断开连接

 Any SI_USER

SI_QUEUE

SI_TIMER

SI_ASYNCIO

SI_MESGQ

kill 发送的信号

sigqueue 发送的信号(实时扩展)

timer_settime 设置的计时器超时(实时扩展)

异步I/O请求完成(实时扩展)

一条消息到达消息队列(实时扩展)

 

五、重启系统调用

如果在一个进程执行系统调用时,向该进程发送一个信号,那么该如何处理呢?等到系统调用结束再处理信号还是中断系统调用,以便尽快将信号投递到该进程?前者显然比较简单。然而,只有在所有系统调用都能够快速结束的情况下,这个方案才能正确运作(前面提到过,信号投递的时机,总是在进程处理完一个系统调用、返回到用户态的时候)。情况并不总是这样,系统调用不仅需要时间,还有可能使进程睡眠(执行一个低速系统调用时),这时采用第一种方案将使信号的投递严重延迟,这是绝不能被允许的。

让我们先认识一下让问题变复杂的低速系统调用吧。它们是可能会使进程永久阻塞的一类系统调用,包括:

  • 在读某些类型的文件(管道、终端设备以为套接字)时,如果不存在可读数据则可能会使进程永远阻塞
  • 在写这些类型的文件时,如果不能立即接受这些数据,也可能使进程永远阻塞
  • 打开某些类型文件,在某些条件发生之前也可能会使调用者阻塞(例如,打开终端设备,它要等到所连接的调制解调器应答了电话)
  • pause 和 wait 函数
  • 某些 ioctl 操作
  • 某些进程间通信函数

在这些低速系统调用中,一个例外是与磁盘I/O有关的系统调用。虽然读、写一个磁盘文件可能暂时阻塞调用者,但除非发生硬件错误,I/O操作总能很快返回。

那么在执行低速系统调用被中断时,内核会向应用程序返回什么样的值呢?我们知道,多数情况下,系统调用返回小于0的值表示调用失败,那么接收到小于0的返回值应用程序怎样判断是普通情况下的出错还是系统调用被中断?Linux(和其他System V变体)将返回 -EINTR 并设置 errno 为 EINTR。

这就迫使我们必须明确检查所有系统调用的返回值,并在返回值为 -EINTR 时,重启被中断的系统调用,直到该调用不再被信号中断。例如进行一个读操作,我们可能需要这样写:

为了帮助应用程序使其不必处理被中断的系统调用,BSD内核在系统调用被中断时,切换到用户态执行信号处理程序,但该系统调用不会有返回值,而是在信号处理程序结束后自动重启该调用。

Linux通过 SA_RESTART 标志支持BSD方案,返回 -EINTR 用作默认方案。

为什么Linux不采用BSD方案做为默认方案呢?因为BSD机制偶尔会导致一些困难,如下面的例子所示:

这段程序会在用户输入一个字符或程序被 SIGINT 信号中断时结束。代码中设置了 SA_RESTART 标志,所以这段代码的模式与BSD方案一样。

因此在 read 被信号中断时,系统将执行信号处理函数,然后重启 read 函数,等待输入一个字符。所以 while 循环控制条件中的 !signaled 无法被求值,导致循环不能结束。因此该程序不能通过向其发送 SIGINT 信号结束。

六、可重入函数

我们已经知道了信号处理机制的执行过程:进程捕捉到信号并对其进行处理时,进程正在执行的指令序列就被信号处理程序临时中断,它首先执行该信号处理程序,从信号处理程序返回后则继续执行在捕捉到信号时进程正在执行的正常指令序列。

但在信号处理程序中,不能判断捕捉到信号时进程在何处执行。如果进程正在执行 malloc,在堆中分配存储空间,而此时由于捕捉到信号而执行信号处理程序,而其中又调用了 malloc,这时会发生什么?这可能会对进程造成破坏,因为 malloc 通常为它所分配的存储区维护了一个链表,而执行信号处理程序时,进程可能正在更改此链表。

简单来说,可重入函数就是当该函数正在运行时,可以再次进入并执行它。

若一个函数是不可重入的,则可能它:

  • 使用或返回了静态或全局数据
  • 调用了 malloc 或 free
  • 是标准I/O函数。标准I/O库的很多实现都以不可重入的方式使用全局数据
  • 调用了其他不可重入函数

如果信号处理程序调用了一个不可重入函数,其结果将是不可预测的,应该极力避免。