在Linux的KCOV代码中,为什么会有这样的 barrier()
放置?
void notrace __sanitizer_cov_trace_pc(void)
{
struct task_struct *t;
enum kcov_mode mode;
t = current;
/*
* We are interested in code coverage as a function of a syscall inputs,
* so we ignore code executed in interrupts.
*/
if (!t || in_interrupt())
return;
mode = READ_ONCE(t->kcov_mode);
if (mode == KCOV_MODE_TRACE) {
unsigned long *area;
unsigned long pos;
/*
* There is some code that runs in interrupts but for which
* in_interrupt() returns false (e.g. preempt_schedule_irq()).
* READ_ONCE()/barrier() effectively provides load-acquire wrt
* interrupts, there are paired barrier()/WRITE_ONCE() in
* kcov_ioctl_locked().
*/
barrier();
area = t->kcov_area;
/* The first word is number of subsequent PCs. */
pos = READ_ONCE(area[0]) + 1;
if (likely(pos < t->kcov_size)) {
area[pos] = _RET_IP_;
WRITE_ONCE(area[0], pos);
}
}
}
A barrier()
调用可以防止编译器重新排序指令。然而,这里与中断有什么关系?为什么需要它来保证语义的正确性?
没有 barrier()
,编译器将可以自由访问 t->kcov_area
之前 t->kcov_mode
. 在实践中想要这样做是不太可能的,但这不是重点。 如果没有某种障碍,C规则允许编译器创建不符合我们要求的asm。 (C11内存模型除了你显式施加的外,没有任何排序保证;在C11中通过 stdatomic
或在Linux GNU C中通过像 barrier()
或 smp_rb()
.)
如评论中所述。barrier()
正在创建一个 获取负载与运行在同一核心上的代码有关。 这是你对中断的全部需求。
mode = READ_ONCE(t->kcov_mode);
if (mode == KCOV_MODE_TRACE) {
...
barrier();
area = t->kcov_area;
...
我对kcov不是很熟悉,但似乎看到某个值在 t->kcov_mode
读取的安全。t->kcov_area
. (因为无论代码写什么,该对象写的都是 kcov_area
首先,然后做一个释放商店到 kcov_mode
.)
https:/preshing.com20120913获取并发布-semantics。 解释了acq rel同步的一般情况。
为什么不是 smp_rb()
需要吗? 即使在弱排序的ISA上,获取排序也需要一个栅栏指令来保证看到另一个核做的其他存储)。
中断处理程序运行在做其他操作的同一个核上,就像信号处理程序中断一个线程并在其上下文中运行一样。 struct task_struct *t = current
意味着我们要看的数据是单个任务的本地数据。 这相当于在用户空间的单个线程内的东西。 内核抢占导致在不同的内核上重新调度,当另一个内核访问这个任务一直使用的内存时,将使用任何必要的内存障碍来保持单线程的正确执行)。
用户空间C11 stdatomic
相当于这个障碍的是 atomic_signal_fence(memory_order_acquire)
. 信号栅栏只需要阻止编译时的重新排序(就像Linux的 barrier()
),不像 atomic_thread_fence
,必须发出内存障碍asm指令。
失序的CPU内部确实会重新排序,但是 OoO exec的主要规则是保持指令一次运行的假象,以便让运行指令的核心. 这就是为什么你不需要一个内存屏障来处理asm的等价物。a = 1; b = a;
来正确加载你刚才存储的1;硬件上保留了串行执行的假象。1 按程序顺序进行。 (通常是通过让负载窥探存储缓冲区,并将尚未提交给L1d缓存的存储从存储转发到负载)。
中断处理程序中的指令在逻辑上运行在中断发生点之后(按照中断返回地址)。 因此,我们只需要按照正确的顺序来执行asm指令(barrier()
),硬件会让一切都正常。
脚注1:有一些显式并行的ISA,比如IA-64和Mill,但它们提供了asm可以遵循的规则,以确保一条指令看到另一条更早指令的效果。 对于经典的MIPS I加载延迟槽之类的东西也是如此。 编译器会对编译后的C语言进行处理。