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

[インデックス 19238] ファイルの概要

このコミットは、Go言語のnetパッケージにおけるファイルディスクリプタの複製処理に関するバグ修正です。具体的には、syscall.ForkLockの不適切なアンロック処理を削除することで、コードの正確性と堅牢性を向上させています。

コミット

commit 4c129c083bd74dcd3192582551e168cde02e7914
Author: Robert Obryk <robryk@gmail.com>
Date:   Sat Apr 26 19:59:00 2014 -0700

    net: Remove an unmatched unlock of ForkLock
    
    Remove an RUnlock of syscall.ForkLock with no matching RLock.
    Holding ForkLock in netFD.dup is unnecessary: dupCloseOnExecOld
    locks and unlocks the lock on its own and dupCloseOnExec doesn't
    need the ForkLock to be held.
    
    LGTM=iant
    R=golang-codereviews, bradfitz, iant
    CC=golang-codereviews
    https://golang.org/cl/99800044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/4c129c083bd74dcd3192582551e168cde02e7914

元コミット内容

net: Remove an unmatched unlock of ForkLock

このコミットは、syscall.ForkLockに対する対応しないアンロック(RUnlock)を削除します。netFD.dup内でForkLockを保持する必要はありません。なぜなら、dupCloseOnExecOldはそれ自身でロックとアンロックを行い、dupCloseOnExecForkLockが保持されている必要がないためです。

変更の背景

この変更の背景には、Goランタイムにおけるファイルディスクリプタのハンドリング、特にfork/execシステムコールに関連する同期の問題があります。

Goプログラムが新しいプロセスを起動する際(例えばos/execパッケージを使用する場合)、内部的にはUnixのforkexecシステムコールが使用されます。forkは現在のプロセスのコピーを作成し、execはそのコピーされたプロセスを新しいプログラムに置き換えます。この際、親プロセスで開かれているファイルディスクリプタ(ネットワーク接続など)が子プロセスに引き継がれることがあります。

syscall.ForkLockは、forkシステムコールが実行される際に、Goランタイム内の特定のグローバルな状態(例えば、ミューテックスやファイルディスクリプタのリストなど)が整合性の取れた状態であることを保証するために使用されるRWMutex(読み書きロック)です。forkが実行される前に、GoランタイムはForkLockをロックし、forkが完了した後にアンロックします。これにより、fork中に他のゴルーチンが重要な共有リソースを変更するのを防ぎ、子プロセスが親プロセスの整合性の取れたスナップショットを受け取ることを保証します。

元のコードでは、netFD.dup関数内でsyscall.ForkLock.RUnlock()が呼び出されていました。しかし、このRUnlockに対応するRLock(読み取りロック)が同じ関数内で存在しませんでした。これは、ForkLockが不適切にアンロックされる可能性を示唆しており、結果としてロックの不均衡や、最悪の場合、デッドロックや競合状態を引き起こす可能性がありました。

コミットメッセージによると、netFD.dupが内部的に呼び出すdupCloseOnExecOld関数は、それ自身でForkLockのロックとアンロックを適切に行っています。また、もう一つの関連関数であるdupCloseOnExecは、そもそもForkLockが保持されていることを前提としていませんでした。したがって、netFD.dup内で追加のRUnlockは不要であり、むしろ誤りであったため、この不整合を解消するために削除されました。

前提知識の解説

1. ファイルディスクリプタ (File Descriptor, FD)

Unix系OSにおいて、ファイルやソケット、パイプなどのI/Oリソースは、整数値で表される「ファイルディスクリプタ」を通じてアクセスされます。プログラムがファイルを開いたり、ネットワーク接続を確立したりすると、OSは対応するファイルディスクリプタを返します。

2. dupシステムコール

dup (duplicate file descriptor) は、既存のファイルディスクリプタを複製するシステムコールです。複製されたファイルディスクリプタは、元のファイルディスクリプタと同じファイル記述エントリ(ファイルオフセット、ファイルステータスフラグなど)を参照します。これにより、同じファイルやソケットに対して複数のファイルディスクリプタからアクセスできるようになります。

3. Close-on-exec フラグ (FD_CLOEXEC)

ファイルディスクリプタにはClose-on-exec (FD_CLOEXEC) というフラグを設定できます。このフラグが設定されているファイルディスクリプタは、execシステムコール(新しいプログラムを実行する際に呼び出される)が成功した際に自動的に閉じられます。これは、子プロセスに不要なファイルディスクリプタが引き継がれるのを防ぎ、リソースリークやセキュリティ上の問題を回避するために重要です。特に、ネットワークソケットなどが意図せず子プロセスに引き継がれるのを防ぐためによく使用されます。

4. forkexecシステムコール

  • fork(): 現在実行中のプロセス(親プロセス)のほぼ完全なコピーである新しいプロセス(子プロセス)を作成します。子プロセスは親プロセスのメモリ空間、開いているファイルディスクリプタ、レジスタの状態などを引き継ぎます。
  • exec() (例: execve()): 現在のプロセスイメージを、指定された新しいプログラムイメージで置き換えます。execが成功すると、元のプログラムは実行を停止し、新しいプログラムがそのプロセスのコンテキストで実行を開始します。ファイルディスクリプタは、FD_CLOEXECフラグが設定されていない限り、exec後も開いたままになります。

Go言語では、os/execパッケージがこれらのシステムコールを抽象化し、外部コマンドの実行を容易にしています。

5. sync.RWMutexsyscall.ForkLock

  • sync.RWMutex: Go言語の標準ライブラリsyncパッケージで提供される読み書きミューテックスです。複数の読み取り操作は同時に許可されますが、書き込み操作は排他的に行われます。RLock()で読み取りロックを取得し、RUnlock()で読み取りロックを解放します。Lock()で書き込みロックを取得し、Unlock()で書き込みロックを解放します。
  • syscall.ForkLock: Goランタイム内部で使用されるsync.RWMutexのインスタンスです。forkシステムコールが実行される際に、Goランタイムの内部状態の整合性を保つために使用されます。forkの前後でこのロックが適切に取得・解放されることで、子プロセスが親プロセスの安定した状態を継承することが保証されます。

技術的詳細

このコミットは、src/pkg/net/fd_unix.goファイル内のnetFD.dup()メソッドから、syscall.ForkLock.RUnlock()の呼び出しを削除しています。

netFD.dup()メソッドは、ネットワークファイルディスクリプタ(netFD)を複製し、*os.Fileとして返す役割を担っています。この関数は、内部的にdupCloseOnExecまたはdupCloseOnExecOldというヘルパー関数を呼び出して、実際のファイルディスクリプタの複製とClose-on-execフラグの設定を行っています。

問題となっていたのは、netFD.dup()内でdupCloseOnExecまたはdupCloseOnExecOldがエラーを返した場合の処理パスでした。元のコードでは、エラーが発生した場合にsyscall.ForkLock.RUnlock()が呼び出されていました。しかし、netFD.dup()のコードパスのどこにも、このRUnlock()に対応するsyscall.ForkLock.RLock()の呼び出しが存在しませんでした。

これは、以下のような問題を引き起こす可能性がありました。

  1. ロックの不均衡: RUnlockRLockなしで呼び出されると、ミューテックスの内部状態が不正になり、その後のロック操作が予期せぬ動作をする可能性があります。例えば、ロックが解放されたと誤認され、複数のゴルーチンが同時にクリティカルセクションに入ってしまう競合状態が発生したり、逆にロックが永遠に解放されないデッドロック状態に陥ったりする可能性があります。
  2. ForkLockの役割の誤解: ForkLockforkシステムコールに関連するグローバルな状態を保護するためのものであり、個々のファイルディスクリプタの複製操作(dup)の度にロック・アンロックされるべきものではありません。dupCloseOnExecOldは、その内部でforkに関連する処理を行うため、自身でForkLockを適切に管理しています。また、dupCloseOnExecForkLockを必要としません。

このコミットは、netFD.dup()におけるsyscall.ForkLock.RUnlock()の削除により、この不整合を解消し、ForkLockの利用をその本来の目的に合致させ、Goランタイムの堅牢性を向上させています。

コアとなるコードの変更箇所

変更はsrc/pkg/net/fd_unix.goファイルの一箇所のみです。

--- a/src/pkg/net/fd_unix.go
+++ b/src/pkg/net/fd_unix.go
@@ -482,7 +482,6 @@ func dupCloseOnExecOld(fd int) (newfd int, err error) {
 func (fd *netFD) dup() (f *os.File, err error) {
 	ns, err := dupCloseOnExec(fd.sysfd)
 	if err != nil {
-\t\tsyscall.ForkLock.RUnlock()
 		return nil, &OpError{"dup", fd.net, fd.laddr, err}
 	}
 

コアとなるコードの解説

変更されたのはnetFD.dup()関数内の以下の部分です。

func (fd *netFD) dup() (f *os.File, err error) {
	ns, err := dupCloseOnExec(fd.sysfd)
	if err != nil {
		// 変更前: syscall.ForkLock.RUnlock()
		return nil, &OpError{"dup", fd.net, fd.laddr, err}
	}
	// ... 後続の処理 ...
}

このコードスニペットは、netFD構造体のsysfd(システムファイルディスクリプタ)をdupCloseOnExec関数に渡して複製を試みています。dupCloseOnExecは、ファイルディスクリプタを複製し、Close-on-execフラグを設定するGoランタイム内部のヘルパー関数です。

変更前は、dupCloseOnExecがエラーを返した場合、syscall.ForkLock.RUnlock()が呼び出されていました。しかし、このRUnlock()に対応するRLock()netFD.dup()のどこにも存在しなかったため、これは論理的な誤りでした。

このコミットにより、syscall.ForkLock.RUnlock()の行が削除されました。これにより、netFD.dup()関数はsyscall.ForkLockの状態に不適切に干渉することがなくなり、Goランタイムのロック管理の整合性が保たれるようになりました。

この修正は、dupCloseOnExecOldが既にForkLockを適切に扱っていること、そしてdupCloseOnExecForkLockを必要としないという理解に基づいています。したがって、netFD.dup自身がForkLockを操作する必要は全くありませんでした。

関連リンク

参考にした情報源リンク

  • Goのコミットメッセージと差分
  • Goのソースコード (src/pkg/net/fd_unix.go)
  • Goのsyncおよびsyscallパッケージのドキュメント
  • Unix/Linuxのシステムコールに関する一般的な知識 (man pages)
  • Goのsyscall.ForkLockに関する議論やドキュメント (Web検索を通じて得られた情報)
  • Goのnetパッケージにおけるファイルディスクリプタのハンドリングに関する情報 (Web検索を通じて得られた情報)