23.4 RCU实现(2) - Memory barrier

在前一部分介绍的方法中,存在一个问题。在前一部分中,如果要更新E2的内容,需要先创建一个E2‘ 并设置好它的内容,然后将E2’ 的next指针指向E3,最后才会将E1的next指针指向E2’。

你们或许还记得在XV6中曾经介绍过(注,详见10.8),许多计算机中都不存在“之后”或者“然后”这回事,通常来说所有的编译器和许多微处理器都会重排内存操作。如果我们用C代码表示刚才的过程:

如果你测试这里的代码,它可能可以较好的运行,但是在实际中就会时不时的出错。这里的原因是编译器或者计算机可能会重排这里的写操作,也有可能编译器或者计算机会重排数据读取者的读操作顺序。如果我们在初始化E2’的内容之前,就将E1的next指针设置成E2‘,那么某些数据读取者可能就会读到垃圾数据并出错。

所以实现RCU的第二个部分就是数据读取者和数据写入者都需要使用memory barriers,这里背后的原因是因为我们这里没有使用锁。对于数据写入者来说,memory barrier应该放置在committing write之前,

这样可以告知编译器和硬件,先完成所有在barrier之前的写操作,再完成barrier之后的写操作。所以在E1设置next指针指向E2‘的时候,E2’必然已经完全初始化完了。

对于数据读取者,需要先将E1的next指针加载到某个临时寄存器中,我们假设r1保存了E1的next指针,之后数据读取者也需要一个memory barrier,然后数据读取者才能查看r1中保存的指针。

这里的barrier表明的意思是,在完成E1的next指针读取之前,不要执行其他的数据读取,这样数据读取者从E1的next指针要么可以读到旧的E2,要么可以读到新的E2‘。通过barrier的保障,我们可以确保成功在r1中加载了E1的next指针之后,再读取r1中指针对应的内容。

因为数据写入者中包含的barrier确保了在committing write时,E2’已经初始化完成。如果数据读取者读到的是E2‘,数据读取者中包含的barrier确保了可以看到初始化之后E2’的内容。

学生提问:什么情况下才可能在将E1的next指针加载到r1之前,就先读取r1中指针指向的内容?

Robert教授:我觉得你难住我了。一种可能是,不论r1指向的是什么,它或许已经在CPU核上有了缓存,或许一分钟之前这段内存被用作其他用途了,我们在CPU的缓存上有了E1->next对应地址的一个旧版本。我不确定这是不是会真的发生,这里都是我编的,如果r1->x可以使用旧的缓存的数据,那么我们将会有大麻烦。

说实话我不知道这个问题的答案,呵呵。我课下会想一个具体的例子。

以上是实现RCU的第二个部分。

Last updated