[讨论] 关于memory_serialize_page的一些疑问

stefmoon 2013-10-25
memory_serialize_page是在HotSpot VM启动时,在polling page后面分配的一个page,是用来在不使用memory barrier系指令的场景下模拟其操作,这样VM Thread可以在Java线程状态发生变化时,及时获取到它们的状态,以正确地进行safe point时的管理。HotSpot VM有一个参数UseMembar来控制是否使用memory barrier系的指令,在x86下默认情况是关的,这样应该是出于性能方面的考虑。之前加入memory barrier系指令是因为出现了线程在safe point时无法正常block的bug,这里有些许介绍。后来曾经尝试过去掉参数UseMembar,相当于把默认情况下的-XX:-UseMembar给固化了,但后来发现还是有bug,于是又重新加上了。

以上都是我个人的理解,如有不对的地方,还请指正。

基本原理是这样,但具体的实现却有很多令人困惑的地方,就是HotSpot是如何通过memory_serialize_page来模拟memory barrier系的指令的。

当线程状态发生改变时,会调用ThreadStateTransition::transition(JavaThread *thread, JavaThreadState from, JavaThreadState to),里面的相应代码如下:
    // Make sure new state is seen by VM thread
    if (os::is_MP()) {
      if (UseMembar) {
        // Force a fence between the write above and read below
        OrderAccess::fence();
      } else {
        // store to serialize page so VM thread can do pseudo remote membar
        os::write_memory_serialize_page(thread);
      }
    } 

这个地方的逻辑是一目了然,问题就出在这个os::write_memory_serialize_page上:
  static inline void write_memory_serialize_page(JavaThread *thread) {
    uintptr_t page_offset = ((uintptr_t)thread >>
                            get_serialize_page_shift_count()) &
                            get_serialize_page_mask();
    *(volatile int32_t *)((uintptr_t)_mem_serialize_page+page_offset) = 1;
  } 


这个offset看起来应该是某个线程在memory_serialize_page里的offset,对应某个线程应该会占int32_t,也就是4个字节,1个page一般是4096字节,也就是说只支持1024个线程?
还有如何保证每个线程对应一个int field,这个地方的计算我也感到很困惑,get_serialize_page_shift_count()是4,计算方法是log2_intptr(sizeof(class JavaThread)) - log2_intptr(64),64是指cache line size,这么算的意义不太明白;get_serialize_page_mask()是4092,即11 11111100,这个值倒很好理解,计算方法是vm_page_size() - sizeof(int32_t)。

我的最新理解:其实每个线程都可以只写memory_serialize_page的一个地方,但出于避免cache竞争的考虑,尽量写到不同的地方,写到哪个cache line和地址后log2_intptr(64)位完全没关系,而JavaThread对象地址除了后log2_intptr(sizeof(class JavaThread))位,前面的部分是完全不同的,因此右移log2_intptr(sizeof(class JavaThread)) - log2_intptr(64)位后,JavaThread对象地址中决定出现在哪个cache line的部分是完全不同的。

好好,问题写着写着感觉自己弄明白了。。。
stefmoon 2013-10-25

在serialize所有线程状态时,VM Thread做的操作如下
// Serialize all thread state variables
void os::serialize_thread_states() {
  // On some platforms such as Solaris & Linux, the time duration of the page
  // permission restoration is observed to be much longer than expected  due to
  // scheduler starvation problem etc. To avoid the long synchronization
  // time and expensive page trap spinning, 'SerializePageLock' is used to block
  // the mutator thread if such case is encountered. See bug 6546278 for details.
  Thread::muxAcquire(&SerializePageLock, "serialize_thread_states");
  os::protect_memory((char *)os::get_memory_serialize_page(),
                     os::vm_page_size(), MEM_PROT_READ);
  os::protect_memory((char *)os::get_memory_serialize_page(),
                     os::vm_page_size(), MEM_PROT_RW);
  Thread::muxRelease(&SerializePageLock);
}

把memory_serialize_page设为只读,我的理解是这样会使之前写过这个page的线程对应的cache write back到内存里,也就能够使相应的线程状态更新到内存里。
LeafInWind 2014-04-25
stefmoon 写道

把memory_serialize_page设为只读,我的理解是这样会使之前写过这个page的线程对应的cache write back到内存里,也就能够使相应的线程状态更新到内存里。


这个理解应该是对的。一直很疑惑代码中为什么要连续的置为只读和可读写。
考虑到serialize_page在init_2中首先通过mmap设置为可读写,这里先置为只读清一下cache,保证之前的对java线程状态的修改操作为其他所有线程都可见,然后再重新置为可读写,等待之后的write_serialization_page操作。
fei1710 2018-06-29
protect_memory由于涉及到页表的更改,需要刷新对应页的CPU的TLB cache。OS会向各CPU发送IPI中断来做这个事。各CPU在处理TLB cache的时候,必定会先将对应页的本地store buffer刷新到内存(如果不做,那将来再刷的时候可能引起内存访问违例,这违反单线程处理语义)。而OS会一直等待,直到各CPU都处理完成。所以事实上protect_memory会跟各CPU对这个页的访问建立一个同步点。而对这个页的访问必须是写,因为TSO架构的CPU写之间是不会发生乱序的,所以可以保证前面对线程状态的写发生在这个保护页的写之前。原理其实挺复杂的。
Global site tag (gtag.js) - Google Analytics