# 6.6 信號處理機制
我們已經知道了 Go 運行時調度以 Goroutine 的方式調度了所有用戶態代碼。 每個 Goroutine 都有可能在不同的線程上重新被執行。 那么如果用戶態的某個 Goroutine 需要接收系統信號, 如何才能確保某個線程的信號能夠正確的發送到可能在其他線程上執行的監聽信號的 Goroutine 呢?
本節我們討論調度器里涉及的 signal 信號處理機制。
## 6.6.1 信號與軟中斷
信號機制是 UNIX、類 UNIX 甚至其他 POSIX 兼容系統上規定的一種進程異步通信的限制形式。 用于提醒某個事件的發生狀態。 信號被定義為整數,產生信號的條件包括用戶使用某些按鍵組合(比如 Control + C)、 硬件異常、`kill`信號等等。
這些信號通常有三種不同的處理方式:忽略、捕獲或者執行系統的默認行為。 忽略與捕獲處理無法處理`SIGKILL`和`SIGSTOP`,默認處理通常為停止進程。 而對于捕獲處理而言,當信號發生時,操作系統將中斷用戶代碼,并保存其執行的上下文,切換到內核空間 并重新切換到用戶空間來執行預先設置好的信號處理回調。當回調執行完畢之后,會重新切換回內核 空間,并從中斷的位置進行恢復,如圖 1 所示。
**圖 1: 用戶空間與內核空間的信號處理流程**
早期的 UNIX 系統特性是,當某個進程執行一個長時間系統調用時發生阻塞, 如果此時捕獲到一個信號,則系統調用便被中斷且不再執行,并以失敗返回。 因此系統調用也會被分為兩類:低速系統調用和高速系統調用。低速系統調用可能使進程永遠阻塞, 例如 IO。
系統調用`sigaltstack`可以用于定義一個備用的信號棧來獲取一個存在的額外信號棧的狀態。 一個額外的信號棧會在信號處理執行中進行使用。
每個進程都包含一個信號屏蔽字(signal mask),規定了當前要阻塞遞送到該進程的信號集。 對于每種可能的信號,屏蔽字中都有一位與之對應。 對于某種信號,若其對應位置已設置,則它當前是被阻塞的。 如果要檢測和修改當前信號屏蔽字,則需要調用`sigprocmask`系統調用來進行。通過`_SIG_SETMASK`可以直接設置想要的屏蔽字,并獲得原先的屏蔽字。
值得注意的是,信號會在所有線程中進行共享。 換句話說,盡管某個線程可以阻止某些信號,但當線程修改了某個信號相關的處理行為后, 所有線程都必須共享這個行為帶來的變化。 如果一個線程選擇忽略某個信號,則其他線程可以恢復信號的默認處理行為,或者設置為信號設置 一個新的處理函數,進而能夠撤銷線程的信號選擇。 進程中信號會被傳遞給單個線程,如果信號與硬件故障或計時器超時有關, 信號便會被發送到引起事件的線程中去,而其他的信號則被發送到任一個線程。
Linux 上的線程以獨立進程進行實現,通過`clone`調用來共享資源,因此 Linux 上的信號 處理與類 Unix 系統略有不同,POSIX.1 線程模型會在異步信號發送到進程后, 且沒有阻塞屏蔽字時接受信號。而 Linux 上每個線程作為獨立進程執行,系統無法選擇沒有 阻塞屏蔽字的線程,進而無法注意到這個信號。所以 Linux 上也存在不可靠信號和可靠信號的 概念。其中不可靠信號可能丟失,多次發送相同的信號只能收到一次,取值從 1 至 31; 可靠信號則可以進行排隊,取值從 32 至 64。
這便是運行時信號處理的基本原理。
## 6.6.2 處理函數的初始化
[6.3 調度循環](https://golang.design/under-the-hood/zh-cn/part2runtime/ch06sched/exec)中討論過了 M 的生命周期,M 可以在兩種情況下被創建:
1. 程序運行之初的 M0,無需創建已經存在的系統線程,只需對其進行初始化即可。其函數調用鏈如下所示:
~~~
schedinit
? mcommoninit
? mpreinit
? msigsave
? initSigmask
? mstart
~~~
2. 需要時創建的 M,某些特殊情況下一定會創建一個新的 M 并進行初始化,而后創建系統線程。這些情況包括:
1. startm 時沒有空閑 m
2. startTemplateThread 時
3. startTheWorldWithSema 時 p 如果沒有 m
4. main 時創建系統監控
5. oneNewExtraM 時
其調用鏈為:
~~~
newm
? allocm
? mcommoninit
? mpreinit
? newm1
? newosproc
? mstart
~~~
在`mcommoninit`里,會在一個父線程(或引導時的主線程)上調用`mpreinit`,并最終會為一個 M 創建`gsignal`,是一個在 M 上用于處理信號的 Goroutine。因此,除了 g0 外,其實第一個創建的 g 應該是它, 但是它并沒有設置 Goid (Goroutine ID):
```
func mcommoninit(mp *m) {
...
// 初始化 gsignal,用于處理 m 上的信號。
mpreinit(mp)
// gsignal 的運行棧邊界處理
if mp.gsignal != nil {
mp.gsignal.stackguard1 = mp.gsignal.stack.lo + _StackGuard
}
...
}
// 從一個父線程上進行調用(引導時為主線程),可以分配內存
func mpreinit(mp *m) {
mp.gsignal = malg(32 * 1024) // OS X 需要 >= 8K,此處創建處理 singnal 的 g
mp.gsignal.m = mp // 指定 gsignal 擁有的 m
}
```
### 獲取原始信號屏蔽字
在調度器的初始化的階段,`initSigmask`目標旨在記錄主線程 M0 創建之初的屏蔽字`sigmask`:
```
func schedinit() {
_g_ := getg()
...
mcommoninit(_g_.m)
...
msigsave(_g_.m)
initSigmask = _g_.m.sigmask
...
}
```
其中`msigsave`通過`sigprocmask`這個系統調用將當前`m0`的屏蔽字保存到`mp.sigmask`上:
```
const _SIG_SETMASK = 3
// msigsave 將當前線程的信號屏蔽字保存到 mp.sigmask。
//go:nosplit
//go:nowritebarrierrec
func msigsave(mp *m) {
sigprocmask(_SIG_SETMASK, nil, &mp.sigmask)
}
```
`sigprocmask`的本質為系統調用,其返回值通過`old`交付給調用者:
```
type sigset uint32
//go:nosplit
//go:nowritebarrierrec
func sigprocmask(how int32, new, old *sigset) {
rtsigprocmask(how, new, old, int32(unsafe.Sizeof(*new)))
}
//go:noescape
func rtsigprocmask(how int32, new, old *sigset, size int32)
```
`rtsigprocmask`在 Linux 上由匯編直接包裝`rt_sigprocmask`調用:
```
TEXT runtime·rtsigprocmask(SB),NOSPLIT,$0-28
MOVL how+0(FP), DI
MOVQ new+8(FP), SI
MOVQ old+16(FP), DX
MOVL size+24(FP), R10
MOVL $SYS_rt_sigprocmask, AX
SYSCALL
CMPQ AX, $0xfffffffffffff001
JLS 2(PC)
MOVL $0xf1, 0xf1 // crash
RET
```
注意,`rt_sigprocmask`只適用于單個線程的調用,多線程上的調用時未定義行為, 不過初始化階段的此時還未創建其他線程,因此此調用時安全的。
在 Darwin 系統中,所有的信號處理函數均通過`pthread_sigmask`來完成:
```
//go:nosplit
//go:cgo_unsafe_args
func sigprocmask(how uint32, new *sigset, old *sigset) {
libcCall(unsafe.Pointer(funcPC(sigprocmask_trampoline)), unsafe.Pointer(&how))
}
func sigprocmask_trampoline()
```
```
TEXT runtime·sigprocmask_trampoline(SB),NOSPLIT,$0
PUSHQ BP
MOVQ SP, BP
MOVQ 8(DI), SI // arg 2 new
MOVQ 16(DI), DX // arg 3 old
MOVL 0(DI), DI // arg 1 how
CALL libc_pthread_sigmask(SB)
TESTL AX, AX
JEQ 2(PC)
MOVL $0xf1, 0xf1 // crash
POPQ BP
RET
```
`msigsave`執行完畢后,`sigmask`最后保存到`initSigmask`這一全局變量中, 用于初始化新創建的 M 的信號屏蔽字:
```
// 用于新創建的 M 的信號掩碼 signal mask 的值。
var initSigmask sigset
func schedinit() {
...
initSigmask = _g_.m.sigmask
...
}
```
用于當新創建 M 時(`newm`),將 M 的`sigmask`進行設置。
### 初始化信號棧
在進入`mstart`后,調用鏈關系就變成了:
~~~
mstart
? mstart1
? minit
? mstartm0 (僅當 m0 調用)
? schedule
? mexit
? sigblock
? unminit
~~~
`mstart1`會調用`minit`進行初始化:
```
func minit() {
minitSignals()
...
}
func minitSignals() {
minitSignalStack()
minitSignalMask()
}
```
M 在初始化過程中,會判定當前線程是否設置了備用信號棧, 正常情況下一個新創建的 M 是沒有備用信號棧的。 如果沒有,則會將`m.gsignal`的執行棧設置為備用信號棧,用于處理產生的信號。
另一種情況是,當使用 cgo 時,非 Go 線程可能調用 Go 代碼, 而這時用戶態的 C 代碼可能已經為非 Go 線程設置了信號棧,這時的替換必須小心。 因此如果 M 已經存在了備用信號棧,則會將現有的信號棧保存到`m.goSigStack`中。
```
type stackt struct { // 信號棧
ss_sp *byte
ss_flags int32
pad_cgo_0 [4]byte
ss_size uintptr
}
// 如果沒有為線程設置備用信號棧(正常情況),則將備用信號棧設置為 gsignal 棧。
// 如果為線程設置了備用信號棧(非 Go 線程設置備用信號棧然后調用 Go 函數的情況),
// 則將 gsignal 棧設置為備用信號棧。
// 如果沒有使用 cgo 我們還設置了額外的 gsignal 信號棧(無論其是否已經被設置)
// 記錄在 newSigstack 中做出的選擇,
// 以便可以在 unminit 中撤消。
func minitSignalStack() {
_g_ := getg()
// 獲取原有的信號棧
var st stackt
sigaltstack(nil, &st)
if st.ss_flags&_SS_DISABLE != 0 {
// 如果禁用了當前的信號棧
// 則將 gsignal 的執行棧設置為備用信號棧
signalstack(&_g_.m.gsignal.stack)
_g_.m.newSigstack = true
} else {
// 否則將 m 的 gsignal 棧設置為從 sigaltstack 返回的備用信號棧
setGsignalStack(&st, &_g_.m.goSigStack)
_g_.m.newSigstack = false
}
}
// 將 s 設置為備用信號棧,此方法僅在信號棧被禁用時調用
//go:nosplit
func signalstack(s *stack) {
st := stackt{ss_size: s.hi - s.lo}
setSignalstackSP(&st, s.lo)
sigaltstack(&st, nil)
}
//go:nosplit
func setSignalstackSP(s *stackt, sp uintptr) {
*(*uintptr)(unsafe.Pointer(&s.ss_sp)) = sp
}
// setGsignalStack 將當前 m 的 gsignal 棧設置為從 sigaltstack 系統調用返回的備用信號堆棧。
// 它將舊值保存在 *old 中以供 restoreGsignalStack 使用。
// 如果非 Go 代碼設置了,則在處理信號時使用備用棧。
//go:nosplit
//go:nowritebarrierrec
func setGsignalStack(st *stackt, old *gsignalStack) {
g := getg()
if old != nil {
old.stack = g.m.gsignal.stack
old.stackguard0 = g.m.gsignal.stackguard0
old.stackguard1 = g.m.gsignal.stackguard1
old.stktopsp = g.m.gsignal.stktopsp
}
stsp := uintptr(unsafe.Pointer(st.ss_sp))
g.m.gsignal.stack.lo = stsp
g.m.gsignal.stack.hi = stsp + st.ss_size
g.m.gsignal.stackguard0 = stsp + _StackGuard
g.m.gsignal.stackguard1 = stsp + _StackGuard
}
```
### 初始化信號屏蔽字
當設置好信號棧后,會開始對 M 設置信號的屏蔽字,通過`sigmask`來獲得當前 M 的屏蔽字,而后通過遍歷所有運行時信號表來對屏蔽字進行初始化:
```
func minitSignalMask() {
nmask := getg().m.sigmask
// 遍歷整個信號表
for i := range sigtable {
// 判斷某個信號是否為不可阻止的信號,
// 如果是不可阻止的信號,則刪除對應的屏蔽字所在位
if !blockableSig(uint32(i)) {
sigdelset(&nmask, i)
}
}
// 重新設置屏蔽字
sigprocmask(_SIG_SETMASK, &nmask, nil)
}
// 判斷某個信號是否為不可阻止的信號
// 1. 當信號是非阻塞信號,則不可阻止
// 2. 當改程序為模塊時,則可阻止
// 3. 當信號為 Kill 或 Throw 時,可阻止,否則不可阻止
func blockableSig(sig uint32) bool {
flags := sigtable[sig].flags
if flags&_SigUnblock != 0 {
return false
}
if isarchive || islibrary {
return true
}
return flags&(_SigKill|_SigThrow) == 0
}
func sigdelset(mask *sigset, i int) {
*mask &^= 1 << (uint32(i) - 1)
}
```
```
type sigTabT struct {
flags int32
name string
}
var sigtable = [...]sigTabT{
/* 0 */ {0, "SIGNONE: no trap"},
/* 1 */ {_SigNotify + _SigKill, "SIGHUP: terminal line hangup"},
...
/* 63 */ {_SigNotify, "signal 63"},
/* 64 */ {_SigNotify, "signal 64"},
}
const (
_SigNotify = 1 << iota // let signal.Notify have signal, even if from kernel
_SigKill // if signal.Notify doesn't take it, exit quietly
_SigThrow // if signal.Notify doesn't take it, exit loudly
_SigPanic // if the signal is from the kernel, panic
_SigDefault // if the signal isn't explicitly requested, don't monitor it
_SigGoExit // cause all runtime procs to exit (only used on Plan 9).
_SigSetStack // add SA_ONSTACK to libc handler
_SigUnblock // always unblock; see blockableSig
_SigIgn // _SIG_DFL action is to ignore the signal
)
```
## 6.6.3 信號處理
萬事俱備,只欠東風。信號處理相關的初始化已經完成,包括了信號的屏蔽字、信號棧等。 正式進入調度循環之前,在 M0 上將調用`mstartm0`,進而調用`initsig`初始化信號,針對每個信號進行單獨處理:
```
//go:yeswritebarrierrec
func mstartm0() {
...
initsig(false)
}
//go:nosplit
//go:nowritebarrierrec
func initsig(preinit bool) {
...
for i := uint32(0); i < _NSIG; i++ {
t := &sigtable[i]
if t.flags == 0 || t.flags&_SigDefault != 0 {
continue
}
// 此時不需要原子操作,因為此時沒有其他運行的 Goroutine
fwdSig[i] = getsig(i)
// 檢查該信號是否需要設置 signal handler
if !sigInstallGoHandler(i) {
// 即使不設置 signal handler,在必要時設置 SA_ONSTACK
if fwdSig[i] != _SIG_DFL && fwdSig[i] != _SIG_IGN {
setsigstack(i)
} else if fwdSig[i] == _SIG_IGN {
sigInitIgnored(i)
}
continue
}
handlingSig[i] = 1
setsig(i, funcPC(sighandler))
}
}
```
對于一個需要設置`sighandler`的信號,會通過`setsig`來設置信號對應的動作(action):
```
//go:nosplit
//go:nowritebarrierrec
func setsig(i uint32, fn uintptr) {
var sa usigactiont
sa.sa_flags = _SA_SIGINFO | _SA_ONSTACK | _SA_RESTART
sa.sa_mask = ^uint32(0)
if fn == funcPC(sighandler) {
if iscgo {
fn = funcPC(cgoSigtramp)
} else {
fn = funcPC(sigtramp)
}
}
*(*uintptr)(unsafe.Pointer(&sa.__sigaction_u)) = fn
sigaction(i, &sa, nil)
}
```
值得注意的是這里有一個特殊處理,當`fn`為`sighandler`時候, 產生信號后的動作并非直接調用`sighandler`,而是被替換為了`sigtramp`:
```
TEXT runtime·sigtramp(SB),NOSPLIT,$72
...
MOVQ DX, ctx-56(SP)
MOVQ SI, info-64(SP)
MOVQ DI, signum-72(SP)
MOVQ $runtime·sigtrampgo(SB), AX
CALL AX
...
RET
```
進而調用`sigtrampgo`。這樣的處理方式是因為,`sighandler`會將產生的信號交給對應的 g ,此時還無法決定究竟誰來進行處理。 因此,當信號發生時,而`sigtrampgo`會被調用:
```
//go:nosplit
//go:nowritebarrierrec
func sigtrampgo(sig uint32, info *siginfo, ctx unsafe.Pointer) {
if sigfwdgo(sig, info, ctx) {
return
}
...
setg(g.m.gsignal)
...
sighandler(sig, info, ctx, g)
setg(g)
...
}
```
而`sigfwdgo`用于約定該信號是否應該由 Go 進行處理, 如果不由 Go 進行處理(例如 cgo)則將其轉發到 Go 代碼之前設置的 handler 上。
我們暫時關注 Go 端的情況,代碼會繼續執行,將 g 設置為`gsignal`,從而來到了`sighandler`:
```
//go:nowritebarrierrec
func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {
_g_ := getg()
c := &sigctxt{info, ctxt}
// profile 時鐘超時
if sig == _SIGPROF {
sigprof(c.sigpc(), c.sigsp(), c.siglr(), gp, _g_.m)
return
}
if sig == _SIGTRAP && testSigtrap != nil && testSigtrap(info, (*sigctxt)(noescape(unsafe.Pointer(c))), gp) {
return
}
// 用戶信號
if sig == _SIGUSR1 && testSigusr1 != nil && testSigusr1(gp) {
return
}
if sig == sigPreempt {
// 可能是一個搶占信號
doSigPreempt(gp, c)
// 即便這是一個搶占信號,它也可能與其他信號進行混合,因此我們
// 繼續進行處理。
}
flags := int32(_SigThrow)
if sig < uint32(len(sigtable)) {
flags = sigtable[sig].flags
}
if flags&_SigPanic != 0 && gp.throwsplit {
// 我們無法安全的 sigpanic 因為它可能造成棧的增長,因此忽略它
flags = (flags &^ _SigPanic) | _SigThrow
}
...
if c.sigcode() != _SI_USER && flags&_SigPanic != 0 {
// 產生 panic 的信號
...
c.preparePanic(sig, gp)
return
}
// 對用戶注冊的信號進行轉發
if c.sigcode() == _SI_USER || flags&_SigNotify != 0 {
if sigsend(sig) {
return
}
}
// 設置為可忽略的用戶信號
if c.sigcode() == _SI_USER && signal_ignored(sig) {
return
}
// 處理 KILL 信號
if flags&_SigKill != 0 {
dieFromSignal(sig)
}
// 非 THROW,返回
if flags&_SigThrow == 0 {
return
}
// 處理一些直接 panic 的情況
...
}
```
注意,在信號處理中,當信號為`sigPreempt`時,將觸發運行時的異步搶占機制,我們會在[6.8 協作與搶占](https://golang.design/under-the-hood/zh-cn/part2runtime/ch06sched/preemption)一節中進行討論。
函數`sigsend`會將用戶信號發送到信號隊列`sig`中:
```
var sig struct {
note note
mask [(_NSIG + 31) / 32]uint32
wanted [(_NSIG + 31) / 32]uint32
ignored [(_NSIG + 31) / 32]uint32
recv [(_NSIG + 31) / 32]uint32
state uint32
delivering uint32
inuse bool
}
func sigsend(s uint32) bool {
bit := uint32(1) << uint(s&31)
if !sig.inuse || s >= uint32(32*len(sig.wanted)) {
return false
}
atomic.Xadd(&sig.delivering, 1)
// We are running in the signal handler; defer is not available.
if w := atomic.Load(&sig.wanted[s/32]); w&bit == 0 {
atomic.Xadd(&sig.delivering, -1)
return false
}
// Add signal to outgoing queue.
for {
mask := sig.mask[s/32]
if mask&bit != 0 {
atomic.Xadd(&sig.delivering, -1)
return true // signal already in queue
}
if atomic.Cas(&sig.mask[s/32], mask, mask|bit) {
break
}
}
// Notify receiver that queue has new bit.
Send:
for {
switch atomic.Load(&sig.state) {
default:
throw("sigsend: inconsistent state")
case sigIdle:
if atomic.Cas(&sig.state, sigIdle, sigSending) {
break Send
}
case sigSending:
// notification already pending
break Send
case sigReceiving:
if atomic.Cas(&sig.state, sigReceiving, sigIdle) {
notewakeup(&sig.note)
break Send
}
}
}
atomic.Xadd(&sig.delivering, -1)
return true
}
```
用戶信號的接收方是通過 os/signal 完成的,我們隨后討論。
## 6.6.4 輔 M 線程
輔 M 是一個用于服務非 Go 線程(cgo 產生的線程)回調的 M。
```
//go:yeswritebarrierrec
func mstartm0() {
// 創建一個額外的 M 服務 non-Go 線程(cgo 調用中產生的線程)的回調,并且只創建一個
// windows 上也需要額外 M 來服務 syscall.NewCallback 產生的回調,見 issue #6751
if (iscgo || GOOS == "windows") && !cgoHasExtraM {
cgoHasExtraM = true
newextram()
}
initsig(false)
}
// newextram 分配一個 m 并將其放入 extra 列表中
// 它會被工作中的本地 m 調用,因此它能夠做一些調用 schedlock 和 allocate 類似的事情。
func newextram() {
c := atomic.Xchg(&extraMWaiters, 0)
if c > 0 {
for i := uint32(0); i < c; i++ {
oneNewExtraM()
}
} else {
// 確保至少有一個額外的 M
mp := lockextra(true)
unlockextra(mp)
if mp == nil {
oneNewExtraM()
}
}
}
// onNewExtraM 分配一個 m 并將其放入 extra list 中
func oneNewExtraM() {
mp := allocm(nil, nil)
gp := malg(4096)
gp.sched.pc = funcPC(goexit) + sys.PCQuantum
gp.sched.sp = gp.stack.hi
gp.sched.sp -= 4 * sys.RegSize
gp.sched.lr = 0
gp.sched.g = guintptr(unsafe.Pointer(gp))
gp.syscallpc = gp.sched.pc
gp.syscallsp = gp.sched.sp
gp.stktopsp = gp.sched.sp
gp.gcscanvalid = true
gp.gcscandone = true
casgstatus(gp, _Gidle, _Gdead)
gp.m = mp
mp.curg = gp
mp.lockedInt++
mp.lockedg.set(gp)
gp.lockedm.set(mp)
gp.goid = int64(atomic.Xadd64(&sched.goidgen, 1))
...
// 給垃圾回收器使用
allgadd(gp)
atomic.Xadd(&sched.ngsys, +1)
// 將 m 添加到 extra m 鏈表中
mnext := lockextra(true)
mp.schedlink.set(mnext)
extraMCount++
unlockextra(mp)
}
```
## 6.6.5 對`os/signal`包的支持
我們已經看到了用戶注冊的信號會通過`sigsend`進行發送,這就是我們使用`os/signal`包的核心。
在使用`os/signal`后,會調用`signal.init`函數,懶惰的注冊一個用戶端的信號處理循環(當調用 Notify 時啟動):
```
var (
watchSignalLoopOnce sync.Once
watchSignalLoop func()
)
func init() {
signal_enable(0) // 首次調用,進行初始化
watchSignalLoop = loop
}
func loop() {
for { process(syscall.Signal(signal_recv())) }
}
```
這個`signal_enable`和`signal_recv`用于激活運行時的信號隊列,并從中接受信號:
```
// 啟用運行時信號隊列
//go:linkname signal_enable os/signal.signal_enable
func signal_enable(s uint32) {
if !sig.inuse {
// The first call to signal_enable is for us
// to use for initialization. It does not pass
// signal information in m.
sig.inuse = true // enable reception of signals; cannot disable
noteclear(&sig.note)
return
}
if s >= uint32(len(sig.wanted)*32) {
return
}
w := sig.wanted[s/32]
w |= 1 << (s & 31)
atomic.Store(&sig.wanted[s/32], w)
i := sig.ignored[s/32]
i &^= 1 << (s & 31)
atomic.Store(&sig.ignored[s/32], i)
sigenable(s)
}
// 從信號隊列中接受信號
//go:linkname signal_recv os/signal.signal_recv
func signal_recv() uint32 {
for {
// Serve any signals from local copy.
for i := uint32(0); i < _NSIG; i++ {
if sig.recv[i/32]&(1<<(i&31)) != 0 {
sig.recv[i/32] &^= 1 << (i & 31)
return i
}
}
// Wait for updates to be available from signal sender.
Receive:
for {
switch atomic.Load(&sig.state) {
default:
throw("signal_recv: inconsistent state")
case sigIdle:
if atomic.Cas(&sig.state, sigIdle, sigReceiving) {
notetsleepg(&sig.note, -1)
noteclear(&sig.note)
break Receive
}
case sigSending:
if atomic.Cas(&sig.state, sigSending, sigIdle) {
break Receive
}
}
}
// Incorporate updates from sender into local copy.
for i := range sig.mask {
sig.recv[i] = atomic.Xchg(&sig.mask[i], 0)
}
}
}
```
當接受到信號后,信號`sig`會被發送到用戶在`Ignore/Notify/Stop`上所注冊的 channel 上:
```
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) {
// 發送
select {
case c <- sig:
default:
}
}
}
... // Stop 的處理
}
```
例如`signal.Notify`,將信號 channel 注冊到 handler 全局變量中:
```
var handlers struct {
sync.Mutex
m map[chan<- os.Signal]*handler
ref [numSig]int64
stopping []stopping
}
func Notify(c chan<- os.Signal, sig ...os.Signal) {
if c == nil {
panic("os/signal: Notify using nil channel")
}
watchSignalLoopOnce.Do(func() {
if watchSignalLoop != nil {
go watchSignalLoop()
}
})
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 // 保存到 handler 中
}
add := func(n int) {
if n < 0 {
return
}
if !h.want(n) {
h.set(n)
if handlers.ref[n] == 0 {
enableSignal(n)
}
handlers.ref[n]++
}
}
if len(sig) == 0 {
for n := 0; n < numSig; n++ {
add(n)
}
} else {
for _, s := range sig {
add(signum(s))
}
}
}
```
## 6.6.6 小結
由于調度器在 Go 程序運行時的特殊地位,以及在進行跨語言調用時需要`cgo`的支持, 運行時信號處理相對而言還是較為復雜的,需要一套完整的機制來對各種情況進行處理, 甚至對用戶態代碼的`os/signal`進行支持。當然,信號處理的功能遠不止如此, 利用此信號機制還可以實現搶占式調度,我們將在[6.7 協作與搶占](https://golang.design/under-the-hood/zh-cn/part2runtime/ch06sched/preemption)中 再來討論這一機制的另一巨大作用。
- 第一部分 :基礎篇
- 第1章 Go語言的前世今生
- 1.2 Go語言綜述
- 1.3 順序進程通訊
- 1.4 Plan9匯編語言
- 第2章 程序生命周期
- 2.1 從go命令談起
- 2.2 Go程序編譯流程
- 2.3 Go 程序啟動引導
- 2.4 主Goroutine的生與死
- 第3 章 語言核心
- 3.1 數組.切片與字符串
- 3.2 散列表
- 3.3 函數調用
- 3.4 延遲語句
- 3.5 恐慌與恢復內建函數
- 3.6 通信原語
- 3.7 接口
- 3.8 運行時類型系統
- 3.9 類型別名
- 3.10 進一步閱讀的參考文獻
- 第4章 錯誤
- 4.1 問題的演化
- 4.2 錯誤值檢查
- 4.3 錯誤格式與上下文
- 4.4 錯誤語義
- 4.5 錯誤處理的未來
- 4.6 進一步閱讀的參考文獻
- 第5章 同步模式
- 5.1 共享內存式同步模式
- 5.2 互斥鎖
- 5.3 原子操作
- 5.4 條件變量
- 5.5 同步組
- 5.6 緩存池
- 5.7 并發安全散列表
- 5.8 上下文
- 5.9 內存一致模型
- 5.10 進一步閱讀的文獻參考
- 第二部分 運行時篇
- 第6章 并發調度
- 6.1 隨機調度的基本概念
- 6.2 工作竊取式調度
- 6.3 MPG模型與并發調度單
- 6.4 調度循環
- 6.5 線程管理
- 6.6 信號處理機制
- 6.7 執行棧管理
- 6.8 協作與搶占
- 6.9 系統監控
- 6.10 網絡輪詢器
- 6.11 計時器
- 6.12 非均勻訪存下的調度模型
- 6.13 進一步閱讀的參考文獻
- 第7章 內存分配
- 7.1 設計原則
- 7.2 組件
- 7.3 初始化
- 7.4 大對象分配
- 7.5 小對象分配
- 7.6 微對象分配
- 7.7 頁分配器
- 7.8 內存統計
- 第8章 垃圾回收
- 8.1 垃圾回收的基本想法
- 8.2 寫屏幕技術
- 8.3 調步模型與強弱觸發邊界
- 8.4 掃描標記與標記輔助
- 8.5 免清掃式位圖技術
- 8.6 前進保障與終止檢測
- 8.7 安全點分析
- 8.8 分代假設與代際回收
- 8.9 請求假設與實務制導回收
- 8.10 終結器
- 8.11 過去,現在與未來
- 8.12 垃圾回收統一理論
- 8.13 進一步閱讀的參考文獻
- 第三部分 工具鏈篇
- 第9章 代碼分析
- 9.1 死鎖檢測
- 9.2 競爭檢測
- 9.3 性能追蹤
- 9.4 代碼測試
- 9.5 基準測試
- 9.6 運行時統計量
- 9.7 語言服務協議
- 第10章 依賴管理
- 10.1 依賴管理的難點
- 10.2 語義化版本管理
- 10.3 最小版本選擇算法
- 10.4 Vgo 與dep之爭
- 第12章 泛型
- 12.1 泛型設計的演進
- 12.2 基于合約的泛型
- 12.3 類型檢查技術
- 12.4 泛型的未來
- 12.5 進一步閱讀的的參考文獻
- 第13章 編譯技術
- 13.1 詞法與文法
- 13.2 中間表示
- 13.3 優化器
- 13.4 指針檢查器
- 13.5 逃逸分析
- 13.6 自舉
- 13.7 鏈接器
- 13.8 匯編器
- 13.9 調用規約
- 13.10 cgo與系統調用
- 結束語: Go去向何方?