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
发送到作业队列。
1条答案
按热度按时间ngynwnxp1#
rseq
系统调用是为了支持restartable sequences
而引入的,而restartable sequences
是为了在用户空间中支持per-cpu variables
而发明的。Per-cpu变量在内核空间中很容易,因为你可以在任何时候引用一个per-cpu变量时禁用抢占。在用户空间中,如果你想一致地处理当前线程/CPU的per-cpu变量,你必须考虑不必要的抢占。为此,引入了
restartable sequences
机制