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

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

このコミットは、Go言語の標準ライブラリにおいて、FreeBSDオペレーティングシステム向けにsendfileシステムコールをサポートするための変更を導入しています。具体的には、netパッケージにおけるsendfileの実装と、syscallパッケージにおけるFreeBSD固有のシステムコールラッパーの追加が含まれます。これにより、FreeBSD上でのネットワークI/Oにおいて、より効率的なデータ転送(ゼロコピー)が可能になります。

コミット

commit a9a8d7b544b62c08095c945cb642d8a307cfb4cf
Author: L Campbell <unpantsu@gmail.com>
Date:   Mon Jun 25 20:26:19 2012 -0400

    syscall, net: sendfile for FreeBSD
    
    R=golang-dev, rsc, bradfitz, devon.odell
    CC=golang-dev
    https://golang.org/cl/6221054

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

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

元コミット内容

syscall, net: sendfile for FreeBSD

R=golang-dev, rsc, bradfitz, devon.odell
CC=golang-dev
https://golang.org/cl/6221054

変更の背景

この変更の主な背景は、FreeBSD環境におけるネットワークI/Oのパフォーマンス向上です。従来のデータ転送では、ファイルからデータを読み込み、それをネットワークソケットに書き込む際に、カーネル空間とユーザー空間の間で複数回のデータコピーが発生していました。これはCPUサイクルとメモリ帯域を消費し、特に大量のデータを扱うアプリケーションではボトルネックとなる可能性がありました。

sendfileシステムコールは、このようなデータコピーのオーバーヘッドを削減するためのメカニズムを提供します。これは「ゼロコピー」技術の一種であり、カーネルが直接ファイルディスクリプタからネットワークソケットへデータを転送することを可能にします。これにより、ユーザー空間へのデータコピーが不要になり、CPU使用率の低減とスループットの向上が期待できます。

Go言語の標準ライブラリがFreeBSD上でsendfileを活用できるようにすることで、Goで書かれたネットワークアプリケーション(特にHTTPサーバーやファイル転送サービスなど)が、FreeBSDのネイティブなパフォーマンス最適化の恩恵を受けられるようになります。

前提知識の解説

sendfileシステムコール

sendfileは、あるファイルディスクリプタから別のファイルディスクリプタへデータを直接転送するためのシステムコールです。一般的なread()write()の組み合わせと比較して、以下のような利点があります。

  1. ゼロコピー: データをユーザー空間のバッファにコピーすることなく、カーネル空間内で直接転送を完了させることができます。これにより、CPUのオーバーヘッドとメモリ帯域の使用量を削減します。
  2. 効率性: 複数回のシステムコール(readwrite)を1回のsendfile呼び出しにまとめることで、システムコールオーバーヘッドを削減します。

sendfileの具体的な動作はOSによって異なりますが、基本的な目的は同じです。FreeBSDのsendfileは、ファイルディスクリプタ、ソケットディスクリプタ、オフセット、バイト数などを引数に取り、指定されたファイルの内容をソケットに転送します。

ゼロコピー (Zero-copy)

ゼロコピーとは、CPUがデータ転送に関与する回数を最小限に抑える技術の総称です。特に、カーネル空間とユーザー空間の間でのデータコピーを排除または削減することを目指します。これにより、CPUの負荷を軽減し、I/Oスループットを向上させることができます。sendfileはその代表的な実装の一つです。

Go言語のsyscallパッケージ

Go言語のsyscallパッケージは、オペレーティングシステムのプリミティブな機能(システムコール)にアクセスするためのインターフェースを提供します。これにより、Goプログラムから直接OSの機能を利用できます。syscallパッケージはOSごとに異なる実装を持ち、各OSのシステムコールをGoの関数としてラップしています。

システムコールを呼び出す際には、通常、SyscallSyscall6などの関数を使用します。これらの関数は、システムコール番号と引数を受け取り、システムコールを実行して結果を返します。引数の数が多いシステムコールの場合、より多くの引数を取れるSyscallNのような関数が必要になります。

FreeBSDのシステムコール規約 (AMD64)

FreeBSDのAMD64アーキテクチャにおけるシステムコール規約は、引数をレジスタ(rdi, rsi, rdx, rcx, r8, r9)とスタックで渡します。システムコール番号はraxレジスタに格納されます。戻り値はraxrdxレジスタに格納され、エラーコードはraxに負の値として返されるか、errnoとして設定されます。

sendfileのように引数が多いシステムコールの場合、Goのsyscallパッケージは、必要な数の引数を渡せるように、対応するSyscallN関数(例: Syscall9)と、それを呼び出すためのアセンブリコードを提供する必要があります。

技術的詳細

このコミットは、FreeBSDにおけるsendfileシステムコールのGo言語バインディングと、それを利用したnetパッケージの機能拡張に焦点を当てています。

net/sendfile_freebsd.goの新規追加

このファイルは、FreeBSD固有のsendfile実装を提供します。 sendFile関数は、io.Readerインターフェースを満たす入力からnetFD(ネットワークファイルディスクリプタ)へデータをコピーします。 FreeBSDのsendfileの特性として、以下の点が考慮されています。

  1. 正確なバイト数指定: FreeBSDのsendfileは、転送するバイト数を正確に指定する必要があります。EOFまで転送するような「0」の指定は、ファイルの内容をループバックして転送し続けるという予期せぬ動作を引き起こすため、GoのsendFile関数は、io.LimitedReaderos.File.Stat()から正確なファイルサイズを取得してremain変数に格納します。
  2. ファイルオフセットの明示的な管理: FreeBSDのsendfileは、ファイルの現在のオフセットを使用せず、常に指定されたオフセットから転送を開始します。そのため、os.File.Seek(0, os.SEEK_CUR)を使用して現在のファイルオフセットを取得し、転送が進行するにつれてオフセットを更新する必要があります。
  3. チャンク転送: maxSendfileSize (4MB) を定義し、一度に転送するデータの最大サイズを制限しています。これは、非常に大きなファイルを扱う際に、システムコールが長時間ブロックされるのを防ぐため、またはカーネルのバッファリング能力に合わせて調整するためと考えられます。
  4. エラーハンドリングと再試行: syscall.EAGAIN(一時的なエラー、非ブロッキングI/Oでよく発生)やsyscall.EINTR(システムコールがシグナルによって中断された)の場合には、適切に再試行するロジックが含まれています。

net/sendfile_stub.goの変更

sendfile_stub.goは、sendfileがサポートされていないOS向けのプレースホルダー実装を提供していました。このコミットでは、ビルドタグからfreebsdが削除されています。これは、FreeBSDがもはやスタブではなく、専用のsendfile_freebsd.goでサポートされるようになったことを意味します。

変更前: // +build darwin freebsd netbsd openbsd 変更後: // +build darwin netbsd openbsd

syscall/asm_freebsd_amd64.sの変更

このアセンブリファイルは、GoのsyscallパッケージがFreeBSDのシステムコールを呼び出すための低レベルな実装を提供します。 Syscall9という新しい関数が追加されています。これは、9つの引数を持つシステムコール(sendfileはFreeBSDで9つの引数を取ります)を呼び出すためのラッパーです。

  • runtime·entersyscall(SB)runtime·exitsyscall(SB)の呼び出し: Goランタイムがシステムコールへの出入りを追跡し、スケジューラがゴルーチンを適切に管理できるようにします。
  • 引数のレジスタへのロード: Goの関数引数(スタック上)を、FreeBSDのシステムコール規約に従ってrdi, rsi, rdx, r10, r8, r9レジスタにロードします。
  • 残りの引数のスタックへの配置: 7番目、8番目、9番目の引数は、システムコール呼び出し時にスタックのトップに配置されるように調整されます。
  • SYSCALL命令: 実際のシステムコールを実行します。
  • 戻り値の処理: システムコールの戻り値(rax, rdx)をGoの戻り値(r1, r2, err)にマッピングします。エラーの場合、errnoを適切に設定します。

syscall/syscall_freebsd.goの変更

このファイルには、FreeBSD固有のGoのシステムコールラッパーが含まれています。 Sendfile関数の実装が追加されています。

func Sendfile(outfd int, infd int, offset int64, count int) (written int, err error) {
	var writtenOut uint64 = 0
	_, _, e1 := Syscall9(SYS_SENDFILE, uintptr(infd), uintptr(outfd), uintptr(offset), uintptr(count), 0, uintptr(unsafe.Pointer(&writtenOut)), 0, 0, 0)

	written = int(writtenOut)

	if e1 != 0 {
		err = e1
	}
	return
}
  • SYS_SENDFILE: FreeBSDのsendfileシステムコールの番号です。
  • Syscall9の利用: 9つの引数を持つsendfileシステムコールを呼び出すために、新しく追加されたSyscall9アセンブリ関数を使用しています。
  • writtenOutのポインタ渡し: FreeBSDのsendfileは、実際に書き込まれたバイト数をポインタ経由で返すため、unsafe.Pointer(&writtenOut)を使用してそのアドレスをシステムコールに渡しています。
  • エラーハンドリング: Syscall9から返されたエラーコードe1をGoのエラー型に変換しています。

syscall/syscall_freebsd_amd64.goの変更

このファイルには、FreeBSD AMD64アーキテクチャ固有のGoのシステムコール定義が含まれています。 Syscall9関数のシグネチャが追加されています。これは、asm_freebsd_amd64.sで実装されたSyscall9アセンブリ関数に対応するGoの関数宣言です。

func Syscall9(num, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2 uintptr, err Errno)

この宣言により、GoのコードからSyscall9を型安全に呼び出すことが可能になります。

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

src/pkg/net/sendfile_freebsd.go (新規ファイル)

// Copyright 2011 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 net

import (
	"io"
	"os"
	"syscall"
)

// maxSendfileSize is the largest chunk size we ask the kernel to copy
// at a time.
const maxSendfileSize int = 4 << 20

// sendFile copies the contents of r to c using the sendfile
// system call to minimize copies.
//
// if handled == true, sendFile returns the number of bytes copied and any
// non-EOF error.
//
// if handled == false, sendFile performed no work.
func sendFile(c *netFD, r io.Reader) (written int64, err error, handled bool) {
	// FreeBSD uses 0 as the "until EOF" value. If you pass in more bytes than the
	// file contains, it will loop back to the beginning ad nauseum until it's sent
	// exactly the number of bytes told to. As such, we need to know exactly how many
	// bytes to send.
	var remain int64 = 0

	lr, ok := r.(*io.LimitedReader)
	if ok {
		remain, r = lr.N, lr.R
		if remain <= 0 {
			return 0, nil, true
		}
	}
	f, ok := r.(*os.File)
	if !ok {
		return 0, nil, false
	}

	if remain == 0 {
		fi, err := f.Stat()
		if err != nil {
			return 0, err, false
		}

		remain = fi.Size()
	}

	// The other quirk with FreeBSD's sendfile implementation is that it doesn't
	// use the current position of the file -- if you pass it offset 0, it starts
	// from offset 0. There's no way to tell it "start from current position", so
	// we have to manage that explicitly.
	pos, err := f.Seek(0, os.SEEK_CUR)
	if err != nil {
		return 0, err, false
	}

	c.wio.Lock()
	defer c.wio.Unlock()
	if err := c.incref(false); err != nil {
		return 0, err, true
	}
	defer c.decref()

	dst := c.sysfd
	src := int(f.Fd())
	for remain > 0 {
		n := maxSendfileSize
		if int64(n) > remain {
			n = int(remain)
		}
		n, err1 := syscall.Sendfile(dst, src, pos, n)
		if n > 0 {
			pos += int64(n)
			written += int64(n)
			remain -= int64(n)
		}
		if n == 0 && err1 == nil {
			break
		}
		if err1 == syscall.EAGAIN && c.wdeadline >= 0 {
			if err1 = pollserver.WaitWrite(c); err1 == nil {
				continue
			}
		}
		if err1 == syscall.EINTR {
			continue
		}
		if err1 != nil {
			// This includes syscall.ENOSYS (no kernel
			// support) and syscall.EINVAL (fd types which
			// don't implement sendfile together)
			err = &OpError{"sendfile", c.net, c.raddr, err1}
			break
		}
	}
	if lr != nil {
		lr.N = remain
	}
	return written, err, written > 0
}

src/pkg/net/sendfile_stub.go

--- a/src/pkg/net/sendfile_stub.go
+++ b/src/pkg/net/sendfile_stub.go
@@ -2,7 +2,7 @@
 // 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
+// +build darwin netbsd openbsd
 
 package net

src/pkg/syscall/asm_freebsd_amd64.s

--- a/src/pkg/syscall/asm_freebsd_amd64.s
+++ b/src/pkg/syscall/asm_freebsd_amd64.s
@@ -8,6 +8,7 @@
 
 // func Syscall(trap int64, a1, a2, a3 int64) (r1, r2, err int64);
 // func Syscall6(trap int64, a1, a2, a3, a4, a5, a6 int64) (r1, r2, err int64);\n+// func Syscall9(trap int64, a1, a2, a3, a4, a5, a6, a7, a8, a9 int64) (r1, r2, err int64)
 // Trap # in AX, args in DI SI DX, return in AX DX
 
 TEXT	·Syscall(SB),7,$0
@@ -56,6 +57,39 @@ ok6:
 	CALL	runtime·exitsyscall(SB)
 	RET
 
+TEXT	·Syscall9(SB),7,$0
+\tCALL	runtime·entersyscall(SB)
+\tMOVQ	8(SP), AX
+\tMOVQ	16(SP), DI
+\tMOVQ	24(SP), SI
+\tMOVQ	32(SP), DX
+\tMOVQ	40(SP), R10
+\tMOVQ	48(SP), R8
+\tMOVQ	56(SP), R9
+
+\t// shift around the last three arguments so they're at the
+\t// top of the stack when the syscall is called.
+\tMOVQ	64(SP), R11 // arg 7
+\tMOVQ	R11, 8(SP)
+\tMOVQ	72(SP), R11 // arg 8
+\tMOVQ	R11, 16(SP)
+\tMOVQ	80(SP), R11 // arg 9
+\tMOVQ	R11, 24(SP)
+
+\tSYSCALL
+\tJCC	ok9
+\tMOVQ	$-1, 88(SP)	// r1
+\tMOVQ	$0, 96(SP)	// r2
+\tMOVQ	AX, 104(SP)  // errno
+\tCALL	runtime·exitsyscall(SB)
+\tRET
+ok9:
+\tMOVQ	AX, 88(SP)	// r1
+\tMOVQ	DX, 96(SP)	// r2
+\tMOVQ	$0, 104(SP)	// errno
+\tCALL	runtime·exitsyscall(SB)
+\tRET
+
 TEXT ·RawSyscall(SB),7,$0
 	MOVQ	16(SP), DI
 	MOVQ	24(SP), SI

src/pkg/syscall/syscall_freebsd.go

--- a/src/pkg/syscall/syscall_freebsd.go
+++ b/src/pkg/syscall/syscall_freebsd.go
@@ -89,9 +89,16 @@ func Pipe(p []int) (err error) {
 	return
 }
 
-// TODO
-func Sendfile(outfd int, infd int, offset *int64, count int) (written int, err error) {\n-\treturn -1, ENOSYS
+func Sendfile(outfd int, infd int, offset int64, count int) (written int, err error) {
+	var writtenOut uint64 = 0
+	_, _, e1 := Syscall9(SYS_SENDFILE, uintptr(infd), uintptr(outfd), uintptr(offset), uintptr(count), 0, uintptr(unsafe.Pointer(&writtenOut)), 0, 0, 0)
+
+	written = int(writtenOut)
+
+	if e1 != 0 {
+		err = e1
+	}
+	return
 }
 
 func GetsockoptIPMreqn(fd, level, opt int) (*IPMreqn, error) {

src/pkg/syscall/syscall_freebsd_amd64.go

--- a/src/pkg/syscall/syscall_freebsd_amd64.go
+++ b/src/pkg/syscall/syscall_freebsd_amd64.go
@@ -40,3 +40,5 @@ func (msghdr *Msghdr) SetControllen(length int) {
 func (cmsg *Cmsghdr) SetLen(length int) {
 	cmsg.Len = uint32(length)
 }\n+\n+func Syscall9(num, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2 uintptr, err Errno)\n```

## コアとなるコードの解説

### `src/pkg/net/sendfile_freebsd.go`

このファイルは、Goの`net`パッケージがFreeBSD上で`sendfile`システムコールを利用するための主要なロジックを含んでいます。

-   **`sendFile`関数**: この関数は、`io.Reader`から`netFD`(ネットワーク接続を表す内部構造体)へデータを効率的にコピーすることを目的としています。
    -   **入力の型チェック**: `io.LimitedReader`と`os.File`の型アサーションを行い、`sendfile`が適用可能なファイルベースのリーダーであるかを確認します。`sendfile`はファイルディスクリプタを必要とするため、`os.File`以外の場合は`handled = false`を返して通常のコピー処理にフォールバックさせます。
    -   **残りのバイト数とオフセットの管理**: FreeBSDの`sendfile`の特性に合わせて、転送すべき正確なバイト数(`remain`)と、ファイルの現在のオフセット(`pos`)を厳密に管理しています。これは、`sendfile`が指定されたバイト数だけを転送し、指定されたオフセットから開始するためです。
    -   **チャンク転送ループ**: `maxSendfileSize`で定義されたチャンクサイズでデータを転送するループを回します。これにより、一度に大量のデータを転送しようとしてシステムコールが長時間ブロックされるのを防ぎます。
    -   **`syscall.Sendfile`の呼び出し**: 実際にFreeBSDの`sendfile`システムコールを呼び出しています。引数には、宛先ソケットのファイルディスクリプタ (`dst`)、ソースファイルのファイルディスクリプタ (`src`)、現在のファイルオフセット (`pos`)、そして転送するバイト数 (`n`) を渡します。
    -   **エラーと再試行**: `syscall.EAGAIN`(非ブロッキングI/Oで一時的に書き込みができない場合)や`syscall.EINTR`(システムコールがシグナルで中断された場合)のような一時的なエラーに対しては、`pollserver.WaitWrite(c)`を呼び出して書き込み可能になるまで待機し、ループを継続して再試行します。その他のエラーが発生した場合は、`OpError`を生成してループを終了します。
    -   **`written`と`remain`の更新**: 転送が成功するたびに、実際に書き込まれたバイト数 (`n`) を`written`に加算し、残りのバイト数 (`remain`) を減算し、ファイルオフセット (`pos`) を更新します。

### `src/pkg/net/sendfile_stub.go`

-   **ビルドタグの変更**: `// +build darwin freebsd netbsd openbsd` から `// +build darwin netbsd openbsd` へと変更されています。これは、FreeBSDが`sendfile`のスタブ実装を使用するOSのリストから除外されたことを意味します。つまり、FreeBSDでは専用の`sendfile_freebsd.go`がコンパイルされるようになります。

### `src/pkg/syscall/asm_freebsd_amd64.s`

-   **`TEXT ·Syscall9(SB)`の追加**: このアセンブリコードは、GoのランタイムとFreeBSDのカーネルの間で、9つの引数を持つシステムコールを呼び出すためのブリッジとして機能します。
    -   Goの関数呼び出し規約(引数がスタックに積まれる)から、FreeBSDのシステムコール規約(引数がレジスタとスタックに配置される)への変換を行います。
    -   `MOVQ`命令を使って、Goの引数を適切なレジスタ(`DI`, `SI`, `DX`, `R10`, `R8`, `R9`)に移動させます。
    -   残りの引数(7番目、8番目、9番目)は、システムコールが期待するスタック上の位置に移動させます。
    -   `SYSCALL`命令を実行して、カーネルにシステムコールを要求します。
    -   システムコールからの戻り値(`AX`, `DX`)をGoの戻り値(`r1`, `r2`, `err`)にマッピングします。エラーの場合、`errno`を適切に設定します。

### `src/pkg/syscall/syscall_freebsd.go`

-   **`Sendfile`関数の実装**: 以前は`ENOSYS`(システムコールが実装されていない)を返していた`Sendfile`関数が、FreeBSDの`sendfile`システムコールを実際に呼び出すように実装されました。
    -   `Syscall9`を呼び出すことで、アセンブリレベルで定義された9引数システムコールラッパーを利用します。
    -   `SYS_SENDFILE`は、FreeBSDにおける`sendfile`システムコールの識別子です。
    -   `unsafe.Pointer(&writtenOut)`を使用して、`sendfile`システムコールが書き込んだバイト数を格納するためのポインタを渡しています。これは、システムコールが結果を直接レジスタで返すのではなく、ポインタ経由で返す場合があるためです。
    -   システムコールからの戻り値`e1`が0でない場合(エラーの場合)、それをGoのエラー型に変換して返します。

### `src/pkg/syscall/syscall_freebsd_amd64.go`

-   **`Syscall9`関数の宣言**: `asm_freebsd_amd64.s`で実装されたアセンブリ関数に対応するGoの関数シグネチャが追加されています。これにより、Goのコードから`Syscall9`を型安全に呼び出すことが可能になります。

これらの変更により、Go言語のネットワークアプリケーションはFreeBSD上で`sendfile`のゼロコピー機能を利用できるようになり、ファイル転送などのI/O集中型タスクのパフォーマンスが大幅に向上します。

## 関連リンク

*   Go言語の`syscall`パッケージのドキュメント: [https://pkg.go.dev/syscall](https://pkg.go.dev/syscall)
*   FreeBSD `sendfile(2)` マニュアルページ: [https://www.freebsd.org/cgi/man.cgi?query=sendfile&sektion=2](https://www.freebsd.org/cgi/man.cgi?query=sendfile&sektion=2)
*   Go言語のコードレビューシステム (Gerrit) の変更リスト: [https://golang.org/cl/6221054](https://golang.org/cl/6221054)

## 参考にした情報源リンク

*   [https://github.com/golang/go/commit/a9a8d7b544b62c08095c945cb642d8a307cfb4cf](https://github.com/golang/go/commit/a9a8d7b544b62c08095c945cb642d8a307cfb4cf)
*   Go言語の`syscall`パッケージのソースコード (FreeBSD関連):
    *   `src/pkg/syscall/syscall_freebsd.go`
    *   `src/pkg/syscall/syscall_freebsd_amd64.go`
    *   `src/pkg/syscall/asm_freebsd_amd64.s`
*   Go言語の`net`パッケージのソースコード (FreeBSD関連):
    *   `src/pkg/net/sendfile_freebsd.go`
    *   `src/pkg/net/sendfile_stub.go`
*   ゼロコピー技術に関する一般的な情報源 (例: Wikipedia, 技術ブログなど)
*   FreeBSDのシステムコール規約に関する情報源 (例: FreeBSD開発者ドキュメント)