Go并发编程信号

0 阅读9分钟

操作系统信号是IPC中唯一一种异步通信方法.它的本质是用软件来模拟硬件的中断机

制.信号用来通知某个事件发生了.例如.在命令行终端按下某些快捷键.就会挂起或停

止正在运行的程序.另外通过kill命令杀死某个进程的操作也有信号的参与.

信号的来源有键盘输入(比如按下快捷键Ctrl-c) 硬件故障 系统函数调用和软件中的

非法运算.进程响应信号的方式有三种.忽略 捕捉和执行默认操作.

Linux对每一个标准信号都有默认的操作方式.针对不同种类的标准信号.其默认的操

作方式一定会是以下操作之一:终止进程 忽略该信号 终止进程并保存内存信息 停止

进程 恢复进程(若进程已停止).

对于大多数标准信号而言.可以自定义程序对它的响应方式.更具体的讲.进程要告知操

作系统内核:当某种信号到来时.需要执行某种操作.在程序中.这些自定义信号响应方

式往往由函数表示.

Go命令会对其中的一些以键盘输入为来源的标准信号做出响应.这是通过标准库代码

包os/signal中的一些API实现的.具体的讲.Go命令指定了需要被处理的信号并用一

种很优雅的方式(用到了通道类型)来监听信号到来.

os.Signal接口:

源码位置:src/os/exec.go

// A Signal represents an operating system signal.
// The usual underlying implementation is operating system-dependent:
// on Unix it is syscall.Signal.
type Signal interface {
    String() string
    Signal() // to distinguish from other Stringers
}

从os.Signal接口的声明可知.其中的Signal方法的声明并没有实际的意义.它只是作

为os.Signal接口类型的一个标识.因此.在Go标准库中.所有实现它的类型的Signal

方法都是空方法(方法体中没有任何语句).所有实现此接口类型的值都可以表示一个

操作系统信号.

在Go标准库中.已经包含了与不同操作系统的信号相对应的程序实体.具体来说.标准

库代码包syscall中有与不同操作系统所支持的每一个标准信号对应的同名常量(以下

简称信号常量).这些信号常量的类型都是syscall.Signal的.syscall.Signal是

os.Signal接口的一个实现类型.同时也是一个int类型的别名类型.也就是说.每一个信

号常量隐含着一个整数值.并且都与它所表示的信号所属操作系统编号一致.

如果查看syscall.Signal类型的String方法的源代码.会发现一个包级私有的 名为

signal的变量.在这个数组类型的变量中.每个索引值都代表一个标准信号的编号.而对

应的元素则是针对该信号的一个简短描述.这些描述会分别出现在那些信号常量的字

符串表示形式中.

代码包中os/signal中的Notify函数用来当操作系统向当前进程发送指定信号时,发

出通知.

源码位置:src/os/signal.go

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))
       }
    }
}

第一个参数类型是通道类型.该参数的类型是chan<- os.Signal.这表示参数c是一个

发送通道.在Notify函数中只能向它发送os.Signal类型的值(以下简称信号值).而不

能从中接收信号值.这一约束是由关键字chan右边的接收操作符<-体现

的.signal.Notify函数会把当前进程接收倒得指定信号放入参数c代表的通道类型值

(以下简称signal接收通道)中.这样该函数的调用方就可以从signal接收通道中按顺

序获取操作系统发来的信号并进行相应的处理了.

第二个参数是一个可变长的参数.这意味着在调用signal.Notify函数时.可以在第一

个参数值之后在附加任意个os.Signal类型的参数值.参数sig代表的参数值包含希望

自行处理的所有信号.接收到需要自行处理的信号后.os/signal包中的程序(以下简称

signal处理程序)会把它封装成syscall.Signal类型的值并放入到signal接收通道中.

也可以只为第一个参数绑定实际值.这种情况下.signal处理程序会把我们意图理解为

想要自行处理所有信号.并把接收到的几乎所有的信号都逐一进行封装并放入到sig

nal接收管道中.

示例:

func main() {
    sigRecv := make(chan os.Signal, 1)
    sigs := []os.Signal{syscall.SIGINT, syscall.SIGTERM}
    signal.Notify(sigRecv, sigs...)
    for sig := range sigRecv {
       fmt.Println("sig:", sig)
    }
}

在调用Notify函数后.立即试图用for循环从signal接收通道中接收信号值.只要si

gRecv中存在元素值.for语句就会把它们按顺序接收并赋值给迭代变量sig.否则.for

语句就会被阻塞.并等待新的元素值发送给sigRecv中,sigRecv通道关闭后.for语句

会立即退出.

注意:signal处理程序在向signal接收通道发送值时.并不会因为通道已满而产生阻塞.

因此signal.Notify函数的调用方应该确保signal接收通道会有足够的空间缓存并传

递到来的信号.一个更好的方式.只创建一个长度为1的signal接收通道.并且时刻准备

从该通道接收信号.

这个示例中的信号处理代码非常简单.即只是把从signal接收通道的信号的简短描述

打印出来.实际场景中.这样做比较危险.因为忽略了当前进程本该处理的信号.如果当

前进程接收到了未自定义处理方法的信号.就会执行由操作系统指定的默认操作.

以SIGINT信号为例子讨论一下.SIGINT信号即中断信号.一般用来停止一个已经失去

控制的程序.如果在一个程序的运行过程中按下快捷键Ctrl+c.那么此程序就会停止运

行.然而.如果这个程序中含有上面那段代码的话.无论我们按下多少次Ctrl-c.都不能

让它停止下来.而仅仅会使标准输出上多几行信息.修改为如下:

func main() {
    sigRecv := make(chan os.Signal, 1)
    signal.Notify(sigRecv)
    for sig := range sigRecv {
       fmt.Println("sig:", sig)
    }
}

如果程序中包含了这段代码.那么发给该进程的所有信号几乎都会被忽略掉.

不过还好在Unix操作系统下SIGKILL和SIGSTOP信号不能自行处理也不能忽略.

对于其他信号.除了能够自行处理它们之外.还可以在之后的任意时刻恢复对它们的系

统默认操作.这就需要用到os/signal包中的Stop函数.

// 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.
func Stop(c chan<- os.Signal) {
    handlers.Lock()

    h := handlers.m[c]
    if h == nil {
       handlers.Unlock()
       return
    }
    delete(handlers.m, c)

    for n := 0; n < numSig; n++ {
       if h.want(n) {
          handlers.ref[n]--
          if handlers.ref[n] == 0 {
             disableSignal(n)
          }
       }
    }

只有一个参数声明.并且与signal.Notify函数的第一个参数声明完全一致.这并不是

巧合.而是有意而为之.

函数signal.Stop会取消掉在之前调用Notify函数时告知signal处理程序需要自行

处理若干信号的行为.只有当初传递给signal.Notify函数的那个signal接收通道作

为调用signal.Stop函数时的参数值.才能如愿的取消掉之前的行为.否则调用不会有

任何作用.调用完Stop函数以后.作为参数的signal接收通道将不会在接收任何信号.

这里存在一个副作用.即在之前示例中那条用于从signal通道接收信号的for语句将会

一直阻塞.为了消除这种副作用.可以在调用Stop函数之后.使用内建函数close关闭该

signal通道.

很多时候.可能并不想完全取消掉所有自行处理信号的行为.只是想取消一部分信号的

自定义处理.只需要再次调用signal.Notify函数.并重新设定与其参数sig绑定参数即可.

如果signal接收通道不同.会怎样?如果先后调用了两次signal.Notify函数.但是两次

传递给函数的signal接收通道不同.那么signal处理程序会视这两次调用毫不相干.它

会分别看待这两次调用.

完整示例:

第一阶段:

func main() {
    sigRecv1 := make(chan os.Signal, 1)
    sigs1 := []os.Signal{
       syscall.SIGINT,
       syscall.SIGQUIT,
    }
    signal.Notify(sigRecv1,sigs1...)

    sigRecv2 := make(chan os.Signal, 1)
    sigs2 := []os.Signal{
       syscall.SIGINT,
       syscall.SIGQUIT,
    }
    signal.Notify(sigRecv2,sigs2...)
}

先后调用了两次signal.Notify函数.并且两次传递给signal接收通道并不相同.

第二阶段:

func main() {
    sigRecv1 := make(chan os.Signal, 1)
    sigs1 := []os.Signal{
       syscall.SIGINT,
       syscall.SIGQUIT,
    }
    signal.Notify(sigRecv1, sigs1...)

    sigRecv2 := make(chan os.Signal, 1)
    sigs2 := []os.Signal{
       syscall.SIGINT,
       syscall.SIGQUIT,
    }
    signal.Notify(sigRecv2, sigs2...)
    
    var wg sync.WaitGroup
    wg.Add(2)
    go func() {
       defer wg.Done()
       for sig := range sigRecv1 {
          fmt.Println("recv1", sig)
       }
    }()

    go func() {
       defer wg.Done()
       for sig := range sigRecv2 {
          fmt.Println("recv2", sig)
       }
    }()
}

先调用sync.WaitGroup类型值的wg的Add方法.添加一个值为2的差量.在每个阶

段结束的时候调用wg的Done方法.这个方法可以视为差量减一.

第三阶段:

func main() {
    sigRecv1 := make(chan os.Signal, 1)
    sigs1 := []os.Signal{
       syscall.SIGINT,
       syscall.SIGQUIT,
    }
    signal.Notify(sigRecv1, sigs1...)

    sigRecv2 := make(chan os.Signal, 1)
    sigs2 := []os.Signal{
       syscall.SIGINT,
       syscall.SIGQUIT,
    }
    signal.Notify(sigRecv2, sigs2...)

    var wg sync.WaitGroup
    wg.Add(2)
    go func() {
       defer wg.Done()
       for sig := range sigRecv1 {
          fmt.Println("recv1", sig)
       }
    }()

    go func() {
       defer wg.Done()
       for sig := range sigRecv2 {
          fmt.Println("recv2", sig)
       }
    }()
    
    fmt.Println("Waiting for 2 seconds...")
    time.Sleep(2 * time.Second)
    fmt.Printf("stop notifycation..")
    signal.Stop(sigRecv1)
    close(sigRecv1)
}

最后阶段:

func main() {
    sigRecv1 := make(chan os.Signal, 1)
    sigs1 := []os.Signal{
       syscall.SIGINT,
       syscall.SIGQUIT,
    }
    signal.Notify(sigRecv1, sigs1...)

    sigRecv2 := make(chan os.Signal, 1)
    sigs2 := []os.Signal{
       syscall.SIGINT,
       syscall.SIGQUIT,
    }
    signal.Notify(sigRecv2, sigs2...)

    var wg sync.WaitGroup
    wg.Add(2)
    go func() {
       defer wg.Done()
       for sig := range sigRecv1 {
          fmt.Println("recv1", sig)
       }
    }()

    go func() {
       defer wg.Done()
       for sig := range sigRecv2 {
          fmt.Println("recv2", sig)
       }
    }()

    fmt.Println("Waiting for 2 seconds...")
    time.Sleep(2 * time.Second)
    fmt.Printf("stop notifycation..")
    signal.Stop(sigRecv1)
    close(sigRecv1)
    wg.Wait()
}

我念如山. 不可转.



语雀地址www.yuque.com/itbosunmian…?

《Go.》 密码:xbkk 欢迎大家访问.提意见.