前些天,給同事 review 一個 MR。MR 自己沒什麼問題,merge 完以後突發奇想跑了一下 golangci-lint 看看有沒有啥問題。看到一個 issue 以下所示:程序員
main.go:102:16: SA1017: the channel used with signal.Notify should be buffered (staticcheck) signal.Notify(ch, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT)
很好奇,之前歷來沒見過這個 issue。因而查看了一下源碼發現了問題。golang
雖然之前看網上的代碼 signal.Notify
也注意到別人都有分配了帶 buffer 的 channel,可是也沒有細想。查看 signal.Notify
的源碼,在 signal.go 中:ide
// Notify causes package signal to relay incoming signals to c. // If no signals are provided, all incoming signals will be relayed to c. // Otherwise, just the provided signals will. // // Package signal will not block sending to c: the caller must ensure // that c has sufficient buffer space to keep up with the expected // signal rate. For a channel used for notification of just one signal value, // a buffer of size 1 is sufficient. // // It is allowed to call Notify multiple times with the same channel: // each call expands the set of signals sent to that channel. // The only way to remove signals from the set is to call Stop. // // It is allowed to call Notify multiple times with different channels // and the same signals: each channel receives copies of incoming // signals independently. func Notify(c chan<- os.Signal, sig ...os.Signal) { if c == nil { panic("os/signal: Notify using nil channel") } handlers.Lock() defer handlers.Unlock() h := handlers.m[c] if h == nil { if handlers.m == nil { handlers.m = make(map[chan<- os.Signal]*handler) } h = new(handler) handlers.m[c] = h } add := func(n int) { if n < 0 { return } if !h.want(n) { h.set(n) if handlers.ref[n] == 0 { enableSignal(n) // The runtime requires that we enable a // signal before starting the watcher. watchSignalLoopOnce.Do(func() { if watchSignalLoop != nil { go watchSignalLoop() } }) } handlers.ref[n]++ } } if len(sig) == 0 { for n := 0; n < numSig; n++ { add(n) } } else { for _, s := range sig { add(signum(s)) } } }
註釋中明確說明了須要傳遞帶 buffer 的 channel。關注其中的 go watchSignalLoop()
,在 signal_unix.go 中:函數
func loop() { for { process(syscall.Signal(signal_recv())) } } func init() { watchSignalLoop = loop }
process(sig os.Signal)
函數定義又在 signal.go 中:oop
func process(sig os.Signal) { n := signum(sig) if n < 0 { return } handlers.Lock() defer handlers.Unlock() for c, h := range handlers.m { if h.want(n) { // send but do not block for it select { case c <- sig: default: } } } // Avoid the race mentioned in Stop. for _, d := range handlers.stopping { if d.h.want(n) { select { case d.c <- sig: default: } } } }
注意中段的 select 代碼塊和註釋,發現 sig 並不會阻塞發送給 c,若是 c 當前沒有被 recv,則 sig 會被丟棄。這就形成了 sig 可能丟失的狀況產生,也就是 golangci-lint 中提示的問題。ui
看 os.signal
的代碼仍是設計的至關精巧和高效的。spa
var handlers struct { sync.Mutex // Map a channel to the signals that should be sent to it. m map[chan<- os.Signal]*handler // Map a signal to the number of channels receiving it. ref [numSig]int64 // Map channels to signals while the channel is being stopped. // Not a map because entries live here only very briefly. // We need a separate container because we need m to correspond to ref // at all times, and we also need to keep track of the *handler // value for a channel being stopped. See the Stop function. stopping []stopping }
用一個 handlers 來存儲關係。m 映射接收 channel 到相關 signal 的關係,ref 映射每一類 signal 有幾個 channel 須要接收。其中 handler
結構體定義:設計
type handler struct { mask [(numSig + 31) / 32]uint32 } func (h *handler) want(sig int) bool { return (h.mask[sig/32]>>uint(sig&31))&1 != 0 } func (h *handler) set(sig int) { h.mask[sig/32] |= 1 << uint(sig&31) } func (h *handler) clear(sig int) { h.mask[sig/32] &^= 1 << uint(sig&31) }
用三個長度的 uint32 來存儲全部的 signal。每一個 signal 佔 1 個 bit 位。unix
仍是不得不感嘆大師級別的程序員寫的東西,連一個字節都不捨得浪費。code