面向应用程序员的 Linux 信号
对于在 Linux 环境中工作的应用程序员来说,充分理解信号非常重要。了解信号机制并熟悉与信号相关的函数有助于更高效地编写程序。
如果每条指令都正确运行,应用程序将按顺序执行。如果在程序执行期间发生错误或任何异常,内核可以使用信号来通知进程。信号也已用于通信和同步进程,并简化进程间通信 (IPC)。尽管我们现在拥有先进的同步工具和许多 IPC 机制,但信号在 Linux 中处理异常和中断方面仍然发挥着至关重要的作用。信号已经使用了大约 30 年,没有任何重大修改。
前 31 个信号是标准信号,其中一些信号可以追溯到 20 世纪 70 年代贝尔实验室的 UNIX。POSIX(可移植操作系统接口)标准引入了一类新的信号,指定为实时信号,编号范围从 32 到 63。
当事件发生时会生成信号,然后内核将事件传递给接收进程。有时,进程可以向其他进程发送信号。除了进程到进程的信号传递之外,在许多情况下,内核会发起信号,例如当文件大小超过限制、I/O 设备准备就绪、遇到非法指令或用户发送终端中断(如 Ctrl-C 或 Ctrl-Z)时。
每个信号都有一个以 SIG 开头的名称,并定义为一个正的唯一整数。在 shell 提示符下,kill -l 命令将显示所有信号及其信号编号和相应的信号名称。信号编号在 /usr/include/bits/signum.h 文件中定义,源文件是 /usr/src/linux/kernel/signal.c。
进程将在用户模式下运行时接收信号。如果接收进程在内核模式下运行,则信号的执行仅在进程返回到用户模式后才开始。
发送到非运行进程的信号必须由内核保存,直到进程恢复执行。睡眠进程可以是可中断的或不可中断的。如果进程在可中断睡眠状态(例如,等待终端 I/O)时收到信号,内核将唤醒该进程以处理该信号。如果进程在不可中断睡眠状态(例如,等待磁盘 I/O)时收到信号,内核会将信号延迟到事件完成。
当进程接收到信号时,可能会发生以下三种情况之一。首先,进程可以忽略该信号。其次,它可以捕获该信号并执行一个称为信号处理程序的特殊函数。第三,它可以执行该信号的默认操作;例如,信号 15 SIGTERM 的默认操作是终止进程。有些信号不能被忽略,而另一些信号则没有默认操作,因此默认情况下会被忽略。有关信号名称、编号、默认操作以及是否可以捕获它们的参考列表,请参阅 signal(7) 手册页。
当进程执行信号处理程序时,如果收到其他信号,则新信号将被阻塞,直到处理程序返回。本文解释了信号机制的基本原理,并详细阐述了与信号相关的函数,包括语法和工作过程。
关于信号的信息存储在进程的什么位置?内核有一个固定大小的 proc 结构数组,称为进程表。proc 结构的 u 或用户区域维护有关进程的控制信息。u 区域中的主要字段包括信号处理程序和相关信息。信号处理程序是一个数组,每个元素对应系统中定义的每种信号类型,指示进程在收到信号时执行的操作。proc 结构维护信号处理信息,例如被忽略、阻塞、发布和处理的信号掩码。
一旦生成信号,内核会在进程表条目的信号字段中设置一位。如果要忽略该信号,内核将返回,而不采取任何操作。由于信号字段每个信号一位,因此不维护同一信号的多次出现。
当信号传递时,接收进程应根据信号采取行动。该操作可能是终止进程、在创建核心转储后终止进程、忽略信号、执行用户定义的信号处理程序(如果信号被进程捕获)或在进程暂时挂起时恢复进程。
核心转储是一个名为 core 的文件,其中包含已终止进程的映像。它包含进程在失败时的变量和堆栈详细信息。程序员可以从核心文件使用调试器调查终止的原因。单词 core 在这里出现是出于历史原因:主存储器曾经由称为电感磁芯的环形磁铁制成。
捕获信号意味着指示内核,如果给定的信号发生,则应执行程序自己的信号处理程序,而不是默认处理程序。两个例外是 SIGKILL 和 SIGSTOP,它们不能被捕获或忽略。
sigset_t 是用于存储信号的基本数据结构。发送到进程的结构是一个 sigset_t 位数组,每个信号类型一位
typedef struct { unsigned long sig[2]; } sigset_t;
由于每个无符号长整型数由 32 位组成,因此 Linux 中可以声明的最大信号数为 64(根据 POSIX 兼容性)。没有信号的编号为 0,因此 sigset_t 第一个元素中的其他 31 位是标准的前 31 个信号,第二个元素中的位是实时信号编号 32-64。sigset_t 的大小为 128 字节。
有许多系统调用和信号支持的库函数,它们提供了一种简单而有效的方式来处理进程中的信号。我们从标准的旧信号系统调用开始,然后我们讨论一些有用的函数,如 sigaction、sigaddset、sigemptyset、sigdelset、sigismember 和 kill。
信号系统调用用于捕获、忽略或设置指定信号的默认操作。它接受两个参数:信号编号和指向用户定义的信号处理程序的指针。Linux 中提供了两个保留的预定义信号处理程序:SIG_IGN 和 SIG_DFL。SIG_IGN 将忽略指定的信号,SIG_DFL 将信号处理程序设置为该信号的默认操作(请参阅 man 2 signal)。
成功时,系统调用返回指定信号的信号处理程序的先前值。如果信号调用失败,则返回 SIG_ERR。列表 1 解释了如何捕获、忽略和设置 SIGINT 的默认操作。在每个部分尝试按 Ctrl-C,这将发送 SIGINT。
可以使用 sigaction 系统调用代替信号,因为它对给定信号具有更多控制权。sigaction 的语法是
int sigaction ( int signum, const struct sigaction *act, struct sigaction *oldact);
第一个参数 signum 是指定的信号;第二个参数 sigaction 用于设置信号 signum 的新操作;第三个参数用于存储之前的操作,通常为 NULL。
sigaction 结构的定义如下
struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; }
sigaction 结构的成员描述如下。
sa_hander: 指向用户定义的信号处理程序或预定义信号处理程序(SIG_IGN 或 SIG_DFL)的指针。
sa_mask: 指定处理信号时信号的掩码。为避免阻塞信号,可以使用 SA_NODEFER 或 SA_NOMASK 标志。
sa_flags: 指定信号的操作。可以使用多组标志以不同的方式控制信号。可以通过 OR 运算使用多个标志
SA_NOCLDSTOP:如果我们指定 SIGCHLD 信号,当子进程停止执行时,它不会收到通知。
SA_ONESHOT 或 SA_RESETHAND:在用户定义的信号处理程序执行后,恢复信号的默认操作。为避免设置默认操作,可以使用 SA_RESTART。
SA_NOMASK 或 SA_NODEFER 防止屏蔽信号。SA_SIGINFO 用于接收与信号相关的信息。
sa_sigaction: 如果 sa_flags 中使用了 SA_SIGINFO 标志,则应使用 sa_sigaction 而不是在 sa_handler 中指定信号处理程序。
sa_sigaction 是指向接受三个参数的函数的指针,而不是像 sa_handler 那样只接受一个参数,例如
void my_handler (int signo, siginfo_t *info, void *context)
在这里,signo 是信号编号,info 是指向 siginfo_t 类型结构的指针,该结构指定与信号相关的信息;context 是指向 ucontext_t 类型对象的指针,该对象引用被传递的信号中断的接收进程上下文。
列表 2 与列表 1 类似,但使用 sigaction 系统调用而不是信号系统调用。列表 3 解释了使用 SIG_INFO 标志的信号相关信息。
到目前为止,我们一直在按 Ctrl-C 从 shell 发送 SIGINT。要从程序中执行此操作,请使用 kill 系统调用,它接受两个参数:进程 ID 和信号编号
int kill ( pid_t process_id, int signal_number );
如果 pid 为正数,则信号将发送到特定进程。如果 pid 为负数,则信号将发送到进程组 ID 与 pid 绝对值匹配的进程。
正如您可能预期的那样,kill 命令(它作为独立程序 (/bin/kill) 存在,并且也内置在 bash 中(尝试 help kill))使用 kill 系统调用来发送信号。
并非所有进程都可以相互发送信号。为了使一个进程向另一个进程发送信号,发送者必须以 root 身份运行,或者发送者的真实或有效用户 ID 必须与接收者的真实或已保存 ID 相同。这意味着您的 shell(以您的身份运行)可以向您启动的 setuid 程序发送信号,但该程序现在以 root 身份运行,例如
cp /bin/sleep ~/rootsleep sudo chmod u+s ~/rootsleep ./rootsleep 40 killall rootsleep rm ~/rootsleep
普通用户无法向系统进程(如 swapper 和 init)发送信号。
您还可以使用 kill 来查明进程是否存在。指定信号编号为 0,如果进程存在,则 kill 返回零;如果不存在,则 kill 返回 -1。
列表 4 和 4a 解释了如何使用 kill 系统调用。首先,在一个窗口中执行 4a 程序并获取其进程 ID。现在,在另一个窗口中运行列表 4 程序,并将 4a 示例的 pid 作为输入。
本文应帮助您理解信号的基本概念及其一些重要性。尝试示例程序,并查看手册页以获取系统调用,并在“资源”中查找更多信息。
电子邮件:balasubramanian.thangaraju@wipro.com
B. Thangaraju 博士 获得了物理学博士学位,并在印度科学研究所担任了五年研究助理。他目前在印度威普罗技术公司人才转型部门担任经理。他已在著名的国际期刊上发表了许多研究论文。他目前的研究、学习和知识传播领域是 Linux 内核、设备驱动程序和实时 Linux。