[インデックス 15163] ファイルの概要
このコミットは、Go言語の標準ライブラリnet
パッケージにおけるAccept
システムコールとsyscall.ForkLock
の相互作用に関する潜在的なデッドロック問題を解決します。具体的には、Accept
がブロッキングモードになった場合にForkLock
を保持し続けることで、fork+exec
操作が不可能になるリスクを排除するための変更です。
コミット
commit 18441e8adeab78b32507fefc84be495873928f8c
Author: Russ Cox <rsc@golang.org>
Date: Thu Feb 7 22:45:12 2013 -0500
net: do not use RLock around Accept
It might be non-blocking, but it also might be blocking.
Cannot take the chance, as Accept might block indefinitely
and make it impossible to acquire ForkLock exclusively
(during fork+exec).
Fixes #4737.
R=golang-dev, dave, iant, mikioh.mikioh
CC=golang-dev
https://golang.org/cl/7309050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/18441e8adeab78b32507fefc84be495873928f8c
元コミット内容
net: do not use RLock around Accept
Accept
呼び出しの周囲でRLock
を使用しないようにする。
Accept
はノンブロッキングであるかもしれないが、ブロッキングである可能性もある。
Accept
が無限にブロックし、ForkLock
を排他的に取得することを不可能にする(fork+exec
中)リスクを冒すことはできない。
Fixes #4737.
変更の背景
このコミットは、Go言語のnet
パッケージがソケットのAccept
操作を行う際に発生しうるデッドロックの問題を解決するために導入されました。具体的には、Goプログラムが新しいプロセスをfork
してexec
する際に使用されるsyscall.ForkLock
と、ネットワーク接続を受け入れるsyscall.Accept
システムコールの間の競合状態が問題でした。
以前の実装では、syscall.Accept
の呼び出しの前後でsyscall.ForkLock
の読み取りロック(RLock
)が取得されていました。これは、Accept
がノンブロッキングモードで動作することを前提としていました。しかし、Goのnet
パッケージは、ファイルディスクリプタ(FD)がFile
メソッドを介してos.File
に変換されると、そのFDがブロッキングモードに戻される可能性があります。もしAccept
がブロッキングモードのFDに対して呼び出され、かつ接続が来るまで無限にブロックした場合、ForkLock
の読み取りロックが解放されなくなります。
syscall.ForkLock
は、fork+exec
操作中に排他的な書き込みロック(Lock
)を必要とします。もしAccept
が読み取りロックを保持したままブロックすると、fork+exec
操作がForkLock
の書き込みロックを取得できなくなり、結果としてプログラム全体がデッドロックに陥る可能性がありました。これは、特にサーバーアプリケーションで、新しいプロセスを起動する(例えば、外部コマンドを実行する)際に問題となります。
この問題はGo issue #4737として報告され、このコミットはその修正として提案されました。
前提知識の解説
このコミットの理解には、以下のGo言語およびUnixシステムプログラミングの概念が不可欠です。
-
syscall.Accept
: Unix系システムにおけるaccept(2)
システムコールのGo言語ラッパーです。これは、ソケット上で保留中の接続を受け入れ、新しいソケットディスクリプタを返します。この新しいディスクリプタは、確立された接続を介した通信に使用されます。accept
はデフォルトでブロッキング操作であり、接続が来るまで呼び出し元をブロックします。 -
ファイルディスクリプタ (File Descriptor, FD): Unix系システムにおいて、ファイルやソケットなどのI/Oリソースを識別するために使用される整数値です。FDは、ブロッキングモードまたはノンブロッキングモードで設定できます。
- ブロッキングモード: I/O操作(読み取り、書き込み、接続受け入れなど)が完了するまで、呼び出し元のスレッドをブロックします。
- ノンブロッキングモード: I/O操作がすぐに完了しない場合、エラー(例:
EAGAIN
やEWOULDBLOCK
)を返してすぐに戻ります。これにより、単一のスレッドで複数のI/O操作を同時に管理できます(通常はepoll
,kqueue
,select
などのI/O多重化メカニズムと組み合わせて使用)。
-
syscall.ForkLock
: Go言語のsyscall
パッケージで提供されるグローバルなsync.RWMutex
(読み書きミューテックス)です。これは、fork(2)
システムコールが安全に実行されることを保証するために使用されます。fork
は、現在のプロセスのメモリ空間を複製して新しいプロセスを作成しますが、この際にミューテックスなどの共有リソースの状態が不整合になる可能性があります。fork
を呼び出す前には、ForkLock
の書き込みロック(Lock()
)が取得されます。これにより、他のゴルーチンがForkLock
の読み取りロックや書き込みロックを取得できなくなり、fork
中に共有リソースが変更されるのを防ぎます。exec
を呼び出す前には、ForkLock
の書き込みロックが解放されます。ForkLock
の読み取りロック(RLock()
)は、fork
中に問題を引き起こす可能性のあるシステムコール(例えば、ファイルディスクリプタを操作するシステムコール)を呼び出す際に取得されることがあります。これは、fork
が進行中にこれらのシステムコールが呼び出されるのを防ぐためです。
-
fork+exec
: Unix系システムで新しいプログラムを実行するための一般的なパターンです。fork()
: 現在のプロセスのコピーを作成します。子プロセスは親プロセスのメモリ空間、ファイルディスクリプタなどを継承します。exec()
: 現在のプロセスを新しいプログラムで置き換えます。これにより、子プロセスは新しいプログラムを実行します。 この操作は、Go言語でos/exec
パッケージを使用して外部コマンドを実行する際などに内部的に使用されます。
-
CloseOnExec
: ファイルディスクリプタに設定できるフラグの一つで、exec
システムコールが実行されたときにそのFDが自動的に閉じられるようにします。これにより、子プロセスに不要なFDが継承されるのを防ぎ、リソースリークやセキュリティリスクを軽減します。
技術的詳細
このコミットの核心は、syscall.Accept
の呼び出しがブロッキングモードになる可能性を考慮し、その周囲からsyscall.ForkLock
の読み取りロック(RLock
/RUnlock
)を削除することです。
Goのnet
パッケージは、通常、ソケットをノンブロッキングモードで設定し、I/O多重化(epoll
など)を使用して効率的に多数の接続を処理します。しかし、netFD
構造体のdup()
メソッドや、ソケットのファイルディスクリプタをos.File
に変換するFile()
メソッドが呼び出されると、そのFDはブロッキングモードに戻される可能性があります。
元のコードでは、syscall.Accept
がノンブロッキングであるという「おそらく」という前提のもとでForkLock.RLock()
が使用されていました。しかし、もし何らかの理由でFDがブロッキングモードに戻り、かつAccept
が接続を待って無限にブロックした場合、ForkLock
の読み取りロックが解放されません。
syscall.ForkLock
は、fork+exec
操作中に排他的な書き込みロック(Lock()
)を必要とします。もしAccept
が読み取りロックを保持したままブロックすると、fork+exec
操作は書き込みロックを取得できず、デッドロックが発生します。これは、Goプログラムが外部コマンドを実行しようとしたときに、ネットワーク接続の受け入れがブロックされているためにハングアップするという形で現れます。
このコミットでは、このリスクを完全に排除するために、syscall.Accept
の呼び出しの前後からForkLock.RLock()
とForkLock.RUnlock()
を削除します。これにより、Accept
がブロッキングモードで動作しても、ForkLock
を保持し続けることはなくなり、fork+exec
操作がブロックされる可能性がなくなります。
この変更の副作用として、fd_unix.go
のdup()
メソッド内のコメントが更新されています。syscall.SetNonblock(ns, false)
(ノンブロッキングを解除し、ブロッキングモードに戻す)が呼び出されると、古いFDもブロッキングモードになることが明記されています。これにより、I/Oがスレッドをブロックするようになりますが、epoll
サーバーを使用しないだけで、機能的には問題なく動作します。これは、Accept
がForkLock
を保持しないことのトレードオフとして許容されます。
コアとなるコードの変更箇所
このコミットでは、以下の3つのファイルが変更されています。
src/pkg/net/fd_unix.go
src/pkg/net/sock_cloexec.go
src/pkg/net/sys_cloexec.go
src/pkg/net/fd_unix.go
--- a/src/pkg/net/fd_unix.go
+++ b/src/pkg/net/fd_unix.go
@@ -661,6 +661,9 @@ func (fd *netFD) dup() (f *os.File, err error) {
syscall.ForkLock.RUnlock()
// We want blocking mode for the new fd, hence the double negative.
+ // This also puts the old fd into blocking mode, meaning that
+ // I/O will block the thread instead of letting us use the epoll server.
+ // Everything will still work, just with more threads.
if err = syscall.SetNonblock(ns, false); err != nil {
return nil, &OpError{"setnonblock", fd.net, fd.laddr, err}
}
src/pkg/net/sock_cloexec.go
--- a/src/pkg/net/sock_cloexec.go
+++ b/src/pkg/net/sock_cloexec.go
@@ -50,14 +50,14 @@ func accept(fd int) (int, syscall.Sockaddr, error) {
}
// See ../syscall/exec_unix.go for description of ForkLock.
- // It is okay to hold the lock across syscall.Accept
+ // It is probably okay to hold the lock across syscall.Accept
// because we have put fd.sysfd into non-blocking mode.
- syscall.ForkLock.RLock()
+ // However, a call to the File method will put it back into
+ // blocking mode. We can't take that risk, so no use of ForkLock here.
nfd, sa, err = syscall.Accept(fd)
if err == nil {
syscall.CloseOnExec(nfd)
}
- syscall.ForkLock.RUnlock()
if err != nil {
return -1, nil, err
}
src/pkg/net/sys_cloexec.go
--- a/src/pkg/net/sys_cloexec.go
+++ b/src/pkg/net/sys_cloexec.go
@@ -35,14 +35,14 @@ func sysSocket(f, t, p int) (int, error) {\n // descriptor as nonblocking and close-on-exec.\n func accept(fd int) (int, syscall.Sockaddr, error) {\n // See ../syscall/exec_unix.go for description of ForkLock.\n- // It is okay to hold the lock across syscall.Accept\n+ // It is probably okay to hold the lock across syscall.Accept\n // because we have put fd.sysfd into non-blocking mode.\n- syscall.ForkLock.RLock()
+ // However, a call to the File method will put it back into\n+ // blocking mode. We can't take that risk, so no use of ForkLock here.\n nfd, sa, err := syscall.Accept(fd)
if err == nil {\n syscall.CloseOnExec(nfd)\n }\n- syscall.ForkLock.RUnlock()
if err != nil {\n return -1, nil, err\n }\
コアとなるコードの解説
src/pkg/net/fd_unix.go
の変更
netFD.dup()
メソッドは、既存のネットワークファイルディスクリプタを複製し、新しいos.File
を返す役割を担っています。このメソッド内で、複製されたFDをブロッキングモードに設定するsyscall.SetNonblock(ns, false)
の呼び出しがあります。
変更点:
- コメントが追加されました。このコメントは、
syscall.SetNonblock(ns, false)
が新しいFDだけでなく、元のFDもブロッキングモードに戻す可能性があることを明確にしています。これにより、I/O操作がepoll
などのノンブロッキングI/O多重化メカニズムではなく、スレッドをブロックするようになることを説明しています。これは、Accept
がForkLock
を保持しないようにするためのトレードオフとして許容される動作です。
src/pkg/net/sock_cloexec.go
および src/pkg/net/sys_cloexec.go
の変更
これら2つのファイルは、それぞれ異なるコンテキスト(sock_cloexec.go
はソケット操作全般、sys_cloexec.go
はシステムコールレベルの操作)でaccept
関数を定義していますが、本質的に同じ変更が加えられています。
変更点:
syscall.ForkLock.RLock()
の呼び出しが削除されました。syscall.ForkLock.RUnlock()
の呼び出しが削除されました。- コメントが更新されました。以前のコメントでは「
fd.sysfd
をノンブロッキングモードに設定しているので、syscall.Accept
の前後でロックを保持しても問題ない」とされていましたが、新しいコメントでは「しかし、File
メソッドの呼び出しにより、ブロッキングモードに戻る可能性がある。そのリスクは冒せないので、ここではForkLock
を使用しない」と変更されています。
これらの変更により、syscall.Accept
がブロッキングモードで動作した場合でも、syscall.ForkLock
を保持し続けることがなくなり、fork+exec
操作がデッドロックする可能性が排除されました。これは、Goのネットワークスタックの堅牢性を高める重要な修正です。
関連リンク
- Go issue #4737: net: Accept can block fork+exec
- Go CL 7309050: net: do not use RLock around Accept
参考にした情報源リンク
- Go issue #4737の議論
- Go言語の
syscall
パッケージのドキュメント - Unix
accept(2)
manページ - Unix
fork(2)
manページ - Unix
exec(3)
manページ - Go言語の
sync.RWMutex
のドキュメント - Go言語の
net
パッケージのソースコード - Go言語の
os/exec
パッケージのソースコード - Go言語の
syscall/exec_unix.go
のソースコード