予焦啦!正如 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 (debian-kernel@lists.debian.org) (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
的堆疊來完成。無論如何,明天就來處理上下文的過程吧。各位讀者,我們明天再會!