分享

理解内存屏障(四)

 mzsm 2015-12-27

作者:新浪微博(@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)

原创技术文章,感悟计算机,透彻理解计算机!

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多