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 不是异步信号安全函数

相关推荐
雯宝4 小时前
mnt_init 函数
linux
律宏阔10 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔10 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴10 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千10 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei3710 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei3710 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei3710 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
GeW10 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
GeW10 小时前
工业场景中的Linux与数据库选型:高端化转型的技术底座
linux