今天以前同事抛给我一个关于golang 信号捕获的问题。
这部分代码是用来捕获自定义信号:55, 并打印捕获的次数:
package main
import (
"log"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
log.Println(os.Getpid())
signalChans := make(chan os.Signal, 1024)
signal.Notify(signalChans, syscall.Signal(55))
i := 0
for {
select {
case signalnumber := <-signalChans:
i = i + 1
log.Printf("receive signal %d %s %s", i, signalnumber.String(), time.Now().String())
}
}
}
下面是产生自定义信号的代码:
package main
import (
"fmt"
"sync"
"syscall"
"time"
)
func main() {
i := 0
wg := sync.WaitGroup{}
for {
i = i + 1
wg.Add(1)
go func() {
err := syscall.Kill(2041194, 55)
fmt.Printf("%s \n",err)
wg.Done()
}()
if i > 100 {
break
}
}
wg.Wait()
time.Sleep(10 * time.Second)
fmt.Println("Done")
}
最终运行这两段代码会出现问题: 明明通过syscall.Kill发送了100次信号,但是捕获到的数量< 100, 数值不定。但是他说用c++能一致。
我自己也运行了这两段代码,确实出现了这个问题。
接下来就看了go发送和接手信号的相关源码:
src/os/signal/signal_unix.go
//signal_unix.go
func loop() {
for {
process(syscall.Signal(signal_recv()))
}
}
这个for循环用于获取信号,并将信号写入用户chan
runtime/sigqueue.go
文件中定义了 signal_recv() 和 sigsend(s uint32) 两个函数的具体实现。
// sig handles communication between the signal handler and os/signal.
// Other than the inuse and recv fields, the fields are accessed atomically.
//
// The wanted and ignored fields are only written by one goroutine at
// a time; access is controlled by the handlers Mutex in os/signal.
// The fields are only read by that one goroutine and by the signal handler.
// We access them atomically to minimize the race between setting them
// in the goroutine calling os/signal and the signal handler,
// which may be running in a different thread. That race is unavoidable,
// as there is no connection between handling a signal and receiving one,
// but atomic instructions should minimize it.
//此处各个数组的size就是3, _NSIG(常量=65) ,可以算出信号值 <96(因为数组下表从0开始,最大信号为95)
//数组的值是uint32, 32位二进制, 这里用每一位代表一种信号的位置,共可以标识96种信号
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
}
const (
sigIdle = iota
sigReceiving
sigSending
sigFixup
)
// sigsend delivers a signal from sighandler to the internal signal delivery queue.
// It reports whether the signal was sent. If not, the caller typically crashes the program.
// It runs from the signal handler, so it's limited in what it can do.
func sigsend(s uint32) bool {
bit := uint32(1) << uint(s&31)
//golang中信号最大值就是32*3,超过的就忽略
if 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.
//尝试sig.mask中信号的标志位
for {
mask := sig.mask[s/32]
if mask&bit != 0 {
// //判断该信号是否已经标记为1了,已经标记的就不再重复设置
atomic.Xadd(&sig.delivering, -1)
return true // signal already in queue
}
if atomic.Cas(&sig.mask[s/32], mask, mask|bit) {
//设置该信号的标志位为1
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) {
if GOOS == "darwin" || GOOS == "ios" {
sigNoteWakeup(&sig.note)
break Send
}
notewakeup(&sig.note)
break Send
}
case sigFixup:
// nothing to do - we need to wait for sigIdle.
mDoFixupAndOSYield()
}
}
atomic.Xadd(&sig.delivering, -1)
return true
}
// sigRecvPrepareForFixup is used to temporarily wake up the
// signal_recv() running thread while it is blocked waiting for the
// arrival of a signal. If it causes the thread to wake up, the
// sig.state travels through this sequence: sigReceiving -> sigFixup
// -> sigIdle -> sigReceiving and resumes. (This is only called while
// GC is disabled.)
//go:nosplit
func sigRecvPrepareForFixup() {
if atomic.Cas(&sig.state, sigReceiving, sigFixup) {
notewakeup(&sig.note)
}
}
// Called to receive the next queued signal.
// Must only be called from a single goroutine at a time.
//go:linkname signal_recv os/signal.signal_recv
func signal_recv() uint32 {
for {
// Serve any signals from local copy.
//读取所有收到的信号, 找到第一个不为0的标志位,设置为0,并返回该信号值
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) {
if GOOS == "darwin" || GOOS == "ios" {
sigNoteSleep(&sig.note)
break Receive
}
notetsleepg(&sig.note, -1)
noteclear(&sig.note)
if !atomic.Cas(&sig.state, sigFixup, sigIdle) {
break Receive
}
// Getting here, the code will
// loop around again to sleep
// in state sigReceiving. This
// path is taken when
// sigRecvPrepareForFixup()
// has been called by another
// thread.
}
case sigSending:
if atomic.Cas(&sig.state, sigSending, sigIdle) {
break Receive
}
}
}
// Incorporate updates from sender into local copy.
//通过原子操作将标志位sig.maks拷贝到 sig.recv中,并重置mask中标志为0
for i := range sig.mask {
sig.recv[i] = atomic.Xchg(&sig.mask[i], 0)
}
}
}
所以如果我们通过两个协程或两个进程分别去产生信号,捕获信号,其实只是并发读写系统sig.mask中的标志位,而信号获取速度由go的调度决定,如果标记为1,但来消费端未来的及读取,生产端又触发了写,消费端自然就丢失了一次数据。