1. 引言:从"中断"到"信号"
想象一下,你正在书房专心致志地写代码,这时厨房的水烧开了,鸣笛声大作。你会怎么做?你会暂停(Interrupt) 手头的工作,跑去厨房关掉烧水壶,然后再回来继续 coding。
在Linux系统中,信号(Signal) 就是一种类似的异步中断机制 。它允许一个进程(或内核)向另一个进程发送一个简单的消息,通知其某个特定事件的发生。接收信号的进程通常会暂停 当前正在执行的指令流,转而去执行一个特殊的信号处理函数,处理完毕后(如果没退出)再回来继续执行。这就是信号最基本的概念。
本文将深入探讨信号的产生、处理以及如何利用它来构建一个简单的音乐播放器控制器。
2. 进程间通信(IPC)与信号概述
进程是操作系统资源分配和独立运行的基本单位。每个进程都拥有自己独立的地址空间,一个进程无法直接访问另一个进程的数据。因此,进程之间需要一种机制来进行通信(Communication) 与同步(Synchronization) ,这就是进程间通信(IPC, Inter-Process Communication)。
常见的IPC方式包括:
-
信号(Signal): 本文焦点,一种异步的、简单的通知机制。
-
管道(Pipe) / 命名管道(FIFO): 单向或双向的字节流通信。
-
套接字(Socket): 功能最强大,可用于网络通信和不同主机间的进程通信。
-
IPC对象 : 包括共享内存 、信号量集 、消息队列,源自System V IPC标准。
信号是其中最轻量、最古老的一种方式。它携带的信息量很小,通常只是一个信号编号,但其响应非常迅速。
3. 信号的深度解析
3.1 信号列表与分类
在Linux系统中,可以使用 kill -l
命令查看所有支持的信号。
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
...
信号可分为两大类:
-
不可靠信号(1 ~ 31) : 源于UNIX早期版本,也称为非实时信号。它们可能会丢失。如果同一个不可靠信号在短时间内多次产生,进程可能只能接收到一次。因为内核可能使用位图来记录它们的发生,多次相同的信号在处理之前会被合并为一次。
-
可靠信号(34 ~ 64) : 在POSIX.1标准中定义,也称为实时信号。它们支持排队,只要信号发送的速度不超过系统队列的上限,信号就不会丢失。
3.2 信号的产生方式
信号的产生源头多种多样:
-
用户终端:
-
Ctrl + C
-> 产生SIGINT
(Interrupt) 信号,通常用于终止前台进程。 -
Ctrl + \
-> 产生SIGQUIT
(Quit) 信号,不仅终止进程,还会生成core dump文件。 -
Ctrl + Z
-> 产生SIGTSTP
(Terminal Stop) 信号,暂停前台进程。
-
-
系统命令:
kill -SIGNO PID
: 向指定PID的进程发送信号。kill -9 1234
是强制杀死进程1234的经典命令。
-
硬件异常:
-
进程执行了非法操作,如访问非法内存(段错误) -> 内核会向其发送
SIGSEGV
。 -
执行了错误的算术运算(如除以0) -> 内核会向其发送
SIGFPE
。
-
-
软件事件:
-
子进程退出时,内核会向其父进程发送
SIGCHLD
。 -
由
alarm
或setitimer
设置的定时器超时后,会发送SIGALRM
。
-
3.3 核心API函数详解

3.3.1 kill()
- 发送信号
cpp
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
-
功能: 向指定进程(或进程组)发送一个信号。
-
参数:
-
pid
> 0: 目标进程的PID。 -
pid
== 0: 发送给与调用进程同进程组的所有进程。 -
pid
== -1: 发送给所有有权限发送的进程(除init进程外)。 -
sig
: 要发送的信号编号,如SIGINT
,SIGKILL
。
-
-
返回值: 成功返回0,失败返回-1并设置errno。
3.3.2 raise()
- 给自己发信号
cpp
#include <signal.h>
int raise(int sig);
功能 : kill(getpid(), sig)
的简化版,向当前进程自身发送信号。
- 参数 :
sig
- 信号编号。
3.3.3 alarm()
- 设置闹钟
cpp
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
-
功能 : 设置一个定时器(闹钟),在
seconds
秒后,内核会向当前进程发送SIGALRM
信号。该信号的默认动作是终止进程。 -
特点 : 重置性 。如果一个进程之前调用过
alarm()
且闹钟还未超时,再次调用会重置 闹钟,新的seconds
值会覆盖旧值。 -
返回值 : 返回上一次设置的闹钟的剩余秒数,如果之前没有闹钟则返回0。
示例:
cpp
#include <stdio.h>
#include <unistd.h>
int main() {
printf("First alarm set for 5 seconds.\n");
unsigned int ret = alarm(5); // ret = 0
sleep(2); // Sleep for 2 seconds
printf("Resetting alarm for 3 seconds from now.\n");
ret = alarm(3); // ret = 5 - 2 = 3 (seconds left from previous alarm)
printf("Previous alarm had %u seconds left.\n", ret);
sleep(10); // Sleep longer than the alarm
printf("This line will not be printed because SIGALRM terminated the process.\n");
return 0;
}
3.3.4 signal()
- 信号处理
cpp
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
-
功能 : 修改进程对特定信号
signum
的处理方式。 -
参数:
-
signum
: 要捕获的信号编号。 -
handler
:-
SIG_IGN
: 忽略此信号。 -
SIG_DFL
: 恢复对此信号的默认处理。 -
函数指针 : 程序员自定义的信号处理函数地址。该函数必须具有
void func(int sig_num)
的格式。
-
-
-
返回值 : 成功时返回上一次的信号处理函数指针,失败返回
SIG_ERR
。
捕获处理示例:
cpp
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
// 自定义信号处理函数
void my_handler(int sig_num) {
printf("\nCaught signal %d! I'm not going to die!\n", sig_num);
// 注意:在信号处理函数中使用printf等标准IO函数可能是不安全的,这里仅作演示
}
int main() {
// 捕获SIGINT信号 (Ctrl+C)
if (signal(SIGINT, my_handler) == SIG_ERR) {
perror("Signal setup failed");
return 1;
}
printf("Process PID: %d. Try pressing Ctrl+C...\n", getpid());
while(1) {
pause(); // 无限期休眠,等待任何信号到来
}
return 0;
}
3.4 重要补充知识
3.4.1 waitpid()
与进程退出状态
waitpid
不仅可以等待子进程结束,还能获取其详细的退出信息。
cpp
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);
wstatus 是一个输出参数,由内核填充状态信息。需要使用一系列宏来解析:
WIFEXITED(wstatus): 如果子进程正常终止(通过 exit 或 return),则返回真。
WEXITSTATUS(wstatus): 如果 WIFEXITED 为真,此宏提取子进程的退出码(exit 的参数)。
WIFSIGNALED(wstatus): 如果子进程是被信号杀死的,则返回真。
WTERMSIG(wstatus): 如果 WIFSIGNALED 为真,此宏提取导致子进程终止的信号编号。
WIFSTOPPED(wstatus) / WSTOPSIG(wstatus): 用于检查暂停的信号。
示例:
cpp
pid_t pid = fork();
if (pid == 0) {
// Child process
// ... maybe do something that causes a segfault
exit(10);
} else {
int wstatus;
waitpid(pid, &wstatus, 0);
if (WIFEXITED(wstatus)) {
printf("Child exited normally with code: %d\n", WEXITSTATUS(wstatus));
} else if (WIFSIGNALED(wstatus)) {
printf("Child was killed by signal: %d\n", WTERMSIG(wstatus));
}
}
3.4.2 atexit()
- 注册退出清理函数
cpp
#include <stdlib.h>
int atexit(void (*function)(void));
-
功能 : 注册一个函数,当进程通过
exit()
函数正常退出时,该注册函数会被自动调用。 -
特点 : 可以注册多个函数,它们的执行顺序与注册顺序相反(LIFO,后进先出)。
-
注意 : 如果进程是被信号杀死 的,这些函数不会被执行。
示例:
cpp
#include <stdio.h>
#include <stdlib.h>
void cleanup1() { printf("Performing cleanup 1...\n"); }
void cleanup2() { printf("Performing cleanup 2...\n"); }
int main() {
atexit(cleanup1);
atexit(cleanup2); // This will be called first
printf("Main function is running...\n");
// exit(0); // atexit functions will be called
// If we use _exit(0) or are killed by a signal, cleanup won't happen.
return 0; // return calls exit implicitly
}
// Output:
// Main function is running...
// Performing cleanup 2...
// Performing cleanup 1...
4. 实战任务:音乐播放器控制器
现在,我们综合运用 fork
, exec
, waitpid
, signal
等知识,实现一个简单的后台音乐播放器控制器。
4.1 需求分析
父进程作为控制器,负责:
-
显示菜单:
1:上一首 2:下一首 3:暂停 4:继续 0:退出
。 -
接收用户输入,根据输入向子进程(播放器)发送不同的控制信号。
-
优雅地处理子进程的退出。
子进程负责:
-
使用
execlp
调用mpg123
程序来播放音乐。 -
根据父进程发来的信号做出反应(播放、暂停、切歌)。
4.2 核心设计思路与流程图
父进程通过 fork
+ exec
创建子进程来播放音乐。父进程通过信号 (SIGINT
, SIGSTOP
, SIGCONT
等) 来控制子进程的状态(暂停、继续、终止)。同时,父进程需要捕获 SIGCHLD
信号,以便在子进程意外结束时(比如一首歌放完了)能及时知晓并可能播放下一首。
图表
代码
4.3 代码实现框架
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <glob.h> // For finding music files
pid_t player_pid = -1;
int current_song_index = 0;
int song_count = 0;
char **song_list = NULL;
// 自定义SIGCHLD处理函数
void child_handler(int sig) {
int wstatus;
pid_t pid;
// 非阻塞地等待所有结束的子进程
while ((pid = waitpid(-1, &wstatus, WNOHANG)) > 0) {
if (pid == player_pid) {
printf("Music player process (PID: %d) ended.\n", player_pid);
player_pid = -1;
// 如果不是父进程主动杀的(比如歌曲放完了),则播下一首
if (WIFEXITED(wstatus) || WIFSIGNALED(wstatus)) {
// 简单策略:一首歌放完就播下一首
current_song_index = (current_song_index + 1) % song_count;
printf("Moving to next song: %d\n", current_song_index);
}
}
}
}
// 退出清理函数
void cleanup() {
system("stty echo"); // 恢复终端回显
printf("\033[?25h"); // 显示光标
if (player_pid > 0) {
kill(player_pid, SIGKILL); // 确保子进程被杀死
}
// 释放song_list内存...
}
// 启动播放器子进程
void start_player() {
if (player_pid > 0) {
kill(player_pid, SIGINT); // 先杀死之前的播放进程
// wait for it to die... (handled by SIGCHLD)
sleep(1);
}
player_pid = fork();
if (player_pid == 0) {
// Child process: become the music player
execlp("mpg123", "mpg123", "-q", song_list[current_song_index], NULL);
perror("execlp failed");
exit(1);
} else if (player_pid < 0) {
perror("fork failed");
}
}
int main() {
// 1. 查找音乐文件 (e.g., *.mp3)
glob_t glob_result;
glob("*.mp3", GLOB_TILDE, NULL, &glob_result);
song_count = glob_result.gl_pathc;
song_list = glob_result.gl_pathv;
if (song_count == 0) {
printf("No MP3 files found!\n");
exit(1);
}
// 2. 设置信号处理和清理函数
signal(SIGCHLD, child_handler);
atexit(cleanup);
// 3. 启动第一首歌
start_player();
// 4. 主控制循环
int choice;
while (1) {
printf("\n1:Prev | 2:Next | 3:Pause | 4:Resume | 0:Exit\n");
scanf("%d", &choice);
switch (choice) {
case 0: // Exit
if (player_pid > 0) {
kill(player_pid, SIGKILL);
}
return 0;
case 1: // Previous
current_song_index = (current_song_index - 1 + song_count) % song_count;
start_player();
break;
case 2: // Next
current_song_index = (current_song_index + 1) % song_count;
start_player();
break;
case 3: // Pause
if (player_pid > 0) kill(player_pid, SIGSTOP);
break;
case 4: // Resume
if (player_pid > 0) kill(player_pid, SIGCONT);
break;
default:
printf("Invalid choice.\n");
}
}
return 0;
}
编译与运行:
cpp
gcc music_player.c -o music_player
./music_player
(确保系统已安装 mpg123
:sudo apt-get install mpg123
)
5. 注意事项
5.1 信号处理的安全问题
信号处理函数是在异步环境中执行的,这意味着它可能在主程序执行的任何点被调用。因此,在信号处理函数中调用诸如 printf
、malloc
等非异步信号安全(async-signal-safe)的函数是不安全 的。POSIX.1 标准定义了一个异步信号安全的函数列表,详见 man 7 signal-safety
。在信号处理函数中,应尽量只做简单的标志设置,或者使用 write
函数向标准输出写入简单消息。
5.2 更现代的信号处理接口:sigaction
虽然 signal()
函数简单易用,但它在不同Unix版本中的行为可能略有差异(可移植性问题)。更现代、更强大的替代者是 sigaction()
函数,它提供了对信号处理更精确的控制,例如:
-
指定在处理信号时是否自动阻塞其他信号。
-
获取信号被触发时的各种上下文信息。
-
避免信号处理函数执行后被重置为默认行为(某些系统下
signal()
会有此问题)。
建议在新代码中使用 sigaction
。