
上面的 demo 目前只能通过 Ctrl C 强制杀死毕竟调度器的 run 是个死循环没法退出。用来做做演示没问题但是要用来开发项目就不行了本着做出工业级强度代码的使命感下面对它进行一番改造看看能否实现完美退出。核心思路是检测用户按下 Ctrl C 让 epoll_wait 感知并退出 run 循环按下 Ctrl C 简单等价于处理 SIGINT 信号但让 epoll 感知比较难查了下 deepseek 给了三种方案* 通过 signalfd 将信号转化为 IO 事件交给 epoll 统一处理* 建立一个进程内的 pipe 通道注册到 epoll在检测到 SIGINT 事件时写入一字节以唤醒 epoll_wait 并退出* 信号处理器设置一个标志位使用 epoll_wait 的超时功能定时检测该标志位方案 III 有延迟首先排除方案 II 就是传说中的 self-pipe trick比较通用但不够高效方案 I 最直接也比较适合 Linux就它了#include signal.h #include sys/signalfd.h class EpollScheduler { private: int epoll_fd; int signal_fd; std::unordered_mapint, std::coroutine_handle io_handles; public: EpollScheduler(int signum) { epoll_fd epoll_create(MAX_EVENTS); if (epoll_fd -1) { std::stringstream ss; ss epoll_create failed, error errno; throw std::runtime_error(ss.str()); } sigset_t mask; sigemptyset(mask); sigaddset(mask, signum); sigprocmask(SIG_BLOCK, mask, NULL); signal_fd signalfd(-1, mask, SFD_NONBLOCK); if (signal_fd -1) { std::stringstream ss; ss signalfd failed, error errno; throw std::runtime_error(ss.str()); } struct epoll_event ev; ev.events EPOLLIN; ev.data.fd signal_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, signal_fd, ev) -1) { std::stringstream ss; ss epoll_ctl failed, error errno; throw std::runtime_error(ss.str()); } std::cout register signal signum as fd signal_fd std::endl; } ~EpollScheduler() { for(auto handle : io_handles) { std::cout coroutine destroy std::endl; handle.second.destroy(); } close(signal_fd); close(epoll_fd); } ... void run() { while (true) { epoll_event events[MAX_EVENTS] { 0 }; int n epoll_wait(epoll_fd, events, MAX_EVENTS, -1); for (int i 0; i n; i) { int ready_fd events[i].data.fd; if (ready_fd signal_fd) { struct signalfd_siginfo fdsi { 0 }; read(signal_fd, fdsi, sizeof(fdsi)); std::cout signal fdsi.ssi_signo detected, exit... std::endl; return; } if (auto it io_handles.find(ready_fd); it ! io_handles.end()) { it-second.resume(); } } } } };改动主要集中在 EpollScheduler 类的构造、析构与 run 方法。内容不长分段解读一下class EpollScheduler { private: int epoll_fd;增加成员记录信号对应的句柄方便后续在 epoll_wait 返回时做对比int signal_fd; std::unordered_mapint, std::coroutine_handle io_handles; public:构造函数接收一个信号作为监听对象main 中会传递 SIGINT 或 SIGQUITEpollScheduler(int signum) { epoll_fd epoll_create(MAX_EVENTS); if (epoll_fd -1) { std::stringstream ss; ss epoll_create failed, error errno; throw std::runtime_error(ss.str()); }构建信号对应的异步文件句柄sigset_t mask; sigemptyset(mask); sigaddset(mask, signum);下面这句是关键如果不屏蔽默认的信号处理方式默认的信号处理器会让进程退出epoll 就没机会啦sigprocmask(SIG_BLOCK, mask, NULL); signal_fd signalfd(-1, mask, SFD_NONBLOCK); if (signal_fd -1) { std::stringstream ss; ss signalfd failed, error errno; throw std::runtime_error(ss.str()); }将信号句柄注册到 epoll成功时打印一条日志失败时抛异常struct epoll_event ev; ev.events EPOLLIN; ev.data.fd signal_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, signal_fd, ev) -1) { std::stringstream ss; ss epoll_ctl failed, error errno; throw std::runtime_error(ss.str()); } std::cout register signal signum as fd signal_fd std::endl; }析构除了增加信号句柄的关闭还增加了挂起协程的销毁如果调度器的生命周期与进程不一致时 (多次初始化与销毁调度器)这就比较关键了可以防止协程泄漏~EpollScheduler() { for(auto handle : io_handles) { std::cout coroutine destroy std::endl; handle.second.destroy(); } close(signal_fd); close(epoll_fd); } ... void run() { while (true) { epoll_event events[MAX_EVENTS] { 0 }; int n epoll_wait(epoll_fd, events, MAX_EVENTS, -1); for (int i 0; i n; i) { int ready_fd events[i].data.fd;epoll_wait 返回时优先处理信号句柄上的事件if (ready_fd signal_fd) { struct signalfd_siginfo fdsi { 0 }; read(signal_fd, fdsi, sizeof(fdsi)); std::cout signal fdsi.ssi_signo detected, exit... std::endl; return; }之后才是普通 IO 事件及协程的恢复if (auto it io_handles.find(ready_fd); it ! io_handles.end()) { it-second.resume(); } } } } };下面是程序运行效果$ ./sample communication.pipe communication2.pipe register signal 2 as fd 4 Read [10] world-war pre-read 30, read 0 Read [30] world-war world-war world-war pre-read 10, read 0 Read [10] world-war Read [0] ... Read [6] hello Read [10] world-war Read [6] hello Read [10] world-war Read [0] Read [6] hello Read [10] world-war ^Csignal 2 detected, exit... coroutine destroy coroutine destroy