予焦啦!Golang 当中的讯号(signal)机制

予焦啦!正如 Golang 自己维护了记忆体管理机制(竞技场、记忆体抽象层、垃圾回收、...)般,让 ethanol 核心取用 RISC-V 硬体功能的部分可以借壳上市。现在我们在探讨中断的话,就很难不联想到使用者空间里面,能够类比於系统中断这种非同步行为的讯号(POSIX signal)机制了。

所以今天,我们来观察看看 Golang 的讯号处理机制,作为之後的参考用。

本节重点概念

  • Golang
    • 讯号使用范例
  • 杂项
    • Debian/riscv64 环境架设
    • 讯号简介

Debian/riscv64 环境架设

为了观察 Golang 的讯号机制,当然可以在 x86 主机上面进行一样的实验,但有监於本系列主打 RISC-V,且系列行进至此还没有一个稳定的 Linux 可以参照,这里就顺便架设一个。

以 Debian 发行版为例,并没有什麽特别理由,只是先前笔者个人经验已经玩过 Fedora,这次体验个不一样的。若要按部就班从头 bootstrap 当然还是得费一番工夫,但是都有现成的便宜可以捡,待後详述。

下载 DQIB(Debian Quick Image Backer)预编映像

若是读者有兴趣浏览 Debian 对 RISC-V 的支援的话,可以连结到官方页面去。但这里我们直接下载预先编译完成、可用於 QEMU 的映像档於 DQIB 页面的 Images for riscv64-virt。

下载完成後的档案名称是 artifacts.zip,可以在命令列解压缩:

$ unzip artifacts.zip
Archive:  artifacts.zip
   creating: artifacts/
  inflating: artifacts/image.qcow2 
  inflating: artifacts/initrd 
  inflating: artifacts/kernel
  inflating: artifacts/readme.txt    
  inflating: artifacts/ssh_user_ecdsa_key  
  inflating: artifacts/ssh_user_ed25519_key  
  inflating: artifacts/ssh_user_rsa_key

readme.txt 当中介绍了用法:

qemu-system-riscv64 \
    -machine virt \
    -cpu rv64 -m 1G \
    -device virtio-blk-device,drive=hd \
    -drive file=image.qcow2,if=none,id=hd \
    -device virtio-net-device,netdev=net \
    -netdev user,id=net,hostfwd=tcp::2222-:22 \
    -bios /usr/lib/riscv64-linux-gnu/opensbi/generic/fw_jump.elf \
    -kernel /usr/lib/u-boot/qemu-riscv64_smode/uboot.elf \
    -object rng-random,filename=/dev/urandom,id=rng -device virtio-rng-device,rng=rng \
    -nographic \
    -append "root=LABEL=rootfs console=ttyS0"

可是,bios 参数所需的 OpenSBI 韧体档案路径与 kernel 参数许虚的 U-Boot 执行档路径都不在笔者的开发主机上,显然是需要特别调度了。

使用 Docker 的 Debian 环境

是的,那些路径的执行档,其实是假设开发者有 Debian 环境,那麽就可以简单透过 apt 软件包管理员直接下载。幸好有 Docker 这种容器工具,我们可以因此省掉另外准备 Debian 开发机的工夫。

以下假设读者有操作 Docker 的经验。若无,也不难学,甚至铁人赛的过去系列也是很多的,不妨一寻。

$ docker run -it --privileged debian
root@aee2fdf7d438:/# apt update
...
root@aee2fdf7d438:/# apt install u-boot-qemu opensbi
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done            
The following NEW packages will be installed: 
  opensbi u-boot-qemu 
0 upgraded, 2 newly installed, 0 to remove and 0 not upgraded.
...
Setting up u-boot-qemu (2021.01+dfsg-5) ...
Setting up opensbi (0.9-1) ...
root@aee2fdf7d438:/# ls /usr/lib/u-boot/qemu-riscv64_smode/uboot.elf 
/usr/lib/u-boot/qemu-riscv64_smode/uboot.elf
root@aee2fdf7d438:/#

笔者这里下载了 DQIB 推荐的 OpenSBI 是先遵守官方的建议。後来经过实测,无论是 hoddarla 专案里面的 misc/opensbi 版本,或是 QEMU 6.1.0 本身内建的版本(也就是移除掉 -bios 参数),都能够使用这个预编的 Debian。

从容器内部将档案复制出来:

$ docker cp aee:/usr/lib/u-boot/qemu-riscv64_smode/uboot.elf ./

重新执行 readme.txt 当中的指令

一开始经过 OpenSBI 进入到 U-Boot 的倒数计时,会有一个选单:

Hit any key to stop autoboot:  0 

Device 0: QEMU VirtIO Block Device
            Type: Hard Disk
            Capacity: 10240.0 MB = 10.0 GB (20971520 x 512)
... is now current device
Scanning virtio 0:1...
Found /boot/extlinux/extlinux.conf
Retrieving file: /boot/extlinux/extlinux.conf
671 bytes read in 1 ms (655.3 KiB/s)
U-Boot menu
1:	Debian GNU/Linux 11 (bullseye) 5.10.0-8-riscv64
2:	Debian GNU/Linux 11 (bullseye) 5.10.0-8-riscv64 (rescue target)
Enter choice:

选一即可,继续开下去,完全没有问题!帐号密码用 root:root 或是 debian:debian 皆可登入。

Enter choice: 1
1:	Debian GNU/Linux 11 (bullseye) 5.10.0-8-riscv64
Retrieving file: /boot/initrd.img-5.10.0-8-riscv64
57240645 bytes read in 172 ms (317.4 MiB/s)
Retrieving file: /boot/vmlinux-5.10.0-8-riscv64
18105856 bytes read in 15 ms (1.1 GiB/s)
append: root=LABEL=rootfs rw noquiet root=LABEL=rootfs
Moving Image from 0x84000000 to 0x80200000, end=813bd000
## Flattened Device Tree blob at bf748af0
   Booting using the fdt blob at 0xbf748af0
   Using Device Tree in place at 00000000bf748af0, end 00000000bf74ce2d

Starting kernel ...

[    0.000000] Linux version 5.10.0-8-riscv64 ([email protected]) (gcc-10 (Debian 10.2.1-6) 10.2.1 20210110, GNU ld (GNU Binutils for Debian) 2.35.2) #1 SMP Debian 5.10.46-4 (2021-08-03)
[    0.000000] OF: fdt: Ignoring memory range 0x80000000 - 0x80200000
...
Welcome to Debian GNU/Linux 11 (bullseye)!

[    8.610897] systemd[1]: Set hostname to <debian>.
[   10.511555] systemd[1]: Queued start job for default target Graphical Interface.
[   10.518599] random: systemd: uninitialized urandom read (16 bytes read)
[   10.541828] systemd[1]: Created slice system-getty.slice.
[  OK  ] Created slice system-getty.slice.
[   10.545311] random: systemd: uninitialized urandom read (16 bytes read)
[   10.550545] systemd[1]: Created slice system-modprobe.slice.
[  OK  ] Created slice system-modprobe.slice.
[   10.551673] random: systemd: uninitialized urandom read (16 bytes read)
[   10.556772] systemd[1]: Created slice system-serial\x2dgetty.slice.
[  OK  ] Created slice system-serial\x2dgetty.slice.
...
Debian GNU/Linux 11 debian ttyS0

debian login: root
Password: 
Linux debian 5.10.0-8-riscv64 #1 SMP Debian 5.10.46-4 (2021-08-03) riscv64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sat Sep  4 03:25:09 UTC 2021 on ttyS0
root@debian:~#

讯号机制

这个机制是一般 Unix 作业系统的机制,并非 hoddarla 专案现阶段想要实作的功能。我们是为了参考非同步行为的处理,才来研究这个部分。

kill 指令

一般 Linux 系统下,可以使用 kill 指令传递讯号给予其他的行程。比方说,kill -2 <pid> 指令,能够给予 pid 号码的行程一个中断讯号,相当於是在该行程的执行控制台(console)前景按下 Ctrl+C。简单的操作:

root@debian:~# sleep 10 &
[1] 243
root@debian:~# kill -2 243
[1]+  Interrupt               sleep 10
root@debian:~#

使用 kill -h 可以观察到所有支援的讯号号码。

Golang 范例

参考 Go by examples 网站的范例,来试试上述的中断讯号的效果:

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
)

func main() {

    sigs := make(chan os.Signal, 1)
    done := make(chan bool, 1)

    signal.Notify(sigs, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)

第一部分我们看到两个频道(channel)的初始化,分别是属於 os 组件的讯号型别,以及布林型别。这些都会再稍後的部分使用到。接下来是 os/signal 组件的 Notify 函数。

这个函数使用不定长度参数,除了第一个参数必须指定一个讯号频道之外,後面可以指定多个讯号。当这个行程真的收到讯号之时,Golang 的各种机制(後续小节简述)会把该讯号传递出来,到给定的频道去。且看这个程序的後半逻辑:

    go func() {
        sig := <-sigs
        fmt.Println()
        fmt.Println(sig)
        done<- true
    }()

    fmt.Println("awaiting signal")
    <-done
    fmt.Println("exiting")
}

令开一个并发(concurrent)的共常式,用以接收讯号;印出讯号之後,透过另外一个传递布林值的频道,知会主函数当中的 <-done 之一行。进而结束整个范例。

这支程序跑起来像是这样:

$ ./test 
awaiting signal
^C   # 使用者按下 Ctrl + C
interrupt
exiting

讯号之初始化

如果我们省略一切使用者空间的设置,只看讯号是怎麽透过作业系统服务的话,Linux 里面最重要的呼叫是 rt_sigaction。使用 strace 工具观察这支程序可以看到中间有一段连续的 rt_sigaction

rt_sigaction(SIGINT, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0                 
rt_sigaction(SIGINT, {sa_handler=0x7aa30, sa_mask=[], sa_flags=SA_ONSTACK|SA_RESTART|SA_SIGINFO}
, NULL, 8) = 0                                                                                  
rt_sigaction(SIGQUIT, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGQUIT, {sa_handler=0x7aa30, sa_mask=[], sa_flags=SA_ONSTACK|SA_RESTART|SA_SIGINFO
}, NULL, 8) = 0
rt_sigaction(SIGILL, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGILL, {sa_handler=0x7aa30, sa_mask=[], sa_flags=SA_ONSTACK|SA_RESTART|SA_SIGINFO}
, NULL, 8) = 0
...

sa_handler 就是告诉作业系统,如果这个行程遇到这个特定的讯号,请对应到这个处理程序(handler)。网路上可以找到很多 C 语言的范例,开发者如果想要的话,也能够依照每一个想要额外处理的讯号指定不同的处理程序。

但 Golang 这里,我们从上小节的范例中可以看到,Golang 的 Notify 将这些细部的操作隐藏起来,让非同步的讯号可以透过 Golang 的频道机制取得。而从 strace 的印出讯息我们发现,几乎所有的讯号都共用这个 0x7aa30 的函数。

讯号之传递

000000000007aa30 <runtime.sigtramp>:
   7aa30:       fa113c23                sd      ra,-72(sp)
   7aa34:       fb810113                addi    sp,sp,-72
   7aa38:       00a12423                sw      a0,8(sp)
   7aa3c:       00b13823                sd      a1,16(sp)
   7aa40:       00c13c23                sd      a2,24(sp)
   ...
      7aa5c:       35850513                addi    a0,a0,856 # 5cdb0 <runtime.sigtrampgo>
   7aa60:       000500e7                jalr    a0
   7aa64:       00013083                ld      ra,0(sp)
   7aa68:       04810113                addi    sp,sp,72
   7aa6c:       00008067                ret

sigtramp 位在 src/runtime/sys_linux_riscv64.s 里面,并且我们先前准备的 opensbi/riscv64 组合其实并没有包含到这个呼叫,因为我们目前与传统的讯号机制完全没有关系。

这个函数之後呼叫到位在 src/runtime/signal_unix.gosigtrampgo,里面的这个片段是笔者特别想要参考的部分:

...
        setg(g.m.gsignal)

        // If some non-Go code called sigaltstack, adjust.
        var gsignalStack gsignalStack
        setStack := adjustSignalStack(sig, g.m, &gsignalStack)
        if setStack {
                g.m.gsignal.stktopsp = getcallersp()
        }

        if g.stackguard0 == stackFork {
                signalDuringFork(sig)
        }

        c.fixsigcode(sig)
        sighandler(sig, info, ctx, g)
        setg(g)
        ...

在进入更直白的 sighandler 之前,透过 setg 函数调整了当前运作的 Golang 共常式。是的,一般使用者函数,会在这里切换共常式,尤其是所使用的堆叠。

虽然 ethanol 核心现在没有使用任何类似讯号的功能,但 os_opensbi.go 里面的 mpreinit 函数还是有初始化 gsignal 共常式。所以理论上,我们有一个完全没有使用到的堆叠

sighandler 之後,会呼叫到位在 ./src/runtime/sigqueue.go 里面的 sigsend。这个会对应到 signal.Notify 之後的一连串处理,针对每一种讯号都会产生一个共常式,以等待讯号的来临,并将之对应到输出给予当初传入的频道,达成通知 main 函数的效果。

小结

予焦啦!今天也是机制考察的一日,主要观察的是一般 Linux 使用者模式底下的 Golang 程序,也藉此机会把一个比较成熟的环境架起来,日後也方便使用。除此之外,虽然没有在今天的篇幅当中介绍,但像 Golang 自己排程共常式的 gogo 呼叫,也都能看到共常式转换堆叠时的过程。

无论如何,我们终究是要跨过这个上下文的实作障碍的。笔者会打算使用 gsignal 的堆叠来完成。无论如何,明天就来处理上下文的过程吧。各位读者,我们明天再会!


<<:  {DAY 17} Pandas 学习笔记part.3

>>:  JavaScript Day 20. BOM 与 DOM

Day42 ( 电子元件 ) OLED 绘制数学图形

OLED 绘制数学图形 教学原文参考:OLED 绘制数学图形 这篇文章会使用 micro:bit 连...

Ruby基本介绍(四)

基本上大叔宅男不是很想放男团K-pop, XD 本篇会提到的 定义方法 回圈(loop) 定义方法 ...

IT铁人DAY 6-UML基本认识

  在进入Pattern的介绍之前,我觉得要先让大家认识一下UML这个东西,尤其是Class Dia...

入门魔法 - JavaScript 是什麽?

前情提要 「我有问题!」 艾草:「来,请说。」 「JavaScript 到底是什麽?」 艾草:「就程...

Day 28:开始来学资料系结:使用目前所学,来个简单实作吧!(二)

前一篇,我们完成了需求一: 当使用者在关键字搜寻这个 input 输入文字时,要在输入框的正下方显示...