[インデックス 14860] ファイルの概要
コミット
commit 98259b92115f24b41c5ba40c3cddb68f6f0077f8
Author: Georg Reinke <guelfey@gmail.com>
Date: Fri Jan 11 08:30:25 2013 -0800
os: use syscall.Pipe2 on Linux
Update #2656
R=golang-dev, iant, minux.ma, bradfitz
CC=golang-dev
https://golang.org/cl/7065063
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/98259b92115f24b41c5ba40c3cddb68f6f0077f8
元コミット内容
os: use syscall.Pipe2 on Linux
このコミットは、Go言語のos
パッケージにおいて、Linux環境でのパイプ作成にsyscall.Pipe2
を使用するように変更するものです。これにより、パイプ作成時のファイルディスクリプタのO_CLOEXEC
フラグの設定をアトミックに行うことが可能になります。
変更の背景
この変更の背景には、Go言語のos.Pipe()
関数が内部で利用するシステムコールpipe()
の挙動と、ファイルディスクリプタのO_CLOEXEC
フラグに関する課題がありました。
従来のpipe()
システムコールは、2つのファイルディスクリプタ(読み込み側と書き込み側)を作成しますが、これらのディスクリプタにはデフォルトでO_CLOEXEC
フラグが設定されていません。O_CLOEXEC
フラグは、exec
系のシステムコール(新しいプログラムを実行する際にプロセスを置き換える)が呼び出された際に、そのファイルディスクリプタを自動的にクローズするよう指示するものです。これにより、子プロセスに不要なファイルディスクリプタが継承されるのを防ぎ、リソースリークやセキュリティ上の問題を回避できます。
従来のos.Pipe()
の実装では、pipe()
を呼び出した後に、別途fcntl(fd, F_SETFD, FD_CLOEXEC)
のようなシステムコールを使ってO_CLOEXEC
フラグを設定していました。この2段階の操作には、競合状態(race condition)のリスクが存在します。具体的には、pipe()
が呼び出されてからO_CLOEXEC
フラグが設定されるまでのごく短い期間に、別のスレッドがfork()
とexec()
を呼び出すと、O_CLOEXEC
が設定されていないファイルディスクリプタが子プロセスに継承されてしまう可能性があります。これは、特にマルチスレッド環境で問題となります。
この問題はGoのIssue #2656で報告されており、このコミットはその解決策として、Linuxカーネル2.6.27以降で利用可能なpipe2()
システムコールを導入することで、この競合状態を解消しようとしています。pipe2()
は、パイプを作成する際にO_CLOEXEC
フラグをアトミックに設定できるため、上記のような競合状態を根本的に回避できます。
前提知識の解説
1. パイプ (Pipe)
パイプは、Unix系OSにおけるプロセス間通信 (IPC: Inter-Process Communication) の一種です。一方のプロセスがパイプにデータを書き込み、もう一方のプロセスがパイプからデータを読み込むことで、データのやり取りを行います。Go言語のos.Pipe()
関数は、このパイプを作成し、読み込み側と書き込み側の*os.File
オブジェクトを返します。
2. ファイルディスクリプタ (File Descriptor, FD)
ファイルディスクリプタは、Unix系OSにおいて、開かれたファイルやソケット、パイプなどのI/Oリソースを識別するためにカーネルが割り当てる非負の整数です。プロセスはファイルディスクリプタを通じてこれらのリソースにアクセスします。
3. O_CLOEXEC
フラグ
O_CLOEXEC
(Close-on-exec) は、ファイルディスクリプタに設定できるフラグの一つです。このフラグが設定されたファイルディスクリプタは、exec
系のシステムコール(例: execve
, execl
, execvp
など)が呼び出され、現在のプロセスイメージが新しいプログラムに置き換えられる際に、自動的にクローズされます。
このフラグの主な目的は以下の通りです。
- リソースリークの防止: 子プロセスに不要なファイルディスクリプタが継承されるのを防ぎ、リソースの枯渇を防ぎます。
- セキュリティの向上: 親プロセスが意図しないファイルディスクリプタを子プロセスに渡してしまうことによるセキュリティリスクを低減します。例えば、親プロセスが持っている特権的なファイルディスクリプタが子プロセスに漏洩するのを防ぎます。
4. syscall.Pipe()
と syscall.Pipe2()
syscall.Pipe(p []int)
: これは、Unix系OSのpipe()
システムコールに対応するGoのラッパーです。p
は2つの整数を格納するスライスで、パイプの読み込み側と書き込み側のファイルディスクリプタが格納されます。このシステムコールは、O_CLOEXEC
フラグをアトミックに設定する機能を持っていません。syscall.Pipe2(p []int, flags int)
: これは、Linux固有のpipe2()
システムコールに対応するGoのラッパーです。pipe2()
は、pipe()
と同様にパイプを作成しますが、flags
引数を通じてO_CLOEXEC
などのフラグをパイプ作成時にアトミックに設定できます。これにより、pipe()
とfcntl()
を別々に呼び出すことによる競合状態を回避できます。pipe2()
はLinuxカーネル2.6.27で導入されました。
5. syscall.ENOSYS
ENOSYS
は、システムコールが実装されていないことを示すエラーコードです。このコミットでは、pipe2()
が利用できない古いLinuxカーネルバージョンでENOSYS
が返される可能性を考慮し、その場合は従来のpipe()
にフォールバックするロジックが実装されています。
技術的詳細
このコミットの主要な変更点は、Goのos.Pipe()
関数の実装を、Linux環境においてsyscall.Pipe2
を使用するように変更したことです。
Goのos.Pipe()
関数は、内部でOSのシステムコールを呼び出してパイプを作成します。従来のGoの実装では、syscall.Pipe()
を呼び出してパイプを作成した後、別途syscall.CloseOnExec()
を呼び出して、作成されたファイルディスクリプタにO_CLOEXEC
フラグを設定していました。
このコミットでは、Linux環境に特化したpipe_linux.go
という新しいファイルが導入され、その中でos.Pipe()
が再実装されています。この新しい実装では、まずsyscall.Pipe2(p[0:], syscall.O_CLOEXEC)
を試みます。
- もし
syscall.Pipe2
が成功すれば、パイプはO_CLOEXEC
フラグが設定された状態でアトミックに作成されます。 - もし
syscall.Pipe2
がsyscall.ENOSYS
エラーを返した場合(これは、実行中のLinuxカーネルがpipe2
システムコールをサポートしていないことを意味します。pipe2
はLinuxカーネル2.6.27で導入されたため、それ以前のバージョンでは利用できません)、従来のsyscall.Pipe()
にフォールバックします。この場合、syscall.Pipe()
でパイプを作成した後、以前と同様にsyscall.CloseOnExec()
を呼び出してO_CLOEXEC
フラグを設定します。
このフォールバックロジックにより、この変更は古いLinuxカーネルバージョンとの互換性を維持しつつ、新しいカーネルバージョンではより堅牢で効率的なpipe2
を利用できるようになります。
また、この変更に伴い、os.Pipe()
の汎用的なUnix実装がfile_unix.go
から削除され、BSD系のOS(Darwin, FreeBSD, NetBSD, OpenBSD)向けにpipe_bsd.go
という新しいファイルに移動されました。これにより、OSごとのパイプ実装の差異が明確になり、コードの保守性が向上しています。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は以下の3つのファイルにわたります。
-
src/pkg/os/file_unix.go
:Pipe()
関数の実装がこのファイルから完全に削除されました。これは、Pipe()
の汎用Unix実装が、OS固有のファイルに分割されたためです。
-
src/pkg/os/pipe_bsd.go
(新規ファイル):+build darwin freebsd netbsd openbsd
ビルドタグを持つ新しいファイルとして追加されました。file_unix.go
から削除された従来のPipe()
関数の実装が、このファイルに移動されました。この実装はsyscall.Pipe()
を使用し、その後syscall.CloseOnExec()
を呼び出します。これは、BSD系のOSがpipe2()
をサポートしていないか、Goがそのサポートを利用していないためです。
-
src/pkg/os/pipe_linux.go
(新規ファイル):- 新しいファイルとして追加されました。
- Linux環境に特化した
Pipe()
関数の実装が含まれています。 - この実装が
syscall.Pipe2()
を優先的に使用し、ENOSYS
エラーの場合にsyscall.Pipe()
にフォールバックするロジックを含んでいます。
--- a/src/pkg/os/file_unix.go
+++ b/src/pkg/os/file_unix.go
@@ -274,25 +274,6 @@ func basename(name string) string {
return name
}
-// Pipe returns a connected pair of Files; reads from r return bytes written to w.
-// It returns the files and an error, if any.
-func Pipe() (r *File, w *File, err error) {
- var p [2]int
-
- // See ../syscall/exec.go for description of lock.
- syscall.ForkLock.RLock()
- e := syscall.Pipe(p[0:])
- if e != nil {
- syscall.ForkLock.RUnlock()
- return nil, nil, NewSyscallError("pipe", e)
- }
- syscall.CloseOnExec(p[0])
- syscall.CloseOnExec(p[1])
- syscall.ForkLock.RUnlock()
-
- return NewFile(uintptr(p[0]), "|0"), NewFile(uintptr(p[1]), "|1"), nil
-}
-
// TempDir returns the default directory to use for temporary files.
func TempDir() string {
dir := Getenv("TMPDIR")
--- /dev/null
+++ b/src/pkg/os/pipe_bsd.go
@@ -0,0 +1,28 @@
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin freebsd netbsd openbsd
+
+package os
+
+import "syscall"
+
+// Pipe returns a connected pair of Files; reads from r return bytes written to w.
+// It returns the files and an error, if any.
+func Pipe() (r *File, w *File, err error) {
+ var p [2]int
+
+ // See ../syscall/exec.go for description of lock.
+ syscall.ForkLock.RLock()
+ e := syscall.Pipe(p[0:])
+ if e != nil {
+ syscall.ForkLock.RUnlock()
+ return nil, nil, NewSyscallError("pipe", e)
+ }
+ syscall.CloseOnExec(p[0])
+ syscall.CloseOnExec(p[1])
+ syscall.ForkLock.RUnlock()
+
+ return NewFile(uintptr(p[0]), "|0"), NewFile(uintptr(p[1]), "|1"), nil
+}
--- /dev/null
+++ b/src/pkg/os/pipe_linux.go
@@ -0,0 +1,33 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package os
+
+import "syscall"
+
+// Pipe returns a connected pair of Files; reads from r return bytes written to w.
+// It returns the files and an error, if any.
+func Pipe() (r *File, w *File, err error) {
+ var p [2]int
+
+ e := syscall.Pipe2(p[0:], syscall.O_CLOEXEC)
+ // pipe2 was added in 2.6.27 and our minimum requirement is 2.6.23, so it
+ // might not be implemented.
+ if e == syscall.ENOSYS {
+ // See ../syscall/exec.go for description of lock.
+ syscall.ForkLock.RLock()
+ e = syscall.Pipe(p[0:])
+ if e != nil {
+ syscall.ForkLock.RUnlock()
+ return nil, nil, NewSyscallError("pipe", e)
+ }
+ syscall.CloseOnExec(p[0])
+ syscall.CloseOnExec(p[1])
+ syscall.ForkLock.RUnlock()
+ } else if e != nil {
+ return nil, nil, NewSyscallError("pipe2", e)
+ }
+
+ return NewFile(uintptr(p[0]), "|0"), NewFile(uintptr(p[1]), "|1"), nil
+}
コアとなるコードの解説
src/pkg/os/file_unix.go
からの削除
file_unix.go
は、Goのos
パッケージにおけるUnix系OS共通のファイル操作に関するコードを格納していました。このコミットでは、Pipe()
関数がこのファイルから削除されました。これは、Pipe()
の実装がOSによって異なる挙動を持つため、より具体的なOS固有のファイルに分割することが適切であると判断されたためです。これにより、コードのモジュール性と可読性が向上します。
src/pkg/os/pipe_bsd.go
の新規追加と内容
このファイルは、Darwin (macOS), FreeBSD, NetBSD, OpenBSDといったBSD系のOS向けに特化したos.Pipe()
の実装を提供します。
+build darwin freebsd netbsd openbsd
というビルドタグは、Goコンパイラに対して、これらのOSでビルドする際にこのファイルを含めるように指示します。
このファイル内のPipe()
関数は、従来のfile_unix.go
にあった実装と全く同じです。すなわち、syscall.Pipe()
を呼び出してパイプを作成し、その後syscall.CloseOnExec()
を呼び出してO_CLOEXEC
フラグを設定します。これは、これらのOSではpipe2()
システムコールが利用できないか、Goがその機能を利用しないためです。
src/pkg/os/pipe_linux.go
の新規追加と内容
このファイルは、Linux環境に特化したos.Pipe()
の実装を提供します。
このファイル内のPipe()
関数は、以下のロジックで動作します。
-
syscall.Pipe2
の試行:e := syscall.Pipe2(p[0:], syscall.O_CLOEXEC)
まず、Linux固有の
syscall.Pipe2
を呼び出し、パイプを作成すると同時にsyscall.O_CLOEXEC
フラグを設定しようとします。これにより、パイプ作成とO_CLOEXEC
設定がアトミックに行われ、競合状態が回避されます。 -
ENOSYS
エラーのハンドリング:if e == syscall.ENOSYS { // ... fallback to syscall.Pipe ... }
もし
syscall.Pipe2
の呼び出しがsyscall.ENOSYS
エラーを返した場合、これは実行中のLinuxカーネルがpipe2
システムコールをサポートしていないことを意味します(pipe2
はLinuxカーネル2.6.27で導入されたため、それ以前のバージョンでは利用できません)。この場合、コードは従来のsyscall.Pipe
にフォールバックします。フォールバックのロジックは以下の通りです。
syscall.ForkLock.RLock()
:syscall.Pipe
はfork
と関連する競合状態を避けるために、ForkLock
というグローバルロックを使用します。これは、fork
とexec
の間にファイルディスクリプタの状態が変更されるのを防ぐためのものです。e = syscall.Pipe(p[0:])
: 従来のpipe()
システムコールを呼び出してパイプを作成します。- エラーチェック:
pipe()
が失敗した場合、エラーを返します。 syscall.CloseOnExec(p[0])
とsyscall.CloseOnExec(p[1])
:pipe()
で作成されたファイルディスクリプタに対して、個別にO_CLOEXEC
フラグを設定します。syscall.ForkLock.RUnlock()
: ロックを解放します。
-
その他のエラーハンドリング:
} else if e != nil { return nil, nil, NewSyscallError("pipe2", e) }
syscall.Pipe2
がENOSYS
以外のエラーを返した場合、そのエラーをNewSyscallError
でラップして返します。 -
*os.File
の作成と返却:return NewFile(uintptr(p[0]), "|0"), NewFile(uintptr(p[1]), "|1"), nil
最終的に、作成されたパイプの読み込み側と書き込み側のファイルディスクリプタ(
p[0]
とp[1]
)を基に、*os.File
オブジェクトを作成して返します。"|0"
と"|1"
は、それぞれ読み込み側と書き込み側を示す内部的な名前です。
この変更により、Goのos.Pipe()
はLinux上でより堅牢になり、特にマルチスレッド環境でのfork
/exec
とパイプ作成の競合状態のリスクが低減されました。同時に、古いLinuxカーネルとの互換性も維持されています。
関連リンク
- Go Issue 2656: https://github.com/golang/go/issues/2656
- Go CL 7065063: https://golang.org/cl/7065063
参考にした情報源リンク
pipe2(2)
- Linux man page: https://man7.org/linux/man-pages/man2/pipe2.2.htmlpipe(2)
- Linux man page: https://man7.org/linux/man-pages/man2/pipe.2.htmlfcntl(2)
- Linux man page: https://man7.org/linux/man-pages/man2/fcntl.2.htmlexec(3)
- Linux man page: https://man7.org/linux/man-pages/man3/exec.3.html- Go
syscall
package documentation: https://pkg.go.dev/syscall - Go
os
package documentation: https://pkg.go.dev/os O_CLOEXEC
and race conditions: https://lwn.net/Articles/500743/ (LWN.net article onO_CLOEXEC
andpipe2
)- Go
syscall.ForkLock
explanation: https://github.com/golang/go/blob/master/src/syscall/exec_unix.go#L17 (Go source code comment onForkLock
)