「Linux 系统编程」信号的概念、生命周期与产生

1 什么是信号

1.1 信号的定义

信号(Signal) 是一种 异步通知机制,用于进程与内核之间或进程与进程之间的简单通信。其具有以下特性:

  • 简单:信号本身不携带大量信息,仅表示某种事件的发生。
  • 条件触发:只有在特定条件满足时才会发送。
  • 异步处理:信号可以在进程执行的任何时刻到达,打断当前执行流程。

1.2 信号的特质

信号 常被形容为 “软件层面的中断”。其特点包括:

  • 一旦信号产生,无论程序执行到何处,都必须 立即停止当前操作,转而去处理信号
  • 信号的处理完成后,程序再恢复执行之前的指令。
  • 所有信号的产生和处理都是由 内核 完成的。

可以把 信号 类比于现实生活中的 中断警报

当你正在工作时,突然电话响了(信号产生),你会停下手中的工作(进程正常执行流程被中断),去接电话(执行信号处理函数),接完电话后再回来继续工作(恢复执行)。


1.3 信号的来源

  1. 内核:内核在检测到系统事件时,会向相关进程发送信号。

    用户按下 Ctrl+C(产生 SIGINT),除零操作(产生 SIGFPE),子进程退出(向父进程发送 SIGCHLD),进程访问非法内存(产生 SIGSEGV)。

  2. 其他进程:一个进程可以通过系统调用 kill() 向另一个进程(或进程组)发送信号。当然,发送者需要有相应的权限(通常是超级用户或目标进程的所有者)。

  3. 进程自身:进程可以通过 kill()raise() 系统调用给自己发送信号。


2 信号生命周期与处理

2.1 信号的生命周期

一个信号从产生到被处理,经历了以下几个步骤:

  • 第 1 步:产生 (Generation)
    信号旅程的起点。内核、其他进程或自身创建了一个信号事件。此时,信号只是一个“想法”,还没有真正送达目标进程。

    类比:手机收到新邮件的服务器推送通知(通知已生成,但还没到你手机上)。

  • 第 2 步:判断阻塞状态 (Blocking?)
    信号产生后,系统会检查目标进程是否 阻塞(屏蔽) 了该信号。

    是进程主动告诉内核:“我现在不想处理信号A,如果它来了,先帮我留着。”

  • 第 3a 步:未被阻塞 -> 递送 (Delivery)

    如果进程没有阻塞这个信号,内核会立即将信号递送给进程。这意味着内核会中断进程当前的正常执行流,强制它去处理这个信号。

    类比:你的手机铃声大作,通知你立即查看邮件(推送被立即送达并提醒)。

  • 第 3b 步:被阻塞 -> 未决 (Pending)

    如果进程 阻塞 了这个信号,那么这个信号会保持在 “未决”(Pending) 状态。它被添加到进程的一个待处理信号列表中,但不会立即送达。

    类比:你开启了手机的“勿扰模式”。新邮件通知来了,但手机不会响也不会亮屏,只会默默地出现在通知中心里(通知被静默保存,处于未决状态)。

    重要特性:在阻塞期间,同种类型的未决信号通常只会被记录一次(除非使用实时信号)。即使用户按了十次 Ctrl+C,进程解除阻塞后也只会收到一个 SIGINT

  • 第 4 步:解除阻塞 -> 递送

    当进程后来通过系统调用解除了对该信号的阻塞时,内核会检查未决信号列表。如果发现该信号在列表中处于未决状态,就会立即将它递送给进程。

    类比:你关闭了“勿扰模式”,手机立刻检查通知中心,并提醒你有未读邮件(递送之前未决的通知)。

Linux 信号生命周期示意图


2.2 信号的处理方式

当一个进程收到信号时,它可以选择以下 三种方式之一 来处理:

  1. 默认动作(Default Action):每个信号都有一个系统预设的默认行为。

    常见的默认动作有:

    • Terminate:终止进程。(如 SIGKILL, SIGTERM

    • Ignore:忽略信号。(如 SIGCHLD 的默认处理就是忽略,但父进程可以捕获它来做一些清理工作)

    • Core:终止进程并生成核心转储文件(core dump),用于调试。(如 SIGSEGV, SIGABRT

    • Stop:停止进程(暂停执行)。(如 SIGSTOP, SIGTSTP

    • Continue:如果进程被停止了,让它继续运行。(如 SIGCONT

  2. 捕获(Catch):进程可以预先注册一个函数,称为信号处理函数(Signal Handler)。

    当信号到来时,内核会中断进程当前的正常控制流,转而去执行这个注册好的处理函数。执行完毕后,进程再回到被中断的地方继续执行。

    • 使用 signal()sigaction() 系统调用可以注册信号处理函数。

    示例:SIGINT(由 Ctrl+C 产生)的默认动作是终止进程。你可以捕获它,让它执行一个自定义的清理函数,然后再优雅地退出,而不是突然被杀死。

  3. 忽略(Ignore):进程告诉内核,当这个信号到来时,直接丢弃它,什么都不做。

    注意:SIGKILLSIGSTOP 这两个信号不能被捕获、阻塞或忽略。这是为了给系统管理员一个最终极的手段来杀死或停止失控的进程。


2.3 信号屏蔽字与未决信号集

  • 阻塞信号集(信号屏蔽字)

    本质:一个位图(bitmap),用于记录哪些信号被屏蔽(阻塞)。

    作用:被屏蔽的信号在解除屏蔽之前,即使产生也不会被递达,而是保持未决状态。

  • 未决信号集

    本质:也是一个位图,用于记录哪些信号已经产生但尚未被处理。

    作用:内核通过该集合管理所有处于未决状态的信号。

    注意:信号屏蔽字和未决信号集都是由内核维护的,用户可以通过 sigprocmask() 等函数修改屏蔽字。



3 信号四要素与常规信号

3.1 信号四要素

使用命令 kill -l 可以查看系统中所有可用的信号(包括常规信号和实时信号)。


每个信号都有以下四个要素:

  • 编号:信号的唯一标识符。

  • 名称:如 SIGINT、SIGTERM 等。

  • 对应事件:触发该信号的条件或事件。

  • 默认处理动作:如终止、忽略、停止、继续等。

注意:在使用信号之前,必须明确其四要素,尤其是默认处理动作,以避免不可预期的行为。


3.2 常规信号

信号编号 信号名 默认行为 描述
1 SIGHUP Terminate 挂起。控制终端关闭或进程组领导终止时发送。也常用于让守护进程重新读取配置文件。
2 SIGINT Terminate 中断。来自键盘的中断,通常是用户按下了 Ctrl+C
3 SIGQUIT Core 退出。来自键盘的退出,通常是用户按下了 Ctrl+\。会产生 core dump。
9 SIGKILL Terminate 杀死。立即无条件终止进程。此信号不能被捕获、阻塞或忽略。
11 SIGSEGV Core 段错误。进程进行了无效的内存访问(如访问不存在的内存,或写只读内存)。
13 SIGPIPE Terminate 管道破裂。向一个没有读端的管道写入数据。
14 SIGALRM Terminate 闹钟。由 alarm()setitimer() 设置的定时器超时后产生。
15 SIGTERM Terminate 终止。这是 kill 命令默认发送的信号。它是一种要求进程优雅终止的请求。进程可以捕获它,完成清理工作后再退出。
17 SIGCHLD Ignore 子进程状态改变。当子进程停止、退出或被恢复时,内核会向父进程发送这个信号。
18 SIGCONT Continue 继续。让一个被停止的进程继续运行。
19 SIGSTOP Stop 停止。暂停进程的执行。此信号不能被捕获、阻塞或忽略。
20 SIGTSTP Stop 终端停止。来自终端的停止信号,通常是用户按下了 Ctrl+Z


4 信号产生示例

4.1 kill 命令和系统调用

4.1.1 kill 系统调用函数

kill 函数 用于 发送信号给指定的进程

kill 函数原型

1
2
#include <signal.h>
int kill(pid_t pid, int signum);

参数说明

  • pid
    • > 0:发送信号给 指定进程(进程ID为 pid)
    • = 0:发送给 当前进程组 的所有进程
    • < -1:取绝对值 |pid|,发送给 指定进程组 的所有成员
    • = -1:发送给有权限的所有进程
  • signum:要发送的信号(如 SIGKILLSIGTERM

返回值

  • 成功:返回 0
  • 失败:返回 -1,并设置 errno

4.1.2 kill 命令用法

1
kill [选项] <PID>

常用选项

  • -9-SIGKILL:强制杀死进程(立即无条件终止,此信号不能被捕获、阻塞或忽略。)
  • -15-SIGTERM:优雅终止进程(默认,可以捕获)

示例

1
2
3
4
5
6
7
8
# 杀死进程ID为 1234 的进程
kill 1234

# 强制杀死进程
kill -9 1234

# 杀死进程组(如组ID为 5678)
kill -9 -5678

4.1.3 kill 系统调用代码示例

  • 子进程通过系统调用 kill 杀死父进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
pid_t pid = fork();

if (pid == 0) {
// 子进程
sleep(3);
printf("Child is killing parent...\n");
kill(getppid(), SIGKILL); // 子进程杀死父进程
} else {
// 父进程
while (1) {
printf("Parent is running...\n");
sleep(1);
}
}

return 0;
}


  • 父进程创建多个子进程,其中一个子进程杀死当前进程组的所有进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() {
pid_t pid;
int i;

// 父进程循环创建多个子进程
for (i = 0; i < 3; i++) {
pid = fork();
if (pid == 0) {
// 子进程
break;
}
}

if (pid == 0) {
// 子进程
printf("Child process %d (PID: %d, PGID: %d) is running\n",
i, getpid(), getpgid(0));

if (i == 1) {
// 第二个子进程负责杀死整个进程组
sleep(3); // 等待其他进程启动
printf("Child %d is killing the whole process group (PGID: %d)\n",
i, getpgid(0));

// 发送SIGTERM信号给整个进程组
// 注意:负的PID表示进程组ID
if (kill(-getpgid(0), SIGTERM) == -1) {
perror("kill error");
}
} else {
// 其他子进程正常执行
while (1) {
printf("Child %d (PID: %d) is still running\n", i, getpid());
sleep(1);
}
}
} else {
// 父进程代码
printf("Parent process (PID: %d, PGID: %d) is running\n",
getpid(), getpgid(0));

// 等待所有子进程结束
while (wait(NULL) > 0);
printf("All child processes have been terminated\n");
}

return 0;
}


  • 父进程创建多个子进程,并杀死一个指定的子进程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>

int main() {
pid_t pid, wpid, tmppid;
int i;

for (i = 0; i < 5; i ++ ) {
pid = fork();
if (pid == 0) {
// 子进程
break;
}
if (i == 2) {
// 保存第三个子进程的PID
tmppid = pid;
printf("------Target child process PID = %d------\n", tmppid);
}
}

if (pid == 0) {
// 子进程执行一些工作
for (int j = 0; j < 5; j ++ ) {
printf("Child %d (PID: %d) working... %d/5\n", i, getpid(), j+1);
sleep(1);
}

printf("Child %d (PID: %d) finished\n", i, getpid());
} else {
// 父进程
sleep(3); // 等待子进程运行一会儿

// 使用kill系统调用杀死指定的子进程
printf("Parent is killing child process %d\n", tmppid);
if (kill(tmppid, SIGKILL) == -1) {
perror("kill error");
} else {
printf("Successfully killed process %d\n", tmppid);
}

// 等待所有子进程结束
while ((wpid = waitpid(-1, NULL, 0)) > 0) {
if (wpid == tmppid) {
printf("Collected target child process %d (killed)\n", wpid);
} else {
printf("Collected child process %d (exited)\n", wpid);
}
}
}

return 0;
}



4.2 alarm 系统调用

4.2.1 alarm 函数介绍

alarm() 是一个用于设置实时(或“闹钟”)信号的系统调用。功能:让内核在指定的 秒数向调用它的进程发送一个 SIGALRM 信号

函数原型

1
2
#include <unistd.h>
unsigned int alarm(unsigned int seconds);

参数

  • seconds
    • 指定一个时间间隔(以 为单位)。
    • 经过 seconds 秒后,内核将向进程发送 SIGALRM 信号。
    • 如果 seconds0,则 取消 之前设置的所有尚未触发的闹钟。

返回值

  • 返回 之前设置的闹钟剩余的秒数
  • 如果之前没有设置闹钟,则返回 0

4.2.2 工作机制和特点

  • 不可重入:一个进程在同一时间只能有一个活跃的 alarm。如果调用 alarm() 时,之前已经设置了一个尚未触发的闹钟,则 新的调用会覆盖旧的闹钟

示例:先设置 alarm(10)(10秒后响),3秒后又设置 alarm(5)。那么原来的10秒闹钟会被取消,新的闹钟将在5秒后触发。并且,第二次调用 alarm(5) 会返回 7(因为第一个闹钟还剩7秒)。

  • 信号交付SIGALRM 的默认动作是终止进程。这意味着如果你只设置了 alarm没有处理这个信号,程序到时会被杀死

因此,通常需要配合信号处理函数使用 signal()捕获 SIGALRM 信号并执行自定义操作

  • 精度alarm() 的精度是 秒级。对于需要更高精度(如微秒)的场景,可以使用 setitimer() 函数。

4.2.3 alarm 系统调用代码示例

以下示例中,设置了 3 秒的闹钟,然后进入一个等待循环。当 3 秒后收到 SIGALRM 信号时,信号处理函数会被调用,修改全局变量 alarm_fired 的值,从而打破主循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>


int alarm_fired = 0;

// SIGALRM 信号处理函数
void alarm_handler(int sig) {
alarm_fired = 1;
}

int main() {
// 注册信号处理函数
signal(SIGALRM, alarm_handler);

// 设置 2 秒后触发 alarm
printf("Setting alarm for 2 seconds...\n");
alarm(2);

// 进入一个等待循环,直到 alarm_fired 被信号处理函数修改
while (!alarm_fired) {
printf("Waiting for alarm...\n");
sleep(1); // 每次等待1秒,避免忙等待
}

printf("Alarm received! Program continuing.\n");

return 0;
}


4.3 setitimer() 系统调用

4.3.1 setitimer 函数介绍

1
2
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

参数

  • which
    • ITIMER_REAL:真实时间,到期发送 SIGALRM
    • ITIMER_VIRTUAL:进程在用户态消耗的时间,到期发送 SIGVTALRM
    • ITIMER_PROF:进程在内核态和用户态消耗的总时间,到期发送 SIGPROF
  • new_value:设置的新定时器值
  • old_value:返回之前的定时器设置(可为 NULL

结构体定义

1
2
3
4
5
6
7
8
9
struct itimerval {
struct timeval it_interval; // 间隔时间
struct timeval it_value; // 第一次触发时间
};

struct timeval {
time_t tv_sec; // 秒
suseconds_t tv_usec; // 微秒
};

可以理解为有2个定时器:

  • 一个用于 第一个闹钟 什么时候触发打印
  • 一个用于之后 间隔多少时间再次触发 闹钟(周期性触发)。

4.3.2 setitimer 系统调用代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <sys/time.h>
#include <signal.h>

void timer_handler(int sig) {
printf("Hello! Signal: %d\n", sig);
}

int main() {
struct itimerval timer;

// 第一次触发时间:2秒
timer.it_value.tv_sec = 2;
timer.it_value.tv_usec = 0;

// 之后每隔5秒触发一次
timer.it_interval.tv_sec = 5;
timer.it_interval.tv_usec = 0;

signal(SIGALRM, timer_handler);
setitimer(ITIMER_REAL, &timer, NULL);

while (1);

return 0;
}


「Linux 系统编程」信号的概念、生命周期与产生
https://marisamagic.github.io/2025/09/09/20250909/
作者
MarisaMagic
发布于
2025年9月9日
许可协议