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 很快完成了) 立即取消闹钟
相关推荐
Gene_20221 小时前
搭建自己的小车 —— 运行livox mid_360
linux·ubuntu
uxiang_blog1 小时前
Linux学习之旅8
linux·运维·学习
虾..1 小时前
Linux 进程等待
linux·运维·服务器
没有医保李先生1 小时前
STM32 Hal库的Uart串口接受
stm32·单片机·嵌入式硬件
稚辉君.MCA_P8_Java2 小时前
DeepSeek Java 多线程打印的19种实现方法
java·linux·jvm·后端·架构
鹿鸣天涯2 小时前
使用VMware Workstation 17虚拟机安装红帽企业版系统RHEL10
linux·运维·服务器
杰 .3 小时前
Linux yum_and_apt
linux·服务器
南棱笑笑生3 小时前
20251129给荣品RD-RK3588开发板跑Rockchip的原厂Buildroot【linux-6.1】系统时适配AP6275P的蓝牙BLE
linux·运维·服务器·rockchip