<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[【 基于 io_uring 的 C++20 协程网络库】02：模块解耦与完备的退出机制]]></title><description><![CDATA[<p dir="auto">在上文中，我们构建了 <code>IOContext</code> 的核心事件循环骨架。然而，随着组件的增加，我们需要解决两个实际的工程问题：</p>
<p dir="auto">一是如何对底层上下文进行合理的抽象与解耦；</p>
<p dir="auto">二是如何优雅、无阻塞地处理外部中断信号并终止事件循环。</p>
<h3>1. 模块解耦与泛型化设计</h3>
<p dir="auto">考虑到未来我们可能会迭代出多个版本的 <code>IOContext</code>，为了最大化代码复用，将具体的协程 Awaiter（如 <code>SleepAwaiter</code>）与底层的 <code>IOContext</code> 实现解耦是必要的。</p>
<p dir="auto">首先，我们将 <code>Operation</code> 接口提取到独立的头文件中。</p>
<p dir="auto">其次，对于 <code>SleepAwaiter</code>，由于它依赖 <code>Context::sqe()</code>，若在头文件中硬编码 <code>IOContext</code>，必须要立刻知道IOContext的定义。因此，我们采用模板化设计，将 <code>Context</code> 泛型化，在调用点推导出确切的类型。</p>
<pre><code class="language-cpp">template&lt;typename Context&gt;
class SleepAwaiter: public Operation {
public:
    template&lt;chrono_duration Duration&gt;
    SleepAwaiter(Context&amp; context, Duration d)
      : context_{ context }
    {
        using namespace std::chrono;
        // 转换 std::chrono 时间为内核认识的 timespec
        timeout_.tv_sec = duration_cast&lt;seconds&gt;(d).count();
        timeout_.tv_nsec = duration_cast&lt;nanoseconds&gt;(d % seconds(1)).count();
    }

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb;
    constexpr auto await_ready() const noexcept -&gt; bool 
    { 
        return false; 
    }

    void await_suspend(std::coroutine_handle&lt;&gt; handle) noexcept
    {
        handle_ = handle;
        auto* sqe = context_.sqe();

        // 提交纯超时指令，count 设为 0 表示只受时间触发
        ::io_uring_prep_timeout(sqe, &amp;timeout_, 0, 0);
        ::io_uring_sqe_set_data(sqe, this);
    }

    auto await_resume() noexcept -&gt; std::expected&lt;void, std::error_code&gt;
    {
        // io_uring 中，超时正常结束会返回 ETIME
        if (error_code_ == ETIME || error_code_ == 0)
            return {};
        
        // 其他错误（如 ECANCELED 被提前强杀）
        return unexpected_system_error(error_code_);
    }

    void complete(int res, std::uint32_t flags) noexcept override
    {
        error_code_ = -res;
        
        if (handle_) {
            auto handle = std::exchange(handle_, nullptr);
            handle.resume();
        }
    }

private:
    Context&amp; context_;
    struct __kernel_timespec timeout_{};
    std::coroutine_handle&lt;&gt; handle_{ nullptr };
    int error_code_{ 0 };
};

template&lt;typename Context, typename Duration&gt;
auto sleep_for(Context&amp; context, Duration duration) noexcept -&gt; SleepAwaiter&lt;Context&gt;
{
    return SleepAwaiter&lt;Context&gt;{ context, duration };
}
</code></pre>
<p dir="auto"><strong>设计注记</strong>：<br />
通常情况下，过度泛型化（滥用模板）会劣化编译时长，并不值得推崇。但在基础设施库的设计中，静态多态（基于模板的 Duck Typing）能够做到零运行时开销（Zero-overhead），且调用方代码无需任何修改即可适配不同版本的 <code>IOContext</code>，这种妥协是极具工程价值的。</p>
<h3>2. 完善事件循环的终止机制 (eventfd)</h3>
<p dir="auto">上个版本中，我们使用 <code>std::atomic&lt;bool&gt; should_stop_</code> 标志来控制循环退出。但这存在一个死锁隐患：如果 <code>IOContext::run()</code> 正阻塞在 <code>io_uring_submit_and_wait</code> 系统调用上，单纯修改布尔变量是无法唤醒内核态线程的。</p>
<p dir="auto">我们需要一种跨越内核与用户态的唤醒机制。在传统的 Reactor 模式中，通常采用管道（pipe）或 <code>eventfd</code>，在 <code>io_uring</code> 体系下，<code>eventfd</code> 依然是开销极小且最适用的方案。</p>
<h4>核心状态重构：区分系统事件与业务逻辑</h4>
<p dir="auto">在引入 <code>eventfd</code> 后，完成队列（CQ）中不仅会包含业务逻辑的事件（如网络 I/O、定时器），还会混入我们内部触发的 wakeup 事件。<br />
这就要求我们必须在状态追踪上做出严格区分：</p>
<ol>
<li>
<p dir="auto"><strong><code>count</code></strong>：追踪当前批次取出的所有 CQE 数量，用于向前推进内核的共享环形缓冲区（<code>io_uring_cq_advance</code>）。</p>
</li>
<li>
<p dir="auto"><strong><code>workdone</code></strong>：追踪实际完成的业务任务数量，仅针对这些任务去扣减 <code>outstanding_works_</code>。如果不对二者加以区分，wakeup 信号会导致业务计数器异常递减，引发程序提前退出或触发断言失败。</p>
</li>
</ol>
<p dir="auto">同时，我们利用 liburing 原生的内联辅助函数 <code>io_uring_cqe_get_data64</code>、<code>io_uring_cqe_get_data</code> 以及 <code>io_uring_sqe_set_data64</code> 来取代底层的直接字段访问，这消除了 <code>reinterpret_cast</code> 的滥用，确保了类型安全的边界。</p>
<p dir="auto">完整的 <code>IOContext</code> 实现如下：</p>
<pre><code class="language-cpp">#include &lt;sys/eventfd.h&gt;
#include &lt;unistd.h&gt;
#include &lt;limits&gt;
#include &lt;cassert&gt;

class IOContext {
public:
    explicit IOContext(unsigned entries = 1024)
    {
        if (auto res = ::io_uring_queue_init(entries, &amp;ring_, 0); res &lt; 0)
            throw_system_error(-res, "io_uring_queue_init");            

        wakeup_fd_ = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
        if (wakeup_fd_ == -1)
            throw_system_error("Failed to create eventfd for stopping IOContext");

        arm_wakeup();
    }

    IOContext(const IOContext&amp;) = delete;
    auto operator=(const IOContext&amp;) -&gt; IOContext&amp; = delete;
    
    // 为了简化实现，我们不支持移动
    IOContext(IOContext&amp;&amp; other) noexcept = delete;
    auto operator=(IOContext&amp;&amp;) -&gt; IOContext&amp; = delete;

    ~IOContext()
    {
        ::io_uring_queue_exit(&amp;ring_);
        ::close(wakeup_fd_);
    }

    void run()
    {
        ::io_uring_cqe* cqe{ nullptr };

        while (!should_stop_.load(std::memory_order_relaxed) &amp;&amp; outstanding_works_ &gt; 0)
        {
            auto res = ::io_uring_submit_and_wait(&amp;ring_, 1);
            if (res &lt; 0) {
                if (res == -EINTR)
                    continue;

                throw_system_error("io_uring_submit_and_wait");
            }
                
            unsigned head;
            unsigned count{ 0 };
            unsigned workdone{ 0 };

            io_uring_for_each_cqe(&amp;ring_, head, cqe) {
                ++count;

                // 探测到内部唤醒信号
                if (io_uring_cqe_get_data64(cqe) == WAKEUP_MARKER) {
                    resume_wakeup();
                    arm_wakeup();
                    continue;
                }

                // 正常的业务逻辑完成事件
                if (io_uring_cqe_get_data64(cqe) != 0) {
                    auto* op = static_cast&lt;Operation*&gt;(io_uring_cqe_get_data(cqe));
                    op-&gt;complete(cqe-&gt;res, cqe-&gt;flags);

                    ++workdone;
                }
            }

            // 推进环形缓冲区必须使用总事件数 count
            if (count &gt; 0)
                ::io_uring_cq_advance(&amp;ring_, count);

            // 扣减未决任务必须使用实际完成的业务数 workdone
            if (workdone &gt; 0)
                outstanding_works_ -= workdone;
        }
    }

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb;
    auto sqe() -&gt; ::io_uring_sqe*
    {
        auto* sqe = ::io_uring_get_sqe(&amp;ring_);
        if (!sqe)
            throw_system_error("io_uring_get_sqe");

        add_work();
        return sqe;
    }

    void stop()
    {
        should_stop_.store(true, std::memory_order_relaxed);
        wakeup();
    }

    auto ring() noexcept -&gt; ::io_uring* { return &amp;ring_; }
    auto ring() const noexcept -&gt; const ::io_uring* { return &amp;ring_; }

    void add_work() noexcept { ++outstanding_works_; }

    void drop_work() noexcept
    {
        assert(outstanding_works_ &gt; 0);
        --outstanding_works_;
    }
    
private:
    static constexpr auto WAKEUP_MARKER = std::numeric_limits&lt;std::uintptr_t&gt;::max();

    ::io_uring ring_{};
    int wakeup_fd_{ -1 };

    // 只用来追踪 io_context 之外的操作，并不需要用户主动来使用相关的接口
    std::size_t outstanding_works_{ 0 };
    // stop 会被跨线程调用，所以需要使用原子变量来保证线程安全
    std::atomic&lt;bool&gt; should_stop_{ false };

    void arm_wakeup() noexcept
    {
        auto* sqe = ::io_uring_get_sqe(&amp;ring_);
        if (!sqe)
            throw_system_error("io_uring_get_sqe failed when re-arming wakeup");

        ::io_uring_prep_poll_add(sqe, wakeup_fd_, POLLIN);
        // 采用 64 位专有 setter，避免指针转换警告
        ::io_uring_sqe_set_data64(sqe, WAKEUP_MARKER);
    }   
    
    void wakeup()
    {
        std::uint64_t val = 1;
        ::write(wakeup_fd_, &amp;val, sizeof(val));
    }

    void resume_wakeup()
    {
        uint64_t val;
        ::read(wakeup_fd_, &amp;val, sizeof(val));
    }
};
</code></pre>
<p dir="auto"><strong>关于未处理完成的 CQE 的处置</strong>：<br />
触发 <code>stop()</code> 退出循环后，队列中如果还有积压的 CQE 怎么办？</p>
<p dir="auto">这正是 RAII 管理机制的优势所在。<code>IOContext</code> 的生命周期与系统资源严格绑定，当程序退出，<code>IOContext</code> 析构时，<code>io_uring_queue_exit</code> 会协同内核彻底销毁共享的环形缓冲区。由于事件循环已经终止，不会再有新的逻辑被触发，因此忽略未决的 CQE 是安全且合理的策略。</p>
<p dir="auto">如果用户确实有在停止后清理特定状态的需求，可以通过暴露的 <code>ring()</code> 接口自行干预。</p>
<h3>3. 基于 signalfd 的统一中断处理</h3>
<p dir="auto">既然已经实现了安全的唤醒与停止语义，顺理成章地，我们应将操作系统的信号（如 <code>SIGINT</code>, <code>SIGTERM</code>）也纳入异步框架。在 Linux 平台上，<code>signalfd</code> 提供了一种将异步中断转化为文件描述符可读事件的机制，它能被完美地集成进 <code>io_uring</code> 轮询模型中。</p>
<h4>3.1 强类型 Signal 封装</h4>
<p dir="auto">避免裸露的魔术整数：</p>
<pre><code class="language-cpp">class Signal {
public:
    explicit constexpr Signal(int signal) noexcept
      : signal_{ signal }
    {}

    auto operator==(const Signal&amp;) const noexcept -&gt; bool = default;
    // 这里的隐式转换是否提供看个人，我觉得不提供更好
    constexpr operator int() const noexcept { return signal_; }
    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; constexpr auto value() const noexcept { return signal_; }

private:
    int signal_;
};

struct signals {
    signals() = delete;
    
    static constexpr auto interrupt = Signal{ SIGINT };
    static constexpr auto terminate = Signal{ SIGTERM };
    static constexpr auto quit = Signal{ SIGQUIT };
    static constexpr auto hangup = Signal{ SIGHUP };
};
</code></pre>
<h4>3.2 信号集 SignalSet 管理</h4>
<p dir="auto">使用折叠表达式优雅地处理变参掩码。另外，为保障跨线程时的健壮性，此处采用了标准的 <code>pthread_sigmask</code>。</p>
<pre><code class="language-cpp">#include &lt;sys/signalfd.h&gt;
#include &lt;signal.h&gt;

template&lt;typename Context&gt;
class SignalSet {
public:
    template&lt;typename... Signals&gt;
        requires (std::same_as&lt;Signals, Signal&gt; &amp;&amp; ...)
    SignalSet(Context&amp; io_context, Signals... signals)
      : io_context_{ io_context }
    {
        ::sigemptyset(&amp;mask_);
        (::sigaddset(&amp;mask_, signals.value()), ...);

        // 屏蔽这些信号的默认异步行为，交由 signalfd 同步读取
        if (::pthread_sigmask(SIG_BLOCK, &amp;mask_, nullptr) == -1)
            throw_system_error("Failed to block signals");

        fd_ = ::signalfd(-1, &amp;mask_, SFD_NONBLOCK | SFD_CLOEXEC);
        if (fd_ == -1)
            throw_system_error("Failed to create signalfd");
    }

    SignalSet(const SignalSet&amp;) = delete;
    auto operator=(const SignalSet&amp;) -&gt; SignalSet&amp; = delete;

    SignalSet(SignalSet&amp;&amp; other) noexcept
      : io_context_{ other.io_context_ }, 
        fd_{ std::exchange(other.fd_, -1) },
        mask_{ other.mask_ }
    {}

    auto operator=(SignalSet&amp;&amp; other) noexcept -&gt; SignalSet&amp; = delete;

    ~SignalSet()
    {
        if (fd_ != -1)
            ::close(fd_);
    }

private:
    Context&amp; io_context_;
    int fd_{ -1 };
    sigset_t mask_;
};
</code></pre>
<h4>3.3 构建 PollAwaiter 与 async_wait</h4>
<p dir="auto">既然是协程库，那我们理所应当的应该将监听行为设计成协程。为监听可读事件提供通用的 <code>PollAwaiter</code>：</p>
<pre><code class="language-cpp">template&lt;typename Context&gt;
class PollAwaiter : public Operation {
public:
    using resume_type = void;

    PollAwaiter(Context&amp; context, int fd, short events) noexcept
      : context_{ context }, 
        fd_{ fd }, 
        events_{ events }
    {}

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; constexpr auto await_ready() const noexcept -&gt; bool { return false; }

    auto await_suspend(std::coroutine_handle&lt;&gt; handle) noexcept -&gt; void
    {
        handle_ = handle;
        auto* sqe = context_.sqe();
        
        ::io_uring_prep_poll_add(sqe, fd_, events_);
        ::io_uring_sqe_set_data(sqe, this);
    }

    auto await_resume() const noexcept -&gt; std::expected&lt;void, std::error_code&gt;
    {
        if (error_code_ != 0)
            return unexpected_system_error(error_code_);
            
        return {};
    }

    void complete(int res, &lsqb;&lsqb;maybe_unused&rsqb;&rsqb; std::uint32_t flags) noexcept override
    {
        error_code_ = res &lt; 0 ? -res : 0;
        
        if (handle_) {
            auto handle = std::exchange(handle_, nullptr);
            handle.resume();
        }
    }

private:
    Context&amp; context_;
    int fd_;
    short events_;
    std::coroutine_handle&lt;&gt; handle_{ nullptr };
    int error_code_{ 0 };
};

// 在 SignalSet 外部实现
template&lt;typename Context&gt;
auto SignalSet&lt;Context&gt;::async_wait() noexcept -&gt; PollAwaiter&lt;Context&gt;
{
    return PollAwaiter&lt;Context&gt;{ io_context_, fd_, POLLIN };
}   
</code></pre>
<h3>4. 系统集成演示</h3>
<p dir="auto">至此，基础组件均已就位。我们可以轻松写出一个支持非阻塞延时、并通过 <code>Ctrl+C</code> 信号安全、优雅退出的并发模型。</p>
<pre><code class="language-cpp">auto shutdown_monitor(IOContext&amp; context) -&gt; Task&lt;void&gt;
{
    SignalSet sets{ context, signals::interrupt, signals::terminate };

    // 挂起协程，等待操作系统向底层派发 SIGINT 或 SIGTERM
    co_await sets.async_wait();

    spdlog::info("Received shutdown signal, stopping IOContext...");
    context.stop();
}

auto demo(IOContext&amp; context) -&gt; Task&lt;void&gt;
{
    using namespace std::chrono_literals;
    spdlog::info("before sleep...");    
    
    // 模拟长耗时异步任务
    co_await sleep_for(context, 10min);

    spdlog::info("after sleep...");
}

int main(int argc, char* argv[])
{
    IOContext context{};

    // 并发派发两个独立协程：一个执行业务，一个负责监听中断
    co_spawn(context, demo(context));
    co_spawn(context, shutdown_monitor(context));

    context.run();

    spdlog::info("IOContext stopped, exiting...");
    return EXIT_SUCCESS;
}
</code></pre>
<p dir="auto"><strong>运行结果</strong>：<br />
在长达 10 分钟的 <code>sleep</code> 任务途中，我们通过 <code>Ctrl+C</code> 触发键盘中断，<code>signalfd</code> 捕获到信号，唤醒了沉睡的 <code>shutdown_monitor</code> 协程，随后成功中断事件循环，程序安全退出。</p>
<pre><code class="language-bash">blog.io_context_v2
[2026-04-19 22:33:21.313] [info] before sleep...
^C[2026-04-19 22:33:22.954] [info] Received shutdown signal, stopping IOContext...
[2026-04-19 22:33:22.954] [info] IOContext stopped, exiting...
</code></pre>
<p dir="auto"><a href="https://github.com/Doomjustin/blog/blob/main/demo/io_context_v2.cpp" rel="nofollow ugc">完整代码</a></p>
]]></description><link>http://forum.d2learn.org/topic/190/基于-io_uring-的-c-20-协程网络库-02-模块解耦与完备的退出机制</link><generator>RSS for Node</generator><lastBuildDate>Mon, 20 Apr 2026 02:57:19 GMT</lastBuildDate><atom:link href="http://forum.d2learn.org/topic/190.rss" rel="self" type="application/rss+xml"/><pubDate>Sun, 19 Apr 2026 14:18:36 GMT</pubDate><ttl>60</ttl></channel></rss>