Go Signal信号处理机制

Signal

我们在程序的处理中,可能会有一些场景,如:

  1. 程序被退出时,将内存中的数据输入文件或db,或者其他必要的处理

  2. 针对特定系统信号针对性处理操作,如可以通过发送信号进行数据采集、日志输出、配置加载等操作

这些情况下就需要针对Signal的处理了,Go中关于信号的处理主要集中于os/signal package中。

os/signal中涉及的function主要有:NotifyStopIgnoreResetNotifyContext

在了解function之前,我们先来看看信号是如何存储的,os/signal中信号的是存储在handlers。

更多内容分享,欢迎关注公众号:Go开发笔记

handlers

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
}

type stopping struct {
    c chan<- os.Signal
    h *handler
}

type handler struct {
    mask [(numSig + 31) / 32]uint32 // mask是一个长度为3的uint32数组,意味着1个handler可以包含96个信号
}

func (h *handler) want(sig int) bool {
    return (h.mask[sig/32]>>uint(sig&31))&1 != 0 // 和存储相反,右移获取指定位是否为1
}

func (h *handler) set(sig int) {
    h.mask[sig/32] |= 1 << uint(sig&31) // 存储时,通过sig/32获取大致存储位置,同时左移sig&31作为具体存储位置。
}

func (h *handler) clear(sig int) { // 通过异或清零指定位
    h.mask[sig/32] &^= 1 << uint(sig&31)
}

handlers包含了锁、具体存储channel及对应信号的map、信号接收map(用于确认信号是否需要开启/关闭)、待stop的channel

  • Mutex锁用于handlers内的数据竞争管理。
  • m中保存了信号需要发送的handler
  • ref记录了每个信号的接收量
  • stopping: 当信号被stop时映射channel到信号,不采用map是因为入口保留的只是很简洁的数据。需要1个独立的存储是因为需要m在任何时刻来对应ref,而且也需要保持对要被stop的channel的*handler值的追踪。

handler

关于handler的设计,当前所有系统的信号总数为65个,需要记录每个信号需要的状况,直接使用二进制可以极大的减少内存占用空间,因此可以选用2个64位或3个32的数用于存储信号需要情况,多余的空间还可以用于以后的扩展。3个32位数字相对于2个64位占用空间更小,因此采用3个32位数字用于存储信号需要状况。

信号的存储

(1)根据sig/32确定uint32在数组中的位置

(2)通过sig&31可以获取在uint32中的位置,左移相应位并设置对应位为1(或操作)

信号持有状态的获取

(1)根据sig/32确定uint32在数组中的位置

(2)通过sig&31可以获取在uint32中的位置,右移相应位,判断对应位是否为1(与操作)

信号持有状态的清空

(1)根据sig/32确定uint32在数组中的位置

(2)通过sig&31可以获取在uint32中的位置,右移相应位,通过异或清零(异或操作)

handler是1个[3]uint32数组,uint32的每位都可以可以存储对应的信号,意味着一个channel至多可以存储96个信号,当前所有系统的信号总数为65个,因此handler足以存储所有的信号。
因为信号要存储在数组中,因此通过sig/32确定uint32的位置,然后再根据设置/获取对应位的数据完成数据的存取。

Notify

Notify用于将信号注册至对应channel,系统收到对应信号时会发送至此channel。

注意:channel一定要有足够的缓存接收信号,否则会因阻塞而导致信号发送失败。

// 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.
// Notify使包信号将输入信号转发至channel c.
// 如果未指定信号,则所有输入信号都会被转发至c;否则仅转发指定的信号。
// 包信号发送至c时不会被阻塞:调用者必须确认c拥有足够的缓存空间来跟上预期的信号速率。
// 对于用于一个信号通知的channel,缓存大小为1是足够的。
// 对于同一channel允许多次调用Notify:
// 每次调用都会扩展发送到channel的信号集
// 调用Stop将信号移除信号集的唯一方式。
// 允许多次调用Notify,即使是不同的channel和同样的信号
// 每个channel独立接收输入信号的副本
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] // 获取当前channel是否已有handler
    if h == nil {// 没有
        if handlers.m == nil { // 初始化m
            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 { // 如果信号的接收为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))
        }
    }
}

Notify过程:

  1. 获取锁

  2. 检查当前channel是否已存入m,若没有,则创建新的handler存入。

  3. 若未指定信号,则依次将所有信号添加到handler中;否则将指定的信号依次添加到handler中

  4. 具体的添加过程如下:

    检查信号,无效直接退出
    检查信号是否已加入handler,没有的话将添加信号,若是全局第一次添加则启动信号监听循环
    将对应信号的待接收数加1(handlers.ref[n]++)

  5. 释放锁

watchSignalLoop

watchSignalLoop,顾名思义就是监听信号的循环,将获取的信号发送至对应channel。

func init() {
    watchSignalLoop = loop
}

// loop是一个process的无限循环,系统接收的信号会交由process处理
func loop() {
    for {
        process(syscall.Signal(signal_recv()))
    }
}

func process(sig os.Signal) {
    n := signum(sig)
    if n < 0 {
        return
    }

    handlers.Lock()
    defer handlers.Unlock()

    for c, h := range handlers.m {// 遍历所有channel及对于的handler
        if h.want(n) { // 若信号是channel监听,则发送给channel
            // send but do not block for it  // 注意此处非阻塞,因此channel必须有足够的缓存,否则因channel阻塞会导致信号发送失败,即channel无法接收到信号
            select {
            case c <- sig:
            default:
            }
        }
    }

    // Avoid the race mentioned in Stop.
    for _, d := range handlers.stopping { // 避免Stop时的数据竞争,stop时未发送完的信号,stop前发送完毕
        if d.h.want(n) {
            select {
            case d.c <- sig:
            default:
            }
        }
    }
}

watchSignalLoop是一个循环接收信号并执行process function。

process的处理过程如下:

  1. 检查信号,无效则退出
  2. 获取锁
  3. 遍历m,获取所有channel及对于的handler,若信号在对应的handler中,即为对应channel注册的信号,发送信号至channel
  4. 处理stopping中数据,将信号发送至对应channel(此处的处理与Stop中的设计对应)

总体来说,Notify时,

  • 将channel对应的信号存入handler,然后存入m,仅开启唯一一个信号转发的协程。
  • 在信号转发协程中,一直循环获取信号并根据m中的数据进行信号的对应channel的转发。
  • 为避免数据竞争,对于处于stopping状态的信号,会转发完成后完成删除。

需要注意是:转发采用的是非阻塞模式,会因channel未能及时收取而导致转发失败,因此接收的channel必须要有足够的处理能力。

Stop

Stop用于取消channel对信号的监听。

// Stop causes package signal to stop relaying incoming signals to c.
// It undoes the effect of all prior calls to Notify using c.
// When Stop returns, it is guaranteed that c will receive no more signals.
// Stop使包信号停止将输入信号转发至c。
// 将会取消所有使用c调用Notify的效果。
// 当Stop返回时,保证c不会收到任何信号
func Stop(c chan<- os.Signal) {
    handlers.Lock()

    h := handlers.m[c]// 当前channel无信号通知
    if h == nil {
        handlers.Unlock()
        return
    }
    delete(handlers.m, c) // 从m中移除c

    for n := 0; n < numSig; n++ {
        if h.want(n) {// 遍历所有需要通知的信号,将接收数减1
            handlers.ref[n]--
            if handlers.ref[n] == 0 { // 接收数为0时,说明当前信号已没有channel需要通知,则关闭信号
                disableSignal(n)
            }
        }
    }

    // Signals will no longer be delivered to the channel.
    // We want to avoid a race for a signal such as SIGINT:
    // it should be either delivered to the channel,
    // or the program should take the default action (that is, exit).
    // To avoid the possibility that the signal is delivered,
    // and the signal handler invoked, and then Stop deregisters
    // the channel before the process function below has a chance
    // to send it on the channel, put the channel on a list of
    // channels being stopped and wait for signal delivery to
    // quiesce before fully removing it.
    // 信号将不再被发送至channel
    // 我们想避免信号的竞争,如SIGINT:
    // 它应该会被发送至channel,或者程序应该采取默认措施(即退出)。
    // 为了避免信号被传递,信号处理程序被调用,然后在下面的处理函数有机会在channel上发送之前Stop取消注册通道,
    // 将channel放在要被停止的通道channel列表中,等待信号传递停止,然后再完全删除它。
    handlers.stopping = append(handlers.stopping, stopping{c, h}) // 加入stopping

    handlers.Unlock()

    signalWaitUntilIdle() // 等待stoping状态的发送完毕等

    handlers.Lock()

    for i, s := range handlers.stopping {
        if s.c == c {// stop完成移出stopping
            handlers.stopping = append(handlers.stopping[:i], handlers.stopping[i+1:]...)
            break
        }
    }

    handlers.Unlock()
}

Stop过程:

  1. 获取锁
  2. 若m中没当前channel,则退出并释放锁。
  3. m中存在当前channel的handler,先从m中移除channel。
  4. 遍历所有信号,若信号已在handler中注册,将待接收数减1,当接收数为0时,说明当前信号已不被Notify,关闭信号
  5. 存入stopping(此处为避免Stop时处于发送与关的竞争,选择发送后移除)
  6. 释放锁
  7. 等待stopping完成信号的发送
  8. 将当前channel的stopping移除
  9. 释放锁
// signalWaitUntilIdle waits until the signal delivery mechanism is idle.
// This is used to ensure that we do not drop a signal notification due
// to a race between disabling a signal and receiving a signal.
// This assumes that signal delivery has already been disabled for
// the signal(s) in question, and here we are just waiting to make sure
// that all the signals have been delivered to the user channels
// by the os/signal package.
//go:linkname signalWaitUntilIdle os/signal.signalWaitUntilIdle
// 等到信号发送机制空闲。
// 用于确保我们在关和收信号之间竞争时没有丢掉信号通知,
// 这假设已经禁用了相关信号的信号传输,这里我们等待只是确保所有信号都已通过s/signal package传输到用户通道。
func signalWaitUntilIdle() {
    // Although the signals we care about have been removed from
    // sig.wanted, it is possible that another thread has received
    // a signal, has read from sig.wanted, is now updating sig.mask,
    // and has not yet woken up the processor thread. We need to wait
    // until all current signal deliveries have completed.
    for atomic.Load(&sig.delivering) != 0 {
        Gosched() // 让出协程
    }

    // Although WaitUntilIdle seems like the right name for this
    // function, the state we are looking for is sigReceiving, not
    // sigIdle.  The sigIdle state is really more like sigProcessing.
    for atomic.Load(&sig.state) != sigReceiving {
        Gosched()
    }
}

Stop时,会将channel对应的数据从m中删除,为了避免数据竞争,对于即将stopping的数据会存储在[]stopping中,然后等待信号发送完毕,最后才移除stop的信号。

Ignore & Reset

Ignore用于全局忽略信号,忽略的信号,不会再被接收到
Reset用于全局重置信号,重置后,可以接收到原已忽略的此信号

// Ignore causes the provided signals to be ignored. If they are received by
// the program, nothing will happen. Ignore undoes the effect of any prior
// calls to Notify for the provided signals.
// If no signals are provided, all incoming signals will be ignored.
func Ignore(sig ...os.Signal) {
    cancel(sig, ignoreSignal)
}

// Reset undoes the effect of any prior calls to Notify for the provided
// signals.
// If no signals are provided, all signal handlers will be reset.
func Reset(sig ...os.Signal) {
    cancel(sig, disableSignal)
}

// Stop relaying the signals, sigs, to any channels previously registered to
// receive them and either reset the signal handlers to their original values
// (action=disableSignal) or ignore the signals (action=ignoreSignal).
func cancel(sigs []os.Signal, action func(int)) {
    handlers.Lock()
    defer handlers.Unlock()

    remove := func(n int) {
        var zerohandler handler

        for c, h := range handlers.m {
            if h.want(n) { // h中有此信号
                handlers.ref[n]-- // 减接收数
                h.clear(n) // 清除信号位
                if h.mask == zerohandler.mask { // handler为空时,则删除
                    delete(handlers.m, c)
                }
            }
        }

        action(n) // ignore/disable
    }

    if len(sigs) == 0 {// 不指定信号则移除全部信号
        for n := 0; n < numSig; n++ {
            remove(n)
        }
    } else {
        for _, s := range sigs {
            remove(signum(s))
        }
    }
}

cancel:

  1. 获取锁
  2. 如果不指定信号,则移除所有的信号(进行忽略或重置)
  3. 指定信号的,则仅移除指定的信号(进行忽略或重置)

remove过程:

  1. 遍历m,获取channel及对应得handler
  2. 检查handler中是否注册此信号,
  3. 若已注册,则接收数减1,清零信号的注册位,若handler为空,则从m中移除
  4. 执行ignore/reset

NotifyContext

// NotifyContext returns a copy of the parent context that is marked done
// (its Done channel is closed) when one of the listed signals arrives,
// when the returned stop function is called, or when the parent context's
// Done channel is closed, whichever happens first.
//
// The stop function unregisters the signal behavior, which, like signal.Reset,
// may restore the default behavior for a given signal. For example, the default
// behavior of a Go program receiving os.Interrupt is to exit. Calling
// NotifyContext(parent, os.Interrupt) will change the behavior to cancel
// the returned context. Future interrupts received will not trigger the default
// (exit) behavior until the returned stop function is called.
//
// The stop function releases resources associated with it, so code should
// call stop as soon as the operations running in this Context complete and
// signals no longer need to be diverted to the context.
// NotifyContext 在当信号之一到达,或返回的stop方法被调用,或者父context的Done channel已关闭时,
// (以先发生的为准),返回已标记done(其done channel已关闭)的父context的副本,
// stop function取消注册信号的行为,和signal.Reset一样,可以恢复指定信号的默认行为。
// 例如:GO程序接收os.Interrupt的默认行为是退出。调用NotifyContext(parent, os.Interrupt)将行为改变至取消已返回的context,
// 在返回的stop被调用前。未来接收的interrupts不会触发默认(退出)行为。
// stop function会释放相关的资源,因此代码应该在该上下文中运行的操作完成并且信号不再需要传递到该上下文时调用stop。
func NotifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc) {
    ctx, cancel := context.WithCancel(parent)
    c := &signalCtx{
        Context: ctx,
        cancel:  cancel,
        signals: signals,
    }
    c.ch = make(chan os.Signal, 1)
    Notify(c.ch, c.signals...) // 使用context内的channel用以接收信号
    if ctx.Err() == nil {
        go func() {
            select {
            case <-c.ch: // 接收到信号,完成cancel
                c.cancel()
            case <-c.Done(): // 执行stop或者父context done已closed
            }
        }()
    }
    return c, c.stop
}

type signalCtx struct {
    context.Context

    cancel  context.CancelFunc
    signals []os.Signal
    ch      chan os.Signal
}

func (c *signalCtx) stop() {
    c.cancel()
    Stop(c.ch)
}

NotifyContext相对Notify,多了context用以传递上下文,返回的stop可用以释放相关资源,内部调用的仍是Notify。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux中的信号处理是通过信号机制实现的。当一个进程接收到一个信号时,它会根据事先定义好的处理方式来处理这个信号。信号的处理方式包括终止进程、忽略信号、终止进程并生成core文件、停止进程和继续运行进程等不同的动作。 在Linux中,信号的处理是通过设置信号的处理函数来完成的。当一个信号到达时,内核会调用相应的处理函数来处理这个信号。可以通过系统提供的函数来设置自定义的信号处理函数。 信号的发送可以通过多种方式,包括按键产生、终端按键产生、系统调用产生、软件条件产生和硬件异常产生等。不同的事件会触发不同的信号发送。例如,按下Ctrl+C会发送SIGINT信号,而按下Ctrl+Z会发送SIGTSTP信号。 对于进程来说,接收到信号后,不管正在执行什么代码,都会暂停运行,去处理信号。这种处理方式类似于硬件中断,被称为“软中断”。对于用户来说,由于信号的实现方式,信号的延迟时间非常短,几乎不可察觉。 总而言之,Linux中的信号处理是通过信号机制实现的,程序在接收到信号后会根据事先定义好的处理方式来处理这个信号。这种处理方式可以通过设置信号的处理函数来自定义。信号的发送可以通过多种方式,不同的事件会触发不同的信号发送。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Linux信号(signal)](https://blog.csdn.net/weixin_43408582/article/details/115523424)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值