在Go语言中,如何以跨平台的方式按名称列出可用的操作系统信号?

5
假设我正在使用Go语言实现kill程序。 我可以从命令行接受数字信号和PID,并将它们发送给syscall.Kill,这没问题。
然而,我不知道如何实现"字符串"形式的信号分发,例如kill -INT 12345
真正的用例是一个更大的程序的一部分,该程序提示用户发送kill信号; 而不是kill的替代品。
问题:
如何在任何支持的平台上,在运行时(或至少不编写针对编译时运行的特定平台代码)将有效的信号名称转换为信号编号?
我尝试过的:
  • 保持一个静态的信号名称到数字的映射。这种方法在跨平台上不起作用(例如,在Mac OSX和现代Linux以及旧版Linux上,kill -l返回的信号列表是不同的)。要使此解决方案通用,唯一的方法是为每个操作系统创建映射,这将需要我了解每个操作系统的行为,并随着它们添加新的信号支持而保持更新。
  • 调用GNU kill工具并捕获其信号列表。这种方法不够优雅,有点自相矛盾,还需要a)能够找到kill,b)具有执行子进程的能力/权限,c)能够预测/解析kill-the-binary的输出。
  • 使用各种Signal类型的String方法。这只返回包含信号编号的字符串,例如os.Signal(4).String() == "signal 4",这并不实用。
  • 调用私有函数runtime.signame,它恰好做我想要的事情。使用go://linkname的方法也可以,但我认为这种做法是被反对的。

我尚未尝试的想法/事情:

  • 以某种方式使用CGo。对于一个否则不需要低级本地集成的项目,我宁愿不涉足CGO领域。如果这是唯一的选择,我会考虑,但不知道从哪里开始。
  • 使用模板和代码生成器在编译时基于外部源构建信号列表。出于与CGo相同的原因,这不是首选方法。
  • 反射并解析以SIG开头的syscall成员。据说这是不可能的,因为名称被编译掉了;对于像信号名称这样基本的东西,有没有地方它们没有被编译掉呢?

难道不能调用strsignal或使用sys_siglist数组吗? - Jean-Baptiste Yunès
可能性很大;我试过了,但之前没有使用过Cgo,所以它一直无法将输出值存储在我正在使用的Go结构中。我会继续尝试并在这里发布结果;你知道有没有相关示例代码吗? - Zac B
在 C 语言中:for (int i=0; i<NSIG; i++) printf("%d %s (short %s)\n",i,sys_siglist[i],sys_signame[i]);。实际上我没有练习过 Go 语言。 - Jean-Baptiste Yunès
2个回答

3

Commit d455e41 于2019年3月加入了该功能,作为sys/unix.SignalNum() ,因此至少从Go 1.13版本开始可用。更多细节请参见GitHub问题#28027

根据golang.org/x/sys/unix包的文档

func SignalNum(s string) syscall.Signal

SignalNum 返回命名为s的系统调用Signal.Signal,如果找不到具有这种名称的信号,则返回0。信号名称应以“SIG”开头。

为了回答类似的问题,“如何列出给定Unix-like平台上所有可用信号的名称”,我们可以使用反函数sys/unix.SignalName()

    import "golang.org/x/sys/unix"

    // See https://github.com/golang/go/issues/28027#issuecomment-427377759
    // for why looping in range 0,255 is enough.
    for i := syscall.Signal(0); i < syscall.Signal(255); i++ {
        name := unix.SignalName(i)
        // Signal numbers are not guaranteed to be contiguous.
        if name != "" {
            fmt.Println(name)
        }
    }

1
在我发布下面的答案后不久,Golang的stdlib获得了这个功能。@marco.m 发布并被接受的回答描述了如何使用该功能;除非你使用的Go版本早于该工具的可用性,否则不建议使用以下内容。
由于没有发布答案,我将发布一个不太理想的解决方案,可以通过“闯入” Go 标准库内的一个私有信号枚举函数来使用它。 signame 内部函数可以获取 Unix 和 Windows 上的信号名称。要调用它,您必须使用 linkname/汇编器解决方法。基本上,为项目创建一个名为 empty.s 或类似的文件,其中没有内容,然后像这样声明一个函数:
//go:linkname signame runtime.signame
func signame(sig uint32) string

然后,您可以通过调用signame并逐个增加数字,直到它不返回值,从而获取操作系统已知的所有信号列表,如下所示:

signum := uint32(0)
signalmap = make(map[uint32]string)
for len(signame(signum)) > 0 {
    words := strings.Fields(signame(signum))
    if words[0] == "signal"  || ! strings.HasPrefix(words[0], "SIG") {
        signalmap[signum] = ""
    } else {
        // Remove leading SIG and trailing colon.
        signalmap[signum] = strings.TrimRight(words[0][3:], ":")
    }
    signum++
}

运行后,signalmap 将拥有当前操作系统上可以发送的每个信号的键。在 Go 认为操作系统没有信号名称的情况下,它将具有空字符串(kill(1) 可能会命名一些 Go 不会返回名称的信号,但通常是更高编号/非标准信号),或者一个字符串名称,例如“INT”。

这种行为未经记录,可能会发生变化,并且在某些平台上可能不成立。 如果这可以公开,那就太好了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接