【Linux C++】采用"回调链设计"实现 signal 的全局处理实现 & 多个中断信号回调
这是个非常关键的问题! 让我解释清楚其中的核心原理。
核心答案:我们只注册了一个全局处理函数,但它内部调用多个用户回调
1. 实际注册的只有一个函数
// 在 initialize() 中只注册了一次
sigaction(SIGINT, &sa, nullptr); // 这里 sa.sa_handler = globalSignalHandler
只注册了 globalSignalHandler 这一个函数给内核。
2. 回调链是用户空间实现的
用户按下 Ctrl+C
↓
内核调用 globalSignalHandler(唯一的注册函数)
↓
┌─────────────────────────────────────────────┐
│ globalSignalHandler(int signal) { │
│ // 1. 设置标志 │
│ m_signalReceived = true; │
│ │
│ // 2. 执行用户注册的所有回调 │
│ for (auto& handler : m_handlers) { │ ← 这里遍历执行多个回调!
│ if (handler.first == signal) { │
│ handler.second(signal); │
│ } │
│ } │
│ } │
└─────────────────────────────────────────────┘
可视化对比
错误理解(你以为的):
SIGINT → handler1 (内核调用)
SIGINT → handler2 (内核调用) ❌ 不可能!每个信号只能注册一个
SIGINT → handler3 (内核调用)
正确实现(我们的):
SIGINT → globalSignalHandler (内核唯一调用)
↓
┌─ handler1() ← 用户空间调用
├─ handler2() ← 用户空间调用
└─ handler3() ← 用户空间调用
详细工作流程
步骤1:初始化(一次)
void SignalHandler::initialize() {
struct sigaction sa;
sa.sa_handler = globalSignalHandler; // 设置我们唯一的处理函数
sigaction(SIGINT, &sa, nullptr); // 告诉内核:"调用globalSignalHandler"
// 注意:这里只注册了一个函数!
// 内核表:SIGINT → 0x12345678 (globalSignalHandler地址)
}
步骤2:用户注册多个回调
// 这些只是往我们的vector里添加,不涉及内核
signalHandler.registerHandler(SIGINT, handler1); // 添加到m_handlers
signalHandler.registerHandler(SIGINT, handler2); // 再添加
signalHandler.registerHandler(SIGINT, handler3); // 再添加
// 此时:
// - 内核只知道:SIGINT → globalSignalHandler
// - 我们知道:m_handlers = [{SIGINT,handler1}, {SIGINT,handler2}, {SIGINT,handler3}]
步骤3:信号发生时的完整流程
// 内核视角:
1. 用户按Ctrl+C → 产生SIGINT
2. 内核查找:SIGINT的处理函数是0x12345678
3. 内核调用:globalSignalHandler(2) // 2是SIGINT
// 我们的globalSignalHandler:
void SignalHandler::globalSignalHandler(int signal) {
// 获取单例
auto& instance = getInstance();
// 遍历执行所有用户注册的回调
instance.executeHandlers(signal);
// 内部会调用:handler1(2), handler2(2), handler3(2)
}
代码示例验证
让我们写个简单验证:
#include <iostream>
#include <csignal>
#include <vector>
#include <functional>
std::vector<std::function<void(int)>> userCallbacks;
// 唯一的全局处理函数
void myGlobalHandler(int sig) {
std::cout << "Global handler called for signal " << sig << std::endl;
// 执行所有用户回调
for (auto& cb : userCallbacks) {
std::cout << " Calling user callback..." << std::endl;
cb(sig);
}
}
int main() {
// 1. 只注册一次给内核
signal(SIGINT, myGlobalHandler);
std::cout << "Registered ONE handler with kernel" << std::endl;
// 2. 用户注册多个回调(不影响内核)
userCallbacks.push_back([](int sig) {
std::cout << " User callback 1: SIGINT received" << std::endl;
});
userCallbacks.push_back([](int sig) {
std::cout << " User callback 2: Doing cleanup..." << std::endl;
});
userCallbacks.push_back([](int sig) {
std::cout << " User callback 3: Exiting..." << std::endl;
});
std::cout << "Registered " << userCallbacks.size()
<< " user callbacks (in user space)" << std::endl;
std::cout << "\nPress Ctrl+C to test..." << std::endl;
// 等待信号
pause();
return 0;
}
运行结果:
Registered ONE handler with kernel
Registered 3 user callbacks (in user space)
Press Ctrl+C to test...
^CGlobal handler called for signal 2
Calling user callback...
User callback 1: SIGINT received
Calling user callback...
User callback 2: Doing cleanup...
Calling user callback...
User callback 3: Exiting...
为什么你的原始代码会冲突?
你的原始代码问题:
// SystemInit.cpp
signal(SIGINT, SystemInit::signalHandler); // 注册SystemInit的
// 然后clsCtrl.cpp又调用:
signal(SIGINT, clsCtrl::signalHandler); // 覆盖了上面的!
// 结果:只有最后注册的生效
覆盖原理:
// signal() 的伪实现
void signal(int signum, void (*handler)(int)) {
// 直接替换内核表中的处理函数
kernel_signal_table[signum] = handler; // 直接替换!
}
SignalHandler 类的精妙之处
精妙点1:中间层设计
内核信号 → 全局处理函数 → 多个用户回调
↑ ↑ ↑
唯一 我们的中间层 用户模块
精妙点2:控制反转
// 用户模块不再直接注册给内核
// signal(SIGINT, myHandler); // ❌ 旧的,会冲突
// 而是注册给我们
signalHandler.registerHandler(SIGINT, myHandler); // ✅ 新的,不会冲突
精妙点3:统一管理
// 所有信号都经过同一个入口
void globalSignalHandler(int signal) {
// 可以在这里统一:
// 1. 日志记录
// 2. 统计信息
// 3. 异常保护
// 4. 执行用户回调
}
在你们项目中的实际应用
在你的项目中,这样修改:
// main.cpp 中
int main() {
// 初始化一次:告诉内核调用我们的globalSignalHandler
SignalHandler::getInstance().initialize();
// 各个模块注册(不会互相覆盖)
SignalHandler::getInstance().registerHandler(SIGINT,
[](int) { SystemInit::getInstance().stop(); });
SignalHandler::getInstance().registerHandler(SIGINT,
[ctrl](int) { if (ctrl) ctrl->stop(); });
// 内核只知道:SIGINT → SignalHandler::globalSignalHandler
// 但实际会调用两个用户函数
}
总结:为什么"初始化一次+回调链"能工作
| 层次 | 负责方 | 内容 | 说明 |
|---|---|---|---|
| 内核层 | 操作系统 | SIGINT → globalSignalHandler |
只注册一次,永久有效 |
| 中间层 | SignalHandler | globalSignalHandler() |
统一入口,管理所有回调 |
| 用户层 | 业务模块 | handler1(), handler2(), ... |
任意多个,通过register注册 |
关键洞察:
-
内核只关心一个地址:你告诉它"发生信号时跳转到这个地址"
-
我们在那个地址的函数里做所有事情:包括调用多个用户回调
-
用户注册不影响内核:只是往我们的列表里添加,不会调用
signal()
这就是为什么只需要初始化一次,却能实现多回调的原因:我们在用户空间实现了自己的"微内核"来分发信号,而内核只负责触发这个分发器。

浙公网安备 33010602011771号