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

相关推荐
君不见,青丝成雪1 小时前
Flink双流join
大数据·数据仓库·flink
EkihzniY2 小时前
结构化 OCR 技术:破解各类检测报告信息提取难题
大数据·ocr
吱吱企业安全通讯软件3 小时前
吱吱企业通讯软件保证内部通讯安全,搭建数字安全体系
大数据·网络·人工智能·安全·信息与通信·吱吱办公通讯
云手机掌柜3 小时前
Tumblr长文运营:亚矩阵云手机助力多账号轮询与关键词布局系统
大数据·服务器·tcp/ip·矩阵·流量运营·虚幻·云手机
拓端研究室5 小时前
专题:2025全球消费趋势与中国市场洞察报告|附300+份报告PDF、原数据表汇总下载
大数据·信息可视化·pdf
阿里云大数据AI技术7 小时前
MaxCompute聚簇优化推荐功能发布,单日节省2PB Shuffle、7000+CU!
大数据
Lx35210 小时前
Hadoop小文件处理难题:合并与优化的最佳实践
大数据·hadoop
激昂网络11 小时前
android kernel代码 common-android13-5.15 下载 编译
android·大数据·elasticsearch