Linux应用开发-9-信号

软中断信号,用于通知进程发生了异步事件,它是 Linux 系统响应某些条件而产生的一个事件

kill -l查看系统中支持的信号种类(具体信息自行查阅)

信号核心区别:非实时信号 - 实时信号,关键在于"是否排队"。

  • 非实时信号 (信号值1-31),不可靠 (Unreliable),不支持排队,如果在进程处理前,您连续发送了 5 次相同的SIGHUP 信号,进程的 PCB(进程控制块)只会记录"收到了 SIGHUP"这个事实,而不会记录次数。进程最终只会处理 1 次SIGHUP 信号,剩下的 4 次被丢弃了。
  • 实时信号 (信号 34-64),可靠 (Reliable),支持排队,如果在进程处理前,您连续发送了 5 次相同的实时信号(SIGRTMIN+1)。内核会将这 5 个信号全部放入队列中。进程最终会处理 5 次该信号,不会丢失。

Linux 信号如何产生和如何处理

1.信号可以由三种主要事件产生:

  • 程序错误:这类错误通常由 CPU 硬件检测到,随后 Linux 内核会向导致此错误的进程发送一个相应的信号(如 SIGFPE - 浮点异常,SIGSEGV - 段错误)
  • 外部事件:由终端驱动程序或内核根据外部变化来生成信号。用户在终端按下 Ctrl+C(产生 SIGINT 信号)、Ctrl+Z(产生 SIGTSTP 信号)。
  • 显式请求:最直接的进程间通信方式。一个进程调用 kill() 函数,主动向另一个进程(或它自己)发送信号。

2.信号的同步与异步

  • 同步信号 (Synchronous):由进程自身的执行动作引起(如"程序错误"或"显式请求"发给自己)。信号的产生与进程的执行是同步的。
  • 异步信号 (Asynchronous):由进程外部的事件引起(如"外部事件"或另一个进程调用 kill())。进程无法预测信号何时会到达,只能被动接收。

3.进程如何处理信号

  • 忽略信号 (Ignore):进程告诉内核,这个信号,可以直接丢弃。
  • 捕获信号 (Capture):当信号到达时,内核会暂停进程的主程序,转而去执行这个自定义函数(信号处理函数"(Signal Handler))。
  • 执行默认动作 (Default Action):如果进程既没有"忽略"也没有"捕获"这个信号,内核将执行该信号的系统默认动作。(终止 (Terminate),终止并转储 (Core Dump),忽略 (Ignore),暂停 (Stop),恢复 (Continue))
c 复制代码
#include <stdio.h>  // 包含 stdio.h 以便使用 printf 函数
#include <unistd.h> // 包含 unistd.h 以便使用 sleep 函数
#include <stdlib.h> // 包含 stdlib.h 以便使用 exit 函数

int main(void)
{
    // 打印一条初始信息
    printf("\nthis is an signal test function\n\n");
    // (1) 进入一个无限循环
    while (1) {
        // (2) 循环打印提示信息
        printf("waiting for the SIGINT signal , please enter \"ctrl + c\"... \n");
        // (3) 暂停 1 秒
        sleep(1);
    }
    exit(0);
}

用户在终端上按下 Ctrl+C 组合键。这个动作被终端驱动程序捕获,并会向当前在前台运行的进程( signal_demo 程序)发送一个名为 SIGINT(SIGnal INTerrupt,中断信号)的信号。代码中,完全没有包含任何用于"捕获"或"忽略"SIGINT 信号的特殊代码,当 signal_demo 进程收到 SIGINT 信号时,它只能执行内核为 SIGINT 规定好的默认动作(终止进程 (Terminate the process))。

捕获信号相关 API 函数

我们使用信号只是通知进程而不是要杀死它,或者在杀死它前我们想进行某些收尾工作,这个时候就是需要我们去捕获这个信号,然后去处理它。

===============================================

signal() (已不推荐)用于为一个信号注册一个处理函数。

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <signal.h> // 必须包含 signal.h
#include <stdlib.h> // 必须包含 stdlib.h

/**
 * 自定义的信号处理函数
 */
void signal_handler(int sig)
{
    printf("\n--- Signal Handler --- \n");
    printf("Received signal: %d\n", sig);

    // (4) 检查是否是 SIGINT 信号
    if (sig == SIGINT) {
        printf("OK, I get it. I will restore the default action.\n\n");
        
        // 将 SIGINT 信号的处理恢复为默认 (SIG_DFL)默认处理方式(即终止进程)
        signal(SIGINT, SIG_DFL);
    }
}

int main(void)
{
    printf("Program starting... (PID: %d)\n\n", getpid());

    // (1) 注册信号处理函数
    // 告诉内核: 当 SIGINT 信号 (Ctrl+C) 到来时,
    // 不要执行默认动作, 而是去调用 signal_handler 函数
    signal(SIGINT, signal_handler);

    // (2) 无限循环, 等待信号
    while (1) {
        printf("waiting for the SIGINT signal , please enter \"ctrl + c\"... \n");
        sleep(1);
    }

    exit(0);
}

第一次按 Ctrl+C:内核查找发现 SIGINT 已经被注册,于是调用 signal_handler。程序打印出 "Received signal..." 等信息,然后执行 signal(SIGINT, SIG_DFL),将信号处理恢复为默认。程序不会退出,继续循环。

第二次按 Ctrl+C:内核再次收到 SIGINT,但此时它的处理方式已经是 SIG_DFL(默认)。因此,内核执行默认动作------终止进程。

===============================================

sigaction(推荐)

原因:signal() 在不同 Unix 系统上的行为不一致(即可移植性差)。sigaction() 是 POSIX 标准定义的现代函数,功能更强,行为更可靠。

就是对结构体进行sigaction填写,然后sigaction(SIGINT, &act, NULL);捕获相应终端后执行结构体内详细操作配置。

c 复制代码
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h> // 包含struct sigaction,以及所有相关的函数sigaction(), sigemptyset()
#include <sys/types.h>
#include <sys/wait.h>

/**
 * 信号处理函数
 */
void signal_handler(int sig)
{
    printf("\n--- Signal Handler --- \n");
    printf("Received signal: %d\n", sig);

    if (sig == SIGINT) {
        printf("I have get SIGINT!\n\n");
        printf("The signal handler will be reset to default automatically.\n\n");
    }
}

int main(void)
{
    // 定义一个 sigaction 结构体变量
    struct sigaction act;
    printf("this is sigaction function test demo! (PID: %d)\n\n", getpid());

    // (2) 设置信号处理的回调函数
    act.sa_handler = signal_handler;

    // (3) 清空屏蔽信号集 (sa_mask)
    // 告诉内核:在执行 signal_handler 期间,不要额外屏蔽其他信号。
    sigemptyset(&act.sa_mask);

    // (4) 设置关键的行为标志
    // SA_RESETHAND 意思是:信号处理函数一旦被调用过一次,
    // 就自动将该信号的处理方式恢复为默认。
    act.sa_flags = SA_RESETHAND;
    /*
    1.SA_RESETHAND: "一次性"处理器。其作用与旧的 signal() 函数类似:信号处理函数一旦被调用过一次, 就自动将该信号的处理方式恢复为默认(SIG_DFL)。
    2.SA_SIGINFO: "切换到扩展模式"。告诉内核请使用 act.sa_sigaction 作为处理函数,而不是 act.sa_handler。
    3.SA_NOCLDSTOP: "不关心子进程暂停"。当用于 SIGCHLD 信号时,如果子进程只是被暂停(SIGSTOP)或继续(SIGCONT),不要通知父进程。
    4.SA_NOCLDWAIT: "自动回收僵尸进程"。当用于 SIGCHLD 信号时,它告诉内核子进程结束后不要产生僵尸进程,父进程也不需要调用 wait() 来回收它们。
    5.SA_NODEFER: "不要自动屏蔽自己"。允许在处理函数A执行期间,如果同一个信号再次到来,立即中断A,再次执行A(这称为"可重入"信号,不推荐新手使用)。
    6.SA_RESTART: "自动重启系统调用"。一个非常有用的标志。如果进程正在执行一个"慢"系统调用(如 read 等待输入)时被信号中断,处理完信号后,内核会自动重新开始那个 read 调用,而不是让它失败。
    */

    // (5) 正式注册信号
    // 告诉内核: 请使用 act 结构体中定义的规则来处理 SIGINT 信号
    sigaction(SIGINT, &act, NULL);

    // 无限循环, 等待信号
    while (1)
    {
        printf("waiting for the SIGINT signal , please enter \"ctrl + c\"... \n\n");
        sleep(1);
    }

    exit(0);
}

第一次 Ctrl+C 触发 signal_handler 并打印信息,第二次 Ctrl+C 终止进程。

sigaction结构体属于头文件signal.h

c 复制代码
struct sigaction {
//1.信号处理函数指针(二选一)
void(*sa_handler)(int);
void(*sa_sigaction)(int, siginfo_t *, void *);
//2.信号掩码:在信号处理函数执行期间需要额外屏蔽的信号 
sigset_t sa_mask;
//3.行为标志 
int sa_flags;
//4.(已废弃,不应使用) 
void(*sa_restorer)(void);
};
//行为标志
act.sa_flags = SA_RESETHAND;
/*
1.SA_RESETHAND: "一次性"处理器。其作用与旧的 signal() 函数类似:信号处理函数一旦被调用过一次, 就自动将该信号的处理方式恢复为默认(SIG_DFL)。
2.SA_SIGINFO: "切换到扩展模式"。告诉内核请使用 act.sa_sigaction 作为处理函数,而不是 act.sa_handler。
3.SA_NOCLDSTOP: "不关心子进程暂停"。当用于 SIGCHLD 信号时,如果子进程只是被暂停(SIGSTOP)或继续(SIGCONT),不要通知父进程。
4.SA_NOCLDWAIT: "自动回收僵尸进程"。当用于 SIGCHLD 信号时,它告诉内核子进程结束后不要产生僵尸进程,父进程也不需要调用 wait() 来回收它们。
5.SA_NODEFER: "不要自动屏蔽自己"。允许在处理函数A执行期间,如果同一个信号再次到来,立即中断A,再次执行A(这称为"可重入"信号,不推荐新手使用)。
6.SA_RESTART: "自动重启系统调用"。一个非常有用的标志。如果进程正在执行一个"慢"系统调用(如 read 等待输入)时被信号中断,处理完信号后,内核会自动重新开始那个 read 调用,而不是让它失败。
*/
标志 (Flag) 默认行为 (当 sa_flags = 0) 设置标志后的行为
SA_RESETHAND 处理函数永久有效 处理函数只执行一次,然后恢复默认
SA_SIGINFO 使用 sa_handler (简单, 1参数) 使用 sa_sigaction (扩展)
SA_NOCLDSTOP 子进程暂停/继续会发送 SIGCHLD 子进程暂停/继续不会发送 SIGCHLD
SA_NOCLDWAIT 子进程会变僵尸 (需wait) 子进程自动回收 (不需wait)
SA_NODEFER 处理期间,自动阻塞当前信号 处理期间,不阻塞当前信号
SA_RESTART 被中断的系统调用失败 (返回EINTR) 被中断的系统调用自动重启

===============================================

Linux 如何在程序中主动发送信号

  • 三种发送信号的API
    • kill() (最常用):向任意进程(或进程组)发送任意信号。只能向自己拥有的进程发送信号(超级用户 root 除外,它可以向任何进程发送)。
    • raise() (向自己发送):专用于向当前进程自己发送信号。不需要知道自己的PID,也永远不会有权限问题。等价于kill(getpid(), sig)。
    • alarm() (定时发送):在 seconds 秒后,由内核向当前进程发送一个特定的 SIGALRM 信号。再次调用 alarm() 会覆盖上一个闹钟。调用 alarm(0) 会取消闹钟。

===================================

kill 命令与 kill() 函数
kill [信号] <PID>。用于从终端向指定PID的进程发送信号。如果不指定信号(如 kill 22142),默认发送的是 SIGTERM(终止信号,kill -9 (强制杀死)。

int kill(pid_t pid, int sig);pid 参数: (1) pid > 0: 发送给指定PID的进程; (2) pid = 0: 发送给当前进程组的所有进程; (3) pid = -1: 发送给除 init (PID 1) 外的所有进程(超级用户权限); (4) pid < -1: 发送给指定进程组(组ID为 pid 的绝对值)的所有进程。

kill() 与 raise()

c 复制代码
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void)
{
    pid_t pid;
    int ret;

    // (1) 创建子进程
    if ((pid = fork()) < 0) {
        printf("Fork error\n");
        exit(1);
    }

    // (2) 子进程代码块
    if (pid == 0) {
        printf("Child(pid : %d) is waiting for any signal\n\n", getpid());
        // (3) 子进程调用 raise() 向自己发送 SIGSTOP 信号,使自己暂停
        raise(SIGSTOP);
        exit(0);
    }
    // (4) 父进程代码块
    else {
        // 等待 1 秒,确保子进程已经运行并执行了 raise(SIGSTOP)
        sleep(1);
        // (5) 父进程使用 waitpid() 以非阻塞(WNOHANG)方式检查子进程状态
        if ((waitpid(pid, NULL, WNOHANG)) == 0) {
            // 返回 0 表示子进程仍然存在(没有终止),但可能已暂停
            // (6) 父进程调用 kill() 向子进程发送 SIGKILL 信号 (信号 9),强制杀死它
            if ((ret = kill(pid, SIGKILL)) == 0) {
                printf("Parent kill %d\n\n",pid); 
            }
        }
        // (7) 父进程阻塞式等待,回收已确认被杀死的子进程(僵尸进程)
        // 因为子进程已被SIGKILL,所以这里会立即返回
        waitpid(pid, NULL, 0); 

        exit(0);
    }
}
  • 父进程创建子进程。
  • 子进程运行,打印信息,然后调用 raise(SIGSTOP) 将自己暂停。
  • 父进程 sleep(1) 确保子进程已暂停,然后 waitpid(...WNOHANG) 检查发现子进程还"活着"(返回0)。
  • 父进程执行 kill(pid, SIGKILL),向已暂停的子进程发送强制杀死信号。
  • 子进程被 SIGKILL 终止。
  • 父进程调用 waitpid(...0) 回收子进程,程序结束。

===================================

alarm() 函数unsigned int alarm(unsigned int seconds);

c 复制代码
int main()
{
    printf("\nthis is an alarm test function\n\n");
    // (1) 设置一个 5 秒后触发的闹钟,这个信号的默认处理动作是"终止进程"+
    alarm(5);
    // (2) 让主进程睡眠 20 秒
    sleep(20);
    // (3) 这一行永远不会被执行
    printf("end!\n");
    return 0;
}
//如果多个alarm,后面的会覆盖前面的,alarm(20)==>alarm(5) 5s后就会结束
  • 程序启动,设置了一个 5 秒后发送 SIGALRM 信号的闹钟。
  • 程序执行 sleep(20),主进程进入休眠状态,等待 20 秒。
  • 在第 5 秒时,闹钟时间到。内核向该进程发送 SIGALRM 信号。
  • 由于程序没有捕获 SIGALRM 信号,内核执行了该信号的默认处理动作------终止进程。
  • 结果:程序在 sleep 到第 5 秒时就被强行终止了,因此 sleep(20) 没有睡满,最后那句 printf("end!\n")也永远不会被执行。终端会(根据系统不同)打印 "Alarm clock" (闹钟)字样。

alarm用处:

  • 实现周期性任务(创建"闹钟") :可以在 SIGALRM 的信号处理函数中,再次调用 alarm()

    来设置下一个闹钟。这样就可以实现一个每隔 N 秒钟执行一次的周期性任务(比如更新时钟、检查网络状态等)。

    c 复制代码
    #include <stdio.h>
    #include <unistd.h>
    #include <signal.h> // 必须包含
    #include <stdlib.h>
    
    /**
     * (1) 这就是我们自定义的"闹钟"处理函数
     * 当 SIGALRM 信号到来时,内核会执行这个函数
     */
    void alarm_handler(int sig)
    {
        // 打印信息,证明我们进来了
        printf("叮! 闹钟响了! (收到的信号: %d)\n", sig);
        // (3) 关键一步:重新设置闹钟!
        // 告诉内核 2 秒后再提醒我一次
        alarm(2);
    }
    
    int main()
    {
        printf("程序启动... (PID: %d)\n", getpid());
    
        // (2) 捕获 SIGALRM 信号
        // 告诉内核: "当 SIGALRM 信号到来时, 不要终止我, 去执行 alarm_handler"
        signal(SIGALRM, alarm_handler);
    
        // (3) 设置第一个闹钟
        alarm(2);
    
        // (4) 主程序进入无限循环,等待闹钟来"中断"它
        while (1) {
            // pause() 是一个特殊的系统调用
            // 它会暂停程序,直到任意一个信号到来为止
            // 这样可以避免 while(1) 空转导致 CPU 100% 占用
            pause(); 
        }
    
        // 程序永远不会执行到这里
        return 0;
    }
    html 复制代码
    程序启动... (PID: 12345)
    叮! 闹钟响了! (收到的信号: 14)
    叮! 闹钟响了! (收到的信号: 14)
    叮! 闹钟响了! (收到的信号: 14)
    ... (每 2 秒打印一次) ...
    (您可以按 Ctrl+C 来终止它)
  • 为I/O操作设置"超时" (Timeout):可以在它之前设置一个闹钟,alarm(20) "一个任务"

    alarm(0),规定时间内执行完就取消闹钟,这样避免了卡死操作。

    c 复制代码
    signal(SIGALRM, my_timeout_handler); // 1. 注册超时处理函数
    alarm(10); // 2. 设置一个 10 秒的"死亡倒计时"
    bytes_read = read(fd, buffer, ...); // 3. 开始执行"慢"操作
    alarm(0); // 4. (如果 read 很快完成了) 立即取消闹钟
相关推荐
maosheng11465 小时前
RHCSA的第一次作业
linux·运维·服务器
busideyang5 小时前
为什么推挽输出不能接收串口数据,而准双向口可以?
c语言·stm32·单片机·嵌入式硬件·嵌入式
wifi chicken5 小时前
Linux 端口扫描及拓展
linux·端口扫描·网络攻击
旺仔.2915 小时前
Linux 信号详解
linux·运维·网络
放飞梦想C6 小时前
CPU Cache
linux·cache
济6176 小时前
STM32定时器进阶:从模式控制器完全指南,一文学会TRGI/TRGO---STM32 HAL库专栏
stm32·单片机·嵌入式·stm32hal库编程
鲨辣椒100866 小时前
单片机在线演绎《当幸福来敲门》------ 中断机制
单片机·嵌入式硬件
Hoshino.416 小时前
基于Linux中的数据库操作——下载与安装(1)
linux·运维·数据库
播播资源8 小时前
CentOS系统 + 宝塔面板 部署 OpenClaw源码开发版完整教程
linux·运维·centos
源远流长jerry8 小时前
在 Ubuntu 22.04 上配置 Soft-RoCE 并运行 RDMA 测试程序
linux·服务器·网络·tcp/ip·ubuntu·架构·ip