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

[インデックス 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.Pipe2syscall.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つのファイルにわたります。

  1. src/pkg/os/file_unix.go:

    • Pipe()関数の実装がこのファイルから完全に削除されました。これは、Pipe()の汎用Unix実装が、OS固有のファイルに分割されたためです。
  2. src/pkg/os/pipe_bsd.go (新規ファイル):

    • +build darwin freebsd netbsd openbsd ビルドタグを持つ新しいファイルとして追加されました。
    • file_unix.goから削除された従来のPipe()関数の実装が、このファイルに移動されました。この実装はsyscall.Pipe()を使用し、その後syscall.CloseOnExec()を呼び出します。これは、BSD系のOSがpipe2()をサポートしていないか、Goがそのサポートを利用していないためです。
  3. 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()関数は、以下のロジックで動作します。

  1. syscall.Pipe2の試行:

    e := syscall.Pipe2(p[0:], syscall.O_CLOEXEC)
    

    まず、Linux固有のsyscall.Pipe2を呼び出し、パイプを作成すると同時にsyscall.O_CLOEXECフラグを設定しようとします。これにより、パイプ作成とO_CLOEXEC設定がアトミックに行われ、競合状態が回避されます。

  2. 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.Pipeforkと関連する競合状態を避けるために、ForkLockというグローバルロックを使用します。これは、forkexecの間にファイルディスクリプタの状態が変更されるのを防ぐためのものです。
    • e = syscall.Pipe(p[0:]): 従来のpipe()システムコールを呼び出してパイプを作成します。
    • エラーチェック: pipe()が失敗した場合、エラーを返します。
    • syscall.CloseOnExec(p[0])syscall.CloseOnExec(p[1]): pipe()で作成されたファイルディスクリプタに対して、個別にO_CLOEXECフラグを設定します。
    • syscall.ForkLock.RUnlock(): ロックを解放します。
  3. その他のエラーハンドリング:

    } else if e != nil {
        return nil, nil, NewSyscallError("pipe2", e)
    }
    

    syscall.Pipe2ENOSYS以外のエラーを返した場合、そのエラーをNewSyscallErrorでラップして返します。

  4. *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カーネルとの互換性も維持されています。

関連リンク

参考にした情報源リンク