關於 signal.Notify 的一個小問題

前些天,給同事 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