示例
在上面的例子中,memory_0是CPU0私有的数据存储区,memory_1是CPU1私有的数据存储区,而memory_01是两者共享的数据存储区。在这样一个多核系统中,如何保证CPU0和CPU1对memory_01的正确访问呢?比如,CPU0写入一批数据到memory_01中,然后CPU1取出这批数据做运算?如何能保证CPU1能够取到CPU0最新写入的数据呢?
屏障
屏障指令用于访存排序,比如RISC-V ISA中的FENCE指令具有保证其之前的所有数据存储器访问指令必须比其后的所有数据存储器访问指令先执行的功能
既然屏障指令有访存排序的功能,那么就可以利用这个它解决示例中的问题。CPU0和CPU1约定一个共享的全局变量作为旗语(semaphore),CPU0完成写memory_01操作后,便将旗语设置为1,CPU1则不断监测旗语,发现旗语为1后,将数据取走。CPU0和和CPU1的程序可以抽象为:
(1)CPU0向memory_01写入数据;
(2)CPU0设置旗语;
(3)CPU1监测旗语直到为1;
(4)CPU1从memory_01读取数据。
但是由于数据和旗语所处的存储器地址不一样,编译器或者处理器硬件本身可能对程序进行优化,是程序最终的执行结果不是程序员期望的那样,例如执行顺序成为(2)(3)(4)(1),这样CPU1将读取到没更新之前的数据。为了解决这个问题,可以插入FENCE指令:
(1)CPU0向memory_01写入数据;
(2)CPU0插入FENCE指令;
(3)CPU0设置旗语;
(4)CPU1监测旗语直到为1;
(5)CPU1插入FENCE指令;
(6)CPU1从memory_01读取数据。
如此,(1)一定在(3)之前执行,(4)一定在(6)之前执行,保证了程序功能正确。但是由于FENCE指令屏障了所有存储访问操作,也存在一些问题,如下示例:
(1)CPU0向memory_01写入数据;
(2)CPU0插入FENCE指令;
(3)CPU0设置旗语;
(4)CPU0向memory_0写入数据;
(5)CPU1从memory_1读取数据;
(6)CPU1监测旗语直到为1;
(7)CPU1插入FENCE指令;
(8)CPU1从memory_01读取数据;
(4)向CPU0私有的数据存储区memory_0写入数据,(5)从CPU1私有的数据存储区memory_1读取数据,也分别受到FENCE指令(2)和(7)的约束,即(4)必须在(1)之后执行,(5)必须在(8)之前执行,这在某些情况下可能影响性能。
释放一致性
为了解决上面的问题,可以采用释放一致性模型中的获取-释放机制。
具有释放语义的指令,例如原子指令amoswap.w.rl,它可以代替(2)+(3),它只屏障其之前的所有存储器访问操作,这时(4)可以在(1)之前执行。
具有获取语义的指令,例如原子指令amoswap.w.aq,它可以代替(6)+(7),它只屏障其之后的所有存储器访问操作,这时(5)可以在(8)之后执行。
原子操作
原子操作的“读”和“写”是一个整体,中间不能被打断,用于“上锁”、“设置旗语”等场景。