1、使用场景
实际项目中,我们希望修改了配置文件后,但又不想通过重启进程让它重新加载配置文件,可以使用signal的方式进行信号传递,或者我们希望通过信号控制,实现一种优雅的退出方式。Golang为我们提供了signal包,实现信号处理机制,允许Go 程序与传入的信号进行交互。
2、常用的Term信号
3、简单的栗子
代码语言:javascript复制package main
import (
"fmt"
"os"
"os/signal"
)
func main() {
c := make(chan os.Signal)
signal.Notify(c)
fmt.Println("start..")
s := <-c
fmt.Println("End...", s)
}
(1)传递SIGINT信号
代码语言:javascript复制[homework@xxxxx signal]$ go run monitor.go
start..
#此时,CTL C发送一个SIGINT信号量,得到输出为:
[homework@xxxxx signal]$ go run monitor.go
start..
^CEnd... interrupt
(2)传递SIGTERM信号
打开2个Term窗口 第一个运行go run monitor.go程序 第二个执行:ps -ef | grep monitor.go | grep grep -v | awk '{print $2}' | xargs kill
代码语言:javascript复制#此时,kill命令发送一个SIGTERM信号量,得到输出为:
[homework@xxxxx signal]$ go run monitor.go
start..
Terminated
4、优雅的退出守护进程
(1)何为优雅(graceful)?
Linux Server端的应用程序经常会长时间运行,在运行过程中,可能申请了很多系统资源,也可能保存了很多状态。
在这些场景下,我们希望进程在退出前,可以释放资源或将当前状态dump到磁盘上或打印一些重要的日志,即希望进程优雅退出。
(2)从对优雅退出的理解不难看出:优雅退出可以通过捕获SIGTERM来实现。
A、注册SIGTERM信号的处理函数并在处理函数中做一些进程退出的准备,信号处理函数的注册sigaction()来实现。
B、在主进程的main()中,通过类似于while(!fQuit)的逻辑来检测那个flag变量,一旦fQuit在signal handler function中被置为true,则主进程退出while()循环,接下来就是一些释放资源或dump进程当前状态或记录日志的动作,完成这些后,主进程退出。
栗子:优雅退出go守护进程
代码语言:javascript复制package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
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("Program Exit...", s)
GracefullExit()
case syscall.SIGUSR1:
fmt.Println("usr1 signal", s)
case syscall.SIGUSR2:
fmt.Println("usr2 signal", s)
default:
fmt.Println("other signal", s)
}
}
}()
fmt.Println("Program Start...")
sum := 0
for {
sum
fmt.Println("sum:", sum)
time.Sleep(time.Second)
}
}
func GracefullExit() {
fmt.Println("Start Exit...")
fmt.Println("Execute Clean...")
fmt.Println("End Exit...")
os.Exit(0)
}
执行程序:
代码语言:javascript复制[homework@xxxx signal]$ go run monitor.go
Program Start...
sum: 1
sum: 2
sum: 3
sum: 4
sum: 5
sum: 6
^CProgram Exit... interrupt
Start Exit...
Execute Clean...
End Exit...
5、信号的订阅
信号的订阅是通过 channel实现的,每个os.Signal channel 都会收听自己相应的事件集。
关于Golang之信号处理的文章就写到这里,Done!
信号量
2.1 共享变量
在理解信号量之前,先了解采用共享变量使用多线程会出现什么问题。下面是一个C代码片段
代码语言:javascript复制1for (i=0; i<niters; i ){
2 cnt ;
3}
cnt
为全局变量,一个线程执行该代码片段的时候的汇编代码如下:
1 movq (%rdi), %rcx
2 testq %rcx, %rcx
3 jle .L2
4 movl $0, �x
5.L3:
6 movq cnt(%rip), %rdx
7 addq �x
8 movq �x, cnt(%rip)
9 addq $1, %rax
10 cmpq %rcx, %rax
11 jne .L3
12.L2
2.2 信号量其中6-8
行分别对应对应着加载cnt
,更新cnt
和存储cnt
。将cnt
变量从内存位置读出,加载到CPU寄存器中,在CPU运算器中加1,然后存储到cnt
的内存位置。虽然代码中cnt
只有一行,但是转换为汇编代码的时候不只有一个操作,也就是说该语句不是原子操作。如果多个线程同时执行代码,按照之前的条件,不对CPU的执行顺序做任何假设,如果其中线程a
在执行7
行汇编代码,而线程b
执行6
行汇编代码,那么b
将"看不到"线程a
对全局变量cnt
加1的操作,那么每次执行的结果cnt
也不完全一致。
计算机领域先驱Dijkstra
提出经典的解决上述问题的方法:信号量(semaphore)。它是一个非负整数的全局变量。而且该变量只能有两个特殊操作来处理: P
和V
。
- P(s):如果
s
非零,那么P
将s
减1
,并且立即返回。如果s
为零,那么就挂起这个线程,知道s
为非零。 - V(s):
V
操作将s
加1
。如果有任何线程阻塞在P
操作等待s
非零,那么V
将重启其中线程中的一个。
Posix
标准定义需要操作信号量的函数
1#include <semaphore.h>
2int sem_init(sem_t *sem, 0, unsigned int value);
3int sem_wait(sem_t *s); /*P(s)*/
4int sem_post(sem_t *s); /*P(s)*/
那么如何使用信号量是的2.1小节出现同步问题解决呢?首先定义全局信号量
代码语言:javascript复制1volatile long cnt = 0; /* global variable */
2sem_t mutex; /*global semaphore*/
初始化信号量,在这里初始值为1
1sem_init(&mutex, 0, 1);
最后使用信号量操作函数将临界区域代码包含起来
代码语言:javascript复制1for (i =0; i<niters; i ){
2 sem_wait(&mutex);
3 cnt ;
4 sem_post(&mutex);
5}
参考资料:
Go语言信号处理
https://zhuanlan.zhihu.com/p/128953024
信号量,锁和 golang 相关源码分析
https://cloud.tencent.com/developer/article/1357636