获取信号量必须是原子的。 Pintos 的 sema_down 安全吗?

Posted

技术标签:

【中文标题】获取信号量必须是原子的。 Pintos 的 sema_down 安全吗?【英文标题】:Taking a semaphore must be atomic. Is Pintos's sema_down safe? 【发布时间】:2016-09-06 22:33:45 【问题描述】:

这段代码来自 Pintos 源码: https://www.cs.usfca.edu/~benson/cs326/pintos/pintos/src/threads/synch.c

void
sema_down (struct semaphore *sema) 

  enum intr_level old_level;

  ASSERT (sema != NULL);
  ASSERT (!intr_context ());

  old_level = intr_disable ();
  while (sema->value == 0) 
    
      list_push_back (&sema->waiters, &thread_current ()->elem);
      thread_block ();
    
  sema->value--;
  intr_set_level (old_level);

获取信号量的事实是sema->value--;。如果它有效,它必须是一个原子操作。 我们怎么知道它实际上是原子操作呢?我知道现代 CPU 保证对齐的内存操作(对于 word/doubleword/quadword- 它取决于)是原子的。但是,在这里,我不相信为什么它是原子的。

【问题讨论】:

请注意,此处的代码设计为在内核中运行,在单核/cpu 系统上运行,启用/禁用中断之间的代码不会被抢占。此代码在 pintos 内核中很有用,但通常没有用处。 生活在 1992 年要简单得多,那时还没有多核 CPU。好吧,反正买不起。 【参考方案1】:

TL:DR:如果您在 UP 系统上禁用中断,那么任何事情都是原子的,只要您不计算使用 DMA 观察内存的系统设备。

注意操作周围的intr_disable (); / intr_set_level (old_level);


现代 CPU 保证对齐的内存操作是原子的

对于多线程观察者,这仅适用于单独加载存储,不适用于读取-修改-写入操作。


要使事物具有原子性,我们必须考虑我们关心哪些潜在观察者。重要的是,没有任何事情可以观察该操作已部分发生。实现这一点的最直接方法是使操作在物理/电气上是瞬时的,并同时影响所有位(例如,并行总线上的加载或存储在时钟周期的边界从未开始到完成,所以它是原子的“免费”直到并行总线的宽度)。这对于 read-modify-write 是不可能的,我们能做的最好的事情就是阻止观察者在加载和存储之间查看。

我在Atomicity on x86 上的回答以不同的方式解释了同一件事,关于原子的含义。


在单处理器 (UP) 系统中,唯一的异步观察者是其他系统设备(例如 DMA)和中断处理程序。如果我们可以排除非 CPU 观察者写入信号量,那么我们关心的只是中断的原子性。

此代码采用简单的方法并禁用中断。这没有必要(或者至少如果我们用 asm 编写就没有必要了)。

An interrupt is handled between two instructions,从不在指令中间。机器的架构状态要么包括内存减量,要么不包括,因为dec [mem] 要么运行要么没有。我们实际上不需要lock dec [mem]

顺便说一句,这是cmpxchg without a lock prefix 的用例。我一直想知道为什么他们不只是将lock 隐含在cmpxchg 中,原因是UP 系统通常不需要lock 前缀。

此规则的例外是可以记录部分进度的可中断指令,例如 rep movsbvpgather / vpscatter 请参阅 Interrupting instruction in the middle of execution 这些不会是原子操作。即使唯一的观察者是同一内核上的其他代码,也会中断。只有rep whatever 的单个迭代,或者聚集或分散的单个元素,会发生与否。

大多数 SIMD 指令无法记录部分进度,例如,vmovdqu ymm0, [rdi] 要么完全发生,要么根本不发生在它运行的核心的 PoV 中。 (但当然不能保证原子 wrt。系统中的其他观察者,如 DMA 或 MMIO,或其他内核。这就是 normal load/store atomicity guarantees 的问题。)


没有可靠的方法来确保编译器发出 dec [value] 而不是这样的:

mov   eax, [value]
                           ;; interrupt here = bad
dec   eax
                           ;; interrupt here = bad
mov   [value], eax

ISO C11 / C++11 没有提供一种方法来请求关于信号处理程序/中断的原子性,但不提供其他线程。他们确实提供了atomic_signal_fence 作为编译器屏障(与 thread_fence 作为其他线程/核心的屏障),但屏障不能创建原子性,只能控制顺序。其他操作。

C11/C++11 volatile sig_atomic_t 确实有这个想法,但它只为单独的加载/存储提供原子性,而不是 RMW。 It's a typedef for int on x86 Linux. See that question for some quotes from the standard.

在具体实现中,gcc -Wa,-momit-lock-prefix=yes 将省略 all 锁定前缀。 (GAS 2.28 docs) 如果您的代码不包含需要在 MMIO 位置上执行原子 RMW 或使用虚拟 @987654348 的设备驱动程序硬件访问,这对于单线程代码或单处理器机器是安全的@作为更快的mfence

但这在需要在 SMP 机器上运行的多线程程序中是不可用的,如果您在线程之间以及线程和信号处理程序之间有一些原子 RMW。

【讨论】:

@Gilgamesz:是的,如果您希望 RMW 相对于 SMP 机器上的其他线程是原子的,那么您当然需要 lock。这(和原子 MMIO 总线周期)就是它存在的原因。关于 DMA 观察者的部分意味着:不要对您的网卡进行编程以查看您的信号量。当然,系统中有 DMA 设备,它们只是不查看您用于信号量的 RAM。它只是同步在 CPU 上运行的代码,而不是将内核代码与也在修改共享数据结构的设备同步。那么你可能需要lock dec,即使在UP。 re:最后两个 cmets:想想如果中断处理程序在“中断这里 = 坏”的地方之一运行会发生什么。中断处理程序递减旧值,因为此代码尚未存储递减值。当然 C 编译成 asm,但它编译成的 what asm 很重要。 @Gilgamesz: dec [mem] 要么在信号处理程序之前运行,要么不运行。这与 UP 内核上的中断情况相同:异步效果只发生在一致的架构状态点。 @Gilgamesz:这不是重点。 dec [mem] 相对于中断处理程序中的任何其他代码的原子。如果没有信号/中断处理程序可以触及信号量,那么一开始就不需要它! (除非您需要在持有锁或其他东西时睡觉,否则软件线程之间的同步是有意义的)。 但这不是在 PintOS 中发生的:相反,它通过禁用中断(以及修改等待列表)使检查和修改原子化,因此没有处理程序可以运行。

以上是关于获取信号量必须是原子的。 Pintos 的 sema_down 安全吗?的主要内容,如果未能解决你的问题,请参考以下文章

Linux 多线程编程(二)2019-08-10

Linux中IPC信号量的使用

Pintos Project1的同步问题

信号量sem 的用法

todo:内核态信号量

C/C++ - sem_t 类型的单个信号量按顺序打印数字