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 -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 信号的产生方式

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

  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

    • alarmsetitimer 设置的定时器超时后,会发送 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. 显示菜单:1:上一首 2:下一首 3:暂停 4:继续 0:退出

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

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

子进程负责:

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

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

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

(确保系统已安装 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

相关推荐
Elastic 中国社区官方博客2 小时前
将你的 Grafana Kubernetes 仪表板迁移到 Elastic Observability:相同的 PromQL,30 倍更快的查询
大数据·人工智能·elasticsearch·搜索引擎·容器·kubernetes·grafana
abcy0712132 小时前
storm核心实时机制
大数据·storm
灵机一物2 小时前
合伙制律所分红和提成律师个税怎么合规优化?
大数据·人工智能
Elastic 中国社区官方博客5 小时前
如何使用 OpenTelemetry 对你的搜索 API 进行埋点,并使用 ES|QL 对其进行查询
大数据·功能测试·elasticsearch·搜索引擎·全文检索·可用性测试
Xzaveir_7776 小时前
OPPO、vivo、荣耀号码认证:多终端拨测矩阵与异常复现
大数据·网络·科技·矩阵·产品经理·ai-native
老徐聊GEO6 小时前
亲测有效的AI品牌检测公司案例分享
大数据·人工智能·python
韭菜学长6 小时前
科技型中小企业如何申请政府补贴?申报指南
大数据·人工智能
延凡科技6 小时前
智慧燃气解决方案:管道煤气监管应用管理系统(三维GIS+IoT+大数据落地实战)
大数据·人工智能·科技·物联网·安全
imbackneverdie7 小时前
知网官宣:禁止 AI 列为论文署名作者,标注 Gemini、DeepSeek 等 AI 为作者的稿件统一下架
大数据·人工智能·ai·职场和发展·aigc·科研·高校
@insist1237 小时前
信息系统管理工程师-数字化转型成熟度模型核心考点解析
大数据·人工智能·软考·软件水平考试·信息系统管理工程师·软考信管