予焦啦!正如 Golang 自己维护了记忆体管理机制(竞技场、记忆体抽象层、垃圾回收、...)般,让 ethanol 核心取用 RISC-V 硬体功能的部分可以借壳上市。现在我们在探讨中断的话,就很难不联想到使用者空间里面,能够类比於系统中断这种非同步行为的讯号(POSIX signal)机制了。
所以今天,我们来观察看看 Golang 的讯号处理机制,作为之後的参考用。
为了观察 Golang 的讯号机制,当然可以在 x86 主机上面进行一样的实验,但有监於本系列主打 RISC-V,且系列行进至此还没有一个稳定的 Linux 可以参照,这里就顺便架设一个。
以 Debian 发行版为例,并没有什麽特别理由,只是先前笔者个人经验已经玩过 Fedora,这次体验个不一样的。若要按部就班从头 bootstrap 当然还是得费一番工夫,但是都有现成的便宜可以捡,待後详述。
若是读者有兴趣浏览 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 执行档路径都不在笔者的开发主机上,显然是需要特别调度了。
是的,那些路径的执行档,其实是假设开发者有 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 ./
一开始经过 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 专案现阶段想要实作的功能。我们是为了参考非同步行为的处理,才来研究这个部分。
一般 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
可以观察到所有支援的讯号号码。
参考 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.go
的 sigtrampgo
,里面的这个片段是笔者特别想要参考的部分:
...
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
OLED 绘制数学图形 教学原文参考:OLED 绘制数学图形 这篇文章会使用 micro:bit 连...
基本上大叔宅男不是很想放男团K-pop, XD 本篇会提到的 定义方法 回圈(loop) 定义方法 ...
在进入Pattern的介绍之前,我觉得要先让大家认识一下UML这个东西,尤其是Class Dia...
前情提要 「我有问题!」 艾草:「来,请说。」 「JavaScript 到底是什麽?」 艾草:「就程...
前一篇,我们完成了需求一: 当使用者在关键字搜寻这个 input 输入文字时,要在输入框的正下方显示...