Day 78:信号丢失与信号屏蔽

上节回顾:我们讨论了结构体直接封包的对齐与填充隐患,强调了跨平台/跨编译器环境下,结构体内存布局不应直接作为网络协议字节流。正确做法是逐字段序列化与字节序转换。


1. 主题原理与细节逐步讲解

1.1 信号的基本机制

  • **信号(signal)**是UNIX/Linux下实现异步事件通知的一种机制。例如:SIGINT(Ctrl+C)、SIGCHLD(子进程退出)、SIGALRM(定时器超时)。
  • 信号通过内核向进程(或线程)发送异步通知,进程可以注册信号处理函数(handler)来响应。

1.2 信号的状态

  • 未决(pending):信号已产生但尚未被内核递送到进程。
  • 屏蔽(blocked):进程临时阻止某些信号递送(通过信号掩码)。
  • 递送(delivered):信号被内核传递,进程执行信号处理函数。

1.3 信号丢失的经典场景

  • 非实时信号(传统信号,编号1~31):同类信号不会排队(pending位只占1bit),如果相同信号多次发生且未及时处理,只有一次会被递送,多余的会“丢失”。
  • 实时信号(编号大于等于SIGRTMIN):支持排队,不会丢失。

2. 相关C语言典型陷阱/缺陷说明及成因剖析

2.1 信号丢失的成因

  • 信号屏蔽期间/处理未及时:如果信号已到来但被屏蔽,解除屏蔽后只会处理一次,多次发生只保留一个pending位。
  • 信号处理函数执行期间再次收到同类信号:同样只会递送一次,多余的被丢弃。

2.2 典型误用

  • 在高并发场景下依赖信号计数(如用SIGUSR1通知事件发生次数),非实时信号会导致计数不准。
  • 在信号处理函数内未恰当处理可重入性问题,或未及时解除屏蔽,导致信号丢失。

3. 规避方法与最佳设计实践

3.1 避免用非实时信号计数

  • 如果需要“每次信号都响应”,必须用实时信号SIGRTMIN及以上),或用其他IPC机制(如管道、事件fd)。

3.2 正确设置信号掩码

  • sigprocmasksigactionsa_mask精确控制信号屏蔽区间,避免不必要的阻塞。
  • 尽量缩短信号处理函数时间,快速处理或设置标志,主循环再处理实际逻辑。

3.3 信号安全的事件通知方式

  • 信号仅用于“轻量级通知”,实际信息或计数建议通过原子变量、管道、信号量等IPC机制传递。

4. 典型错误代码与优化后正确代码对比

错误示例:依赖信号个数计数(信号丢失)

#include <signal.h>
#include <stdio.h>
#include <unistd.h>
volatile int counter = 0;

void handler(int signo) {
    counter++;  // 多次SIGUSR1同时到来,counter可能小于实际次数
}

int main() {
    signal(SIGUSR1, handler);
    while (1) {
        pause();
        printf("counter: %d\n", counter);
    }
}

问题:如果在pause期间收到多次SIGUSR1,只有一次被递送,counter不准确。


正确示例:用实时信号或主进程主动轮询

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

volatile sig_atomic_t flag = 0;

void handler(int signo) {
    flag = 1; // 只设置标志
}

int main() {
    struct sigaction sa = {0};
    sa.sa_handler = handler;
    sigaction(SIGUSR1, &sa, NULL);

    while (1) {
        if (flag) {
            // 主循环处理实际业务
            printf("SIGUSR1 received!\n");
            flag = 0;
        }
        // 其他业务处理
    }
}

解释:只作为“通知”,由主循环处理计数/业务。


用实时信号避免丢失

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void handler(int signo, siginfo_t *info, void *ucontext) {
    printf("SIGRTMIN+1 received: %d\n", signo);
}

int main() {
    struct sigaction sa = {0};
    sa.sa_sigaction = handler;
    sa.sa_flags = SA_SIGINFO;
    sigaction(SIGRTMIN+1, &sa, NULL);

    // 发送多次
    for (int i = 0; i < 5; ++i) {
        kill(getpid(), SIGRTMIN+1);
    }
    sleep(1);
    return 0;
}

输出:会收到5次信号,不会丢失。


5. 底层原理补充

  • 非实时信号pending只占1bit:同类信号同时到达时只记录“有”或“没有”,不计次数,所以会丢失。
  • 实时信号维护队列:即使同类信号多次发生,也会全部递送。
  • 信号屏蔽:进程通过信号掩码,可以临时阻断信号递送,未决信号解除屏蔽后才递送,但依然只递送一次。

6. SVG辅助图:非实时信号丢失原理

在这里插入图片描述


7. 总结与实际建议

  • 非实时信号不会排队,同类信号多次发生只递送一次,多余的会丢失。
  • 信号屏蔽和信号处理未及时,会加剧信号丢失。
  • 用信号做计数或高频通知极易出错,建议用实时信号或IPC机制。
  • 信号仅作“通知”用途,实际业务处理应放在主循环或安全的IPC通道。

正确理解信号的递送、屏蔽和丢失机制,是编写健壮可靠系统级C程序的基础。信号设计只做“轻通知”,不要依赖其计数和频度!

公众号 | FunIO
微信搜一搜 “funio”,发现更多精彩内容。
个人博客 | blog.boringhex.top

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值