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

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

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)系统调用以潜在地提高其性能?

  1. class mpsc_list_node
  2. {
  3. mpsc_list_node* _next;
  4. template<typename T>
  5. requires std::derived_from<T, mpsc_list_node>
  6. friend class mpsc_list;
  7. };
  8. template<typename T>
  9. requires std::derived_from<T, mpsc_list_node>
  10. class mpsc_list
  11. {
  12. private:
  13. std::atomic<T*> head{ nullptr };
  14. private:
  15. static constexpr size_t COMPLETED_SENTINEL = 42;
  16. public:
  17. mpsc_list() noexcept = default;
  18. mpsc_list(mpsc_list&& other) noexcept :
  19. head{ other.head.exchange(reinterpret_cast<T*>(COMPLETED_SENTINEL), std::memory_order_relaxed) }
  20. {
  21. }
  22. bool try_enqueue(T& to_append)
  23. {
  24. T* old_head = head.load(std::memory_order_relaxed);
  25. do
  26. {
  27. if (reinterpret_cast<size_t>(old_head) == COMPLETED_SENTINEL)
  28. [[unlikely]]
  29. {
  30. return false;
  31. }
  32. to_append._next = old_head;
  33. } while (!head.compare_exchange_weak(old_head, &to_append, std::memory_order_release, std::memory_order_relaxed));
  34. return true;
  35. }
  36. template<typename Func>
  37. void complete_and_iterate(Func&& func) noexcept(std::is_nothrow_invocable_v<Func, T&>)
  38. {
  39. T* p = head.exchange(reinterpret_cast<T*>(COMPLETED_SENTINEL), std::memory_order_acquire);
  40. while (p)
  41. [[likely]]
  42. {
  43. T* cur = p;
  44. T* next = static_cast<T*>(p->_next);
  45. p = next;
  46. func(*cur);
  47. }
  48. }
  49. };

这个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机制

相关问题