信号(signal)是一种进程间通信机制,它给应用程序提供一种异步的软件中断,使应用程序有机会接受其他程序活终端发送的命令(即信号)。应用程序收到信号后,有三种处理方式:忽略,默认,或捕捉。进程收到一个信号后,会检查对该信号的处理机制。如果是SIG_IGN,就忽略该信号;如果是SIG_DFT,则会采用系统默认的处理动作,通常是终止进程或忽略该信号;如果给该信号指定了一个处理函数(捕捉),则会中断当前进程正在执行的任务,转而去执行该信号的处理函数,返回后再继续执行被中断的任务。
Java中提供了Signal的机制,Signal机制在Linux中是一个非常常用的进程间通信机制。在sun.misc包下,属于非标准包。重要涉及到两个类:Signal和SignalHandler。其中Signal主要使用了静态方法Signal.handle(Signal, SignalHandler),而SignalHandler是一个接口,有一个抽象方法void handle(Signal var1);需要我们自己实现SignalHandler接口处理。
在Linux下支持的信号(具体信号kill -l命令查看):
SEGV, ILL, FPE, BUS, SYS, CPU, FSZ, ABRT, INT, TERM, HUP, USR1, USR2, QUIT, BREAK, TRAP, PIPE
kill -15 ${pid} 来杀死程序,这样,在jvm退出的时候,会将ShutdownHook中的内容执行一遍,这样就可以在里面定义自己的资源释放(一般是连接池)的操作了 。
不要使用kill -9 ${pid} ,这个会直接杀死jvm的进程,hook是来不及执行的,kill后面的数字参数如下:
HUP 1 终端断线
INT 2 中断(同 Ctrl + C)
QUIT 3 退出(同 Ctrl + \)
TERM 15 终止
KILL 9 强制终止
CONT 18 继续(与STOP相反, fg/bg命令)
STOP 19 暂停(同 Ctrl + Z)
默认的kill参数是 -15 (TERM) .
在Windows下支持的信号:
SIGINT(INT) Ctrl+C中断
SIGILL (ILL) 非法指令
SIGFPE(FPE) 浮点异常
SIGSEGV(SEGV) 无效的内存引用
SIGTERM(TERM) kill发出的软件终止
SIGBREAK(BREAK) Ctrl+Break中断
SIGABRT(ABRT) 调用abort导致
一、信号类型
Linux系统共定义了64种信号,分为两大类:可靠信号与不可靠信号,前32种信号为不可靠信号,后32种为可靠信号。
-
不可靠信号: 也称为非实时信号,不支持排队,信号可能会丢失, 比如发送多次相同的信号, 进程只能收到一次. 信号值取值区间为1~31;
-
可靠信号: 也称为实时信号,支持排队, 信号不会丢失, 发多少次, 就可以收到多少次. 信号值取值区间为32~64
一、首先看下java中Signal枚举有哪些各代表什么含义:每个新号都对应一个整数值
SIGUSR1 (USR1):用户自定义信号1(10) 默认处理:进程终止
SIGUSR2 (USR2):用户自定义信号2(12) 默认处理:进程终止
推荐:用户自定义信号USR1和USR2,其他信号我同事说可能会隐患别的问题,最好不用.
TERM:终止信号(15)
KILL:Kill信号(9)
INT:键盘中断(2)
HUP :终端挂起或者控制进程终止(1)
BUS:总线错误(7)
上面这5个出现的比较多,下面是其他的信号
ALRM:警告
CHLD:子进程结束信号
CONT:进程继续(曾被停止的进程)
FPE:浮点异常
ILL:非法指令
IO:某I/O操作现在可以进行了
IOT:IO捕获指令
PIPE:管道破裂: 写一个没有读端口的管道
PROF:Profiling定时器到
PWR:电源故障
QUIT:键盘的退出键被按下
SEGV:无效的内存引用
STKFLT:协处理器堆栈错误
STOP:终止进程
TRAP:跟踪/断点捕获
TSTP:控制终端(tty)上按下停止键
TTIN:后台进程企图从控制终端读
TTOU:后台进程企图从控制终端写
VTALRM: 实际时间报警时钟信号
WINCH:窗口大小改变
XCPU:超出设定的CPU时间限制
XFSZ:超出设定的文件大小限制
二、信号产生
信号来源分为硬件类和软件类:
2.1 硬件方式
- 用户输入:比如在终端上按下组合键ctrl+C,产生SIGINT信号;
- 硬件异常:CPU检测到内存非法访问等异常,通知内核生成相应信号,并发送给发生事件的进程;
2.2 软件方式
通过系统调用,发送signal信号:kill(),raise(),sigqueue(),alarm(),setitimer(),abort()
- kernel,使用 kill_proc_info()等
- native,使用 kill() 或者raise()等
- java,使用 Procees.sendSignal()等
Linux中signal机制的模型:
每个进程都会采用一个进程控制块对其进行描述,进程控制块中设计了一个signal的位图信息,其中的每位与具体的signal相对应,这与中断机制是保持一致的。当系统中一个进程A通过signal系统调用向进程B发送signal时,设置进程B的对应signal位图,类似于触发了signal对应中断。发送signal只是“中断”触发的一个过程,具体执行会在两个阶段发生:
1、 system call返回。进程B由于调用了system call后,从内核返回用户态时需要检查他拥有的signal位图信息表,此时是一个执行点。
2、 中断返回。进程被系统中断打断之后,系统将CPU交给进程时,需要检查即将执行进程所拥有的signal位图信息表,此时也是一个执行点。
综上所述,signal的执行点可以理解成从内核态返回用户态时,在返回时,如果发现待执行进程存在被触发的signal,那么在离开内核态之后(也就是将CPU切换到用户模式),执行用户进程为该signal绑定的signal处理函数,从这一点上看,signal处理函数是在用户进程上下文中执行的。当执行完signal处理函数之后,再返回到用户进程被中断或者system call(软中断或者指令陷阱)打断的地方。
Signal机制实现的比较灵活,用户进程由于中断或者system call陷入内核之后,将断点信息都保存到了堆栈中,在内核返回用户态时,如果存在被触发的signal,那么直接将待执行的signal处理函数push到堆栈中,在CPU切换到用户模式之后,直接pop堆栈就可以执行signal处理函数并且返回到用户进程了。Signal处理函数应用了进程上下文,并且应用实际的中断模拟了进程的软中断过程。
import sun.misc.*;
@SuppressWarnings("restriction")
public class TestSignal implements SignalHandler {
public void handle(Signal arg0) {
System.out.println(arg0.getName() + "is recevied.");
System.exit(0);
}
}
import sun.misc.*;
public class App {
@SuppressWarnings("restriction")
public static void main(String[] args) {
TestSignal handler = new TestSignal();
Signal.handle(new Signal("TERM"), handler);
Signal.handle(new Signal("INT"), handler);
Signal.handle(new Signal("ILL"), handler);
for (;;) {
System.out.println("do something");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
深入JVM关闭与关闭钩子
Java虚拟机退出包括两个阶段:
第一个阶段:会以某种未指定的顺序启动所有已注册钩子,并且允许它们同时运行直至结束
第二个阶段:如果已启用runFinalizersOnExit设置为true,则运行所有未调用的终结方法(finalizer方法)
System.exit()底层调用的Runtime.getRuntime().exit(),调用后终止当前正在运行的Java虚拟机。参数作为状态代码,按照惯例,一个非零状态码表示异常终止。
前面说到, Signal的一大作用是关闭进程, 然而Java提供了Shutdown Hook(关闭钩子)机制,它让我们在程序正常退出或者发生异常时能有机会做一些清场工作。
关闭钩子使用的方法也很简单,Runtime.getRuntime().addShutdownHook(Thread hook)
即可。关闭钩子其实可以看成是一个已经初始化了的但还没启动的线程,当JVM关闭时会并发地执行注册的所有关闭钩子。
JVM的关闭方式可以分为三种:
- 正常关闭:当最后一个非守护线程结束或者调用了System.exit或者通过其他特定平台的方法关闭(发送SIGINT,SIGTERM信号等)
- 强制关闭:通过调用Runtime.halt方法或者是在操作系统中直接kill(发送SIGKILL信号)掉JVM进程
- 异常关闭:运行中遇到RuntimeException异常, OOM错误等。
注意: Hook线程在JVM 正常关闭才会执行,在强制关闭和异常关闭时不会执行。Spring在初始化容器的时候就会注册一个hook线程用于清理容器.
- 不能在钩子调用
System.exit()
,否则卡住JVM的关闭过程,但是可以调用Runtime.halt()
。halt方法不会执行钩子函数和finalizer方法,而是直接退出。 - 不能再钩子中再进行钩子的添加和删掉操作,否则将会抛出IllegalStateException。
- 在
System.exit()
之后添加的钩子无效。 - 当JVM收到SIGTERM命令(比如操作系统在关闭时)后,如果钩子线程在一定时间没有完成,那么Hook线程可能在执行过程中被终止。
- Hool线程中同样会抛出异常,如果抛出异常又不处理,那么钩子的执行序列就会被停止。