[インデックス 14852] ファイルの概要
このコミットは、Go言語のsyscall
パッケージにおいて、LinuxシステムコールPipe2
の実装を追加し、それを用いてForkExec
関数におけるパイプの作成処理を改善するものです。これにより、パイプ作成時のファイルディスクリプタのクローズオンエグゼック(close-on-exec)フラグの設定をより効率的かつアトミックに行えるようになり、競合状態のリスクを低減します。
コミット
commit e32d1154ec3176a81659720c1ca665e68ac95c42
Author: Georg Reinke <guelfey@gmail.com>
Date: Thu Jan 10 17:04:55 2013 -0800
syscall: implement Pipe2 on Linux and use it in ForkExec
Fixes #2656.
R=golang-dev, bradfitz, iant, minux.ma
CC=golang-dev
https://golang.org/cl/7062057
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e32d1154ec3176a81659720c1ca665e68ac95c42
元コミット内容
syscall: implement Pipe2 on Linux and use it in ForkExec
このコミットは、Linux上でPipe2
システムコールを実装し、ForkExec
関数内でそれを使用するように変更します。これにより、Issue #2656で報告された問題が修正されます。
変更の背景
この変更の主な背景は、Go言語のsyscall
パッケージがプロセスをフォークして新しいプログラムを実行する際に使用するForkExec
関数における、パイプ作成の堅牢性と効率性の向上です。
従来のUnix系システムでは、プロセス間通信のためにパイプを作成する際には、まずpipe()
システムコールを呼び出して2つのファイルディスクリプタ(読み込み側と書き込み側)を取得します。その後、これらのファイルディスクリプタが子プロセスに意図せず継承されるのを防ぐため、fcntl()
システムコールとFD_CLOEXEC
フラグを使用して、それぞれのファイルディスクリプタに「クローズオンエグゼック(close-on-exec)」属性を設定する必要がありました。
この2段階の操作には、以下のような問題点がありました。
- 競合状態(Race Condition):
pipe()
呼び出しとfcntl()
呼び出しの間に、別のスレッドがfork()
を呼び出すと、FD_CLOEXEC
フラグが設定されていないパイプのファイルディスクリプタが子プロセスに継承されてしまう可能性がありました。これは、子プロセスが予期せぬファイルディスクリプタを保持することになり、セキュリティ上の脆弱性やデッドロックの原因となる可能性があります。 - 効率性: 2つのシステムコールを連続して呼び出すことは、1つのシステムコールで同等の操作を行うよりもオーバーヘッドが大きくなります。
Linuxカーネル2.6.27以降で導入されたpipe2()
システムコールは、これらの問題を解決するために設計されました。pipe2()
は、パイプを作成する際に、同時にO_CLOEXEC
などのフラグをアトミックに設定できる機能を提供します。これにより、競合状態のリスクを排除し、システムコールの呼び出し回数を減らすことで効率を向上させることができます。
このコミットは、Go言語のsyscall
パッケージがLinuxの新しい機能を利用できるようにすることで、より堅牢で効率的なプロセス管理を実現することを目的としています。特に、Issue #2656で報告された問題の修正が明記されており、これはおそらく従来のパイプ作成方法に起因するバグや不安定性に関連していると考えられます。
前提知識の解説
このコミットを理解するためには、以下の概念について理解しておく必要があります。
1. パイプ (Pipe)
パイプは、Unix/Linuxシステムにおけるプロセス間通信(IPC: Inter-Process Communication)の最も基本的なメカニズムの一つです。単方向のデータフローを提供し、一方のプロセスがパイプの書き込み端にデータを書き込み、もう一方のプロセスがパイプの読み込み端からデータを読み取ります。
pipe()
システムコール:int pipe(int pipefd[2]);
- このシステムコールは、2つの新しいファイルディスクリプタを作成し、
pipefd[0]
に読み込み端、pipefd[1]
に書き込み端を格納します。 - 成功すると0を返し、失敗すると-1を返します。
2. ファイルディスクリプタ (File Descriptor, FD)
ファイルディスクリプタは、Unix/Linuxシステムにおいて、開かれたファイルやソケット、パイプなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる非負の整数です。
3. クローズオンエグゼック (Close-on-exec, CLOEXEC)
CLOEXEC
は、ファイルディスクリプタに設定できるフラグの一つです。このフラグが設定されたファイルディスクリプタは、execve()
(または関連するexec
ファミリーの関数)システムコールによって新しいプログラムが実行される際に、自動的に閉じられます。
- なぜ必要か?:
- 子プロセスが親プロセスから不要なファイルディスクリプタを継承するのを防ぎます。
- これにより、子プロセスが親プロセスのリソースに意図せずアクセスしたり、デッドロックを引き起こしたりするのを防ぎ、セキュリティと堅牢性を向上させます。
- 特に、
fork()
とexec()
を組み合わせて新しいプロセスを起動する際に重要です。fork()
は親プロセスのファイルディスクリプタをすべて子プロセスにコピーしますが、exec()
の前にCLOEXEC
を設定することで、子プロセスが不要なFDを保持するのを防ぎます。
4. fcntl()
システムコール
fcntl()
(file control)システムコールは、開かれたファイルディスクリプタの属性を操作するために使用されます。
int fcntl(int fd, int cmd, ... /* arg */ );
cmd
引数には様々な操作を指定できます。F_SETFD
: ファイルディスクリプタフラグを設定します。FD_CLOEXEC
:F_SETFD
と共に使用され、ファイルディスクリプタにCLOEXEC
フラグを設定します。
5. pipe2()
システムコール
pipe2()
はLinux固有のシステムコールで、pipe()
の拡張版です。
int pipe2(int pipefd[2], int flags);
pipe()
と同様に2つのファイルディスクリプタを作成しますが、flags
引数によってパイプ作成時に追加の属性をアトミックに設定できます。O_CLOEXEC
フラグ:pipe2()
のflags
引数にO_CLOEXEC
を指定することで、作成される両方のファイルディスクリプタにCLOEXEC
フラグをアトミックに設定できます。これにより、pipe()
とfcntl()
を別々に呼び出す必要がなくなり、競合状態を回避できます。- 導入時期: Linuxカーネル2.6.27で導入されました。
6. ForkExec
関数 (Go言語のsyscall
パッケージ)
Go言語のsyscall
パッケージは、オペレーティングシステムの低レベルなシステムコールへのインターフェースを提供します。ForkExec
関数は、Unix系システムにおいて、新しいプロセスをフォークし、その子プロセスで指定されたプログラムを実行するために使用されるGo言語の関数です。内部的にはfork()
とexecve()
システムコールを組み合わせて利用します。
7. ENOSYS
エラー
ENOSYS
は、システムコールが実装されていないことを示すエラーコードです。このコミットの文脈では、古いLinuxカーネルでpipe2()
が利用できない場合に返される可能性があります。
これらの前提知識を理解することで、このコミットがなぜ重要であり、どのような技術的課題を解決しようとしているのかが明確になります。
技術的詳細
このコミットの技術的詳細は、主にLinuxシステムコールpipe2()
の導入と、それを用いたForkExec
におけるパイプ作成処理の改善に集約されます。
pipe2()
の導入と利点
従来のpipe()
システムコールでは、パイプを作成した後に、fcntl()
システムコールを使って個別にファイルディスクリプタにFD_CLOEXEC
フラグを設定する必要がありました。この2段階の操作は、以下のような問題を引き起こす可能性がありました。
- 競合状態の発生:
pipe()
が呼び出されてファイルディスクリプタが作成された直後から、fcntl()
でFD_CLOEXEC
が設定されるまでの短い期間に、別のスレッドがfork()
を呼び出すと、CLOEXEC
フラグが設定されていないファイルディスクリプタが子プロセスに継承されてしまう可能性があります。これは、子プロセスが意図しないファイルディスクリプタを保持することになり、予期せぬ動作やリソースリーク、セキュリティ上の問題を引き起こす可能性があります。 - 効率性の低下: 2つのシステムコール(
pipe()
とfcntl()
を2回)を呼び出すことは、1つのシステムコールで同等の操作を行うよりも、カーネルとユーザー空間間のコンテキストスイッチのオーバーヘッドが大きくなります。
Linuxカーネル2.6.27で導入されたpipe2()
システムコールは、これらの問題を解決します。pipe2(pipefd, flags)
は、パイプを作成する際に、flags
引数を通じてO_CLOEXEC
などのフラグをアトミックに設定できます。O_CLOEXEC
フラグをpipe2()
に渡すことで、パイプの読み込み側と書き込み側の両方のファイルディスクリプタに、作成と同時にCLOEXEC
属性が設定されます。これにより、競合状態のリスクが完全に排除され、システムコールの呼び出し回数が削減されるため、効率も向上します。
forkExecPipe
関数の変更
このコミットでは、syscall
パッケージ内のforkExecPipe
関数が変更されています。この関数は、ForkExec
内で子プロセスとの通信に使用されるパイプを作成する役割を担っています。
-
Linux固有の実装 (
exec_linux.go
):Pipe2(p, O_CLOEXEC)
を最初に試みます。これは、pipe2()
システムコールを直接呼び出し、O_CLOEXEC
フラグを渡すことで、アトミックにCLOEXEC
属性を持つパイプを作成しようとします。- もし
Pipe2
がENOSYS
エラー(システムコールが実装されていない)を返した場合、これは実行中のLinuxカーネルがpipe2()
をサポートしていないことを意味します。この場合、従来のフォールバックメカニズムが実行されます。 - フォールバックメカニズムでは、まず
Pipe(p)
を呼び出してパイプを作成し、その後、fcntl(p[0], F_SETFD, FD_CLOEXEC)
とfcntl(p[1], F_SETFD, FD_CLOEXEC)
を呼び出して、それぞれのファイルディスクリプタにFD_CLOEXEC
フラグを個別に設定します。このフォールバックにより、古いカーネルバージョンでもGoプログラムが正しく動作することが保証されます。
-
BSD系システムの実装 (
exec_bsd.go
):- BSD系システム(macOSなど)には
pipe2()
に相当するシステムコールがないため、引き続き従来のPipe()
とfcntl()
を組み合わせた方法が使用されます。このコミットでは、forkExecPipe
関数が新しく導入され、Pipe()
と2回のfcntl()
呼び出しをカプセル化しています。これにより、コードの重複が避けられ、Linux版とのインターフェースの整合性が保たれます。
- BSD系システム(macOSなど)には
zsyscall_linux_*.go
ファイルの更新
zsyscall_linux_386.go
, zsyscall_linux_amd64.go
, zsyscall_linux_arm.go
といったファイルは、Goのsyscall
パッケージが各アーキテクチャのシステムコールを呼び出すための低レベルなラッパーを自動生成する際に使用されます。このコミットでは、pipe2
システムコールに対応するエントリがこれらのファイルに追加されています。これにより、Goプログラムからpipe2()
システムコールを直接呼び出すことが可能になります。
具体的には、RawSyscall(SYS_PIPE2, ...)
を使用して、カーネルのpipe2
システムコールを呼び出すためのGoの関数が定義されています。
全体的な影響
この変更により、Go言語で外部プロセスを起動する際のパイプ処理が、Linuxカーネルの新しい機能を利用してより安全かつ効率的になります。特に、競合状態のリスクが排除されることは、信頼性の高いアプリケーションを構築する上で非常に重要です。古いカーネルとの互換性も維持されているため、幅広い環境でこの改善の恩恵を受けることができます。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。
src/pkg/syscall/exec_linux.go
: Linux固有のforkExecPipe
関数の実装。pipe2
システムコールを優先的に使用し、フォールバックロジックを含む。src/pkg/syscall/exec_bsd.go
: BSD系システム(macOSなど)向けのforkExecPipe
関数の実装。従来のpipe
とfcntl
を使用。src/pkg/syscall/exec_unix.go
:forkExec
関数内で、従来のパイプ作成ロジックをforkExecPipe
関数呼び出しに置き換える。src/pkg/syscall/syscall_linux.go
:Pipe2
Go関数(pipe2
システムコールのラッパー)の宣言と実装。src/pkg/syscall/zsyscall_linux_386.go
,src/pkg/syscall/zsyscall_linux_amd64.go
,src/pkg/syscall/zsyscall_linux_arm.go
: 各Linuxアーキテクチャ向けのpipe2
システムコール呼び出しの低レベルな実装(自動生成される部分)。
以下に、特に重要な変更箇所を抜粋して示します。
src/pkg/syscall/exec_linux.go
の変更
--- a/src/pkg/syscall/exec_linux.go
+++ b/src/pkg/syscall/exec_linux.go
@@ -233,3 +233,20 @@ childerror:
// and this shuts up the compiler.
panic("unreached")
}
+
+// Try to open a pipe with O_CLOEXEC set on both file descriptors.
+func forkExecPipe(p []int) (err error) {
+ err = Pipe2(p, O_CLOEXEC)
+ // pipe2 was added in 2.6.27 and our minimum requirement is 2.6.23, so it
+ // might not be implemented.
+ if err == ENOSYS {
+ if err = Pipe(p); err != nil {
+ return
+ }
+ if _, err = fcntl(p[0], F_SETFD, FD_CLOEXEC); err != nil {
+ return
+ }
+ _, err = fcntl(p[1], F_SETFD, FD_CLOEXEC)
+ }
+ return
+}
src/pkg/syscall/exec_bsd.go
の変更
--- a/src/pkg/syscall/exec_bsd.go
+++ b/src/pkg/syscall/exec_bsd.go
@@ -221,3 +221,17 @@ childerror:
// and this shuts up the compiler.\n \tpanic(\"unreached\")
}\n+\n+// Try to open a pipe with O_CLOEXEC set on both file descriptors.
+func forkExecPipe(p []int) error {\n+\terr := Pipe(p)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\t_, err = fcntl(p[0], F_SETFD, FD_CLOEXEC)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\t_, err = fcntl(p[1], F_SETFD, FD_CLOEXEC)\n+\treturn err\n+}\n
src/pkg/syscall/exec_unix.go
の変更
--- a/src/pkg/syscall/exec_unix.go
+++ b/src/pkg/syscall/exec_unix.go
@@ -183,13 +183,7 @@ func forkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error)\n \tForkLock.Lock()\n \n \t// Allocate child status pipe close on exec.\n-\tif err = Pipe(p[0:]); err != nil {\n-\t\tgoto error\n-\t}\n-\tif _, err = fcntl(p[0], F_SETFD, FD_CLOEXEC); err != nil {\n-\t\tgoto error\n-\t}\n-\tif _, err = fcntl(p[1], F_SETFD, FD_CLOEXEC); err != nil {\n+\tif err = forkExecPipe(p[:]); err != nil {\n \t\tgoto error\n \t}\n \n```
### `src/pkg/syscall/syscall_linux.go` の変更
```diff
--- a/src/pkg/syscall/syscall_linux.go
+++ b/src/pkg/syscall/syscall_linux.go
@@ -39,6 +39,18 @@ func Pipe(p []int) (err error) {\n \treturn\n }\n \n+//sysnb pipe2(p *[2]_C_int, flags int) (err error)\n+func Pipe2(p []int, flags int) (err error) {\n+\tif len(p) != 2 {\n+\t\treturn EINVAL\n+\t}\n+\tvar pp [2]_C_int\n+\terr = pipe2(&pp, flags)\n+\tp[0] = int(pp[0])\n+\tp[1] = int(pp[1])\n+\treturn\n+}\n+\n //sys\tutimes(path string, times *[2]Timeval) (err error)\n func Utimes(path string, tv []Timeval) (err error) {\n \tif len(tv) != 2 {\
src/pkg/syscall/zsyscall_linux_386.go
(およびamd64, arm) の変更
--- a/src/pkg/syscall/zsyscall_linux_386.go
+++ b/src/pkg/syscall/zsyscall_linux_386.go
@@ -49,6 +49,16 @@ func pipe(p *[2]_C_int) (err error) {\n \n // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT\n \n+func pipe2(p *[2]_C_int, flags int) (err error) {\n+\t_, _, e1 := RawSyscall(SYS_PIPE2, uintptr(unsafe.Pointer(p)), uintptr(flags), 0)\n+\tif e1 != 0 {\n+\t\terr = e1\n+\t}\n+\treturn\n+}\n+\n // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT\n \n func utimes(path string, times *[2]Timeval) (err error) {\
コアとなるコードの解説
src/pkg/syscall/exec_linux.go
の forkExecPipe
関数
この関数は、Linuxシステムにおけるパイプ作成の主要なロジックを含んでいます。
func forkExecPipe(p []int) (err error) {
err = Pipe2(p, O_CLOEXEC)
// pipe2 was added in 2.6.27 and our minimum requirement is 2.6.23, so it
// might not be implemented.
if err == ENOSYS {
if err = Pipe(p); err != nil {
return
}
if _, err = fcntl(p[0], F_SETFD, FD_CLOEXEC); err != nil {
return
}
_, err = fcntl(p[1], F_SETFD, FD_CLOEXEC)
}
return
}
err = Pipe2(p, O_CLOEXEC)
: まず、pipe2()
システムコールをGoのラッパー関数Pipe2
を通じて呼び出します。O_CLOEXEC
フラグを渡すことで、作成されるパイプの読み込み側と書き込み側の両方のファイルディスクリプタに、アトミックにCLOEXEC
属性を設定しようとします。これが成功すれば、競合状態のリスクなしに効率的にパイプが作成されます。if err == ENOSYS
:Pipe2
の呼び出しがENOSYS
エラーを返した場合、これは実行中のLinuxカーネルがpipe2()
システムコールをサポートしていないことを意味します(pipe2
はLinux 2.6.27で導入され、Goの最小要件は2.6.23であるため、このような状況が発生する可能性があります)。- フォールバックロジック:
ENOSYS
の場合、従来のpipe()
とfcntl()
を組み合わせた方法にフォールバックします。if err = Pipe(p); err != nil
: まずpipe()
システムコールを呼び出してパイプを作成します。if _, err = fcntl(p[0], F_SETFD, FD_CLOEXEC); err != nil
: パイプの読み込み側ファイルディスクリプタp[0]
にFD_CLOEXEC
フラグを設定します。_, err = fcntl(p[1], F_SETFD, FD_CLOEXEC)
: パイプの書き込み側ファイルディスクリプタp[1]
にFD_CLOEXEC
フラグを設定します。- このフォールバックにより、古いLinuxカーネルバージョンでもGoプログラムが正しく動作することが保証されます。
src/pkg/syscall/exec_bsd.go
の forkExecPipe
関数
BSD系システム(macOSなど)ではpipe2()
に相当するシステムコールがないため、この関数は従来のpipe()
とfcntl()
を組み合わせた方法をカプセル化しています。
func forkExecPipe(p []int) error {
err := Pipe(p)
if err != nil {
return err
}
_, err = fcntl(p[0], F_SETFD, FD_CLOEXEC)
if err != nil {
return err
}
_, err = fcntl(p[1], F_SETFD, FD_CLOEXEC)
return err
}
- この関数は、
Pipe()
でパイプを作成し、その後、fcntl()
を2回呼び出して両方のファイルディスクリプタにFD_CLOEXEC
フラグを設定します。これは、Linux版のフォールバックロジックと本質的に同じです。これにより、異なるOS間でのforkExecPipe
のインターフェースの整合性が保たれます。
src/pkg/syscall/exec_unix.go
の forkExec
関数
このファイルは、Unix系システム全般に共通するforkExec
関数のロジックを含んでいます。変更は非常にシンプルで、従来のパイプ作成とfcntl
呼び出しの複数の行を、新しく導入されたforkExecPipe
関数への単一の呼び出しに置き換えています。
// Allocate child status pipe close on exec.
if err = forkExecPipe(p[:]); err != nil {
goto error
}
- これにより、
forkExec
関数のコードが簡潔になり、パイプ作成のOS固有の詳細はforkExecPipe
関数に抽象化されます。
src/pkg/syscall/syscall_linux.go
の Pipe2
関数
このファイルは、Go言語のsyscall
パッケージが提供するLinux固有のシステムコールラッパーを定義しています。
//sysnb pipe2(p *[2]_C_int, flags int) (err error)
func Pipe2(p []int, flags int) (err error) {
if len(p) != 2 {
return EINVAL
}
var pp [2]_C_int
err = pipe2(&pp, flags)
p[0] = int(pp[0])
p[1] = int(pp[1])
return
}
//sysnb pipe2(p *[2]_C_int, flags int) (err error)
: これはGoのツールチェーンに対する指示で、pipe2
という名前のシステムコールを、引数と戻り値の型に基づいてGoの関数として自動生成するように伝えます。sysnb
は"system call, no blocking"を意味し、このシステムコールがブロックしないことを示唆します。func Pipe2(p []int, flags int) (err error)
: ユーザーがGoコードから呼び出すPipe2
関数です。if len(p) != 2 { return EINVAL }
: 引数p
が2つの整数を保持するスライスであることを確認します。そうでない場合、EINVAL
(無効な引数)エラーを返します。var pp [2]_C_int
: C言語のint
型に対応するGoの型_C_int
の2要素配列を宣言します。これは、システムコールがCスタイルの配列ポインタを期待するためです。err = pipe2(&pp, flags)
: 低レベルのpipe2
システムコールラッパー(後述のzsyscall_linux_*.go
で定義される)を呼び出します。p[0] = int(pp[0])
とp[1] = int(pp[1])
: システムコールから返されたCスタイルの配列の値を、Goの[]int
スライスにコピーします。
src/pkg/syscall/zsyscall_linux_386.go
(およびamd64, arm) の pipe2
関数
これらのファイルは、Goのビルドプロセスによって自動生成される、特定のアーキテクチャ向けの低レベルなシステムコール呼び出しラッパーを含んでいます。
func pipe2(p *[2]_C_int, flags int) (err error) {
_, _, e1 := RawSyscall(SYS_PIPE2, uintptr(unsafe.Pointer(p)), uintptr(flags), 0)
if e1 != 0 {
err = e1
}
return
}
RawSyscall(SYS_PIPE2, ...)
: これはGoのsyscall
パッケージの最も低レベルな関数で、直接カーネルのシステムコールを呼び出します。SYS_PIPE2
:pipe2
システムコールに対応するシステムコール番号です。uintptr(unsafe.Pointer(p))
: パイプのファイルディスクリプタを格納する配列p
のアドレスを、システムコールが期待するuintptr
型に変換して渡します。uintptr(flags)
:flags
引数を渡します。
e1
: システムコールがエラーを返した場合、そのエラーコードがe1
に格納されます。if e1 != 0 { err = e1 }
: エラーがあれば、それをGoのエラーとして返します。
これらの変更により、Go言語のsyscall
パッケージは、Linuxの最新のシステムコール機能を活用し、より安全で効率的なプロセス間通信を実現できるようになりました。同時に、古いカーネルとの互換性も維持されています。
関連リンク
- Go Issue #2656: https://code.google.com/p/go/issues/detail?id=2656 (Goプロジェクトの古いIssueトラッカーのリンクですが、現在はGitHubに移行している可能性があります。GitHubのIssue #2656を検索すると、関連する情報が見つかるかもしれません。)
- Go CL 7062057: https://golang.org/cl/7062057 (Go Code Reviewのリンク)
参考にした情報源リンク
- pipe2(2) - Linux man page: https://man7.org/linux/man-pages/man2/pipe2.2.html
- fcntl(2) - Linux man page: https://man7.org/linux/man-pages/man2/fcntl.2.html
- pipe(2) - Linux man page: https://man7.org/linux/man-pages/man2/pipe.2.html
- execve(2) - Linux man page: https://man7.org/linux/man-pages/man2/execve.2.html
- Go syscall package documentation: https://pkg.go.dev/syscall
- Go issue #2656 on GitHub (if migrated): (Web検索で確認) - 検索結果から、このIssueはGoの古いIssueトラッカーにあり、GitHubには直接移行されていないようです。しかし、関連する議論や修正はGoのコミット履歴やメーリングリストで追跡できます。
- Understanding
O_CLOEXEC
: https://lwn.net/Articles/57803/ (LWN.netの記事は、O_CLOEXEC
の重要性と競合状態について詳しく解説しています。) - Go's
syscall
package andzsyscall
files: Goのソースコードとドキュメントを直接参照することで、syscall
パッケージの内部動作とzsyscall
ファイルの役割について理解を深めることができます。
[インデックス 14852] ファイルの概要
このコミットは、Go言語のsyscall
パッケージにおいて、LinuxシステムコールPipe2
の実装を追加し、それを用いてForkExec
関数におけるパイプの作成処理を改善するものです。これにより、パイプ作成時のファイルディスクリプタのクローズオンエグゼック(close-on-exec)フラグの設定をより効率的かつアトミックに行えるようになり、競合状態のリスクを低減します。
コミット
commit e32d1154ec3176a81659720c1ca665e68ac95c42
Author: Georg Reinke <guelfey@gmail.com>
Date: Thu Jan 10 17:04:55 2013 -0800
syscall: implement Pipe2 on Linux and use it in ForkExec
Fixes #2656.
R=golang-dev, bradfitz, iant, minux.ma
CC=golang-dev
https://golang.org/cl/7062057
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e32d1154ec3176a81659720c1ca665e68ac95c42
元コミット内容
syscall: implement Pipe2 on Linux and use it in ForkExec
このコミットは、Linux上でPipe2
システムコールを実装し、ForkExec
関数内でそれを使用するように変更します。これにより、Issue #2656で報告された問題が修正されます。
変更の背景
この変更の主な背景は、Go言語のsyscall
パッケージがプロセスをフォークして新しいプログラムを実行する際に使用するForkExec
関数における、パイプ作成の堅牢性と効率性の向上です。
従来のUnix系システムでは、プロセス間通信のためにパイプを作成する際には、まずpipe()
システムコールを呼び出して2つのファイルディスクリプタ(読み込み側と書き込み側)を取得します。その後、これらのファイルディスクリプタが子プロセスに意図せず継承されるのを防ぐため、fcntl()
システムコールとFD_CLOEXEC
フラグを使用して、それぞれのファイルディスクリプタに「クローズオンエグゼック(close-on-exec)」属性を設定する必要がありました。
この2段階の操作には、以下のような問題点がありました。
- 競合状態(Race Condition):
pipe()
呼び出しとfcntl()
呼び出しの間に、別のスレッドがfork()
を呼び出すと、FD_CLOEXEC
フラグが設定されていないパイプのファイルディスクリプタが子プロセスに継承されてしまう可能性がありました。これは、子プロセスが予期せぬファイルディスクリプタを保持することになり、セキュリティ上の脆弱性やデッドロックの原因となる可能性があります。 - 効率性: 2つのシステムコールを連続して呼び出すことは、1つのシステムコールで同等の操作を行うよりもオーバーヘッドが大きくなります。
Linuxカーネル2.6.27以降で導入されたpipe2()
システムコールは、これらの問題を解決するために設計されました。pipe2()
は、パイプを作成する際に、同時にO_CLOEXEC
などのフラグをアトミックに設定できる機能を提供します。これにより、競合状態のリスクを排除し、システムコールの呼び出し回数を減らすことで効率を向上させることができます。
このコミットは、Go言語のsyscall
パッケージがLinuxの新しい機能を利用できるようにすることで、より堅牢で効率的なプロセス管理を実現することを目的としています。特に、Issue #2656で報告された問題の修正が明記されており、これはおそらく従来のパイプ作成方法に起因するバグや不安定性に関連していると考えられます。
前提知識の解説
このコミットを理解するためには、以下の概念について理解しておく必要があります。
1. パイプ (Pipe)
パイプは、Unix/Linuxシステムにおけるプロセス間通信(IPC: Inter-Process Communication)の最も基本的なメカニズムの一つです。単方向のデータフローを提供し、一方のプロセスがパイプの書き込み端にデータを書き込み、もう一方のプロセスがパイプの読み込み端からデータを読み取ります。
pipe()
システムコール:int pipe(int pipefd[2]);
- このシステムコールは、2つの新しいファイルディスクリプタを作成し、
pipefd[0]
に読み込み端、pipefd[1]
に書き込み端を格納します。 - 成功すると0を返し、失敗すると-1を返します。
2. ファイルディスクリプタ (File Descriptor, FD)
ファイルディスクリプタは、Unix/Linuxシステムにおいて、開かれたファイルやソケット、パイプなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる非負の整数です。
3. クローズオンエグゼック (Close-on-exec, CLOEXEC)
CLOEXEC
は、ファイルディスクリプタに設定できるフラグの一つです。このフラグが設定されたファイルディスクリプタは、execve()
(または関連するexec
ファミリーの関数)システムコールによって新しいプログラムが実行される際に、自動的に閉じられます。
- なぜ必要か?:
- 子プロセスが親プロセスから不要なファイルディスクリプタを継承するのを防ぎます。
- これにより、子プロセスが親プロセスのリソースに意図せずアクセスしたり、デッドロックを引き起こしたりするのを防ぎ、セキュリティと堅牢性を向上させます。
- 特に、
fork()
とexec()
を組み合わせて新しいプロセスを起動する際に重要です。fork()
は親プロセスのファイルディスクリプタをすべて子プロセスにコピーしますが、exec()
の前にCLOEXEC
を設定することで、子プロセスが不要なFDを保持するのを防ぎます。
4. fcntl()
システムコール
fcntl()
(file control)システムコールは、開かれたファイルディスクリプタの属性を操作するために使用されます。
int fcntl(int fd, int cmd, ... /* arg */ );
cmd
引数には様々な操作を指定できます。F_SETFD
: ファイルディスクリプタフラグを設定します。FD_CLOEXEC
:F_SETFD
と共に使用され、ファイルディスクリプタにCLOEXEC
フラグを設定します。
5. pipe2()
システムコール
pipe2()
はLinux固有のシステムコールで、pipe()
の拡張版です。
int pipe2(int pipefd[2], int flags);
pipe()
と同様に2つのファイルディスクリプタを作成しますが、flags
引数によってパイプ作成時に追加の属性をアトミックに設定できます。O_CLOEXEC
フラグ:pipe2()
のflags
引数にO_CLOEXEC
を指定することで、作成される両方のファイルディスクリプタにCLOEXEC
フラグをアトミックに設定できます。これにより、pipe()
とfcntl()
を別々に呼び出す必要がなくなり、競合状態を回避できます。- 導入時期: Linuxカーネル2.6.27で導入されました。
6. ForkExec
関数 (Go言語のsyscall
パッケージ)
Go言語のsyscall
パッケージは、オペレーティングシステムの低レベルなシステムコールへのインターフェースを提供します。ForkExec
関数は、Unix系システムにおいて、新しいプロセスをフォークし、その子プロセスで指定されたプログラムを実行するために使用されるGo言語の関数です。内部的にはfork()
とexecve()
システムコールを組み合わせて利用します。
7. ENOSYS
エラー
ENOSYS
は、システムコールが実装されていないことを示すエラーコードです。このコミットの文脈では、古いLinuxカーネルでpipe2()
が利用できない場合に返される可能性があります。
これらの前提知識を理解することで、このコミットがなぜ重要であり、どのような技術的課題を解決しようとしているのかが明確になります。
技術的詳細
このコミットの技術的詳細は、主にLinuxシステムコールpipe2()
の導入と、それを用いたForkExec
におけるパイプ作成処理の改善に集約されます。
pipe2()
の導入と利点
従来のpipe()
システムコールでは、パイプを作成した後に、fcntl()
システムコールを使って個別にファイルディスクリプタにFD_CLOEXEC
フラグを設定する必要がありました。この2段階の操作は、以下のような問題を引き起こす可能性がありました。
- 競合状態の発生:
pipe()
が呼び出されてファイルディスクリプタが作成された直後から、fcntl()
でFD_CLOEXEC
が設定されるまでの短い期間に、別のスレッドがfork()
を呼び出すと、CLOEXEC
フラグが設定されていないファイルディスクリプタが子プロセスに継承されてしまう可能性があります。これは、子プロセスが意図しないファイルディスクリプタを保持することになり、予期せぬ動作やリソースリーク、セキュリティ上の問題を引き起こす可能性があります。 - 効率性の低下: 2つのシステムコール(
pipe()
とfcntl()
を2回)を呼び出すことは、1つのシステムコールで同等の操作を行うよりも、カーネルとユーザー空間間のコンテキストスイッチのオーバーヘッドが大きくなります。
Linuxカーネル2.6.27で導入されたpipe2()
システムコールは、これらの問題を解決します。pipe2(pipefd, flags)
は、パイプを作成する際に、flags
引数を通じてO_CLOEXEC
などのフラグをアトミックに設定できます。O_CLOEXEC
フラグをpipe2()
に渡すことで、パイプの読み込み側と書き込み側の両方のファイルディスクリプタに、作成と同時にCLOEXEC
属性が設定されます。これにより、競合状態のリスクが完全に排除され、システムコールの呼び出し回数が削減されるため、効率も向上します。
forkExecPipe
関数の変更
このコミットでは、syscall
パッケージ内のforkExecPipe
関数が変更されています。この関数は、ForkExec
内で子プロセスとの通信に使用されるパイプを作成する役割を担っています。
-
Linux固有の実装 (
exec_linux.go
):Pipe2(p, O_CLOEXEC)
を最初に試みます。これは、pipe2()
システムコールを直接呼び出し、O_CLOEXEC
フラグを渡すことで、アトミックにCLOEXEC
属性を持つパイプを作成しようとします。- もし
Pipe2
がENOSYS
エラー(システムコールが実装されていない)を返した場合、これは実行中のLinuxカーネルがpipe2()
をサポートしていないことを意味します。この場合、従来のフォールバックメカニズムが実行されます。 - フォールバックメカニズムでは、まず
Pipe(p)
を呼び出してパイプを作成し、その後、fcntl(p[0], F_SETFD, FD_CLOEXEC)
とfcntl(p[1], F_SETFD, FD_CLOEXEC)
を呼び出して、それぞれのファイルディスクリプタにFD_CLOEXEC
フラグを個別に設定します。このフォールバックにより、古いカーネルバージョンでもGoプログラムが正しく動作することが保証されます。
-
BSD系システムの実装 (
exec_bsd.go
):- BSD系システム(macOSなど)には
pipe2()
に相当するシステムコールがないため、引き続き従来のPipe()
とfcntl()
を組み合わせた方法が使用されます。このコミットでは、forkExecPipe
関数が新しく導入され、Pipe()
と2回のfcntl()
呼び出しをカプセル化しています。これにより、コードの重複が避けられ、Linux版とのインターフェースの整合性が保たれます。
- BSD系システム(macOSなど)には
zsyscall_linux_*.go
ファイルの更新
zsyscall_linux_386.go
, zsyscall_linux_amd64.go
, zsyscall_linux_arm.go
といったファイルは、Goのsyscall
パッケージが各アーキテクチャのシステムコールを呼び出すための低レベルなラッパーを自動生成する際に使用されます。このコミットでは、pipe2
システムコールに対応するエントリがこれらのファイルに追加されています。これにより、Goプログラムからpipe2()
システムコールを直接呼び出すことが可能になります。
具体的には、RawSyscall(SYS_PIPE2, ...)
を使用して、カーネルのpipe2
システムコールを呼び出すためのGoの関数が定義されています。
全体的な影響
この変更により、Go言語で外部プロセスを起動する際のパイプ処理が、Linuxカーネルの新しい機能を利用してより安全かつ効率的になります。特に、競合状態のリスクが排除されることは、信頼性の高いアプリケーションを構築する上で非常に重要です。古いカーネルとの互換性も維持されているため、幅広い環境でこの改善の恩恵を受けることができます。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。
src/pkg/syscall/exec_linux.go
: Linux固有のforkExecPipe
関数の実装。pipe2
システムコールを優先的に使用し、フォールバックロジックを含む。src/pkg/syscall/exec_bsd.go
: BSD系システム(macOSなど)向けのforkExecPipe
関数の実装。従来のpipe
とfcntl
を使用。src/pkg/syscall/exec_unix.go
:forkExec
関数内で、従来のパイプ作成ロジックをforkExecPipe
関数呼び出しに置き換える。src/pkg/syscall/syscall_linux.go
:Pipe2
Go関数(pipe2
システムコールのラッパー)の宣言と実装。src/pkg/syscall/zsyscall_linux_386.go
,src/pkg/syscall/zsyscall_linux_amd64.go
,src/pkg/syscall/zsyscall_linux_arm.go
: 各Linuxアーキテクチャ向けのpipe2
システムコール呼び出しの低レベルな実装(自動生成される部分)。
以下に、特に重要な変更箇所を抜粋して示します。
src/pkg/syscall/exec_linux.go
の変更
--- a/src/pkg/syscall/exec_linux.go
+++ b/src/pkg/syscall/exec_linux.go
@@ -233,3 +233,20 @@ childerror:
// and this shuts up the compiler.
panic("unreached")
}
+
+// Try to open a pipe with O_CLOEXEC set on both file descriptors.
+func forkExecPipe(p []int) (err error) {
+ err = Pipe2(p, O_CLOEXEC)
+ // pipe2 was added in 2.6.27 and our minimum requirement is 2.6.23, so it
+ // might not be implemented.
+ if err == ENOSYS {
+ if err = Pipe(p); err != nil {
+ return
+ }
+ if _, err = fcntl(p[0], F_SETFD, FD_CLOEXEC); err != nil {
+ return
+ }
+ _, err = fcntl(p[1], F_SETFD, FD_CLOEXEC)
+ }
+ return
+}
src/pkg/syscall/exec_bsd.go
の変更
--- a/src/pkg/syscall/exec_bsd.go
+++ b/src/pkg/syscall/exec_bsd.go
@@ -221,3 +221,17 @@ childerror:
// and this shuts up the compiler.\n \tpanic(\"unreached\")
}\n+\n+// Try to open a pipe with O_CLOEXEC set on both file descriptors.
+func forkExecPipe(p []int) error {\n+\terr := Pipe(p)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\t_, err = fcntl(p[0], F_SETFD, FD_CLOEXEC)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\t_, err = fcntl(p[1], F_SETFD, FD_CLOEXEC)\n+\treturn err\n+}\n
src/pkg/syscall/exec_unix.go
の変更
--- a/src/pkg/syscall/exec_unix.go
+++ b/src/pkg/syscall/exec_unix.go
@@ -183,13 +183,7 @@ func forkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error)\n \tForkLock.Lock()\n \n \t// Allocate child status pipe close on exec.\n-\tif err = Pipe(p[0:]); err != nil {\n-\t\tgoto error\n-\t}\n-\tif _, err = fcntl(p[0], F_SETFD, FD_CLOEXEC); err != nil {\n-\t\tgoto error\n-\t}\n-\tif _, err = fcntl(p[1], F_SETFD, FD_CLOEXEC); err != nil {\n+\tif err = forkExecPipe(p[:]); err != nil {\n \t\tgoto error\n \t}\n \n```
### `src/pkg/syscall/syscall_linux.go` の変更
```diff
--- a/src/pkg/syscall/syscall_linux.go
+++ b/src/pkg/syscall/syscall_linux.go
@@ -39,6 +39,18 @@ func Pipe(p []int) (err error) {\n \treturn\n }\n \n+//sysnb pipe2(p *[2]_C_int, flags int) (err error)\n+func Pipe2(p []int, flags int) (err error) {\n+\tif len(p) != 2 {\n+\t\treturn EINVAL\n+\t}\n+\tvar pp [2]_C_int\n+\terr = pipe2(&pp, flags)\n+\tp[0] = int(pp[0])\n+\tp[1] = int(pp[1])\n+\treturn\n+}\n+\n //sys\tutimes(path string, times *[2]Timeval) (err error)\n func Utimes(path string, tv []Timeval) (err error) {\n \tif len(tv) != 2 {\
src/pkg/syscall/zsyscall_linux_386.go
(およびamd64, arm) の変更
--- a/src/pkg/syscall/zsyscall_linux_386.go
+++ b/src/pkg/syscall/zsyscall_linux_386.go
@@ -49,6 +49,16 @@ func pipe(p *[2]_C_int) (err error) {\n \n // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT\n \n+func pipe2(p *[2]_C_int, flags int) (err error) {\n+\t_, _, e1 := RawSyscall(SYS_PIPE2, uintptr(unsafe.Pointer(p)), uintptr(flags), 0)\n+\tif e1 != 0 {\n+\t\terr = e1\n+\t}\n+\treturn\n+}\n+\n // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT\n \n func utimes(path string, times *[2]Timeval) (err error) {\
コアとなるコードの解説
src/pkg/syscall/exec_linux.go
の forkExecPipe
関数
この関数は、Linuxシステムにおけるパイプ作成の主要なロジックを含んでいます。
func forkExecPipe(p []int) (err error) {
err = Pipe2(p, O_CLOEXEC)
// pipe2 was added in 2.6.27 and our minimum requirement is 2.6.23, so it
// might not be implemented.
if err == ENOSYS {
if err = Pipe(p); err != nil {
return
}
if _, err = fcntl(p[0], F_SETFD, FD_CLOEXEC); err != nil {
return
}
_, err = fcntl(p[1], F_SETFD, FD_CLOEXEC)
}
return
}
err = Pipe2(p, O_CLOEXEC)
: まず、pipe2()
システムコールをGoのラッパー関数Pipe2
を通じて呼び出します。O_CLOEXEC
フラグを渡すことで、作成されるパイプの読み込み側と書き込み側の両方のファイルディスクリプタに、アトミックにCLOEXEC
属性を設定しようとします。これが成功すれば、競合状態のリスクなしに効率的にパイプが作成されます。if err == ENOSYS
:Pipe2
の呼び出しがENOSYS
エラーを返した場合、これは実行中のLinuxカーネルがpipe2()
システムコールをサポートしていないことを意味します(pipe2
はLinux 2.6.27で導入され、Goの最小要件は2.6.23であるため、このような状況が発生する可能性があります)。- フォールバックロジック:
ENOSYS
の場合、従来のpipe()
とfcntl()
を組み合わせた方法にフォールバックします。if err = Pipe(p); err != nil
: まずpipe()
システムコールを呼び出してパイプを作成します。if _, err = fcntl(p[0], F_SETFD, FD_CLOEXEC); err != nil
: パイプの読み込み側ファイルディスクリプタp[0]
にFD_CLOEXEC
フラグを設定します。_, err = fcntl(p[1], F_SETFD, FD_CLOEXEC)
: パイプの書き込み側ファイルディスクリプタp[1]
にFD_CLOEXEC
フラグを設定します。- このフォールバックにより、古いLinuxカーネルバージョンでもGoプログラムが正しく動作することが保証されます。
src/pkg/syscall/exec_bsd.go
の forkExecPipe
関数
BSD系システム(macOSなど)ではpipe2()
に相当するシステムコールがないため、この関数は従来のpipe()
とfcntl()
を組み合わせた方法をカプセル化しています。
func forkExecPipe(p []int) error {
err := Pipe(p)
if err != nil {
return err
}
_, err = fcntl(p[0], F_SETFD, FD_CLOEXEC)
if err != nil {
return err
}
_, err = fcntl(p[1], F_SETFD, FD_CLOEXEC)
return err
}
- この関数は、
Pipe()
でパイプを作成し、その後、fcntl()
を2回呼び出して両方のファイルディスクリプタにFD_CLOEXEC
フラグを設定します。これは、Linux版のフォールバックロジックと本質的に同じです。これにより、異なるOS間でのforkExecPipe
のインターフェースの整合性が保たれます。
src/pkg/syscall/exec_unix.go
の forkExec
関数
このファイルは、Unix系システム全般に共通するforkExec
関数のロジックを含んでいます。変更は非常にシンプルで、従来のパイプ作成とfcntl
呼び出しの複数の行を、新しく導入されたforkExecPipe
関数への単一の呼び出しに置き換えています。
// Allocate child status pipe close on exec.
if err = forkExecPipe(p[:]); err != nil {
goto error
}
- これにより、
forkExec
関数のコードが簡潔になり、パイプ作成のOS固有の詳細はforkExecPipe
関数に抽象化されます。
src/pkg/syscall/syscall_linux.go
の Pipe2
関数
このファイルは、Go言語のsyscall
パッケージが提供するLinux固有のシステムコールラッパーを定義しています。
//sysnb pipe2(p *[2]_C_int, flags int) (err error)
func Pipe2(p []int, flags int) (err error) {
if len(p) != 2 {
return EINVAL
}
var pp [2]_C_int
err = pipe2(&pp, flags)
p[0] = int(pp[0])
p[1] = int(pp[1])
return
}
//sysnb pipe2(p *[2]_C_int, flags int) (err error)
: これはGoのツールチェーンに対する指示で、pipe2
という名前のシステムコールを、引数と戻り値の型に基づいてGoの関数として自動生成するように伝えます。sysnb
は"system call, no blocking"を意味し、このシステムコールがブロックしないことを示唆します。func Pipe2(p []int, flags int) (err error)
: ユーザーがGoコードから呼び出すPipe2
関数です。if len(p) != 2 { return EINVAL }
: 引数p
が2つの整数を保持するスライスであることを確認します。そうでない場合、EINVAL
(無効な引数)エラーを返します。var pp [2]_C_int
: C言語のint
型に対応するGoの型_C_int
の2要素配列を宣言します。これは、システムコールがCスタイルの配列ポインタを期待するためです。err = pipe2(&pp, flags)
: 低レベルのpipe2
システムコールラッパー(後述のzsyscall_linux_*.go
で定義される)を呼び出します。p[0] = int(pp[0])
とp[1] = int(pp[1])
: システムコールから返されたCスタイルの配列の値を、Goの[]int
スライスにコピーします。
src/pkg/syscall/zsyscall_linux_386.go
(およびamd64, arm) の pipe2
関数
これらのファイルは、Goのビルドプロセスによって自動生成される、特定のアーキテクチャ向けの低レベルなシステムコール呼び出しラッパーを含んでいます。
func pipe2(p *[2]_C_int, flags int) (err error) {
_, _, e1 := RawSyscall(SYS_PIPE2, uintptr(unsafe.Pointer(p)), uintptr(flags), 0)
if e1 != 0 {
err = e1
}
return
}
RawSyscall(SYS_PIPE2, ...)
: これはGoのsyscall
パッケージの最も低レベルな関数で、直接カーネルのシステムコールを呼び出します。SYS_PIPE2
:pipe2
システムコールに対応するシステムコール番号です。uintptr(unsafe.Pointer(p))
: パイプのファイルディスクリプタを格納する配列p
のアドレスを、システムコールが期待するuintptr
型に変換して渡します。uintptr(flags)
:flags
引数を渡します。
e1
: システムコールがエラーを返した場合、そのエラーコードがe1
に格納されます。if e1 != 0 { err = e1 }
: エラーがあれば、それをGoのエラーとして返します。
これらの変更により、Go言語のsyscall
パッケージは、Linuxの最新のシステムコール機能を活用し、より安全で効率的なプロセス間通信を実現できるようになりました。同時に、古いカーネルとの互換性も維持されています。
関連リンク
- Go Issue #2656: https://code.google.com/p/go/issues/detail?id=2656 (Goプロジェクトの古いIssueトラッカーのリンクですが、現在はGitHubに移行している可能性があります。GitHubのIssue #2656を検索すると、関連する情報が見つかるかもしれません。)
- Go CL 7062057: https://golang.org/cl/7062057 (Go Code Reviewのリンク)
参考にした情報源リンク
- pipe2(2) - Linux man page: https://man7.org/linux/man-pages/man2/pipe2.2.html
- fcntl(2) - Linux man page: https://man7.org/linux/man-pages/man2/fcntl.2.html
- pipe(2) - Linux man page: https://man7.org/linux/man-pages/man2/pipe.2.html
- execve(2) - Linux man page: https://man7.org/linux/man-pages/man2/execve.2.html
- Go syscall package documentation: https://pkg.go.dev/syscall
- Go issue #2656 on GitHub (if migrated): (Web検索で確認) - 検索結果から、このIssueはGoの古いIssueトラッカーにあり、GitHubには直接移行されていないようです。しかし、関連する議論や修正はGoのコミット履歴やメーリングリストで追跡できます。
- Understanding
O_CLOEXEC
: https://lwn.net/Articles/57803/ (LWN.netの記事は、O_CLOEXEC
の重要性と競合状態について詳しく解説しています。) - Go's
syscall
package andzsyscall
files: Goのソースコードとドキュメントを直接参照することで、syscall
パッケージの内部動作とzsyscall
ファイルの役割について理解を深めることができます。