[インデックス 11189] ファイルの概要
このコミットは、Go言語のランタイムにおいて、FreeBSDオペレーティングシステム上の32ビット(i386)および64ビット(amd64)アーキテクチャ向けにruntime.usleep
関数を実装するものです。これまではusleep
の実装が「TODO」として残されており、このコミットによってGoプログラムがFreeBSD上でマイクロ秒単位の正確なスリープ(一時停止)を行えるようになります。具体的には、OSが提供するnanosleep
システムコールを呼び出すためのアセンブリコードが追加されています。
コミット
commit c30ba7e65a1d5562ef28b9fae45873329cb71f41
Author: Shenghou Ma <minux.ma@gmail.com>
Date: Tue Jan 17 03:22:34 2012 +1100
runtime: implement runtime.usleep for FreeBSD/386 and amd64.
R=golang-dev, jsing
CC=golang-dev
https://golang.org/cl/5528106
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c30ba7e65a1d5562ef28b9fae45873329cb71f41
元コミット内容
このコミットが適用される前は、src/pkg/runtime/sys_freebsd_386.s
および src/pkg/runtime/sys_freebsd_amd64.s
ファイル内の runtime.usleep
関数は、単にRET
(リターン)命令のみを持つスタブとして存在し、コメントで「// TODO: Implement usleep」と記されていました。これは、FreeBSD上でのマイクロ秒単位のスリープ機能が未実装であることを示していました。
変更の背景
Go言語のランタイムは、ゴルーチンのスケジューリングやシステムコールとの連携など、プログラムの低レベルな実行を管理します。runtime.usleep
のようなスリープ関数は、プログラムが特定の時間だけ実行を一時停止する必要がある場合に不可欠です。例えば、ポーリング処理、リソースの待機、または単に処理の遅延を導入する場合などに使用されます。
FreeBSD環境においてruntime.usleep
が未実装であったため、Goプログラムはマイクロ秒単位での正確なスリープを行うことができませんでした。これは、GoプログラムがFreeBSD上で時間精度を要求する処理を行う際の制約となっていました。このコミットは、この機能の欠落を解消し、GoプログラムがFreeBSD上でも他のOSと同様に、より柔軟な時間制御を行えるようにするために行われました。
前提知識の解説
Goランタイム (Go Runtime)
Goランタイムは、Goプログラムの実行を管理する低レベルなコンポーネントです。これには、ゴルーチンのスケジューリング、メモリ管理(ガベージコレクションを含む)、チャネル操作、システムコールとのインターフェースなどが含まれます。Goプログラムは、OSのネイティブスレッド上で実行されますが、ゴルーチンはランタイムによって管理される軽量なスレッドのようなものです。runtime
パッケージは、これらの低レベルな機能へのインターフェースを提供します。
runtime.usleep
runtime.usleep
は、Goランタイムが提供する内部関数の一つで、指定されたマイクロ秒数(us: microsecond)だけ現在のゴルーチン(または基盤となるOSスレッド)の実行を一時停止させるために使用されます。この関数は通常、OSが提供するより低レベルなスリープ機能(例えば、nanosleep
やusleep
システムコール)をラップして実装されます。
nanosleep
システムコール
nanosleep
は、POSIX標準で定義されているシステムコールであり、現在のスレッドの実行を指定された時間(ナノ秒単位)だけ一時停止させます。このシステムコールは、timespec
という構造体を引数として受け取ります。
struct timespec { time_t tv_sec; // 秒 long tv_nsec; // ナノ秒 (0から999,999,999まで) };
tv_sec
は秒数を、tv_nsec
はナノ秒数を表します。nanosleep
は、マイクロ秒単位のスリープよりも高い精度で時間制御を可能にします。
x86/amd64アセンブリ言語とシステムコール規約
このコミットは、FreeBSDのi386(32ビット)とamd64(64ビット)アーキテクチャ向けのアセンブリコードで実装されています。
- レジスタ:
AX
(Accumulator Register): 演算結果やシステムコール番号を格納。DX
(Data Register): 演算の補助や、AX
と組み合わせて64ビット値を扱う。CX
(Count Register): ループカウンタや、除算の除数を格納。SP
(Stack Pointer): スタックの最上位アドレスを指す。FP
(Frame Pointer): 関数呼び出し時のスタックフレームの基準点を指す。DI
(Destination Index) /SI
(Source Index): 汎用レジスタ。amd64ではシステムコールの引数にも使われる。
- 命令:
MOVL
/MOVQ
: データをレジスタやメモリ間で転送する命令。L
は32ビット、Q
は64ビット。DIVL
: 符号なし除算命令。EDX:EAX
(64ビット値)をオペランドで割り、商をEAX
に、剰余をEDX
に格納する。MULL
: 符号なし乗算命令。EAX
とオペランドを乗算し、結果をEDX:EAX
に格納する。LEAL
: 有効アドレスをレジスタにロードする命令。メモリの内容ではなく、アドレス自体を計算してロードする。INT $0x80
: i386アーキテクチャにおけるソフトウェア割り込み命令。FreeBSDでは、これを用いてシステムコールを呼び出す。システムコール番号はAX
レジスタに、引数はスタックに積む。SYSCALL
: amd64アーキテクチャにおけるシステムコール呼び出し命令。システムコール番号はRAX
レジスタに、引数は特定のレジスタ(RDI
,RSI
,RDX
,R10
,R8
,R9
)に渡す。JAE
(Jump if Above or Equal) /JCC
(Jump if Carry Clear): 条件付きジャンプ命令。システムコールが成功したかどうか(エラーが発生しなかったか)を判断するために使用される。通常、システムコールが成功するとキャリーフラグがクリアされる。CALL
: 関数呼び出し命令。RET
: 関数からのリターン命令。
- スタックフレーム: 関数が呼び出されると、引数、リターンアドレス、ローカル変数などを格納するためのスタックフレームが作成されます。
SP
はスタックの現在のトップを指し、FP
は現在のスタックフレームのベースを指します。引数はFP
からのオフセットで、ローカル変数はSP
からのオフセットでアクセスされることが多いです。
技術的詳細
このコミットの主要な技術的詳細は、runtime.usleep
関数が受け取るマイクロ秒単位の時間を、nanosleep
システムコールが要求する秒とナノ秒に変換し、それぞれのアーキテクチャ(i386とamd64)のシステムコール規約に従ってnanosleep
を呼び出す点にあります。
時間単位の変換
runtime.usleep
は引数としてマイクロ秒(usec
)を受け取りますが、nanosleep
は秒(tv_sec
)とナノ秒(tv_nsec
)で時間を指定するtimespec
構造体を必要とします。この変換は以下の計算で行われます。
- 秒の計算:
tv_sec = usec / 1,000,000
- ナノ秒の計算:
tv_nsec = (usec % 1,000,000) * 1,000
アセンブリコードでは、DIVL
命令を使用して除算と剰余の計算を同時に行い、その後MULL
命令でナノ秒への変換を行っています。
FreeBSD/i386 (sys_freebsd_386.s
) での nanosleep
呼び出し
i386アーキテクチャでは、システムコールはINT $0x80
命令を使用して呼び出されます。システムコール番号はAX
レジスタに格納され、引数はスタックに積まれます。
- スタックフレームの準備:
TEXT runtime·usleep(SB),7,$20
は、この関数が20バイトのスタックフレームを使用することを示します。 - 引数の取得と変換:
MOVL usec+0(FP), AX
:関数引数usec
をAX
レジスタにロードします。MOVL $1000000, CX
:除数(1,000,000)をCX
レジスタにロードします。DIVL CX
:DX:AX
(usec
の値)をCX
で除算します。商(秒)はAX
に、剰余(マイクロ秒)はDX
に格納されます。MOVL AX, 12(SP)
:計算された秒数をスタック上のtv_sec
の位置(SP+12
)に格納します。MOVL $1000, AX
:乗数(1,000)をAX
にロードします。MULL DX
:AX
とDX
(マイクロ秒の剰余)を乗算し、結果(ナノ秒)をDX:AX
に格納します。MOVL AX, 16(SP)
:計算されたナノ秒数をスタック上のtv_nsec
の位置(SP+16
)に格納します。
nanosleep
引数の準備:MOVL $0, 0(SP)
:スタックの先頭(SP+0
)にダミー値を置きます。これはシステムコール呼び出し規約の一部である可能性があります。LEAL 12(SP), AX
:スタック上のtimespec
構造体(tv_sec
が始まる位置)のアドレスをAX
にロードします。これがnanosleep
の第一引数rqtp
(要求時間)となります。MOVL AX, 4(SP)
:rqtp
のアドレスをスタック上のSP+4
に積みます。MOVL $0, 8(SP)
:nanosleep
の第二引数rmtp
(残り時間)は使用しないため、NULL
(0)をスタック上のSP+8
に積みます。
- システムコール呼び出し:
MOVL $240, AX
:nanosleep
システムコールの番号(FreeBSD/i386では240)をAX
レジスタにロードします。INT $0x80
:システムコールを実行します。
- エラーチェック:
JAE 2(PC)
:システムコールが成功した場合(キャリーフラグがクリアされている場合)、次の命令をスキップしてリターンします。CALL runtime·notok(SB)
:システムコールが失敗した場合、runtime·notok
関数を呼び出してエラー処理を行います。
FreeBSD/amd64 (sys_freebsd_amd64.s
) での nanosleep
呼び出し
amd64アーキテクチャでは、システムコールはSYSCALL
命令を使用して呼び出されます。システムコール番号はRAX
レジスタに格納され、引数は特定のレジスタ(RDI
, RSI
, RDX
など)に渡されます。
- スタックフレームの準備:
TEXT runtime·usleep(SB),7,$16
は、この関数が16バイトのスタックフレームを使用することを示します。 - 引数の取得と変換:
MOVL usec+0(FP), AX
:関数引数usec
をAX
レジスタにロードします。MOVL $1000000, CX
:除数(1,000,000)をCX
レジスタにロードします。DIVL CX
:DX:AX
(usec
の値)をCX
で除算します。商(秒)はAX
に、剰余(マイクロ秒)はDX
に格納されます。MOVQ AX, 0(SP)
:計算された秒数をスタック上のtv_sec
の位置(SP+0
)に格納します。MOVL $1000, AX
:乗数(1,000)をAX
にロードします。MULL DX
:AX
とDX
(マイクロ秒の剰余)を乗算し、結果(ナノ秒)をDX:AX
に格納します。MOVQ AX, 8(SP)
:計算されたナノ秒数をスタック上のtv_nsec
の位置(SP+8
)に格納します。
nanosleep
引数の準備:MOVQ SP, DI
:スタックポインタSP
の値をDI
レジスタにロードします。これがnanosleep
の第一引数rqtp
(要求時間)となります。timespec
構造体はスタック上に配置されているため、そのアドレスを渡します。MOVQ $0, SI
:nanosleep
の第二引数rmtp
(残り時間)は使用しないため、NULL
(0)をSI
レジスタにロードします。
- システムコール呼び出し:
MOVL $240, AX
:nanosleep
システムコールの番号(FreeBSD/amd64でも240)をAX
レジスタにロードします。SYSCALL
:システムコールを実行します。
- エラーチェック:
JCC 2(PC)
:システムコールが成功した場合(キャリーフラグがクリアされている場合)、次の命令をスキップしてリターンします。CALL runtime·notok(SB)
:システムコールが失敗した場合、runtime·notok
関数を呼び出してエラー処理を行います。
両アーキテクチャでnanosleep
システムコール番号が240であること、そしてエラーチェックのロジックが共通している点も注目に値します。
コアとなるコードの変更箇所
src/pkg/runtime/sys_freebsd_386.s
--- a/src/pkg/runtime/sys_freebsd_386.s
+++ b/src/pkg/runtime/sys_freebsd_386.s
@@ -199,8 +199,24 @@ TEXT runtime·sigaltstack(SB),7,$0
CALL runtime·notok(SB)
RET
-// TODO: Implement usleep
-TEXT runtime·usleep(SB),7,$0
+TEXT runtime·usleep(SB),7,$20
+\tMOVL $0, DX
+\tMOVL usec+0(FP), AX
+\tMOVL $1000000, CX
+\tDIVL CX
+\tMOVL AX, 12(SP) // tv_sec
+\tMOVL $1000, AX
+\tMULL DX
+\tMOVL AX, 16(SP) // tv_nsec
+
+\tMOVL $0, 0(SP)
+\tLEAL 12(SP), AX
+\tMOVL AX, 4(SP) // arg 1 - rqtp
+\tMOVL $0, 8(SP) // arg 2 - rmtp
+\tMOVL $240, AX // sys_nanosleep
+\tINT $0x80
+\tJAE 2(PC)
+\tCALL runtime·notok(SB)
RET
/*
src/pkg/runtime/sys_freebsd_amd64.s
--- a/src/pkg/runtime/sys_freebsd_amd64.s
+++ b/src/pkg/runtime/sys_freebsd_amd64.s
@@ -184,8 +184,22 @@ TEXT runtime·sigaltstack(SB),7,$-8
CALL runtime·notok(SB)
RET
-// TODO: Implement usleep
-TEXT runtime·usleep(SB),7,$0
+TEXT runtime·usleep(SB),7,$16
+\tMOVL $0, DX
+\tMOVL usec+0(FP), AX
+\tMOVL $1000000, CX
+\tDIVL CX
+\tMOVQ AX, 0(SP) // tv_sec
+\tMOVL $1000, AX
+\tMULL DX
+\tMOVQ AX, 8(SP) // tv_nsec
+
+\tMOVQ SP, DI // arg 1 - rqtp
+\tMOVQ $0, SI // arg 2 - rmtp
+\tMOVL $240, AX // sys_nanosleep
+\tSYSCALL
+\tJCC 2(PC)
+\tCALL runtime·notok(SB)
RET
// set tls base to DI
コアとなるコードの解説
src/pkg/runtime/sys_freebsd_386.s
の解説
TEXT runtime·usleep(SB),7,$20
:runtime.usleep
関数の定義。$20
は、この関数がスタック上に20バイトのローカル変数領域を確保することを示します。MOVL $0, DX
:DX
レジスタをゼロクリアします。これはDIVL
命令の準備のためです。MOVL usec+0(FP), AX
: 関数に渡された引数usec
(マイクロ秒)をAX
レジスタにロードします。usec+0(FP)
は、フレームポインタFP
からのオフセットで引数にアクセスしています。MOVL $1000000, CX
: 除数である1,000,000(1秒あたりのマイクロ秒数)をCX
レジスタにロードします。DIVL CX
:DX:AX
(usec
の値)をCX
で除算します。商(秒数)はAX
に、剰余(マイクロ秒数)はDX
に格納されます。MOVL AX, 12(SP)
: 計算された秒数(AX
)をスタック上のSP+12
の位置に格納します。これはtimespec
構造体のtv_sec
フィールドに相当します。MOVL $1000, AX
: 乗数である1,000(1マイクロ秒あたりのナノ秒数)をAX
レジスタにロードします。MULL DX
:AX
とDX
(マイクロ秒の剰余)を乗算します。結果(ナノ秒数)はDX:AX
に格納されます。MOVL AX, 16(SP)
: 計算されたナノ秒数(AX
)をスタック上のSP+16
の位置に格納します。これはtimespec
構造体のtv_nsec
フィールドに相当します。MOVL $0, 0(SP)
: スタックの先頭に0をプッシュします。これはシステムコール呼び出し規約の一部です。LEAL 12(SP), AX
: スタック上のSP+12
(timespec
構造体の開始アドレス)をAX
レジスタにロードします。これはnanosleep
の第一引数rqtp
(要求時間)のアドレスとなります。MOVL AX, 4(SP)
:rqtp
のアドレスをスタック上のSP+4
に積みます。MOVL $0, 8(SP)
:nanosleep
の第二引数rmtp
(残り時間)は使用しないため、NULL
(0)をスタック上のSP+8
に積みます。MOVL $240, AX
:nanosleep
システムコールの番号(240)をAX
レジスタにロードします。INT $0x80
: システムコールを実行します。JAE 2(PC)
: システムコールが成功した場合(キャリーフラグがクリアされている場合)、次のCALL runtime·notok(SB)
命令をスキップして、RET
命令にジャンプします。CALL runtime·notok(SB)
: システムコールが失敗した場合に呼び出されるエラー処理関数です。RET
: 関数からリターンします。
src/pkg/runtime/sys_freebsd_amd64.s
の解説
TEXT runtime·usleep(SB),7,$16
:runtime.usleep
関数の定義。$16
は、この関数がスタック上に16バイトのローカル変数領域を確保することを示します。MOVL $0, DX
:DX
レジスタをゼロクリアします。MOVL usec+0(FP), AX
: 関数に渡された引数usec
をAX
レジスタにロードします。MOVL $1000000, CX
: 除数である1,000,000をCX
レジスタにロードします。DIVL CX
:DX:AX
(usec
の値)をCX
で除算します。商(秒数)はAX
に、剰余(マイクロ秒数)はDX
に格納されます。MOVQ AX, 0(SP)
: 計算された秒数(AX
)をスタック上のSP+0
の位置に格納します。これはtimespec
構造体のtv_sec
フィールドに相当します。MOVQ
は64ビット値を転送します。MOVL $1000, AX
: 乗数である1,000をAX
レジスタにロードします。MULL DX
:AX
とDX
(マイクロ秒の剰余)を乗算します。結果(ナノ秒数)はDX:AX
に格納されます。MOVQ AX, 8(SP)
: 計算されたナノ秒数(AX
)をスタック上のSP+8
の位置に格納します。これはtimespec
構造体のtv_nsec
フィールドに相当します。MOVQ SP, DI
: スタックポインタSP
の値をDI
レジスタにロードします。amd64のシステムコール規約では、第一引数はDI
レジスタに渡されます。これはnanosleep
の第一引数rqtp
(要求時間)のアドレスとなります。MOVQ $0, SI
:nanosleep
の第二引数rmtp
(残り時間)は使用しないため、NULL
(0)をSI
レジスタにロードします。amd64のシステムコール規約では、第二引数はSI
レジスタに渡されます。MOVL $240, AX
:nanosleep
システムコールの番号(240)をAX
レジスタにロードします。SYSCALL
: システムコールを実行します。JCC 2(PC)
: システムコールが成功した場合(キャリーフラグがクリアされている場合)、次のCALL runtime·notok(SB)
命令をスキップして、RET
命令にジャンプします。CALL runtime·notok(SB)
: システムコールが失敗した場合に呼び出されるエラー処理関数です。RET
: 関数からリターンします。
両アーキテクチャで、runtime.usleep
がnanosleep
システムコールを呼び出すための基本的なロジックは共通していますが、システムコール呼び出しのメカニズム(INT $0x80
vs SYSCALL
)と引数の渡し方(スタック vs レジスタ)がそれぞれのアーキテクチャの規約に従って異なっている点が重要です。
関連リンク
- Go CL 5528106: https://golang.org/cl/5528106
参考にした情報源リンク
- FreeBSD man page for
nanosleep
: https://www.freebsd.org/cgi/man.cgi?query=nanosleep&sektion=2 - Go Assembly Language (Plan 9 Assembler): https://go.dev/doc/asm
- FreeBSD System Call Table (for syscall numbers): https://www.freebsd.org/cgi/man.cgi?query=syscall&sektion=2 (Note: specific syscall numbers can vary by version and architecture, but 240 for nanosleep is common for i386/amd64 on FreeBSD)
- x86 Assembly/GAS Syntax: https://en.wikibooks.org/wiki/X86_Assembly/GAS_Syntax
- System V Application Binary Interface AMD64 Architecture Processor Supplement: https://refspecs.linuxfoundation.org/LSB_5.0.0/LSB-Core-AMD64/LSB-Core-AMD64/x86-64-syscalls.html (While this is Linux ABI, the general principles of syscalls on amd64 are similar across Unix-like systems)