[インデックス 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()
の組み合わせと比較して、以下のような利点があります。
- ゼロコピー: データをユーザー空間のバッファにコピーすることなく、カーネル空間内で直接転送を完了させることができます。これにより、CPUのオーバーヘッドとメモリ帯域の使用量を削減します。
- 効率性: 複数回のシステムコール(
read
とwrite
)を1回のsendfile
呼び出しにまとめることで、システムコールオーバーヘッドを削減します。
sendfile
の具体的な動作はOSによって異なりますが、基本的な目的は同じです。FreeBSDのsendfile
は、ファイルディスクリプタ、ソケットディスクリプタ、オフセット、バイト数などを引数に取り、指定されたファイルの内容をソケットに転送します。
ゼロコピー (Zero-copy)
ゼロコピーとは、CPUがデータ転送に関与する回数を最小限に抑える技術の総称です。特に、カーネル空間とユーザー空間の間でのデータコピーを排除または削減することを目指します。これにより、CPUの負荷を軽減し、I/Oスループットを向上させることができます。sendfile
はその代表的な実装の一つです。
Go言語のsyscall
パッケージ
Go言語のsyscall
パッケージは、オペレーティングシステムのプリミティブな機能(システムコール)にアクセスするためのインターフェースを提供します。これにより、Goプログラムから直接OSの機能を利用できます。syscall
パッケージはOSごとに異なる実装を持ち、各OSのシステムコールをGoの関数としてラップしています。
システムコールを呼び出す際には、通常、Syscall
、Syscall6
などの関数を使用します。これらの関数は、システムコール番号と引数を受け取り、システムコールを実行して結果を返します。引数の数が多いシステムコールの場合、より多くの引数を取れるSyscallN
のような関数が必要になります。
FreeBSDのシステムコール規約 (AMD64)
FreeBSDのAMD64アーキテクチャにおけるシステムコール規約は、引数をレジスタ(rdi
, rsi
, rdx
, rcx
, r8
, r9
)とスタックで渡します。システムコール番号はrax
レジスタに格納されます。戻り値はrax
とrdx
レジスタに格納され、エラーコードはrax
に負の値として返されるか、errno
として設定されます。
sendfile
のように引数が多いシステムコールの場合、Goのsyscall
パッケージは、必要な数の引数を渡せるように、対応するSyscallN
関数(例: Syscall9
)と、それを呼び出すためのアセンブリコードを提供する必要があります。
技術的詳細
このコミットは、FreeBSDにおけるsendfile
システムコールのGo言語バインディングと、それを利用したnet
パッケージの機能拡張に焦点を当てています。
net/sendfile_freebsd.go
の新規追加
このファイルは、FreeBSD固有のsendfile
実装を提供します。
sendFile
関数は、io.Reader
インターフェースを満たす入力からnetFD
(ネットワークファイルディスクリプタ)へデータをコピーします。
FreeBSDのsendfile
の特性として、以下の点が考慮されています。
- 正確なバイト数指定: FreeBSDの
sendfile
は、転送するバイト数を正確に指定する必要があります。EOFまで転送するような「0」の指定は、ファイルの内容をループバックして転送し続けるという予期せぬ動作を引き起こすため、GoのsendFile
関数は、io.LimitedReader
やos.File.Stat()
から正確なファイルサイズを取得してremain
変数に格納します。 - ファイルオフセットの明示的な管理: FreeBSDの
sendfile
は、ファイルの現在のオフセットを使用せず、常に指定されたオフセットから転送を開始します。そのため、os.File.Seek(0, os.SEEK_CUR)
を使用して現在のファイルオフセットを取得し、転送が進行するにつれてオフセットを更新する必要があります。 - チャンク転送:
maxSendfileSize
(4MB) を定義し、一度に転送するデータの最大サイズを制限しています。これは、非常に大きなファイルを扱う際に、システムコールが長時間ブロックされるのを防ぐため、またはカーネルのバッファリング能力に合わせて調整するためと考えられます。 - エラーハンドリングと再試行:
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開発者ドキュメント)