如何以跨平台的方式使用Go列出可用的操作系统信号名称?

huangapple go评论71阅读模式
英文:

How can I list available operating system signals by name in a cross-platform way in Go?

问题

假设我正在使用Go语言实现kill程序。我可以从命令行接受数字信号和进程ID,并将它们发送给syscall.Kill函数,没有问题。

然而,我不知道如何实现信号的字符串形式分发,例如kill -INT 12345

真正的用例是一个更大程序的一部分,它提示用户发送kill信号;而不是kill命令的替代品。

问题:

如何在任何支持的平台上将有效的信号名称转换为信号编号,在运行时(或者至少不需要编写针对每个平台的代码在编译时运行)?

我尝试过的方法:

  • 维护一个静态映射表,将信号名称映射到信号编号。这种方法在跨平台上不起作用(例如,在Mac OSX上,kill -l返回的信号列表与现代Linux和旧版Linux上返回的信号列表不同)。要使此解决方案在一般情况下起作用,唯一的方法是为每个操作系统创建映射表,这要求我了解每个操作系统的行为,并随着它们添加新的信号支持而保持更新。
  • 调用GNU kill工具并捕获其信号列表。这种方法不够优雅,有点自相矛盾,而且需要:a)能够找到kill工具,b)具备执行子进程的能力/权限,c)能够预测/解析kill工具的输出。
  • 使用各种Signal类型的String方法。这只返回包含信号编号的字符串,例如os.Signal(4).String() == "signal 4",这并不实用。
  • 调用私有函数runtime.signame,它正好做我想要的事情。使用go://linkname技巧可以实现,但我假设这种做法是不被赞同的,有其原因。

想法/尚未尝试的事情:

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

Let's say I'm implementing the kill program in Go. I can accept numeric signals and PIDs from the commandline and send them to syscall.Kill no problem.

However, I don't know how to implement the "string" form of signal dispatch, e.g. kill -INT 12345.

The real use case is a part of a larger program that prompts the user to send kill signals; not a replacement for kill.

Question:

How can I convert valid signal names to signal numbers on any supported platform, at runtime (or at least without writing per-platform code to be run at compile time)?

What I've tried:

  • Keep a static map of signal names to numbers. This doesn't work in a cross-platform way (for example, different signal lists are returned by kill -l on Mac OSX versus a modern Linux versus an older Linux, for example). The only way to make this solution work in general would be to make maps for every OS, which would require me to know the behavior of every OS, and keep up to date as they add new signal support.
  • Shell out to the GNU kill tool and capture the signal lists from it. This is inelegant and kind of a paradox, and also requires a) being able to find kill, b) having the ability/permission to exec subprocesses, and c) being able to predict/parse the output of kill-the-binary.
  • Use the various Signal types' String method. This just returns strings containing the signal number, e.g. os.Signal(4).String() == "signal 4", which is not useful.
  • Call the private function runtime.signame, which does exactly what I want. go://linkname hacks will work, but I'm assuming that this sort of thing is frowned-upon for a reason.

Ideas/Things I Haven't Tried:

  • Use CGo somehow. I'd rather not venture into CGO territory for a project that is otherwise not low-level/needful of native integration at all. If that's the only option, I will, but have no idea where to start.
  • Use templating and code generation to build lists of signals based on external sources at compile time. This is not preferable for the same reasons as CGo.
  • Reflect and parse the members of syscall that start with SIG somehow. I am told that this is not possible because names are compiled away; is it possible that, for something as fundamental as signal names, there's someplace they're not compiled away?

答案1

得分: 3

d455e41 提交于2019年3月添加了这个功能,作为 sys/unix.SignalNum(),因此至少从Go 1.13开始可用。更多详细信息请参阅GitHub问题#28027

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

> func SignalNum(s string) syscall.Signal
>
> SignalNum返回名称为s的信号的syscall.Signal,如果找不到具有该名称的信号,则返回0。信号名称应以"SIG"开头。

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

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

	// 有关为什么在范围0,255内循环的详细信息,请参阅https://github.com/golang/go/issues/28027#issuecomment-427377759。
	for i := syscall.Signal(0); i < syscall.Signal(255); i++ {
		name := unix.SignalName(i)
		// 信号编号不保证连续。
		if name != "" {
			fmt.Println(name)
		}
	}
英文:

Commit d455e41 added this feature in March 2019 as sys/unix.SignalNum() and is thus available at least since Go 1.13. More details in GitHub issue #28027.

From the documentation of the golang.org/x/sys/unix package:

> func SignalNum(s string) syscall.Signal
>
> SignalNum returns the syscall.Signal for signal named s, or 0 if a signal with such name is not found. The signal name should start with "SIG".

To answer a similar question, "how can I list the names of all available signals (on a given Unix-like platform)", we can use the inverse function sys/unix.SignalName():

    import &quot;golang.org/x/sys/unix&quot;

	// 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 &lt; syscall.Signal(255); i++ {
		name := unix.SignalName(i)
		// Signal numbers are not guaranteed to be contiguous.
		if name != &quot;&quot; {
			fmt.Println(name)
		}
	}

答案2

得分: 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”。

**此行为未记录,可能会更改,并且在某些平台上可能不成立。**如果这能够公开,那将是很好的。

英文:

Update some time after I posted the below answer, Golang's stdlib acquired this functionality. An answer describing how to use that functionality was posted by @marco.m and accepted; the below is not recommended unless the version of Go you are using pre-dates the availability of the right tool for the job.

Since no answers were posted, I'll post the less-than-ideal solution I was able to use by "breaking into" a private signal-enumeration function inside Go's standard library.

The signame internal function can get a signal name by number on Unix and Windows. To call it, you have to use the linkname/assembler workaround. Basically, make a file in your project called empty.s or similar, with no contents, and then a function declaration like so:

//go:linkname signame runtime.signame
func signame(sig uint32) string

Then, you can get a list of all signals known by the operating system by calling signame on an increasing number until it doesn't return a value, like so:

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

After that runs, signalmap will have keys for every signal that can be sent on the current operating system. It will have an empty string where Go doesn't think the OS has a name for the signal (the kill(1) may name some signals that Go won't return names for, I've found, but it's usually the higher-numbered/nonstandard ones), or a string name, e.g. "INT" where a name can be found.

This behavior is undocumented, subject to change, and may not hold true on some platforms. It would be nice if this were made public, though.

huangapple
  • 本文由 发表于 2017年3月5日 00:43:10
  • 转载请务必保留本文链接:https://go.coder-hub.com/42598522.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定