Linux 信号(Signal)

1. 什么是信号

信号(Signal)是 Linux/Unix 中进程间通信的一种机制。

本质:

  • 软件层面的"中断"
  • 用于通知进程发生了某种事件

例如:

信号 含义
SIGINT Ctrl + C
SIGSEGV 段错误
SIGFPE 除0异常
SIGALRM 闹钟超时
SIGCHLD 子进程退出

2. 信号的生命周期

一个信号从产生到处理:

text 复制代码
产生 -> 保存(pending) -> 阻塞(block)判断 -> 递达(deliver) -> 处理

Linux 内核维护:

  • pending 表(是否收到)
  • block 表(是否阻塞)

3. 信号产生方式

Linux 中常见五种信号产生方式:

方式 示例
终端按键 Ctrl + C
硬件异常 除0、段错误
软件条件 alarm
显式发送 kill
系统事件 子进程退出

4. kill 发送信号


4.1 kill 系统调用

函数:

cpp 复制代码
int kill(pid_t pid, int sig);

作用:

向指定进程发送信号。


4.2 示例:实现 mykill

源代码

cpp 复制代码
//实现kill命令

void Usage(string proc)
{
    cout << "Usage:\n\t " << proc << " <pid>\n" << endl;
}

//mykill signum pid
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    int signum = stoi(argv[1]);
    pid_t pid = stoi(argv[2]);

    int n = kill(pid, signum);

    if(n == -1)
    {
        perror("kill");
        cout << "kill process failed" << endl;
        exit(2);
    }

    cout << "kill process success" << endl;

    return 0;
}

4.3 使用方式

bash 复制代码
./mykill 2 12345

表示:

向 pid 为 12345 的进程发送:

text 复制代码
SIGINT(2)

5. signal 信号捕获


5.1 signal 函数

函数原型:

cpp 复制代码
void (*signal(int signum, void (*handler)(int)))(int);

作用:

为指定信号注册处理函数。


5.2 示例

源代码

cpp 复制代码
void myhandler(int signum)
{
    cout << "process get a signal: " << signum << endl;
}

int main()
{
    signal(SIGINT, myhandler);

    int cnt = 10;

    while(cnt--)
    {
        cout << "i am a process" << endl;
        sleep(1);
    }

    return 0;
}

5.3 运行效果

按:

text 复制代码
Ctrl + C

会触发:

text 复制代码
SIGINT

然后执行:

cpp 复制代码
myhandler()

6. alarm 闹钟信号


6.1 alarm 函数

cpp 复制代码
unsigned int alarm(unsigned int seconds);

作用:

指定 seconds 秒后发送:

text 复制代码
SIGALRM

6.2 示例

源代码

cpp 复制代码
void work()
{
    cout << "work..." << endl;
}

void MyHandler(int signum)
{
    work();

    int n = alarm(5);

    cout << "剩余时间: " << n << endl;
}

int main()
{
    signal(SIGALRM, MyHandler);

    int n = alarm(50);

    while(1)
    {
        cout << "running..." << endl;
        sleep(1);
    }

    return 0;
}

6.3 特点

alarm 只触发一次

需要在处理函数中:

cpp 复制代码
alarm(5);

重新设置。


6.4 返回值

cpp 复制代码
alarm()

返回:

上一次闹钟剩余时间。


7. 硬件异常信号


7.1 除0异常

源代码

cpp 复制代码
signal(SIGFPE, myhandler);

int a = 10;
a /= 0;

会产生:

text 复制代码
SIGFPE

7.2 段错误

例如:

cpp 复制代码
int* p = nullptr;
*p = 100;

产生:

text 复制代码
SIGSEGV

8. 子进程退出信号 SIGCHLD


8.1 子进程退出

当子进程退出时:

内核向父进程发送:

text 复制代码
SIGCHLD

8.2 waitpid 获取退出信息

源代码

cpp 复制代码
pid_t rid = waitpid(id, &status, 0);

if(rid == id)
{
    cout << "child quit info, rid: "
         << rid
         << ", exit code: "
         << ((status>>8)&0xFF)
         << ", exit signal: "
         << (status&0x7F)
         << ", core dump: "
         << ((status>>7)&1)
         << endl;
}

8.3 status 解析

内容 提取方式
退出码 (status >> 8) & 0xFF
终止信号 status & 0x7F
core dump (status >> 7) & 1

9. 信号阻塞与 pending


9.1 sigprocmask

函数:

cpp 复制代码
int sigprocmask(int how,
                const sigset_t *set,
                sigset_t *oldset);

how 参数

参数 含义
SIG_BLOCK 添加阻塞
SIG_UNBLOCK 解除阻塞
SIG_SETMASK 替换阻塞集

9.2 sigpending

函数:

cpp 复制代码
int sigpending(sigset_t *set);

作用:

获取 pending 信号集。


9.3 pending 打印函数

源代码

cpp 复制代码
void PrintPending(sigset_t &pending)
{
    for(int signo = 1; signo < 32; signo++)
    {
        if(sigismember(&pending, signo))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }

    cout << endl;
}

9.4 屏蔽 SIGINT 示例

源代码

cpp 复制代码
signal(2, handler);

sigset_t bset, oldset;

sigemptyset(&bset);
sigemptyset(&oldset);

sigaddset(&bset, 2);

sigprocmask(SIG_SETMASK, &bset, &oldset);

int cnt = 0;

sigset_t pending;

while(1)
{
    sigpending(&pending);

    PrintPending(pending);

    sleep(1);

    cnt++;

    if(cnt == 20)
    {
        cout<<"unblock signal 2"<<endl;

        sigprocmask(SIG_SETMASK,
                    &oldset,
                    nullptr);
    }
}

9.5 现象

按:

text 复制代码
Ctrl + C

后:

  • 信号不会立即处理
  • pending 对应位变成 1

解除阻塞后:

cpp 复制代码
handler()

立即执行。


10. 哪些信号不能阻塞

以下信号不能被阻塞:

信号 含义
SIGKILL(9) 强制杀死
SIGSTOP(19) 强制暂停

原因:

内核必须保证:

text 复制代码
进程一定能被终止

11. sigaction

相比 signal:

cpp 复制代码
sigaction

更可靠、更强大。


11.1 struct sigaction

源代码

cpp 复制代码
 struct sigaction 结构体
 struct sigaction 
{
     void     (*sa_handler)(int);      // 信号处理函数指针(类似 signal)
     void     (*sa_sigaction)(int, siginfo_t *, void *); // 扩展处理函数
     sigset_t sa_mask;                 // 在处理该信号时额外要阻塞的信号集
     int      sa_flags;                // 控制信号处理行为的标志位
     void     (*sa_restorer)(void);    // 已废弃,不再使用
};

11.2 注册信号处理

源代码

cpp 复制代码
struct sigaction act, oact;

memset(&act, 0, sizeof(act));
memset(&oact, 0, sizeof(oact));

sigemptyset(&act.sa_mask);

sigaddset(&act.sa_mask, 4);
sigaddset(&act.sa_mask, 3);

act.sa_handler = handler;

sigaction(2, &act, &oact);

11.3 sa_mask 作用

表示:

在处理当前信号期间:

额外阻塞哪些信号。

例如:

cpp 复制代码
sigaddset(&act.sa_mask, 3);

则处理 SIGINT 时:

text 复制代码
SIGQUIT

也会被阻塞。


12. pending 位什么时候清零?

问题:

text 复制代码
pending 位什么时候从1变0?

答案:

text 复制代码
在调用处理函数之前

即:

text 复制代码
先清 pending
再执行 handler

13. 信号处理期间自动阻塞

Linux 规定:

当某信号正在处理时:

该信号会自动加入 block 表。

防止:

text 复制代码
信号处理函数嵌套调用

14. volatile 与信号


volatile 关键字用于确保 flag 变量在每次访问时都从内存中读取, 而不是从寄存器中读取,这在多线程环境中特别有用, 因为多个线程可能会同时访问 flag 变量, 而优化器可能会假设 flag 变量的值不会改变

14.1 示例

源代码

cpp 复制代码
volatile int flag = 0;

void handler_2(int signum)
{
    cout<< "catch a signal, signal number: "
        << signum << endl;

    flag = 1;
}

void test_2()
{
    signal(2, handler_2);

    while(!flag)
    {
        cout << "process quit normal" << endl;
        sleep(2);
    }
}

14.2 为什么需要 volatile

避免:

text 复制代码
编译器优化

导致:

cpp 复制代码
while(!flag)

死循环。


15. SIGCHLD 异步回收子进程


15.1 信号方式回收

源代码

cpp 复制代码
void handler_3(int signum)
{
    pid_t rid;

    while((rid = waitpid(-1,
                         nullptr,
                         WNOHANG)) > 0)
    {
        cout<< "child process quit: "
            << rid
            << endl;
    }
}

15.2 WNOHANG

表示:

text 复制代码
非阻塞回收

避免:

text 复制代码
handler 阻塞

15.3 自动回收子进程

Linux 支持:父进程调用sigaction,将SIGCHLD信号处理为SIG_IGN, 则父进程不会收到SIGCHLD信号, 子进程结束时, 会自动回收子进程的资源

cpp 复制代码
signal(SIGCHLD, SIG_IGN);

效果:

text 复制代码
子进程退出自动回收

无需:

cpp 复制代码
wait()

15.4 示例

源代码

cpp 复制代码
void test_3()
{
    signal(17, SIG_IGN);

    for(int i = 0; i < 10; i++)
    {
        pid_t id = fork();

        if(id == 0)
        {
            while(1)
            {
                cout<< "I am a child process: "
                    << getpid()
                    << endl;

                sleep(5);

                break;
            }

            cout << "child process quit !!!"
                 << endl;

            exit(0);
        }

        sleep(rand() % 5 + 1);
    }

    while(1)
    {
        cout<< "I am a parent process: "
            << getpid()
            << endl;

        sleep(1);
    }
}

16. Linux 信号核心知识总结


16.1 信号处理流程

text 复制代码
产生
 ↓
pending
 ↓
block 判断
 ↓
递达
 ↓
handler

16.2 signal 与 sigaction

函数 特点
signal 简单
sigaction 推荐使用

16.3 pending 与 block

名称 含义
pending 已收到未处理
block 是否阻塞

16.4 常考问题

Q1:SIGKILL 能捕获吗?

不能。


Q2:SIGSTOP 能屏蔽吗?

不能。


Q3:信号会排队吗?

普通信号:

text 复制代码
不会

实时信号:

text 复制代码

Q4:handler 中能调用 printf 吗?

严格来说:

text 复制代码
不安全

因为:

text 复制代码
printf 不是异步信号安全函数

相关推荐
cui_ruicheng3 小时前
Linux网络编程(九):应用层协议与序列化
linux·运维·服务器·网络
kobe_OKOK_4 小时前
ubuntu server 存儲空間占滿的原因
linux·运维·ubuntu
诸神缄默不语4 小时前
在Linux中使用Vim编辑文本
linux·vim
菜鸟是大神4 小时前
07-Claude Code 的常用命令和快捷键
linux·运维·服务器
hj2862515 小时前
Linux存储空间管理完整笔记
linux·运维·笔记
Championship.23.245 小时前
Linux 3.0 中断机制深度解析:从传统PIC到现代中断架构的转折点
linux·运维·架构·中断
小猫咪015 小时前
Linux OOM Killer 是什么?程序为什么突然被杀?
linux·运维·服务器
404是NotFound呀5 小时前
[FPGA] Ubuntu 22.04 安装 Vivado 2023.1 和 PetaLinux 踩坑记录
linux·ubuntu·fpga开发
lightqjx5 小时前
【Linux】从冯·诺依曼体系到操作系统的理解
linux·运维·服务器·冯·诺依曼体系