linux 什么是RSEQ(可重启序列)以及如何使用它们?

0kjbasz6  于 2023-10-16  发布在  Linux
关注(0)|答案(1)|浏览(258)

Linux 4.18引入了rseq(2)系统调用。我在SO上发现只有一个question提到了rseq,网上关于它的信息相对较少,所以我决定问一下。什么是 * 可重启序列 * 以及程序员如何使用它们?
我不得不搜索restartable sequences: fast user-space percpu critical sections以获得任何有意义的结果。我能够找到向内核添加相关功能的commit。进一步的研究使我想到了2013 presentation,我认为它是这个概念的第一个介绍。一个名为EfficiOS的公司的团队已经完成了很多工作。他们描述了their intentions是什么,并将此特性贡献给了Linux内核。
看起来这个特性鲜为人知,但显然它在TCMalloc分配器中用于optimize performance。一般情况下,它似乎是某种concurrency optimization
虽然所列来源提供了背景信息,但尚未对SO上提供的RSEQ进行解释。了解其他地方以及在实践中如何使用这些规则将是有益的。

编辑:示例

假设I am creating a C++ job system。它的一部分是无锁的multi-producer single-consumer queue。如何在代码中引入rseq(2)系统调用以潜在地提高其性能?

class mpsc_list_node
{
    mpsc_list_node* _next;

    template<typename T>
        requires std::derived_from<T, mpsc_list_node>
    friend class mpsc_list;
};

template<typename T>
    requires std::derived_from<T, mpsc_list_node>
class mpsc_list
{
private:
    std::atomic<T*> head{ nullptr };

private:
    static constexpr size_t COMPLETED_SENTINEL = 42;

public:
    mpsc_list() noexcept = default;
    mpsc_list(mpsc_list&& other) noexcept :
        head{ other.head.exchange(reinterpret_cast<T*>(COMPLETED_SENTINEL), std::memory_order_relaxed) }
    {
    }

    bool try_enqueue(T& to_append)
    {
        T* old_head = head.load(std::memory_order_relaxed);
        do
        {
            if (reinterpret_cast<size_t>(old_head) == COMPLETED_SENTINEL)
                [[unlikely]]
                {
                    return false;
                }
            to_append._next = old_head;
        } while (!head.compare_exchange_weak(old_head, &to_append, std::memory_order_release, std::memory_order_relaxed));
        return true;
    }

    template<typename Func>
    void complete_and_iterate(Func&& func) noexcept(std::is_nothrow_invocable_v<Func, T&>)
    {
        T* p = head.exchange(reinterpret_cast<T*>(COMPLETED_SENTINEL), std::memory_order_acquire);
        while (p)
            [[likely]]
            {
                T* cur  = p;
                T* next = static_cast<T*>(p->_next);
                p       = next;
                func(*cur);
            }
    }
};

这个mpsc_list的目的及其在作业系统中的位置在我的README中得到了很好的解释:

作业间同步

唯一使用的同步原语是原子计数器。这个想法源于著名的GDC talk关于在Naughty Dog的游戏引擎中使用光纤来实现作业系统。
作业的公共promise类型(promise_base)实际上是从mpsc_list派生的类型,mpsc_list是一个多生产者单消费者列表。此列表存储与当前作业相关的作业。它是一个使用原子操作实现的无锁链表。每个节点存储一个指向依赖项的promise和下一个节点的指针。有趣的是,这个链表没有使用任何动态内存分配。
当一个作业co_await是一组(可能是1大小的)依赖性作业时,它会做一些事情。首先,它的promise将自己的内部原子计数器设置为依赖作业的数量。然后,它(在堆栈上)分配一个依赖计数大小的notifier对象数组。notifier类型是链表节点的类型。创建的notifier s都指向正在挂起的作业。他们没有下一个节点。
然后,作业遍历它的每个依赖项作业,并尝试将相应的notifier追加到依赖项的列表中。如果该依赖项已完成,则此操作将失败。这是因为当一个作业返回时,它会将其列表的头设置为一个特殊的sentinel值。如果依赖项已经完成(例如,在另一个线程上),则挂起作业只需递减其自己的原子计数器。如果依赖项尚未完成,则将notifier追加到该依赖项的列表中。它使用CAS loop来实现。
在完成每个依赖项之后,挂起作业检查有多少依赖项已经完成。如果它们都有,那么它不会挂起并立即继续执行。这不仅仅是优化。这是职务制度正常运作所必需的。这是因为此作业系统 * 没有 * 挂起的作业 * 队列。作业系统只有一个 ready job 队列。挂起的作业仅存储在其依赖关系的链表中。因此,如果一个作业挂起,而没有任何依赖关系,它将永远不会恢复。
当作业返回时,它将遍历其依赖项的链表。首先,它将列表的头部设置为特殊的sentinel值。然后,它遍历所有作业,原子地递减它们的原子计数器。递减量为RMW operation,因此作业读取计数器的前一个值。如果它是一个,那么它知道它是该作业要完成的最后一个依赖项,并且它将其push发送到作业队列。

ngynwnxp

ngynwnxp1#

rseq系统调用是为了支持restartable sequences而引入的,而restartable sequences是为了在用户空间中支持per-cpu variables而发明的。Per-cpu变量在内核空间中很容易,因为你可以在任何时候引用一个per-cpu变量时禁用抢占。
在用户空间中,如果你想一致地处理当前线程/CPU的per-cpu变量,你必须考虑不必要的抢占。为此,引入了restartable sequences机制

相关问题