作者:新浪微博(@NP等不等于P) 计算机学习微信公众号(jsj_xx) 先回顾一下。 上次讲过compiler相关的优化屏障和ACCESS_ONCE()。对于ACCESS_ONCE()粒度更细,重在理解ONCE字眼,compiler需要其使用次数不是(compiler优化可能导致的)多次也不是0次,而是1次。 2.3.1.2 cpu内存屏障 linux内核的8个内存屏障实现:(强制(MANDATORY)屏障可以用在UP系统) TYPE MANDATORY SMP CONDITIONAL ====== ======== ===== GENERAL mb() smp_mb() WRITE wmb() smp_wmb() READ rmb() smp_rmb() DATA DEP read_barrier_depends()smp_read_barrier_depends()
除了数据依赖屏障,其它屏障都内含优化屏障。看看数据依赖屏障,它不需要优化屏障的配合,因为compiler会感知这种依赖关系。显然,在UP系统中, SMP内存屏障将退化为优化屏障(因为内存屏障本质是在协调多cpu之间的操作)。而SMP系统上的共享内存的(有序)访问必须使用内存屏障或者锁。 还有一些高级的函数,封装了特定的操作:(和ACCESS_ONCE()一样,这样控制粒度更精细精准了)
(*) set_mb(var, value) value赋值给var之后插入一个通用内存屏障。显然,在UP系统,插入的屏障退化为一个优化屏障。 (*) smp_mb__before_atomic()/smp_mb__after_atomic(); 用于没有返回值的原子(加/减/清bit/置bit)操作,一般用于引入计数,该原子操作本身不含屏障,用该函数插入一个通用内存屏障。比如:(先宣告死亡,然后计数减1) obj->dead = 1; smp_mb__before_atomic(); atomic_dec(&obj->ref_count);
这样,就保证了先宣告死亡后改变计数的有序顺序。 2.3.1.3 mmio写屏障 对内存映射的io空间使用mmiowb()来保证写顺序,后文的另一个部分会讲,此处略过。 2.3.2 内核隐式内存屏障 2.3.2.1 锁 内核中的一些锁结构都是基于ACQUIRE/RELEASE操作,也就是隐含含有内存屏障的,比如: (*) spin locks (*) R/W spin locks (*) mutexes (*) semaphores (*) R/W semaphores (*) RCU 回忆一下之前讲的ACQUIRE/RELEASE操作,我们知道: *A = a; ACQUIRE M RELEASE M *B = b;
可能会有这样的顺序:
ACQUIRE M, STORE *B, STORE *A, RELEASE M
或者更狠的:(ACQUIRE和RELEASE操作的是两个不相关的临界区) *A = a; RELEASE M ACQUIRE N *B = b;
可能会有这样的执行顺序:
ACQUIRE N, STORE *B, STORE *A, RELEASE M
总结就是,这两个配合使用不是一个完全意义的内存屏障,但是确实对临界区内的代码有保护作用。
2.3.2.2 睡眠和唤醒
考虑下睡眠和唤醒两个操作,它们之间需要有屏障的保护。 睡眠代码: for (;;) { set_current_state(TASK_UNINTERRUPTIBLE); if (event_indicated) break; schedule(); }
唤醒代码: event_indicated = 1; wake_up(&event_wait_queue);
此时,正确的使用是: CPU 1 CPU 2 ================= ============================ set_current_state(); STORE event_indicated set_mb(); wake_up(); STORE current->state <write barrier> <general barrier> STORE current->state LOAD event_indicated
这样才能保证cpu1在被唤醒之后能够感知到条件变量event_indicated的最新值。
2.4 cpu间的锁和屏障的关系
cpu间的锁,比如自旋锁,也需要考虑顺序问题,比如: CPU 1 CPU 2 ==== ========= ACCESS_ONCE(*A) = a; ACCESS_ONCE(*E) = e; ACQUIRE M ACQUIRE Q ACCESS_ONCE(*B) = b; ACCESS_ONCE(*F) = f; ACCESS_ONCE(*C) = c; ACCESS_ONCE(*G) = g; RELEASE M RELEASE Q ACCESS_ONCE(*D) = d; ACCESS_ONCE(*H) = h;
cpu1和cpu2在各自的cpu上能够保证各自的有序序列,但是cpu3上看到的则是cpu1和cpu2的两个有序序列的交叉。再看个交叉的例子:(对设备的io空间做写操作) CPU 1 CPU 2 ======== ============ spin_lock(Q) writel(0, ADDR) writel(1, DATA); spin_unlock(Q); spin_lock(Q); writel(4, ADDR); writel(5, DATA); spin_unlock(Q);
此时的如果cpu1和cpu2的写操作交叉执行就出错,解决交叉的方法是使用mmiowb()写屏障: CPU 1 CPU 2 === ======= spin_lock(Q) writel(0, ADDR) writel(1, DATA); mmiowb(); spin_unlock(Q); spin_lock(Q); writel(4, ADDR); writel(5, DATA); mmiowb(); spin_unlock(Q);
2.5 哪里需要使用屏障
要唤醒一个等待进程, 信号量唤醒函数有这些处理步骤: 1)读进程的waiter->list.next,记住下一个等待进程; 2)读进程的waiter->task,记住task指针; 3)清进程的waiter->task指针;(表示该进程已经获得该信号量,可以理解为:清完该进程就能调度运行了) 4)调用wakeup唤醒该进程; 5)释放waiter->task的引用计数; 用代码表示就是:
LOAD waiter->list.next; LOAD waiter->task; STORE waiter->task; CALL wakeup RELEASE task
这里前两个是读操作,第三个是写操作,如果三者之间发生乱序: CPU 1 CPU 2 ===== =========== down_xxx() Queue waiter Sleep up_yyy() LOAD waiter->task; STORE waiter->task; Woken up by other event <preempt> Resume processing down_xxx() returns call foo() foo() clobbers *waiter </preempt> LOAD waiter->list.next; --- OOPS ---
cpu1上OOPS原因就是没有让两个读操作先于写操作导致,解决的方法是插入通用屏障:
LOAD waiter->list.next; LOAD waiter->task; smp_mb(); STORE waiter->task; CALL wakeup RELEASE task
2.6 内核中io屏障的作用 在操作io空间时,需要使用适当的函数,比如mmiowb()写屏障。
2.7 执行有序的最小假想模型 x86对程序执行顺序有较强约束,但是我们也应该看到,很多其它cpu都是弱约束的:
Some CPUs (such as i386 or x86_64) are more constrained than others (such as powerpc or frv), and so the most relaxed case (namely DEC Alpha) must be assumed outside of arch-specific code. 关于我们新浪微博(@NP等不等于P)
计算机学习微信公众号(jsj_xx) 原创技术文章,感悟计算机,透彻理解计算机!
|