Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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が提供するより低レベルなスリープ機能(例えば、nanosleepusleepシステムコール)をラップして実装されます。

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レジスタに格納され、引数はスタックに積まれます。

  1. スタックフレームの準備: TEXT runtime·usleep(SB),7,$20 は、この関数が20バイトのスタックフレームを使用することを示します。
  2. 引数の取得と変換:
    • MOVL usec+0(FP), AX:関数引数usecAXレジスタにロードします。
    • MOVL $1000000, CX:除数(1,000,000)をCXレジスタにロードします。
    • DIVL CXDX:AXusecの値)をCXで除算します。商(秒)はAXに、剰余(マイクロ秒)はDXに格納されます。
    • MOVL AX, 12(SP):計算された秒数をスタック上のtv_secの位置(SP+12)に格納します。
    • MOVL $1000, AX:乗数(1,000)をAXにロードします。
    • MULL DXAXDX(マイクロ秒の剰余)を乗算し、結果(ナノ秒)をDX:AXに格納します。
    • MOVL AX, 16(SP):計算されたナノ秒数をスタック上のtv_nsecの位置(SP+16)に格納します。
  3. 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に積みます。
  4. システムコール呼び出し:
    • MOVL $240, AXnanosleepシステムコールの番号(FreeBSD/i386では240)をAXレジスタにロードします。
    • INT $0x80:システムコールを実行します。
  5. エラーチェック:
    • JAE 2(PC):システムコールが成功した場合(キャリーフラグがクリアされている場合)、次の命令をスキップしてリターンします。
    • CALL runtime·notok(SB):システムコールが失敗した場合、runtime·notok関数を呼び出してエラー処理を行います。

FreeBSD/amd64 (sys_freebsd_amd64.s) での nanosleep 呼び出し

amd64アーキテクチャでは、システムコールはSYSCALL命令を使用して呼び出されます。システムコール番号はRAXレジスタに格納され、引数は特定のレジスタ(RDI, RSI, RDXなど)に渡されます。

  1. スタックフレームの準備: TEXT runtime·usleep(SB),7,$16 は、この関数が16バイトのスタックフレームを使用することを示します。
  2. 引数の取得と変換:
    • MOVL usec+0(FP), AX:関数引数usecAXレジスタにロードします。
    • MOVL $1000000, CX:除数(1,000,000)をCXレジスタにロードします。
    • DIVL CXDX:AXusecの値)をCXで除算します。商(秒)はAXに、剰余(マイクロ秒)はDXに格納されます。
    • MOVQ AX, 0(SP):計算された秒数をスタック上のtv_secの位置(SP+0)に格納します。
    • MOVL $1000, AX:乗数(1,000)をAXにロードします。
    • MULL DXAXDX(マイクロ秒の剰余)を乗算し、結果(ナノ秒)をDX:AXに格納します。
    • MOVQ AX, 8(SP):計算されたナノ秒数をスタック上のtv_nsecの位置(SP+8)に格納します。
  3. nanosleep引数の準備:
    • MOVQ SP, DI:スタックポインタSPの値をDIレジスタにロードします。これがnanosleepの第一引数rqtp(要求時間)となります。timespec構造体はスタック上に配置されているため、そのアドレスを渡します。
    • MOVQ $0, SInanosleepの第二引数rmtp(残り時間)は使用しないため、NULL(0)をSIレジスタにロードします。
  4. システムコール呼び出し:
    • MOVL $240, AXnanosleepシステムコールの番号(FreeBSD/amd64でも240)をAXレジスタにロードします。
    • SYSCALL:システムコールを実行します。
  5. エラーチェック:
    • 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:AXusecの値)をCXで除算します。商(秒数)はAXに、剰余(マイクロ秒数)はDXに格納されます。
  • MOVL AX, 12(SP): 計算された秒数(AX)をスタック上のSP+12の位置に格納します。これはtimespec構造体のtv_secフィールドに相当します。
  • MOVL $1000, AX: 乗数である1,000(1マイクロ秒あたりのナノ秒数)をAXレジスタにロードします。
  • MULL DX: AXDX(マイクロ秒の剰余)を乗算します。結果(ナノ秒数)はDX:AXに格納されます。
  • MOVL AX, 16(SP): 計算されたナノ秒数(AX)をスタック上のSP+16の位置に格納します。これはtimespec構造体のtv_nsecフィールドに相当します。
  • MOVL $0, 0(SP): スタックの先頭に0をプッシュします。これはシステムコール呼び出し規約の一部です。
  • LEAL 12(SP), AX: スタック上のSP+12timespec構造体の開始アドレス)を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: 関数に渡された引数usecAXレジスタにロードします。
  • MOVL $1000000, CX: 除数である1,000,000をCXレジスタにロードします。
  • DIVL CX: DX:AXusecの値)をCXで除算します。商(秒数)はAXに、剰余(マイクロ秒数)はDXに格納されます。
  • MOVQ AX, 0(SP): 計算された秒数(AX)をスタック上のSP+0の位置に格納します。これはtimespec構造体のtv_secフィールドに相当します。MOVQは64ビット値を転送します。
  • MOVL $1000, AX: 乗数である1,000をAXレジスタにロードします。
  • MULL DX: AXDX(マイクロ秒の剰余)を乗算します。結果(ナノ秒数)は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.usleepnanosleepシステムコールを呼び出すための基本的なロジックは共通していますが、システムコール呼び出しのメカニズム(INT $0x80 vs SYSCALL)と引数の渡し方(スタック vs レジスタ)がそれぞれのアーキテクチャの規約に従って異なっている点が重要です。

関連リンク

参考にした情報源リンク