這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
我們在生產環境下啟動並執行系統要求優雅退出,即程式接收退出通知後,會有機會先執行一段清理代碼,將收尾工作做完後再真正退出。我們採用系統Signal來 通知系統退出,即kill pragram-pid。我們在程式中針對一些系統訊號設定了處理函數,當收到訊號後,會執行相關清理程式或通知各個子進程做自清理。kill -9強制殺掉程式是不能被接受的,那樣會導致某些處理過程被強制中斷,留下無法恢複的現場,導致訊息被破壞,影響下次系統啟動運行。
最近用Golang實現的一個代理程式也需要優雅退出,因此我嘗試瞭解了一下Golang中對系統Signal的處理方式,這裡和大家分享。Golang 的系統訊號處理主要涉及os包、os.signal包以及syscall包。其中最主要的函數是signal包中的Notify函數:
func Notify(c chan<- os.Signal, sig …os.Signal)
該函數會將進程收到的系統Signal轉寄給channel c。轉寄哪些訊號由該函數的可變參數決定,如果你沒有傳入sig參數,那麼Notify會將系統收到的所有訊號轉寄給c。如果你像下面這樣調用Notify:
signal.Notify(c, syscall.SIGINT, syscall.SIGUSR1, syscall.SIGUSR2)
則Go只會關注你傳入的Signal類型,其他Signal將會按照預設處理,大多都是進程退出。因此你需要在Notify中傳入你要關注和處理的Signal類型,也就是攔截它們,提供自訂處理函數來改變它們的行為。
下面是一個較為完整的例子:
//signal.go
package main
import "fmt"
import "time"
import "os"
import "os/signal"
import "syscall"
type signalHandler func(s os.Signal, arg interface{})
type signalSet struct {
m map[os.Signal]signalHandler
}
func signalSetNew()(*signalSet){
ss := new(signalSet)
ss.m = make(map[os.Signal]signalHandler)
return ss
}
func (set *signalSet) register(s os.Signal, handler signalHandler) {
if _, found := set.m[s]; !found {
set.m[s] = handler
}
}
func (set *signalSet) handle(sig os.Signal, arg interface{})(err error) {
if _, found := set.m[sig]; found {
set.m[sig](sig, arg)
return nil
} else {
return fmt.Errorf("No handler available for signal %v", sig)
}
panic("won't reach here")
}
func main() {
go sysSignalHandleDemo()
time.Sleep(time.Hour) // make the main goroutine wait!
}
func sysSignalHandleDemo() {
ss := signalSetNew()
handler := func(s os.Signal, arg interface{}) {
fmt.Printf("handle signal: %v\n", s)
}
ss.register(syscall.SIGINT, handler)
ss.register(syscall.SIGUSR1, handler)
ss.register(syscall.SIGUSR2, handler)
for {
c := make(chan os.Signal)
var sigs []os.Signal
for sig := range ss.m {
sigs = append(sigs, sig)
}
signal.Notify(c)
sig := <-c
err := ss.handle(sig, nil)
if (err != nil) {
fmt.Printf("unknown signal received: %v\n", sig)
os.Exit(1)
}
}
}
上例中Notify函數只有一個參數,沒有傳入要關注的sig,因此程式會將收到的所有類型Signal都轉寄到channel c中。build該源檔案並執行程式:
$> go build signal.go
$> signal
在另外一個視窗下執行如下命令:
$> ps -ef|grep signal
tonybai 25271 1087 0 16:27 pts/1 00:00:00 signal
$> kill -n 2 25271
$> kill -n 12 25271
$> kill 25271
我們在第一個視窗會看到如下輸出:
$> signal
handle signal: interrupt
handle signal: user defined signal 2
unknown signal received: terminated
在sysSignalHandleDemo中我們也可以為Notify傳入我們所關注的Signal集合:
signal.Notify(c, sigs…)
這樣只有在該集合中的訊號我們才能捕獲,收到未在集合中的訊號時,程式多直接退出。上面只是一個Demo,只是說明了我們可以捕捉到我們所關注的訊號,並未體現程式如何優雅退出,不同程式的退出方式不同,這裡沒有通用方法,就不細說了,你的程式需要你專門的設計。
另外我們生產環境下的程式多是以Daemon守護進程的形式啟動並執行。我們用C實現的程式多參考“Unix進階編程”中的方法將程式轉為Daemon Process,但在Go中目前尚提供相關方式,網上有一些實現,但據說都不理想。更多的Go開發人員建議不要在代碼中實現Daemon轉換,建議直接利用 第三方工具。比如在Ubuntu下我們可以使用start-stop-daemon這個小程式輕鬆將你的程式轉換為Daemon:
$> start-stop-daemon –start –pidfile ./signal.pid –startas /home/tonybai/test/go/signal –background -m
$> start-stop-daemon –stop –pidfile ./signal.pid –startas /home/tonybai/test/go/signal
這裡注意:只有加上-m選項,pidfile才能成功建立。
start-stop-daemon在Debian系的Linux發行版中都是預設內建的。但在Redhat系Linux發行版中卻沒有該工具,我們可以自行安裝:
wget -c http://developer.axis.com/download/distribution/apps-sys-utils-start-stop-daemon-IR1_9_18-2.tar.gz
tar -xzf apps-sys-utils-start-stop-daemon-IR1_9_18-2.tar.gz
cd apps/sys-utils/start-stop-daemon-IR1_9_18-2
gcc start-stop-daemon.c -o start-stop-daemon
切換到root下
cp start-stop-daemon /sbin/
chmod +x /sbin/start-stop-daemon
另外Go 1.0.2提供的二進位安裝包直接在Redhat 5.6(Linux tonybai 2.6.18-238.el5 #1 SMP Sun Dec 19 14:22:44 EST 2010 x86_64 x86_64 x86_64 GNU/Linux)下面運行出錯,提示無法找到GLIBC 2.7版本。目前解決這一問題的方法似乎只有從源碼編譯安裝。進入到$GOROOT/src下,執行./all.bash即可。重現編譯連結後的go可執 行程式則運行一切正常。
2012, bigwhite. 著作權.