C语言signal函数如何正确使用?

99ANYc3cd6
预计阅读时长 20 分钟
位置: 首页 C语言 正文

signal 是 C 标准库中一个非常重要的函数,它允许程序异步地响应和处理特定的“信号”(Signals),信号是 Unix/Linux/Windows 等操作系统中进程间通信的一种机制,用于通知进程发生了某个特定的事件。

c 语言 signal
(图片来源网络,侵删)

什么是信号?

你可以把信号想象成操作系统发给进程的“中断”或“通知”,这些通知通常由以下事件触发:

  • 硬件异常:比如非法访问内存(SIGSEGV)、除以零(SIGFPE)。
  • 软件事件:比如用户按下 Ctrl+CSIGINT)、Ctrl+\SIGQUIT)、Ctrl+ZSIGTSTP)。
  • 系统调用kill 命令可以发送任意信号给一个进程。

当进程接收到一个信号时,它会中断当前的正常执行流程,转而去执行一个预先设定好的“信号处理函数”,处理完毕后再返回到之前被中断的地方继续执行。


signal 函数的原型

signal 函数在 <signal.h> 头文件中声明。

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

参数解释:

c 语言 signal
(图片来源网络,侵删)
  1. int signum:指定要捕获的信号编号,通常使用 <signal.h> 中定义的宏来表示,SIGINTSIGSEGV 等。
  2. sighandler_t handler:一个指向信号处理函数的指针,它指定了当 signum 信号发生时,应该执行哪个函数,这个类型 sighandler_t 本质上是一个函数指针类型,定义为 void (*)(int),表示它指向一个接受一个 int 参数(信号编号)并返回 void 的函数。

返回值:

  • 成功时:返回该信号之前的信号处理函数的指针。
  • 失败时:返回 SIG_ERR(这是一个宏),errno 会被设置为相应的错误码。

handler 参数的三种可能性

signal 函数的第二个参数 handler 可以有以下三种值:

a) SIG_DFL (Default Action - 默认处理)

告诉操作系统对信号采取默认处理方式,每个信号都有一个默认行为,常见的有:

  • SIGINT (中断,通常是 Ctrl+C):终止进程。
  • SIGQUIT (退出,通常是 Ctrl+\):终止进程,并生成核心转储文件。
  • SIGSEGV (段错误):终止进程,并生成核心转储文件。
  • SIGTERM (终止请求):终止进程。
  • SIGTSTP (终端停止,通常是 Ctrl+Z):暂停进程。

b) SIG_IGN (Ignore - 忽略)

告诉操作系统完全忽略这个信号,这个信号将被丢弃,不会产生任何效果。

c 语言 signal
(图片来源网络,侵删)

一个经典例子:忽略 SIGINT (Ctrl+C)

#include <stdio.h>
#include <signal.h>
#include <unistd.h> // for sleep()
int main() {
    printf("程序正在运行... 尝试按下 Ctrl+C 看看会发生什么,\n");
    printf("现在我将忽略 SIGINT 信号,\n");
    // 忽略 SIGINT 信号
    if (signal(SIGINT, SIG_IGN) == SIG_ERR) {
        perror("signal");
        return 1;
    }
    // 程序将在这里无限循环,无法被 Ctrl+C 中断
    while(1) {
        sleep(1);
        printf("程序仍在运行...\n");
    }
    return 0;
}

编译并运行这个程序,你会发现按下 Ctrl+C 并不能终止它,直到你使用 kill 命令或者关闭终端。

c) 自定义处理函数 (Custom Handler)

提供一个指向你自己编写的函数的指针,当信号发生时,这个函数会被调用。

函数原型要求: void my_handler(int signum);

它必须接受一个 int 参数(即收到的信号编号),并且返回 void

一个经典例子:捕获 SIGINT

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
// 自定义的信号处理函数
void my_handler(int signum) {
    printf("\n收到信号 %d! 正在优雅地退出...\n", signum);
    // 在这里可以进行清理工作,比如关闭文件、释放内存等
    exit(0); // 主动退出程序
}
int main() {
    printf("程序正在运行... 按下 Ctrl+C 来触发信号处理函数,\n");
    // 注册 my_handler 函数来处理 SIGINT 信号
    if (signal(SIGINT, my_handler) == SIG_ERR) {
        perror("signal");
        return 1;
    }
    while(1) {
        sleep(1);
        printf("程序正在运行...\n");
    }
    return 0;
}

在这个例子中,当你按下 Ctrl+C 时,my_handler 函数会被调用,打印一条消息并退出程序。


信号处理的注意事项(非常重要)

直接使用 signal 函数有一些固有的问题,这些问题在现代 C 编程中通常被更可靠的 sigaction 函数所解决。

a) 可重入性

信号处理函数应该尽可能简单,并且应该是可重入的,这意味着在信号处理函数执行期间,如果再次接收到同一个信号,它仍然可以安全地被调用。

避免在信号处理函数中做的事情:

  • 调用标准 I/O 库函数(如 printf, scanf, malloc 等),因为它们通常使用全局状态,不安全。
  • 调用不可重入的函数。
  • 修改非 volatile sig_atomic_t 类型的全局变量。

推荐做法: 在信号处理函数中只设置一个标志位(最好是 volatile sig_atomic_t 类型),然后在主程序循环中检查这个标志位来执行复杂的操作。

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdatomic.h> // C11 原子类型
// 使用 volatile sig_atomic_t 确保原子操作
volatile sig_atomic_t keep_running = 1;
void my_handler(int signum) {
    printf("\n收到信号 %d,准备退出...\n", signum);
    keep_running = 0; // 安全地修改标志位
}
int main() {
    signal(SIGINT, my_handler);
    while (keep_running) {
        sleep(1);
        printf("程序正在运行...\n");
    }
    printf("程序正常退出,\n");
    return 0;
}

这个例子比上一个更安全、更健壮。

b) 信号的丢失

当一个信号被处理时,如果该信号再次发生,可能会被“丢失”。sigaction 函数提供了 SA_RESTART 选项,可以尝试在被信号中断的系统调用(如 read, write)自动重新启动,但这不能解决信号本身丢失的问题。

c) signal 的行为在不同系统间可能不一致

早期的 signal 函数实现存在差异,在处理完一个信号后,系统是否会自动将信号处理函数重置为 SIG_DFL(这被称为 " unreliable " 的信号)是不确定的。


signal vs. sigaction

为了解决 signal 函数的上述问题,POSIX 标准引入了 sigaction 函数,它提供了更强大、更可靠的控制。

特性 signal sigaction
可靠性 不可靠,行为可能因系统而异 可靠,行为在 POSIX 系统中标准
功能 基本功能:设置处理函数 功能强大:设置处理函数、查询当前设置、控制信号集
可重入性 无法保证 可以指定 SA_RESTART 标志来尝试重启系统调用
信号掩码 无法在处理期间自动屏蔽其他信号 可以指定一个信号集,在处理函数执行期间自动屏蔽这些信号
推荐度 简单示例,不推荐用于生产环境 强烈推荐用于任何严肃的程序

sigaction 的基本用法:

#include <signal.h>
#include <stdio.h>
void my_handler(int signum) {
    printf("收到信号 %d\n", signum);
}
int main() {
    struct sigaction sa;
    sa.sa_handler = my_handler; // 设置处理函数
    sigemptyset(&sa.sa_mask);   // 初始化信号集,不屏蔽任何信号
    sa.sa_flags = 0;           // 默认标志
    // 使用 sigaction 注册 SIGINT 的处理函数
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        return 1;
    }
    while(1) {
        pause(); // 暂停进程,等待信号
    }
    return 0;
}

常见信号列表

信号名 默认动作 说明
SIGINT Term 中断信号 (Ctrl+C)
SIGQUIT Core 退出信号 (Ctrl+),会产生 core dump
SIGTERM Term 终止信号,程序可捕获并优雅退出
SIGSEGV Core 段错误,访问非法内存地址
SIGFPE Core 浮点异常,如除以零
SIGKILL Term 无法捕获的终止信号,强制杀死进程
SIGSTOP Stop 无法捕获的停止信号,暂停进程
SIGTSTP Stop 终端停止信号 (Ctrl+Z)
SIGCONT Cont 继续执行一个被暂停的进程
SIGALRM Term alarm() 函数设置的定时器到期时发送
SIGUSR1 Term 用户自定义信号 1
SIGUSR2 Term 用户自定义信号 2

  1. signal 是什么? 它是 C 语言中用于捕获和处理异步信号的机制。
  2. 如何使用? 调用 signal(signum, handler)handler 可以是 SIG_DFL(默认)、SIG_IGN(忽略)或一个自定义函数。
  3. 自定义函数要注意什么? 必须简单、可重入,最好只设置一个标志位,在主程序中处理复杂逻辑,避免调用标准 I/O 和不可重入函数。
  4. 现代实践是什么? 虽然在简单示例中 signal 可以工作,但对于健壮的程序,强烈推荐使用功能更强大、更可靠的 sigaction 函数。
  5. 信号的生命周期:从操作系统产生 -> 内核传递给进程 -> 进程执行处理函数 -> 处理完毕后返回,这个过程是异步的。
-- 展开阅读全文 --
头像
dede首页滚动图片如何实现?
« 上一篇 04-16
dede文章主栏目是什么?如何设置与使用?
下一篇 » 04-16

相关文章

取消
微信二维码
支付宝二维码

目录[+]