当前位置: 首页 > news >正文

Linux系统:C语言进程间通信信号(Signal)

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 -l1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP6) 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 信号的产生方式

信号的产生源头多种多样:

  1. 用户终端

    • Ctrl + C -> 产生 SIGINT (Interrupt) 信号,通常用于终止前台进程。

    • Ctrl + \ -> 产生 SIGQUIT (Quit) 信号,不仅终止进程,还会生成core dump文件。

    • Ctrl + Z -> 产生 SIGTSTP (Terminal Stop) 信号,暂停前台进程。

  2. 系统命令

    • kill -SIGNO PID: 向指定PID的进程发送信号。kill -9 1234 是强制杀死进程1234的经典命令。

  3. 硬件异常

    • 进程执行了非法操作,如访问非法内存(段错误) -> 内核会向其发送 SIGSEGV

    • 执行了错误的算术运算(如除以0) -> 内核会向其发送 SIGFPE

  4. 软件事件

    • 子进程退出时,内核会向其父进程发送 SIGCHLD

    • 由 alarm 或 setitimer 设置的定时器超时后,会发送 SIGALRM

3.3 核心API函数详解

3.3.1 kill() - 发送信号
#include <sys/types.h>
#include <signal.h>int kill(pid_t pid, int sig);
  • 功能: 向指定进程(或进程组)发送一个信号。

  • 参数

    • pid > 0: 目标进程的PID。

    • pid == 0: 发送给与调用进程同进程组的所有进程。

    • pid == -1: 发送给所有有权限发送的进程(除init进程外)。

    • sig: 要发送的信号编号,如 SIGINTSIGKILL

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

3.3.2 raise() - 给自己发信号
#include <signal.h>int raise(int sig);

功能: kill(getpid(), sig) 的简化版,向当前进程自身发送信号。

  • 参数: sig - 信号编号。

3.3.3 alarm() - 设置闹钟
#include <unistd.h>unsigned int alarm(unsigned int seconds);
  • 功能: 设置一个定时器(闹钟),在 seconds 秒后,内核会向当前进程发送 SIGALRM 信号。该信号的默认动作是终止进程。

  • 特点: 重置性。如果一个进程之前调用过 alarm() 且闹钟还未超时,再次调用会重置闹钟,新的 seconds 值会覆盖旧值。

  • 返回值: 返回上一次设置的闹钟的剩余秒数,如果之前没有闹钟则返回0。

示例

#include <stdio.h>
#include <unistd.h>int main() {printf("First alarm set for 5 seconds.\n");unsigned int ret = alarm(5); // ret = 0sleep(2); // Sleep for 2 secondsprintf("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 alarmprintf("This line will not be printed because SIGALRM terminated the process.\n");return 0;
}
3.3.4 signal() - 信号处理
#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

捕获处理示例

#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 不仅可以等待子进程结束,还能获取其详细的退出信息。

#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): 用于检查暂停的信号。

示例

pid_t pid = fork();
if (pid == 0) {// Child process// ... maybe do something that causes a segfaultexit(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() - 注册退出清理函数
#include <stdlib.h>
int atexit(void (*function)(void));
  • 功能: 注册一个函数,当进程通过 exit() 函数正常退出时,该注册函数会被自动调用。

  • 特点: 可以注册多个函数,它们的执行顺序与注册顺序相反(LIFO,后进先出)。

  • 注意: 如果进程是被信号杀死的,这些函数不会被执行。

示例

#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 firstprintf("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. 实战任务:音乐播放器控制器

现在,我们综合运用 forkexecwaitpidsignal 等知识,实现一个简单的后台音乐播放器控制器。

4.1 需求分析

父进程作为控制器,负责:

  1. 显示菜单:1:上一首 2:下一首 3:暂停 4:继续 0:退出

  2. 接收用户输入,根据输入向子进程(播放器)发送不同的控制信号。

  3. 优雅地处理子进程的退出。

子进程负责:

  1. 使用 execlp 调用 mpg123 程序来播放音乐。

  2. 根据父进程发来的信号做出反应(播放、暂停、切歌)。

4.2 核心设计思路与流程图

父进程通过 fork + exec 创建子进程来播放音乐。父进程通过信号 (SIGINTSIGSTOPSIGCONT 等) 来控制子进程的状态(暂停、继续、终止)。同时,父进程需要捕获 SIGCHLD 信号,以便在子进程意外结束时(比如一首歌放完了)能及时知晓并可能播放下一首。

图表

代码

4.3 代码实现框架

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <glob.h> // For finding music filespid_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 playerexeclp("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: // Exitif (player_pid > 0) {kill(player_pid, SIGKILL);}return 0;case 1: // Previouscurrent_song_index = (current_song_index - 1 + song_count) % song_count;start_player();break;case 2: // Nextcurrent_song_index = (current_song_index + 1) % song_count;start_player();break;case 3: // Pauseif (player_pid > 0) kill(player_pid, SIGSTOP);break;case 4: // Resumeif (player_pid > 0) kill(player_pid, SIGCONT);break;default:printf("Invalid choice.\n");}}return 0;
}

编译与运行

gcc music_player.c -o music_player
./music_player

(确保系统已安装 mpg123sudo apt-get install mpg123)

5. 注意事项

5.1 信号处理的安全问题

信号处理函数是在异步环境中执行的,这意味着它可能在主程序执行的任何点被调用。因此,在信号处理函数中调用诸如 printfmalloc 等非异步信号安全(async-signal-safe)的函数是不安全的。POSIX.1 标准定义了一个异步信号安全的函数列表,详见 man 7 signal-safety。在信号处理函数中,应尽量只做简单的标志设置,或者使用 write 函数向标准输出写入简单消息。

5.2 更现代的信号处理接口:sigaction

虽然 signal() 函数简单易用,但它在不同Unix版本中的行为可能略有差异(可移植性问题)。更现代、更强大的替代者是 sigaction() 函数,它提供了对信号处理更精确的控制,例如:

  • 指定在处理信号时是否自动阻塞其他信号。

  • 获取信号被触发时的各种上下文信息。

  • 避免信号处理函数执行后被重置为默认行为(某些系统下signal()会有此问题)。

建议在新代码中使用 sigaction

http://www.dtcms.com/a/340359.html

相关文章:

  • RK3576赋能无人机巡检:多路视频+AI识别引领智能化变革
  • deque的原理与实现(了解即可)
  • 基于截止至 2025 年 6 月 4 日,在 App Store 上进行交易的设备数据统计,iOS/iPadOS 各版本在所有设备中所占比例详情
  • 比剪映更轻量!SolveigMM 视频无损剪切实战体验
  • shell变量进阶
  • 基于51单片机自动浇花1602液晶显示设计
  • Ubuntu-安装Epics Archiver Appliance教程
  • 玳瑁的嵌入式日记D21-08020(数据结构)
  • 服务器内存条不识别及服务器内存位置图
  • 认识Node.js及其与 Nginx 前端项目区别
  • 动手学深度学习(pytorch版):第五章节—多层感知机(1)层和块
  • 从异构计算视角审视ARM与FPGA:架构融合驱动智能时代计算范式革新
  • mybatis xml中表名 字段报红解决
  • S32K328(Arm Cortex-M7)适配CmBacktrace错误追踪
  • 生产电路板的公司有哪些?国内生产电路板的公司
  • 05-网关服务开发指南
  • 从零实现自定义顺序表:万字详解 + 完整源码 + 图文分析
  • 虚幻引擎目录结构
  • MYSQL-增删查改CRUD
  • Protobuf
  • AIStarter服务器版深度解析:与桌面版对比,解锁云端AI开发新体
  • STM32F4 外扩SRAM介绍及应用
  • word——如何给封面、目录、摘要、正文设置不同的页码
  • Web网站的运行原理1
  • 使用 mongosh 设置 MongoDB 账号密码
  • word——快速删除页眉横线
  • 微软宣布开源大模型gpt-oss在Azure平台实现性能突破
  • Azure 使用记录
  • Claude Code NPM 包发布命令
  • 【Linux系统】匿名管道以及进程池的简单实现