<?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 协程网络库】01：基础骨架与 Awaiter 机制]]></title><description><![CDATA[<h3>目标与设计边界</h3>
<p dir="auto">本文旨在实现一个基于 <code>io_uring</code> 封装的 C++ 协程网络库。在着手编码前，我们先确立一个严格的设计边界：</p>
<p dir="auto"><strong>不考虑跨平台，不考虑兼容 epoll 等传统多路复用机制。</strong></p>
<p dir="auto">为什么舍弃跨平台等通用性？<br />
一旦引入跨平台封装，不仅维护成本陡增，更关键的是<strong>性能势必要做出妥协</strong>。不同操作系统的异步 API 在机制上存在根本分歧，强行封装通常只能取它们的公共子集，或者在用户态引入额外的抽象层来模拟缺失的语义。无论哪种方式，都会对最终性能造成不可预期的损耗。</p>
<p dir="auto">在基础设施级别的系统库中，性能是无法在项目后期通过“手法”来弥补的，必须在架构初期就定下基调。因此，我们选择不给自己埋雷，直接将底层与 <code>io_uring</code> 强绑定。</p>
<h3>为什么选择 io_uring？</h3>
<p dir="auto">相比于 epoll，<code>io_uring</code> 的心智模型更加契合协程。<br />
epoll 暴露的是 Reactor 模型接口（就绪通知），本质上依然是接近线程回调的处理方式。而 <code>io_uring</code> 是标准的 Proactor 模型（完成通知）。C++20 的协程天然就是一个异步操作状态机，也是标准的 Proactor 范式。两者的结合能最大程度地降低封装阻抗，减少无谓的状态转换代码。</p>
<h3>IOContext 是什么？</h3>
<p dir="auto">对于初接触异步网络库的读者，可以简单将 <code>IOContext</code> 理解为<strong>事件收割机与协程调度中枢</strong>。</p>
<p dir="auto">在代码中，你提交的各种异步操作（即 <code>io_uring</code> 的 SQE 事件），最终都需要一个统一的执行流去收割它们的完成结果（CQE）。成熟的模式是借鉴 Boost.Asio 的 <code>io_context</code> 抽象：通过阻塞调用 <code>IOContext::run()</code> 来消耗掉所有已就绪事件，唤醒对应的协程，然后继续等待下一轮事件就绪。</p>
<h3>构建基础框架</h3>
<p dir="auto">基于 C++ 的 RAII 原则，<code>IOContext</code> 的首要任务是管理 <code>io_uring</code> 实例的生命周期。</p>
<h4>1. 实例初始化</h4>
<pre><code class="language-cpp">int io_uring_queue_init(unsigned entries, struct io_uring* ring, unsigned flags);
</code></pre>
<ul>
<li><code>entries</code>: 提交队列（SQ）的深度。必须是 2 的幂（如 128, 256）。内核会基于此分配共享内存环。</li>
<li><code>ring</code>: 指向待初始化的实例。成功后，内存映射地址、队列掩码等状态将被写入该结构。</li>
<li><code>flags</code>: 控制行为的标志位（如启用 <code>SQPOLL</code> 消除系统调用）。我们这里默认置 0 即可。</li>
</ul>
<p dir="auto">失败时直接返回负值的系统错误码，不依赖全局 <code>errno</code>。</p>
<h4>2. 实例销毁</h4>
<pre><code class="language-cpp">void io_uring_queue_exit(struct io_uring* ring);
</code></pre>
<p dir="auto">该函数负责解除内存映射 (<code>munmap</code>)，并关闭 <code>io_uring</code> 在内核中对应的匿名文件描述符，防止虚拟内存与文件句柄泄漏。</p>
<h4>IOContext 资源管理骨架</h4>
<p dir="auto">基于上述 API，我们搭建出 <code>IOContext</code> 的核心轮廓。由于该上下文作为核心中枢运转，移动语义会引发悬垂指针等复杂问题，因此我们在设计上严格禁用拷贝与移动。</p>
<pre><code class="language-cpp">#include &lt;liburing.h&gt;
#include &lt;atomic&gt;

class IOContext {
public:
    explicit IOContext(unsigned entries)
    {
        if (::io_uring_queue_init(entries, &amp;ring_, 0) &lt; 0) {
            xin::throw_system_error("io_uring_queue_init");            
        }
    }

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

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

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

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

private:
    ::io_uring ring_;
};
</code></pre>
<h3>收割已就绪事件 (CQE)</h3>
<p dir="auto">接下来实现核心引擎 <code>IOContext::run()</code>。这涉及三个底层操作流：</p>
<ol>
<li>
<p dir="auto"><strong>等待事件就绪</strong>：<br />
<code>io_uring_submit_and_wait(struct io_uring* ring, unsigned wait_nr);</code></p>
<p dir="auto">将提交操作和阻塞等待融合成一次系统调用。<code>wait_nr</code> 指明线程必须阻塞到至少出现多少个完成事件才唤醒返回。</p>
<p dir="auto"><strong>返回值陷阱</strong>：成功时返回的是提交的 SQE 数量，而非完成的 CQE 数量。</p>
</li>
<li>
<p dir="auto"><strong>遍历完成队列 (CQ)</strong>：<br />
<code>io_uring_for_each_cqe</code> 是一个纯用户态宏。它通过带有 <em>Acquire</em> 语义的内存屏障读取 CQ 尾指针，无锁且零拷贝地遍历就绪事件。</p>
<p dir="auto"><strong>状态剥离陷阱</strong>：该宏只是只读遍历，不修改内核视角的头部指针。如果仅仅遍历而不推进状态，队列最终会溢出导致 <code>-EBUSY</code>。</p>
</li>
<li>
<p dir="auto"><strong>确认事件消费</strong>：<br />
<code>io_uring_cq_advance(struct io_uring* ring, unsigned nr);</code></p>
<p dir="auto">修改用户空间的 Head 指针，并通过 <em>Store-Release</em> 语义发布给内核，正式确认这些事件已被收割。</p>
</li>
</ol>
<h4>user_data 与类型擦除</h4>
<p dir="auto"><code>io_uring_cqe</code> 结构中包含一个 <code>__u64 user_data</code> 字段。当我们在 SQE 中设置它时，内核会原封不动地将其带入 CQE 返回。这使得我们能够将该标识强制转换回 C++ 对象的指针。</p>
<p dir="auto">为此，我们提供一个 <code>Operation</code> 基类，所有协程 Awaiter 都必须继承此接口：</p>
<pre><code class="language-cpp">struct Operation {
    virtual ~Operation() = default;
    virtual void complete(int res, unsigned flags) = 0;
};
</code></pre>
<h4>优雅的退出：should_stop_ 的无锁设计</h4>
<p dir="auto">为了安全退出事件循环，我们引入 <code>should_stop_</code> 变量。即便网络库采用 Core Per Thread 模型，不涉及跨业务线程的同步，但 <code>stop()</code> 操作往往是由操作系统的信号处理器（Signal Handler，如处理 Ctrl+C）触发的。信号中断具有强抢占性，因此必须使用 <code>std::atomic</code>。</p>
<p dir="auto">值得注意的是，这里我们<strong>不使用 CAS（Compare-And-Swap）</strong>。由于停止是一个幂等且无条件的覆盖动作，我们完全不关心过去的运行状态。直接使用 <code>store</code> 配合最松散的 <code>std::memory_order_relaxed</code> 即可。这提供了硬件级别的防数据撕裂保证，同时将同步开销降到了绝对的最低点。</p>
<blockquote>
<p dir="auto">WARN: 只有这个变量显然是不足以完整实现 stop 功能的，还需要考虑如何取消已经提交但尚未完成的 I/O 请求，以及如何通知正在等待的 run() 方法尽快返回。我们将在未来的版本中逐步完善这个功能。</p>
</blockquote>
<h4>完整的 run() 实现</h4>
<p dir="auto">结合外部任务追踪机制，事件循环的最终代码如下：</p>
<pre><code class="language-cpp">class IOContext {
    // ... 构造与析构保持不变 ...

    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)
                xin::throw_system_error("io_uring_submit_and_wait");

            unsigned head;
            unsigned count{ 0 };

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

                if (cqe-&gt;user_data != 0) {
                    auto* op = reinterpret_cast&lt;Operation*&gt;(cqe-&gt;user_data);
                    op-&gt;complete(cqe-&gt;res, cqe-&gt;flags);
                }
            }

            if (count &gt; 0) {
                outstanding_works_ -= count;
                ::io_uring_cq_advance(&amp;ring_, count);
            }
        }
    }

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

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

    // 为了搭配co_spawn，需要暴露add_work和drop_work方法
    void add_work() noexcept { ++outstanding_works_; }

    void drop_work() noexcept
    {
        assert(outstanding_works_ &gt; 0);
        --outstanding_works_;
    }

private:
    ::io_uring ring_;
    std::size_t outstanding_works_{ 0 };
    std::atomic&lt;bool&gt; should_stop_{ false };

};
</code></pre>
<h3>深入 Awaiter 机制：SleepAwaiter 实践</h3>
<p dir="auto">单有一个 <code>IOContext</code> 是跑不起来的，我们需要验证它与 C++20 协程的交互机制。在此，我们实现一个 <code>SleepAwaiter</code>，封装 <code>io_uring</code> 的 <code>IORING_OP_TIMEOUT</code> 定时器。</p>
<pre><code class="language-cpp">#include &lt;chrono&gt;
#include &lt;coroutine&gt;
#include &lt;expected&gt;
#include &lt;system_error&gt;
#include &lt;utility&gt;

class SleepAwaiter : public Operation {
public:
    template&lt;typename Duration&gt;
    SleepAwaiter(IOContext&amp; context, Duration d) noexcept
      : context_{ context }
    {
        using namespace std::chrono;
        ts_.tv_sec = duration_cast&lt;seconds&gt;(d).count();
        ts_.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;ts_, 0, 0);
        ::io_uring_sqe_set_data(sqe, this);
    }

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

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

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

template&lt;typename Duration&gt;
&lsqb;&lsqb;nodiscard&rsqb;&rsqb; auto sleep_for(IOContext&amp; context, Duration duration) noexcept -&gt; SleepAwaiter
{
    return SleepAwaiter{ context, duration };
}
</code></pre>
<h4>零开销生命周期管理</h4>
<p dir="auto">留意 <code>await_suspend</code> 中的 <code>::io_uring_prep_timeout(sqe, &amp;ts_, 0, 0);</code>。我们将局部对象 <code>ts_</code> 的地址交给了内核。在传统的异步回调编程中，这是一个极易触发悬垂指针的致命错误，通常需要用 <code>std::shared_ptr</code> 在堆上分配来强行续命。</p>
<p dir="auto">但在这里，它是<strong>绝对安全的</strong>。因为 <code>SleepAwaiter</code> 本身的生命周期被牢牢绑定在了协程帧内部。直到 <code>complete</code> 回调中触发 <code>handle.resume()</code> 彻底唤醒协程后，该 Awaiter 才会被销毁。协程从语言底层提供了天然的内存安全保障，这也是 C++ 追求零开销抽象的绝佳体现。</p>
<h4>测试示例</h4>
<p dir="auto">最后，我们用一段简单的代码来验证整个基建流转：</p>
<pre><code class="language-cpp">auto demo(IOContext&amp; context) -&gt; xin::async::Task&lt;void&gt;
{
    using namespace std::chrono_literals;
    xin::log::info("before sleep...");    
    
    co_await sleep_for(context, 5s);

    xin::log::info("after sleep...");
}

int main(int argc, char* argv[])
{
    IOContext context{128};
    // 搭配协程任务分离器运行
    xin::async::co_spawn(context, demo(context));

    context.run();
    return EXIT_SUCCESS;
}
</code></pre>
<p dir="auto">输出如下，可以看到，5s后再次输出内容，这证明从请求提交、内核响应、上下文分发到协程唤醒的全链路已完全贯通：</p>
<pre><code class="language-bash">blog.io_context_v1
[2026-04-19 16:18:17.642] [info] before sleep...
[2026-04-19 16:18:22.642] [info] after sleep...
</code></pre>
<p dir="auto"><a href="https://github.com/Doomjustin/xin/blob/main/blog/io_context_v1.cpp" rel="nofollow ugc">完整代码详见</a></p>
]]></description><link>http://forum.d2learn.org/topic/189/基于-io_uring-的-c-20-协程网络库-01-基础骨架与-awaiter-机制</link><generator>RSS for Node</generator><lastBuildDate>Sun, 19 Apr 2026 11:59:32 GMT</lastBuildDate><atom:link href="http://forum.d2learn.org/topic/189.rss" rel="self" type="application/rss+xml"/><pubDate>Sun, 19 Apr 2026 07:30:23 GMT</pubDate><ttl>60</ttl></channel></rss>