Linux学习日记(十四)——Linux系统中的信号
目录
1 基本概念
2 信号的分类
2.1 非实时信号(传统信号)
2.2 实时信号(非传统信号)
3 常见信号的默认行为(系统默认操作)
4 进程对信号的处理方式
4.1 signal()函数
4.2 sigaction()函数
5 如何向进程发送信号
5.1 kill()函数
5.2 killpg()函数
5.3 raise()函数
6 小结
1 基本概念
在Linux系统中,信号(Signal)是一种轻量级的进程间通信方式,是事件发生时对进程的通知机制,它用于进程间的交互,特别是处理一些异常或特定事件。信号是一种软件中断机制,是在软件层次上对中断机制的一种模拟,当发生特定事件时,系统会向进程发送一个信号,进程接收到信号后会采取相应的行动。2 信号的分类
在Linux系统中,信号可以分为两大类: 非实时信号(传统信号、不可靠信号)和 实时信号(非传统信号、可靠信号)。Linux 信号机制基本上是从 UNIX 系统中继承过来的,早期 UNIX 系统中的信号机制比较简单和原始,在进程每次处理信号后,就将对信号的响应设置为系统默认操作,并且进程可能对信号做出错误的反应以及信号可能丢失(处理信号时又来了新的信号,则导致信号丢失)。
2.1 非实时信号(传统信号)
编号为1~31的信号为传统UNIX支持的信号,也称作不可靠信号。例如,SIGINT(编号2)表示键盘中断(通常是Ctrl+C),SIGTERM(编号15)表示终止进程的请求。Linux 支持传统的非实时的不可靠信号,但是对这种不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用signal()。
这些信号在头文件signum.h中定义,每个信号都是以 SIGxxx 开头:
#define SIGHUP 1 /* Hangup (POSIX). */
#define SIGINT 2 /* Interrupt (ANSI). */
#define SIGQUIT 3 /* Quit (POSIX). */
#define SIGILL 4 /* Illegal instruction (ANSI). */
#define SIGTRAP 5 /* Trace trap (POSIX). */
#define SIGABRT 6 /* Abort (ANSI). */
#define SIGIOT 6 /* IOT trap (4.2 BSD). */
#define SIGBUS 7 /* BUS error (4.2 BSD). */
#define SIGFPE 8 /* Floating-point exception (ANSI). */
#define SIGKILL 9 /* Kill, unblockable (POSIX). */
#define SIGUSR1 10 /* User-defined signal 1 (POSIX). */
#define SIGSEGV 11 /* Segmentation violation (ANSI). */
#define SIGUSR2 12 /* User-defined signal 2 (POSIX). */
#define SIGPIPE 13 /* Broken pipe (POSIX). */
#define SIGALRM 14 /* Alarm clock (POSIX). */
#define SIGTERM 15 /* Termination (ANSI). */
#define SIGSTKFLT 16 /* Stack fault. */
#define SIGCLD SIGCHLD /* Same as SIGCHLD (System V). */
#define SIGCHLD 17 /* Child status has changed (POSIX). */
#define SIGCONT 18 /* Continue (POSIX). */
#define SIGSTOP 19 /* Stop, unblockable (POSIX). */
#define SIGTSTP 20 /* Keyboard stop (POSIX). */
#define SIGTTIN 21 /* Background read from tty (POSIX). */
#define SIGTTOU 22 /* Background write to tty (POSIX). */
#define SIGURG 23 /* Urgent condition on socket (4.2 BSD). */
#define SIGXCPU 24 /* CPU limit exceeded (4.2 BSD). */
#define SIGXFSZ 25 /* File size limit exceeded (4.2 BSD). */
#define SIGVTALRM 26 /* Virtual alarm clock (4.2 BSD). */
#define SIGPROF 27 /* Profiling alarm clock (4.2 BSD). */
#define SIGWINCH 28 /* Window size change (4.3 BSD, Sun). */
#define SIGPOLL SIGIO /* Pollable event occurred (System V). */
#define SIGIO 29 /* I/O now possible (4.2 BSD). */
#define SIGPWR 30 /* Power failure restart (System V). */
#define SIGSYS 31 /* Bad system call. */
#define SIGUNUSED 31
2.2 实时信号(非传统信号)
编号为32~63的信号是后来扩充的,也称做可靠信号。实时信号的特点是可以排队,即如果同一个实时信号多次产生,系统会将其多次排队,直到进程处理它们为止。实时信号保证了发送的多个信号都能被接收,实时信号是 POSIX 标准的一部分,可用于应用进程。
3 常见信号的默认行为(系统默认操作)
SIGINT
当用户在终端按下中断字符(通常是 CTRL + C)时,内核将发送 SIGINT 信号给前台进程组中的每一个进程。该信号的系统默认操作是终止进程的运行。所以通常我们都会使用 CTRL + C 来终止一个占用前台的进程,原因在于大部分的进程会将该信号交给系统去处理,从而执行该信号的系统默认操作。
SIGQUIT
当用户在终端按下退出字符(通常是 CTRL + \)时,内核将发送 SIGQUIT 信号给前台进程组中的每一个进程。该信号的系统默认操作是终止进程的运行、并生成可用于调试的核心转储文件。进程如果陷入无限循环、或不再响应时,使用 SIGQUIT 信号就很合适。所以对于一个前台进程,既可以在终端按下中断字符CTRL + C、也可以按下退出字符 CTRL + \来终止,当然前提条件是,此进程会将 SIGINT 信号或 SIGQUIT信号交给系统处理(也就是没有将信号忽略或捕获),进入执行该信号所对应的系统默认操作。
SIGILL
如果进程试图执行非法(即格式不正确)的机器语言指令,系统将向进程发送该信号。该信号的系统默认操作是终止进程的运行。
SIGABRT
当进程调用 abort()系统调用时(进程异常终止),系统会向该进程发送 SIGABRT 信号。该信号的系统默认操作是终止进程、并生成核心转储文件。
SIGBUS
产生该信号(总线错误,bus error)表示发生了某种内存访问错误。该信号的系统默认操作是终止进程。
SIGFPE
该信号因特定类型的算术错误而产生,譬如除以 0。该信号的系统默认操作是终止进程。
SIGKILL
此信号为“必杀(sure kill)”信号,用于杀死进程的终极办法,此信号无法被进程阻塞、忽略或者捕获,故而“一击必杀”,总能终止进程。使用 SIGINT 信号和 SIGQUIT 信号虽然能终止进程,但是前提条件是该进程并没有忽略或捕获这些信号,如果使用 SIGINT 或 SIGQUIT 无法终止进程,那就使用“必杀信号”SIGKILL 吧。Linux 下有一个 kill 命令,kill 命令可用于向进程发送信号,我们会使用"kill -9 xxx"命令来终止一个进程(xxx 表示进程的 pid),这里的-9 其实指的就是发送编号为 9 的信号,也就是 SIGKILL 信号。
SIGUSR1
该信号和 SIGUSR2 信号供程序员自定义使用,内核绝不会为进程产生这些信号,在我们的程序中,可以使用这些信号来互通通知事件的发生,或是进程彼此同步操作。该信号的系统默认操作是终止进程。
SIGSEGV
这一信号非常常见,当应用程序对内存的引用无效时,操作系统就会向该应用程序发送该信号。引起对内存无效引用的原因很多,C 语言中引发这些事件往往是解引用的指针里包含了错误地址(譬如,未初始化的指针),或者传递了一个无效参数供函数调用等。该信号的系统默认操作是终止进程。
SIGUSR2
与 SIGUSR1 信号相同。
SIGPIPE
涉及到管道和 socket,当进程向已经关闭的管道、FIFO 或套接字写入信息时,那么系统将发送该信号给进程。该信号的系统默认操作是终止进程。
SIGALRM
与系统调用 alarm()或 setitimer()有关,应用程序中可以调用 alarm()或 setitimer()函数来设置一个定时器,当定时器定时时间到,那么内核将会发送 SIGALRM 信号给该应用程序,关于 alarm()或 setitimer()函数的使用,后面将会进行讲解。该信号的系统默认操作是终止进程。
SIGTERM
这是用于终止进程的标准信号,也是 kill 命令所发送的默认信号(kill xxx,xxx 表示进程 pid),有时我们会直接使用"kill -9 xxx"显式向进程发送 SIGKILL 信号来终止进程,然而这一做法通常是错误的,精心设计的应用程序应该会捕获 SIGTERM 信号、并为其绑定一个处理函数,当该进程收到 SIGTERM 信号时,会在处理函数中清除临时文件以及释放其它资源,再而退出程序。如果直接使用 SIGKILL 信号终止进程,从而跳过了 SIGTERM 信号的处理函数,通常 SIGKILL 终止进程是不友好的方式、是暴力的方式,这种方式应该作为最后手段,应首先尝试使用 SIGTERM,实在不行再使用最后手段 SIGKILL。
SIGCHLD
当父进程的某一个子进程终止时,内核会向父进程发送该信号。当父进程的某一个子进程因收到信号而停止或恢复时,内核也可能向父进程发送该信号。注意这里说的停止并不是终止,你可以理解为暂停。该信号的系统默认操作是忽略此信号,如果父进程希望被告知其子进程的这种状态改变,则应捕获此信号。
SIGCLD
与 SIGCHLD 信号同义。
SIGCONT
将该信号发送给已停止的进程,进程将会恢复运行。当进程接收到此信号时并不处于停止状态,系统默认操作是忽略该信号,但如果进程处于停止状态,则系统默认操作是使该进程继续运行。
SIGSTOP
这是一个“必停”信号,用于停止进程(注意停止不是终止,停止只是暂停运行、进程并没有终止),应用程序无法将该信号忽略或者捕获,故而总能停止进程。
SIGTSTP
这也是一个停止信号,当用户在终端按下停止字符(通常是 CTRL + Z),那么系统会将 SIGTSTP 信号发送给前台进程组中的每一个进程,使其停止运行。
SIGXCPU
当进程的 CPU 时间超出对应的资源限制时,内核将发送此信号给该进程。
SIGVTALRM
应用程序调用 setitimer()函数设置一个虚拟定时器,当定时器定时时间到时,内核将会发送该信号给进程。
SIGWINCH
在窗口环境中,当终端窗口尺寸发生变化时(譬如用户手动调整了大小,应用程序调用 ioctl()设置了大小等),系统会向前台进程组中的每一个进程发送该信号。
SIGPOLL/SIGIO
这两个信号同义。这两个信号将会在高级 IO 章节内容中使用到,用于提示一个异步 IO 事件的发生,譬如应用程序打开的文件描述符发生了 I/O 事件时,内核会向应用程序发送 SIGIO 信号。
SIGSYS
如果进程发起的系统调用有误,那么内核将发送该信号给对应的进程。
这些常见信号行为如下图所示。term 表示终止进程,core 表示生成核心转储文件,ignore 表示忽略信号,cont 表示继续运行进程,stop 表示停止进程(注意停止不等于终止,而是暂停)。
4 进程对信号的处理方式
当进程接收到内核或用户发送过来的信号之后,进程对信号的处理有三种方式:忽略信号、执行默认处理动作、提供信号处理函数。Linux 系统提供了 signal()和 sigaction()这两个系统调用函数用于设置信号的处理方式。 忽略信号:进程可以选择忽略某些信号,这意味着即使信号产生,进程也不会采取任何行动。 执行默认处理动作:每个信号都有一个默认的处理动作,例如SIGINT的默认动作是终止进程。 提供信号处理函数:进程可以注册一个信号处理函数,当信号产生时,内核会切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。4.1 signal()函数
signal()函数是 Linux 系统下设置信号处理方式最简单的接口,可将信号(这些信号可以由程序错误、外部事件或显示请求等产生)的处理方式设置为捕获信号、忽略信号以及系统默认操作。函数声明在头文件signal.h中:
sighandler_t signal(int sig, sighandler_t handler);
sig: 表示要设置信号处理函数的信号编号。
handler: 信号处理函数。可以是以下三种之一:
SIG_DFL: 设置信号处理函数为默认信号处理函数。
SIG_IGN: 忽略信号。
指向函数的指针:自定义的信号处理函数。
返回值: 此函数的返回值也是一个 sig_t 类型的函数指针,成功情况下的返回值则是指向在此之前的信号处理函数;如果出错则返回 SIG_ERR,并会设置 errno。由此可知,signal()函数可以根据第二个参数 handler 的不同设置情况,可对信号进行不同的处理
以下是一个简单的示例,展示了如何使用signal()函数来捕获SIGINT信号(即Ctrl+C):
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int signum) {
printf("Caught signal %d\n", signum);
}
int main() {
// 为SIGINT信号注册自定义处理函数
signal(SIGINT, handler);
// 程序进入休眠状态,等待信号
while (1) {
sleep(1);
}
return 0;
}
在这个示例中,当用户按下Ctrl+C时,程序不会立即终止,而是会调用自定义的处理函数handler,输出一条消息。
4.2 sigaction()函数
在Linux编程中,sigaction()函数用于处理软中断信号,这些信号可以由程序错误、外部事件或显示请求等产生。通过sigaction()函数,可以为特定的信号注册自定义的处理函数,并且相比signal()函数,sigaction()提供了更多的控制选项。函数声明在头文件signal.h中:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum: 需要设置的信号,除了 SIGKILL 信号和 SIGSTOP 信号之外的任何信号。
act: act 参数是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构,该数据结构描述了信号的处理方式。如果参数 act 不为 NULL,则表示需要为信号设置新的处理方式;如果参数 act 为 NULL,则表示无需改变信号当前的处理方式。
oldact: oldact 参数也是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构,用于保存之前的信号处理方式。如果参数oldact 不为 NULL,则会将信号之前的处理方式等信息通过参数 oldact 返回出来;如果无意获取此类信息,那么可将该参数设置为 NULL。
返回值: 成功返回 0;失败将返回-1,并设置 errno。
struct sigaction结构体定义:
struct sigaction {
void (*sa_handler)(int); // 信号处理函数,指定信号捕捉后的处理函数名(即注册函数)。
sigset_t sa_mask; // 额外的信号屏蔽字,调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。
int sa_flags; // 信号处理的标志,通常设置为0,表示使用默认属性。
void (*sa_sigaction)(int, siginfo_t *, void *); // 实时信号处理函数,用于对实时信号的捕获,与sa_handler类似,但提供了更多的信息。
};
常见的sa_flags标志:
SA_RESETHAND: 当调用信号处理函数时,将信号的处理函数重置为缺省值。
SA_RESTART: 如果信号中断了进程的某个系统调用,则系统自动重启该系统调用。
SA_NODEFER: 当信号处理函数运行时,内核将阻塞该给定信号。但如果设置了SA_NODEFER标记,那么在该信号处理函数运行时,内核将不会阻塞该信号。
以下是一个简单的示例,展示了如何使用sigaction()函数来捕获SIGINT信号(即Ctrl+C):
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int signum) {
printf("Caught signal %d\n", signum);
}
int main() {
struct sigaction act;
// 设置信号处理函数
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
// 为SIGINT信号注册自定义处理函数
if (sigaction(SIGINT, &act, NULL) == -1) {
perror("sigaction");
return 1;
}
// 程序进入休眠状态,等待信号
while (1) {
sleep(1);
}
return 0;
}
5 如何向进程发送信号
在Linux 系统提供了 kill()系统调用,一个进程可通过 kill()向另一个进程发送信号; 除 kill()系统调用外,系统调用 killpg()和库函数 raise()也可以用于实现给进程发送信号的功能。5.1 kill()函数
kill()系统调用可将信号发送给指定的进程或进程组中的每一个进程(使用时要包含头文件int kill(pid_t pid, int sig);
pid: 参数 pid 为正数的情况下,用于指定接收此信号的进程 pid;除此之外,参数 pid 也可设置为 0 或-1 以及小于-1 等不同值。
pid 为正: 信号 sig 将发送到 pid 指定的进程。
pid 等于 0: 信号 sig 将发送到当前进程的进程组中的每个进程。
pid 等于-1:将 sig 发送到当前进程有权发送信号的每个进程,但进程 1(init)除外。
pid 小于-1:将 sig 发送到 ID 为-pid 的进程组中的每个进程。
sig: 用于指定需要发送的信号,也可设置为 0,如果参数 sig 设置为 0 则表示不发送信号,但任执行错误检查,这通常可用于检查参数 pid 指定的进程是否存在。
返回值: :成功返回 0;失败将返回-1,并设置 errno。
进程中将信号发送给另一个进程是需要权限的,并不是可以随便给任何一个进程发送信号,超级用户 root 进程可以将信号发送给任何进程,但对于非超级用户(普通用户)进程来说,其基本规则是发送者进程的实际用户 ID 或有效用户 ID 必须等于接收者进程的实际用户 ID 或有效用户 ID。
以下是一个简单的示例,展示了如何使用 kill()函数向一个指定的进程发送信号:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int pid;
// 判断传参个数
if (2 > argc) {
exit(-1);
}
// 将传入的字符串转为整形数字
pid = atoi(argv[1]);
printf("pid: %d\n", pid);
// 向 pid 指定的进程发送信号
if (-1 == kill(pid, SIGINT)) {
perror("kill error");
exit(-1);
}
exit(0);
}
5.2 killpg()函数
killpg()函数是Linux系统中的一个系统调用,用于向进程组发送信号。进程组是由一组进程组成的集合,通常用于管理和控制一组相关的进程。killpg()函数可以向整个进程组发送信号,从而实现对多个进程的同时控制。其函数原型如下所示:int killpg(pid_t pgrp, int sig);
pgrp: 一个pid_t类型的参数,表示目标进程组的进程组ID(PGID)。进程组ID是一个正整数,用于唯一标识一个进程组。
sig: 一个int类型的参数,表示要发送的信号编号。信号编号可以是标准的信号常量(如SIGKILL、SIGTERM等),也可以是自定义的信号编号。
返回值: :成功发送信号后返回0;如果发生错误,killpg()函数返回-1,并设置errno变量以指示具体的错误原因。常见的错误原因包括:EPERM(发送信号的进程没有足够的权限)、ESRCH(指定的进程组不存在)、EINVAL(信号编号无效)。
同样的使用killpg()发送信号的进程也必须具有足够的权限,通常情况下,只有进程的所有者或者超级用户(root)才能向进程组发送信号。并且目标进程组中的进程必须能够处理接收到的信号,如果进程没有为特定信号注册处理函数,可能会导致进程异常终止。killpg()函数中的pgrp参数必须是一个有效的进程组ID,如果pgrp参数为0,killpg()函数将向调用进程所属的进程组发送信号。
以下是一个简单的示例代码,演示如何使用killpg()函数向进程组发送信号:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
int main() {
pid_t pgid;
int sig;
// 获取当前进程的进程组ID
pgid = getpgid(0);
// 设定要发送的信号
sig = SIGKILL;
if (killpg(pgid, sig) == -1) {
perror("killpg");
exit(EXIT_FAILURE);
}
printf("Signal sent to process group %d\n", pgid);
return 0;
}
5.3 raise()函数
有时进程需要向自身发送信号,C库函数raise()函数可用于实现这一要求,函数声明在头文件signal.h中,raise()函数原型为:int raise(int sig);
sig: 表示要设置信号处理函数的信号编号。
返回值: 成功返回 0;失败将返回非零值。
以下是一个简单的示例,展示了如何使用signal()函数来捕获SIGINT信号(即Ctrl+C):
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
printf("Received signal: %d\n", sig);
}
int main(int argc, char *argv[])
{
int ret;
struct sigaction sig = {0};
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
ret = sigaction(SIGINT, &sig, NULL);
if (-1 == ret) {
perror("sigaction error");
exit(-1);
}
// 向自身发送 SIGINT 信号
while (1) {
if (0 != raise(SIGINT)) {
printf("raise error\n");
exit(-1);
}
sleep(5);
}
exit(0);
}