万事皆有因
随着公司的业务不断扩大,我们在2013年底开始逐步的进入Java体系的阶段,不过谁都没有Java的经验,我们就决定自己动手丰衣足食的策略,学习,请教和顾问。经过2014年的一年的努力,成功的组建了一个Java团队,并尝试做了一些新业务和基础性组件。虽然比较顺利,但还是在我心中留下了许多疑问,例如说JVM的安全点和安全区,正好上周出差回来得到了一段时间的放松,又开始阅读了下JVM中的代码。
那么收获有什么呢?
纠正了自己以前一个错误的认识,VMThread的是JVM用来完成JVM内的事情的线程,并非执行OpCode的线程,而这正执行OpCode的线程是JavaThread。
发现了JVM进入安全区的一些原理,该原理和Linux的信号以及线程是有非常大的关系的。
Linux的线程
在Linux的上古时代,Linux的线程技术和POSIX的标准是不同的,它使用自己的LinuxThread库。这会为我们带来什么影响呢?
首先,我们说下POSIX是如何定义多线程的,POSIX下一个多线程的进程只有一个PID。从这个定义中,大家可能已经猜出LinuxThread的实现有什么不同了,对,就是LinuxThread下每个线程都有一个PID,每个线程在系统中的表现就如同进程一般。
其次,现代的Linux的线程已经完全符合POSIX的标准了。
总结就是,我们可以忽略这件事情(不要丢鸡蛋)。
Linux的信号
随着Linux的内核版本不断提升,Linux的信号现在已经可以按照线程级别的触发了,换句话说就是,每个线程可以关注自己的信号了,并且可以区别性对待了。那我们需要注意什么呢?
-
在多线程应用中,我们应当使用sigaction来代替singal函数,因为按POSIX的说法singal函数并没有明确定义自己在多线程应用中的行为。
-
可以使用pthread_sigmask来为每个线程设置独立的信号掩码。同时在多线程应用中应当避免使用sigprocmask这个函数,原因也是POSIX中该函数病没有明确定义自己在多线程应用中的行为。
这个时候,有人会产生疑问了,那么多线程下kill发出的进程级别的信号A怎么办?Linux是这样解决的,它会把这个信号交付给任意一个没有屏蔽信号A的线程。如果这信号没有被任何线程设置handler进行处理,就会触发POSIX规定的默认动作。
接着有人就会问,我怎么向某个线程发消息呢,POSIX为我们准备了pthread_kill函数,我们可以直接向特定的线程发送消息。那么如果一个线程收到信号A,但是自己没有安装handler会发生什么?其实和进程级别的信号处理方法一样,直接触发默认动作,同样会结束整个进程。
说到这里了,好像已经没什么可说的了,但是我在看JVM中的时候,发现了一个非常重要的信号SIGSEGV。
信号SIGSEGV
这个信号,也许是大家最不想见到,为什么呢?我们看这个信号的定义:
当当前程序对内存的引用无效时,就会产生当前信号,也就是我们常说的“段违例”。
以下几种情况会产生该信号:
1.进程引用的内存页面不存在(例如,该页面位于堆和栈之间的映射的区域)
2.进程试图更新只读内存页(例如,程序文本段或已经被标记为只读的内存映射区域)
3.进程试图在用户态去访问内核部分的内存
OK,我们都知道这个信号引发的结果就是进程退出。不过我们都忽视了一个问题,在现代的Linux上,按照POSIX的定义,
这个信号是系统产生的线程级别的信号。换句话说,如果某个线程A出现了内存引用无效,那么产生的信号,会投递到线程A的信号队列中,而不是像进程级别的信号无法确定接受者是谁。
JVM的安全区域
如果我们想让所有Java线程停下来的时候,在JVM的JavaThread执行到大家所知道的test 特定页面的指令时,就会因为更新不可读页面而触发SIGSEGV信号。那么对于那些正在执行native代码的JavaThread该怎么办,JVM中的注释写的非常清楚,native返回JVM时会检查是否能返回的。
好了再多说一句,JVM是如果将特定内存保护起来的呢?这个需要看操作系统的API了,在Linux中是mprotect。
总结下
多读读POSIX标准和Intel的CPU体系结构,会让自己在开发变的容易些。
转自TTalkIM,深入阅读点击 https://ttalk.im/articles/19