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

[インデックス 12992] ファイルの概要

このコミットは、Go言語のsyscallパッケージに、Unixドメインソケットを介したファイルディスクリプタ(FD)の受け渡しをテストする新しいファイルsrc/pkg/syscall/passfd_test.goを追加するものです。

コミット

commit fe2ce5285e379703c28d7f9efae02c63321854db
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Apr 30 15:31:14 2012 +1000

    syscall: add a test for passing an fd over a unix socket

    Re-submitting previously reverted change 160ec5506cb7.

    R=golang-dev, r, r
    CC=golang-dev
    https://golang.org/cl/6129052

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/fe2ce5285e379703c28d7f9efae02c63321854db

元コミット内容

このコミットは、以前にリバートされた変更 160ec5506cb7 を再提出するものです。元のコミット内容は、Unixソケットを介してファイルディスクリプタを渡すためのテストを追加することでした。

変更の背景

ファイルディスクリプタの受け渡しは、プロセス間通信(IPC)において非常に強力なメカニズムであり、特に特権分離やリソース共有のシナリオで利用されます。Go言語のsyscallパッケージは、低レベルのシステムコールへのアクセスを提供するため、このような高度なIPC機能もサポートする必要があります。

このテストの追加は、GoのsyscallパッケージがUnixドメインソケットを介したファイルディスクリプタの受け渡し機能を正しく実装し、安定して動作することを保証するために行われました。特に、以前にリバートされた変更を再提出していることから、この機能の重要性と、その実装の堅牢性を確保する必要性が示唆されます。リバートされた背景には、おそらく何らかのバグや不安定性があったと考えられ、今回の再提出はそれらの問題が解決されたことを意味します。

前提知識の解説

1. ファイルディスクリプタ (File Descriptor, FD)

ファイルディスクリプタは、Unix系OSにおいて、開かれたファイルやI/Oリソース(ソケット、パイプ、デバイスなど)を識別するためにカーネルがプロセスに割り当てる非負の整数です。プロセスはFDを通じてこれらのリソースにアクセスします。

2. Unixドメインソケット (Unix Domain Socket)

Unixドメインソケットは、同じホスト上のプロセス間で通信を行うためのIPCメカニズムです。ネットワークソケット(TCP/IPなど)とは異なり、ファイルシステム上のパス名(例: /tmp/mysocket)に関連付けられ、ネットワークプロトコルスタックを介さずに直接カーネル内で通信が行われるため、非常に高速です。Unixドメインソケットは、ストリーム(SOCK_STREAM)またはデータグラム(SOCK_DGRAM)モードで動作します。

3. ファイルディスクリプタの受け渡し (Passing File Descriptors)

Unixドメインソケットの特別な機能の一つに、開かれたファイルディスクリプタをプロセス間で受け渡す能力があります。これにより、あるプロセスがオープンしたファイルやソケットを、別のプロセスが自身のFDとして受け取り、利用することができます。これは、以下のようなシナリオで非常に有用です。

  • 特権分離: 特権を持つプロセスがリソース(例: ネットワークポート)を開き、そのFDを非特権プロセスに渡すことで、非特権プロセスは特権なしにリソースを利用できます。
  • リソース共有: 複数のプロセスが同じファイルやソケットを共有し、効率的に協調作業を行うことができます。
  • サーバーの再起動: サーバープロセスがダウンタイムなしに再起動する際に、既存の接続ソケットを新しいプロセスに引き継ぐことができます。

FDの受け渡しは、通常、sendmsgおよびrecvmsgシステムコールを使用して行われます。これらのシステムコールは、通常のデータに加えて、補助データ(ancillary data)としてFDを送信・受信する機能を提供します。

4. syscallパッケージ

Go言語のsyscallパッケージは、オペレーティングシステムが提供する低レベルのプリミティブ(システムコール)へのアクセスを提供します。これにより、GoプログラムからOSの機能を直接利用できます。ファイルディスクリプタの操作、ソケット通信、プロセス管理など、OSに密接に関連する機能はsyscallパッケージを通じて行われます。

5. Socketpair

syscall.Socketpairは、Unixドメインソケットのペアを作成するシステムコールです。これにより、2つの接続されたソケットが作成され、それぞれが読み書き可能で、互いに通信できます。これは、親子プロセス間の通信や、テストハーネスでよく使用されます。

6. ReadMsgUnixWriteMsgUnix

これらは、Unixドメインソケットでメッセージを送受信するためのGoのnetパッケージのメソッドです。特に、ReadMsgUnixWriteMsgUnixは、通常のデータ(buf)に加えて、補助データ(oob - out-of-band data)を扱うことができます。ファイルディスクリプタは、この補助データとして送受信されます。

7. ParseSocketControlMessageParseUnixRights

syscall.ParseSocketControlMessageは、ReadMsgUnixで受信した補助データ(oobバイトスライス)を解析し、SocketControlMessageのリストに変換します。SocketControlMessageは、補助データのヘッダとペイロードを含みます。 syscall.ParseUnixRightsは、SocketControlMessageのペイロードから、実際に受け渡されたファイルディスクリプタのリスト(整数のスライス)を抽出します。

8. os/execパッケージ

os/execパッケージは、外部コマンドを実行するためのGoの標準ライブラリです。このテストでは、親プロセスが自身のバイナリを子プロセスとして起動し、子プロセスに特定の環境変数や追加のファイルディスクリプタを渡すために使用されます。

技術的詳細

このテストは、Unixドメインソケットを介したファイルディスクリプタの受け渡しが正しく機能するかを検証するために、親プロセスと子プロセスの2つのプロセスを使用します。

  1. 親プロセスのセットアップ (TestPassFD):

    • 一時ディレクトリを作成し、テスト中に作成されるファイルを格納します。
    • syscall.Socketpairを呼び出して、2つの接続されたUnixドリームソケット(fds[0]fds[1])を作成します。これらは親プロセスと子プロセスの間の通信チャネルとして機能します。
    • fds[0]os.Fileにラップし、writeFileとして子プロセスに渡すための準備をします。
    • fds[1]os.Fileにラップし、readFileとして親プロセスが子プロセスからのデータを受信するために使用します。
    • 現在のテストバイナリ自体を子プロセスとして起動するためのexec.Commandを設定します。
      • os.Args[0]は現在の実行可能ファイルのパスです。
      • -test.run=^TestPassFD$ は、子プロセスが起動されたときにTestPassFD関数が再度実行されるように指定しますが、環境変数GO_WANT_HELPER_PROCESS=1が設定されている場合は、passFDChild関数が実行されるように分岐します。
      • --, tempDir は、子プロセスに一時ディレクトリのパスを引数として渡します。
      • cmd.EnvGO_WANT_HELPER_PROCESS=1を追加し、子プロセスがヘルパープロセスとして動作するように指示します。
      • cmd.ExtraFiles = []*os.File{writeFile} を使用して、writeFilefds[0]に対応)を子プロセスに渡します。これにより、子プロセスは親プロセスが作成したソケットの片側を自身のファイルディスクリプタとして受け取ります。
  2. 子プロセスの実行 (passFDChild):

    • GO_WANT_HELPER_PROCESS環境変数が設定されている場合、TestPassFD関数内でpassFDChild()が呼び出されます。
    • 子プロセスは、親プロセスから渡されたUnixソケットのファイルディスクリプタ(通常はFD 3以降)を特定します。これは、os.NewFilenet.FileConnを使用して行われます。
    • 子プロセスは、一時ディレクトリ内に新しい一時ファイルを作成します。
    • この一時ファイルに「Hello from child process!」という文字列を書き込みます。
    • syscall.UnixRights(int(f.Fd())) を使用して、新しく作成した一時ファイルのファイルディスクリプタを補助データとしてエンコードします。
    • uc.WriteMsgUnix を使用して、ダミーの1バイトデータと、エンコードされたファイルディスクリプタを含む補助データを親プロセスに送信します。
  3. 親プロセスでのFDの受信と検証 (TestPassFDの続き):

    • 親プロセスは、net.FileConn(readFile) を使用して、子プロセスから渡されたソケット接続をnet.UnixConnとして取得します。
    • uc.ReadMsgUnix を使用して、子プロセスから送信されたメッセージと補助データを受信します。
    • syscall.ParseSocketControlMessage を使用して、受信した補助データを解析し、SocketControlMessageのリストを取得します。
    • syscall.ParseUnixRights を使用して、SocketControlMessageから実際に受け渡されたファイルディスクリプタ(この場合は1つ)を抽出します。
    • 抽出されたファイルディスクリプタをos.NewFileos.Fileオブジェクトに変換します。
    • このos.Fileからioutil.ReadAllを使用して内容を読み込み、「Hello from child process!」という期待される文字列と一致するかを検証します。

このテストは、Goのsyscallパッケージが提供する低レベルの機能が、異なるプロセス間でファイルディスクリプタを安全かつ正確に受け渡すことができることを保証します。

コアとなるコードの変更箇所

src/pkg/syscall/passfd_test.go が新規追加されています。

// +build linux darwin

package syscall_test

import (
	"flag"
	"fmt"
	"io/ioutil"
	"net"
	"os"
	"os/exec"
	"syscall"
	"testing"
	"time"
)

// TestPassFD tests passing a file descriptor over a Unix socket.
// ... (コメント省略)
func TestPassFD(t *testing.T) {
	if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
		passFDChild()
		return
	}

	tempDir, err := ioutil.TempDir("", "TestPassFD")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tempDir)

	fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_STREAM, 0)
	if err != nil {
		t.Fatalf("Socketpair: %v", err)
	}
	defer syscall.Close(fds[0])
	defer syscall.Close(fds[1])
	writeFile := os.NewFile(uintptr(fds[0]), "child-writes")
	readFile := os.NewFile(uintptr(fds[1]), "parent-reads")
	defer writeFile.Close()
	defer readFile.Close()

	cmd := exec.Command(os.Args[0], "-test.run=^TestPassFD$", "--", tempDir)
	cmd.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)
	cmd.ExtraFiles = []*os.File{writeFile}

	out, err := cmd.CombinedOutput()
	if len(out) > 0 || err != nil {
		t.Fatalf("child process: %q, %v", out, err)
	}

	c, err := net.FileConn(readFile)
	if err != nil {
		t.Fatalf("FileConn: %v", err)
	}
	defer c.Close()

	uc, ok := c.(*net.UnixConn)
	if !ok {
		t.Fatalf("unexpected FileConn type; expected UnixConn, got %T", c)
	}

	buf := make([]byte, 32) // expect 1 byte
	oob := make([]byte, 32) // expect 24 bytes
	closeUnix := time.AfterFunc(5*time.Second, func() {
		t.Logf("timeout reading from unix socket")
		uc.Close()
	})
	_, oobn, _, _, err := uc.ReadMsgUnix(buf, oob)
	closeUnix.Stop()

	scms, err := syscall.ParseSocketControlMessage(oob[:oobn])
	if err != nil {
		t.Fatalf("ParseSocketControlMessage: %v", err)
	}
	if len(scms) != 1 {
		t.Fatalf("expected 1 SocketControlMessage; got scms = %#v", scms)
	}
	scm := scms[0]
	gotFds, err := syscall.ParseUnixRights(&scm)
	if err != nil {
		t.Fatalf("syscall.ParseUnixRights: %v", err)
	}
	if len(gotFds) != 1 {
		t.Fatalf("wanted 1 fd; got %#v", gotFds)
	}

	f := os.NewFile(uintptr(gotFds[0]), "fd-from-child")
	defer f.Close()

	got, err := ioutil.ReadAll(f)
	want := "Hello from child process!\\n"
	if string(got) != want {
		t.Errorf("child process ReadAll: %q, %v; want %q", got, err, want)
	}
}

// passFDChild is the child process used by TestPassFD.
func passFDChild() {
	defer os.Exit(0)

	// Look for our fd. It should be fd 3, but we work around an fd leak
	// bug here (http://golang.org/issue/2603) to let it be elsewhere.
	var uc *net.UnixConn
	for fd := uintptr(3); fd <= 10; fd++ {
		f := os.NewFile(fd, "unix-conn")
		var ok bool
		netc, _ := net.FileConn(f)
		uc, ok = netc.(*net.UnixConn)
		if ok {
			break
		}
	}
	if uc == nil {
		fmt.Println("failed to find unix fd")
		return
	}

	// Make a file f to send to our parent process on uc.
	// We make it in tempDir, which our parent will clean up.
	flag.Parse()
	tempDir := flag.Arg(0)
	f, err := ioutil.TempFile(tempDir, "")
	if err != nil {
		fmt.Printf("TempFile: %v", err)
		return
	}

	f.Write([]byte("Hello from child process!\\n"))
	f.Seek(0, 0)

	rights := syscall.UnixRights(int(f.Fd()))
	dummyByte := []byte("x")
	n, oobn, err := uc.WriteMsgUnix(dummyByte, rights, nil)
	if err != nil {
		fmt.Printf("WriteMsgUnix: %v", err)
		return
	}
	if n != 1 || oobn != len(rights) {
		fmt.Printf("WriteMsgUnix = %d, %d; want 1, %d", n, oobn, len(rights))
		return
	}
}

コアとなるコードの解説

TestPassFD 関数 (親プロセス)

  • +build linux darwin: このテストがLinuxとmacOS(Darwin)でのみビルド・実行されることを示します。ファイルディスクリプタの受け渡しはUnix系OS特有の機能であるためです。
  • os.Getenv("GO_WANT_HELPER_PROCESS") == "1": この条件で、現在のプロセスが親プロセスとして動作しているのか、それともexec.Commandによって起動された子プロセスとして動作しているのかを判断します。子プロセスの場合、passFDChild()が呼び出されます。
  • syscall.Socketpair: 親子プロセス間の通信チャネルとして機能するUnixドメインソケットのペアを作成します。
  • os.NewFile(uintptr(fds[0]), "child-writes"): fds[0]os.Fileにラップし、子プロセスに渡すための準備をします。uintptrはファイルディスクリプタの整数値をポインタ型に変換するために使用されます。
  • cmd.ExtraFiles = []*os.File{writeFile}: exec.Commandのこのフィールドは、子プロセスに継承させるファイルディスクリプタを指定します。writeFilefds[0])が子プロセスに渡されます。
  • uc.ReadMsgUnix(buf, oob): 子プロセスから送信されたデータと補助データ(ファイルディスクリプタを含む)を受信します。oobスライスに補助データが格納されます。
  • syscall.ParseSocketControlMessage(oob[:oobn]): 受信した補助データからSocketControlMessageを解析します。
  • syscall.ParseUnixRights(&scm): SocketControlMessageから、実際に受け渡されたファイルディスクリプタのリスト(gotFds)を抽出します。
  • os.NewFile(uintptr(gotFds[0]), "fd-from-child"): 受け取ったファイルディスクリプタをos.Fileオブジェクトに変換し、その内容を読み込んで検証します。

passFDChild 関数 (子プロセス)

  • defer os.Exit(0): 子プロセスが終了する際に、正常終了コード0で終了することを保証します。
  • for fd := uintptr(3); fd <= 10; fd++: 親プロセスから渡されたUnixソケットのファイルディスクリプタを探索します。通常、標準入力(0)、標準出力(1)、標準エラー(2)の後に続くため、FD 3から探索を開始します。これは、Goのバグ(http://golang.org/issue/2603)へのワークアラウンドとして、FDがどこに割り当てられるか不確実な場合に対応しています。
  • ioutil.TempFile(tempDir, ""): 親プロセスから渡された一時ディレクトリ内に新しい一時ファイルを作成します。
  • f.Write([]byte("Hello from child process!\\n")): 作成した一時ファイルにテスト用の文字列を書き込みます。
  • syscall.UnixRights(int(f.Fd())): 送信するファイルディスクリプタ(一時ファイルのFD)を補助データとしてエンコードします。
  • uc.WriteMsgUnix(dummyByte, rights, nil): 親プロセスに、ダミーの1バイトデータと、エンコードされたファイルディスクリプタを含む補助データを送信します。dummyByteは、WriteMsgUnixが少なくとも1バイトのデータを必要とするためです。

このテストは、Goのsyscallパッケージが提供する低レベルの機能が、異なるプロセス間でファイルディスクリプタを安全かつ正確に受け渡すことができることを保証します。

関連リンク

参考にした情報源リンク