信号类型

每个平台的信号定义或许有些不同。下面列出了POSIX中定义的信号。 Linux 使用34-64信号用作实时系统。 命令 man signals 提供了官方的信号介绍。

在POSIX.1-1990标准中定义的信号列表

信号动作说明
SIGHUP1Term终端控制进程结束(终端连接断开)
SIGINT2Term用户发送INTR字符(Ctrl+C)触发
SIGQUIT3Core用户发送QUIT字符(Ctrl+/)触发
SIGILL4Core非法指令(程序错误、试图执行数据段、栈溢出等)
SIGABRT6Core调用abort函数触发
SIGFPE8Core算术运行错误(浮点运算错误、除数为零等)
SIGKILL9Term无条件结束程序(不能被捕获、阻塞或忽略)
SIGSEGV11Core无效内存引用(试图访问不属于自己的内存空间、对只读内存空间进行写操作)
SIGPIPE13Term消息管道损坏(FIFO/Socket通信时,管道未打开而进行写操作)
SIGALRM14Term时钟定时信号
SIGTERM15Term结束程序(可以被捕获、阻塞或忽略)
SIGUSR130,10,16Term用户保留
SIGUSR231,12,17Term用户保留
SIGCHLD20,17,18Ign子进程结束(由父进程接收)
SIGCONT19,18,25Cont继续执行已经停止的进程(不能被阻塞)
SIGSTOP17,19,23Stop停止进程(不能被捕获、阻塞或忽略)
SIGTSTP18,20,24Stop停止进程(可以被捕获、阻塞或忽略)
SIGTTIN21,21,26Stop后台程序从终端中读取数据时触发
SIGTTOU22,22,27Stop后台程序向终端中写数据时触发

在SUSv2和POSIX.1-2001标准中的信号列表:

信号动作说明
SIGTRAP5CoreTrap指令触发(如断点,在调试器中使用)
SIGBUS0,7,10Core非法地址(内存地址对齐错误)
SIGPOLLTermPollable event (Sys V). Synonym for SIGIO
SIGPROF27,27,29Term性能时钟信号(包含系统调用时间和进程占用CPU的时间)
SIGSYS12,31,12Core无效的系统调用(SVr4)
SIGURG16,23,21Ign有紧急数据到达Socket(4.2BSD)
SIGVTALRM26,26,28Term虚拟时钟信号(进程占用CPU的时间)(4.2BSD)
SIGXCPU24,24,30Core超过CPU时间资源限制(4.2BSD)
SIGXFSZ25,25,31Core超过文件大小资源限制(4.2BSD)

第 1 列为信号名;

第 2 列为对应的信号值,需要注意的是,有些信号名对应着 3 个信号值,这是因为这些信号值与平台相关,将 man 手册中对 3 个信号值的说明摘出如下,the first one is usually valid for alpha and sparc, the middle one for i386, ppc and sh, and the last one for mips.

第 3 列为操作系统收到信号后的动作,Term 表明默认动作为终止进程,Ign 表明默认动作为忽略该信号,Core 表明默认动作为终止进程同时输出 core dump,Stop 表明默认动作为停止进程。

第 4 列为对信号作用的注释性说明,浅显易懂,这里不再赘述。 需要特别说明的是,SIGKILL 和 SIGSTOP 这两个信号既不能被应用程序捕获,也不能被操作系统阻塞或忽略。

kill pid 与 kill -9 pid 的区别

kill pid 向进程号为 pid 的进程发送 SIGTERM(这是 kill 默认发送的信号),该信号是一个结束进程的信号且可以被应用程序捕获。若应用程序没有捕获并响应该信号的逻辑代码,则该信号的默认动作是 kill 掉进程。这是终止指定进程的推荐做法。

kill -9 pid 则是向进程号为 pid 的进程发送 SIGKILL(该信号的编号为 9),从本文上面的说明可知,SIGKILL 既不能被应用程序捕获,也不能被阻塞或忽略,其动作是立即结束指定进程。通俗地说,应用程序根本无法“感知”SIGKILL 信号,它在完全无准备的情况下,就被收到 SIGKILL 信号的操作系统给干掉了,显然,在这种“暴力”情况下,应用程序完全没有释放当前占用资源的机会。事实上,SIGKILL 信号是直接发给 init 进程的,它收到该信号后,负责终止 pid 指定的进程。在某些情况下(如进程已经 hang 死,无法响应正常信号),就可以使用 kill -9 来结束进程。

若通过 kill 结束的进程是一个创建过子进程的父进程,则其子进程就会成为孤儿进程(Orphan Process),这种情况下,子进程的退出状态就不能再被应用进程捕获(因为作为父进程的应用程序已经不存在了),不过应该不会对整个 linux 系统产生什么不利影响。

应用程序如何优雅退出

Linux Server 端的应用程序经常会长时间运行,在运行过程中,可能申请了很多系统资源,也可能保存了很多状态,在这些场景下,我们希望进程在退出前,可以释放资源或将当前状态 dump 到磁盘上或打印一些重要的日志,也就是希望进程优雅退出(exit gracefully)。

从上面的介绍不难看出,优雅退出可以通过捕获 SIGTERM 来实现。具体来讲,通常只需要两步动作:

1)注册 SIGTERM 信号的处理函数并在处理函数中做一些进程退出的准备。信号处理函数的注册可以通过 signal()或 sigaction()来实现,其中,推荐使用后者来实现信号响应函数的设置。信号处理函数的逻辑越简单越好,通常的做法是在该函数中设置一个 bool 型的 flag 变量以表明进程收到了 SIGTERM 信号,准备退出。

2)在主进程的 main()中,通过类似于 while(!bQuit)的逻辑来检测那个 flag 变量,一旦 bQuit 在 signal handler function 中被置为 true,则主进程退出 while()循环,接下来就是一些释放资源或 dump 进程当前状态或记录日志的动作,完成这些后,主进程退出。

Go 中的 Signal 发送和处理

golang 中对信号的处理主要使用 os/signal 包中的两个方法: notify 方法用来监听收到的信号 stop 方法用来取消监听

1.监听全部信号

package main

import (
"fmt"
"os"
"os/signal"
)

// 监听全部信号
func main() {
//合建 chan
c := make(chan os.Signal)
//监听所有信号
signal.Notify(c)
//阻塞直到有信号传入
fmt.Println("启动")
s := <-c
fmt.Println("退出信号", s)
}
go run demo1.go
启动

ctrl+c 退出,输出
退出信号 interrupt

kill pid 输出
退出信号 terminated

2.监听指定信号

package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"
)

// 监听指定信号
func main() {
	// 合建 chan
	c := make(chan os.Signal)
	// 监听指定信号 ctrl+c kill
	signal.Notify(c, os.Interrupt, os.Kill, syscall.SIGUSR1, syscall.SIGUSR2)
	// 阻塞直到有信号传入
	fmt.Println("启动")
	// 阻塞直至有信号传入
	s := <-c
	fmt.Println("退出信号", s)
}

运行

go run demo2.go
启动

ctrl+c 退出,输出
退出信号 interrupt

kill pid 输出
退出信号 terminated

kill -USR1 pid 输出
退出信号 user defined signal 1

kill -USR2 pid 输出
退出信号 user defined signal 2

3.优雅退出 go 守护进程

package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"
	"time"
)

// 优雅退出 go 守护进程
func main() {
	// 创建监听退出 chan
	c := make(chan os.Signal)
	// 监听指定信号 ctrl+c kill
	signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2)
	go func() {
		for s := range c {
			switch s {
			case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
				fmt.Println("退出", s)
				ExitFunc()
			case syscall.SIGUSR1:
				fmt.Println("usr1", s)
			case syscall.SIGUSR2:
				fmt.Println("usr2", s)
			default:
				fmt.Println("other", s)
			}
		}
	}()

	fmt.Println("进程启动...")
	sum := 0
	for {
		sum++
		fmt.Println("sum:", sum)
		time.Sleep(time.Second)
	}

}

func ExitFunc() {
	fmt.Println("开始退出...")
	fmt.Println("执行清理...")
	fmt.Println("结束退出...")
	os.Exit(0)
}
kill -USR1 pid 输出
usr1 user defined signal 1

kill -USR2 pid
usr2 user defined signal 2

kill pid
退出 terminated
开始退出...
执行清理...
结束退出...
执行输出
go run example-3.go
进程启动...
sum: 1
sum: 2
sum: 3
sum: 4
sum: 5
sum: 6
sum: 7
sum: 8
sum: 9
usr1 user defined signal 1
sum: 10
sum: 11
sum: 12
sum: 13
sum: 14
usr2 user defined signal 2
sum: 15
sum: 16
sum: 17
退出 terminated
开始退出...
执行清理...
结束退出...

参考

http://www.cnblogs.com/jkkkk/p/6180016.html
http://blog.csdn.net/zzhongcy/article/details/50601079
https://www.douban.com/note/484935836/
https://gist.github.com/reiki4040/be3705f307d3cd136e85#file-signal-go-L1

作者:水滴穿石 链接:https://www.jianshu.com/p/ae72ad58ecb6 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。