[インデックス 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
はそれ自身でロックとアンロックを行い、dupCloseOnExec
はForkLock
が保持されている必要がないためです。
変更の背景
この変更の背景には、Goランタイムにおけるファイルディスクリプタのハンドリング、特にfork
/exec
システムコールに関連する同期の問題があります。
Goプログラムが新しいプロセスを起動する際(例えばos/exec
パッケージを使用する場合)、内部的にはUnixのfork
とexec
システムコールが使用されます。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. fork
とexec
システムコール
fork()
: 現在実行中のプロセス(親プロセス)のほぼ完全なコピーである新しいプロセス(子プロセス)を作成します。子プロセスは親プロセスのメモリ空間、開いているファイルディスクリプタ、レジスタの状態などを引き継ぎます。exec()
(例:execve()
): 現在のプロセスイメージを、指定された新しいプログラムイメージで置き換えます。exec
が成功すると、元のプログラムは実行を停止し、新しいプログラムがそのプロセスのコンテキストで実行を開始します。ファイルディスクリプタは、FD_CLOEXEC
フラグが設定されていない限り、exec
後も開いたままになります。
Go言語では、os/exec
パッケージがこれらのシステムコールを抽象化し、外部コマンドの実行を容易にしています。
5. sync.RWMutex
と syscall.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()
の呼び出しが存在しませんでした。
これは、以下のような問題を引き起こす可能性がありました。
- ロックの不均衡:
RUnlock
がRLock
なしで呼び出されると、ミューテックスの内部状態が不正になり、その後のロック操作が予期せぬ動作をする可能性があります。例えば、ロックが解放されたと誤認され、複数のゴルーチンが同時にクリティカルセクションに入ってしまう競合状態が発生したり、逆にロックが永遠に解放されないデッドロック状態に陥ったりする可能性があります。 ForkLock
の役割の誤解:ForkLock
はfork
システムコールに関連するグローバルな状態を保護するためのものであり、個々のファイルディスクリプタの複製操作(dup
)の度にロック・アンロックされるべきものではありません。dupCloseOnExecOld
は、その内部でfork
に関連する処理を行うため、自身でForkLock
を適切に管理しています。また、dupCloseOnExec
はForkLock
を必要としません。
このコミットは、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
を適切に扱っていること、そしてdupCloseOnExec
がForkLock
を必要としないという理解に基づいています。したがって、netFD.dup
自身がForkLock
を操作する必要は全くありませんでした。
関連リンク
- Go言語の
sync
パッケージドキュメント: https://pkg.go.dev/sync - Go言語の
syscall
パッケージドキュメント: https://pkg.go.dev/syscall fork
システムコール (man page): https://man7.org/linux/man-pages/man2/fork.2.htmlexecve
システムコール (man page): https://man7.org/linux/man-pages/man2/execve.2.htmldup
システムコール (man page): https://man7.org/linux/man-pages/man2/dup.2.htmlfcntl
システムコール (man page,FD_CLOEXEC
について): https://man7.org/linux/man-pages/man2/fcntl.2.html
参考にした情報源リンク
- Goのコミットメッセージと差分
- Goのソースコード (
src/pkg/net/fd_unix.go
) - Goの
sync
およびsyscall
パッケージのドキュメント - Unix/Linuxのシステムコールに関する一般的な知識 (man pages)
- Goの
syscall.ForkLock
に関する議論やドキュメント (Web検索を通じて得られた情報) - Goの
net
パッケージにおけるファイルディスクリプタのハンドリングに関する情報 (Web検索を通じて得られた情報)