[インデックス 18334] ファイルの概要
このコミットは、Go言語のnet
パッケージにおけるPlan 9固有のネットワーク処理に関する複数の改善とバグ修正を目的としています。具体的には、デフォルトのネットワークディレクトリの扱い、fdMutex
の導入による並行処理の安全性向上、そしてTCPキープアライブ機能の追加が含まれています。
コミット
commit 52125738f3ca6f30364eebf0f4d673f73a71c248
Author: Jeff Sickel <jas@corpus-callosum.com>
Date: Wed Jan 22 22:21:53 2014 +0100
net: plan9 changes for default net directory
This change include updates to the probeIPv4Stack
and probeIPv6Stack to ensure that one or both
protocols are supported by ip(3).
The addition of fdMutex to netFD fixes the
TestTCPConcurrentAccept failures.
Additional changes add support for keepalive.
R=golang-codereviews, 0intro
CC=golang-codereviews, rsc
https://golang.org/cl/49920048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/52125738f3ca6f30364eebf0f4d673f73a71c248
元コミット内容
net: plan9 changes for default net directory
この変更には、probeIPv4Stack
とprobeIPv6Stack
の更新が含まれており、ip(3)
によってIPv4またはIPv6のいずれか、あるいは両方のプロトコルがサポートされていることを確認します。
netFD
へのfdMutex
の追加は、TestTCPConcurrentAccept
の失敗を修正します。
追加の変更として、キープアライブのサポートが追加されています。
変更の背景
このコミットは、Go言語のnet
パッケージがPlan 9オペレーティングシステム上でより堅牢かつ機能的に動作するようにするためのものです。主な背景は以下の点が挙げられます。
- Plan 9のネットワークモデルへの適応: Plan 9は「すべてがファイルである」という哲学に基づいており、ネットワークリソースもファイルシステムを通じてアクセスされます。これまでの実装では、
/net
ディレクトリへのパスがハードコードされている部分があり、より柔軟な設定(例えば、異なるネットワークディレクトリを使用する場合)に対応できていませんでした。このコミットでは、デフォルトのネットワークディレクトリをnetdir
変数で管理するように変更し、より適応性の高い実装を目指しています。 - IPv4/IPv6スタックのプローブの改善: Plan 9の
ip(3)
インターフェースを通じて、システムがどのIPプロトコル(IPv4、IPv6)をサポートしているかを正確に検出する必要がありました。これにより、Goのネットワークスタックが利用可能なプロトコルを適切に利用できるようになります。 - 並行処理における競合状態の解消:
TestTCPConcurrentAccept
テストの失敗は、複数のゴルーチンが同時にTCP接続を受け入れる際に、共有リソース(特にファイルディスクリプタ)へのアクセスで競合状態が発生していたことを示唆しています。これは、netFD
構造体におけるファイルディスクリプタのライフサイクル管理とアクセス同期が不十分であったためと考えられます。fdMutex
の導入は、この問題に対処し、並行アクセスの安全性を確保することを目的としています。 - TCPキープアライブ機能の追加: ネットワーク接続の健全性を維持し、アイドル状態の接続がファイアウォールやNATによって切断されるのを防ぐために、TCPキープアライブ機能は重要です。Plan 9のファイルシステムベースのネットワークインターフェースを通じて、この機能を実現する必要がありました。
前提知識の解説
Plan 9のネットワークモデルとip(3)
Plan 9は、そのユニークな設計思想「すべてがファイルである」をネットワークにも適用しています。ネットワークリソースは、/net
という特殊なディレクトリ以下のファイルとして表現されます。例えば、TCP接続は/net/tcp
以下に、UDP接続は/net/udp
以下に、それぞれ対応するファイルやディレクトリとして存在します。
ctl
ファイル: 各ネットワーク接続やプロトコルには、その動作を制御するためのctl
(コントロール)ファイルが存在します。このファイルに特定のコマンドを書き込むことで、接続の設定変更(例: キープアライブの有効化)や状態の取得が行えます。data
ファイル: 実際のデータ送受信はdata
ファイルを通じて行われます。ip(3)
: Plan 9のCライブラリルーチンで、IPアドレスやイーサネットアドレスを操作するためのインターフェースを提供します。このコミットでは、ip(3)
がどのIPスタック(IPv4またはIPv6)をサポートしているかをプローブする機能が言及されています。Plan 9では、IPv6がネイティブに扱われ、IPv4アドレスはIPv6空間内で表現されることが多いです。
Go言語のnet
パッケージとfdMutex
Go言語のnet
パッケージは、OSに依存しないネットワークインターフェースを提供しますが、内部的には各OSのシステムコールを利用してネットワーク操作を行います。
netFD
構造体:netFD
は、Goのnet
パッケージが内部的に使用するネットワークファイルディスクリプタを表す構造体です。これは、OSレベルのファイルディスクリプタ(Plan 9では*os.File
)をラップし、ネットワーク接続のプロトコル、アドレス、制御ファイル、データファイルなどの情報を含みます。fdMutex
:fdMutex
は、Goのnet
パッケージ内でファイルディスクリプタへの並行アクセスを安全に管理するために設計された特殊なミューテックス(相互排他)メカニズムです。これは、複数のゴルーチンが同時に同じファイルディスクリプタに対して読み書きを行おうとした際に発生する競合状態を防ぎます。fdMutex
は、ファイルディスクリプタのクローズ状態、読み書きロック、参照カウントなどを単一の整数にパックして効率的に管理します。TestTCPConcurrentAccept
のような並行性の高いテストで発生する失敗は、通常、このような共有リソースへの不適切な同期が原因で発生します。
TCPキープアライブ
TCPキープアライブは、アイドル状態のTCP接続がまだ有効であるかどうかを確認するために、定期的に小さなパケットを送信するメカニズムです。これにより、以下のような利点があります。
- 接続の維持: ファイアウォールやNATデバイスが、一定期間アイドル状態の接続を自動的に切断するのを防ぎます。
- デッドピアの検出: 接続の相手側がクラッシュしたり、ネットワークから切断されたりした場合に、それを検出し、リソースを解放することができます。
- リソースの解放: 不要になった接続が長時間リソースを占有し続けるのを防ぎます。
Plan 9では、このキープアライブ機能もctl
ファイルへの書き込みを通じて制御されます。
技術的詳細
このコミットは、Plan 9環境におけるGoのネットワークスタックの堅牢性と機能性を向上させるために、以下の主要な技術的変更を導入しています。
-
デフォルトネットワークディレクトリの抽象化:
- これまでのコードでは、
/net
というパスが多くの場所でハードコードされていました。このコミットでは、netdir
というグローバル変数を導入し、sysInit()
関数で/net
に初期化するように変更しました。 - これにより、
newFD
関数やfile_plan9.go
、ipsock_plan9.go
、lookup_plan9.go
内のファイルパス構築において、ハードコードされた/net/
の代わりにnetdir + "/"
を使用するようになりました。これは、将来的に異なるネットワークディレクトリ構成に対応するための柔軟性を提供します。
- これまでのコードでは、
-
fdMutex
の導入によるnetFD
の同期強化:netFD
構造体にfdmu fdMutex
フィールドが追加されました。これは、ファイルディスクリプタ(ctl
とdata
)へのアクセスを同期するためのものです。netFD
のRead
およびWrite
メソッドにfd.readLock()
/fd.readUnlock()
およびfd.writeLock()
/fd.writeUnlock()
が導入されました。これにより、複数のゴルーチンが同時に同じnetFD
に対して読み書きを行おうとした際に、競合状態が発生するのを防ぎます。incref()
,decref()
,destroy()
などのメソッドが追加され、netFD
の参照カウントとライフサイクル管理が強化されました。これにより、ファイルディスクリプタが適切にクローズされ、リソースリークが防止されます。特に、Close()
メソッドはfdmu.IncrefAndClose()
を呼び出すことで、安全にクローズ処理を開始します。acceptPlan9()
メソッドにもl.readLock()
/l.readUnlock()
が追加され、並行して接続を受け入れる際の安全性が向上しました。これは、TestTCPConcurrentAccept
の失敗を修正する直接的な対応です。
-
IPv4/IPv6スタックプローブの改善:
probeIPv4Stack()
とprobeIPv6Stack()
関数が更新され、Plan 9のip(3)
インターフェースを通じて、システムがどのIPプロトコルをサポートしているかをより正確に検出するようになりました。- 新しいヘルパー関数
probe(filename, query string, bufSize int)
が導入され、指定されたファイル(例:/net/ipselftab
,/net/iproute
)を読み込み、特定のクエリ文字列が存在するかどうかを確認することで、プロトコルサポートを判断します。 probeIPv4Stack()
は/net/ipselftab
をチェックしてIPv4のサポートを、probeIPv6Stack()
は/net/iproute
をチェックしてIPv6およびIPv4-mappingのサポートを判断します。
-
TCPキープアライブ機能の追加:
src/pkg/net/sockopt_plan9.go
とsrc/pkg/net/tcpsockopt_plan9.go
という新しいファイルが追加されました。sockopt_plan9.go
にはsetKeepAlive(fd *netFD, keepalive bool) error
関数が定義され、fd.ctl.WriteAt([]byte("keepalive"), 0)
を通じてキープアライブを有効にするコマンドをctl
ファイルに書き込みます。tcpsockopt_plan9.go
にはsetKeepAlivePeriod(fd *netFD, d time.Duration) error
関数が定義され、"keepalive " + string(int64(d/time.Millisecond))
という形式のコマンドをctl
ファイルに書き込むことで、キープアライブの間隔を設定します。tcpsock_plan9.go
のTCPConn
構造体にSetKeepAlive
とSetKeepAlivePeriod
メソッドが実装され、これらの新しい関数を呼び出すことで、GoのAPIを通じてTCPキープアライブを設定できるようになりました。
これらの変更は、Plan 9環境におけるGoのネットワーク機能の信頼性、並行処理の安全性、および機能の完全性を大幅に向上させています。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
src/pkg/net/fd_plan9.go
:netFD
構造体の変更、fdMutex
の導入、参照カウントとロックメカニズムの追加。src/pkg/net/file_plan9.go
: デフォルトネットワークディレクトリのパス変更。src/pkg/net/ipsock_plan9.go
: IPv4/IPv6スタックプローブロジックの更新。src/pkg/net/lookup_plan9.go
: デフォルトネットワークディレクトリのパス変更。src/pkg/net/sockopt_plan9.go
(新規): TCPキープアライブ有効化のヘルパー関数。src/pkg/net/tcpsock_plan9.go
:TCPConn
にキープアライブ設定メソッドの実装。src/pkg/net/tcpsockopt_plan9.go
(新規): TCPキープアライブ期間設定のヘルパー関数。src/pkg/net/udpsock_plan9.go
:ListenUDP
の戻り値の型変更。
特に、src/pkg/net/fd_plan9.go
におけるnetFD
構造体と関連メソッドの変更、およびsrc/pkg/net/ipsock_plan9.go
におけるprobeIPv4Stack
/probeIPv6Stack
の変更がコアとなります。
src/pkg/net/fd_plan9.go
の変更例
--- a/src/pkg/net/fd_plan9.go
+++ b/src/pkg/net/fd_plan9.go
@@ -13,12 +13,23 @@ import (
// Network file descritor.
type netFD struct {
- proto, name, dir string
- ctl, data *os.File
- laddr, raddr Addr
+ // locking/lifetime of sysfd + serialize access to Read and Write methods
+ fdmu fdMutex
+
+ // immutable until Close
+ proto string
+ n string
+ dir string
+ ctl, data *os.File
+ laddr, raddr Addr
}
+var (
+ netdir string // default network
+)
+
func sysInit() {
+ netdir = "/net"
}
func dial(net string, ra Addr, dialer func(time.Time) (Conn, error), deadline time.Time) (Conn, error) {
@@ -27,16 +38,99 @@ func dial(net string, ra Addr, dialer func(time.Time) (Conn, error), deadline ti
return dialChannel(net, ra, dialer, deadline)
}
-func newFD(proto, name string, ctl, data *os.File, laddr, raddr Addr) *netFD {
- return &netFD{proto, name, "/net/" + proto + "/" + name, ctl, data, laddr, raddr}
+func newFD(proto, name string, ctl, data *os.File, laddr, raddr Addr) (*netFD, error) {
+ return &netFD{proto: proto, n: name, dir: netdir + "/" + proto + "/" + name, ctl: ctl, data: data, laddr: laddr, raddr: raddr}, nil
+}
+
+func (fd *netFD) init() error {
+ // stub for future fd.pd.Init(fd)
+ return nil
+}
+
+func (fd *netFD) name() string {
+ var ls, rs string
+ if fd.laddr != nil {
+ ls = fd.laddr.String()
+ }
+ if fd.raddr != nil {
+ rs = fd.raddr.String()
+ }
+ return fd.proto + ":" + ls + "->" + rs
}
func (fd *netFD) ok() bool { return fd != nil && fd.ctl != nil }
+func (fd *netFD) destroy() {
+ if !fd.ok() {
+ return
+ }
+ err := fd.ctl.Close()
+ if fd.data != nil {
+ if err1 := fd.data.Close(); err1 != nil && err == nil {
+ err = err1
+ }
+ }
+ fd.ctl = nil
+ fd.data = nil
+}
+
+// Add a reference to this fd.
+// Returns an error if the fd cannot be used.
+func (fd *netFD) incref() error {
+ if !fd.fdmu.Incref() {
+ return errClosing
+ }
+ return nil
+}
+
+// Remove a reference to this FD and close if we've been asked to do so
+// (and there are no references left).
+func (fd *netFD) decref() {
+ if fd.fdmu.Decref() {
+ fd.destroy()
+ }
+}
+
+// Add a reference to this fd and lock for reading.
+// Returns an error if the fd cannot be used.
+func (fd *netFD) readLock() error {
+ if !fd.fdmu.RWLock(true) {
+ return errClosing
+ }
+ return nil
+}
+
+// Unlock for reading and remove a reference to this FD.
+func (fd *netFD) readUnlock() {
+ if fd.fdmu.RWUnlock(true) {
+ fd.destroy()
+ }
+}
+
+// Add a reference to this fd and lock for writing.
+// Returns an error if the fd cannot be used.
+func (fd *netFD) writeLock() error {
+ if !fd.fdmu.RWLock(false) {
+ return errClosing
+ }
+ return nil
+}
+
+// Unlock for writing and remove a reference to this FD.
+func (fd *netFD) writeUnlock() {
+ if fd.fdmu.RWUnlock(false) {
+ fd.destroy()
+ }
+}
+
func (fd *netFD) Read(b []byte) (n int, err error) {
if !fd.ok() || fd.data == nil {
return 0, syscall.EINVAL
}
+ if err := fd.readLock(); err != nil {
+ return 0, err
+ }
+ defer fd.readUnlock()
n, err = fd.data.Read(b)
if fd.proto == "udp" && err == io.EOF {
n = 0
@@ -49,6 +143,10 @@ func (fd *netFD) Write(b []byte) (n int, err error) {
if !fd.ok() || fd.data == nil {
return 0, syscall.EINVAL
}
+ if err := fd.writeLock(); err != nil {
+ return 0, err
+ }
+ defer fd.writeUnlock()
return fd.data.Write(b)
}
@@ -67,6 +165,9 @@ func (fd *netFD) CloseWrite() error {
}
func (fd *netFD) Close() error {
+ if !fd.fdmu.IncrefAndClose() {
+ return errClosing
+ }
if !fd.ok() {
return syscall.EINVAL
}
src/pkg/net/ipsock_plan9.go
の変更例
--- a/src/pkg/net/ipsock_plan9.go
+++ b/src/pkg/net/ipsock_plan9.go
@@ -12,19 +12,45 @@ import (
"syscall"
)
+func probe(filename, query string, bufSize int) bool {
+ var file *file
+ var err error
+ if file, err = open(filename); err != nil {
+ return false
+ }
+
+ r := false
+ for line, ok := file.readLine(); ok && !r; line, ok = file.readLine() {
+ f := getFields(line)
+ if len(f) < 3 {
+ continue
+ }
+ for i := 0; i < len(f); i++ {
+ if query == f[i] {
+ r = true
+ break
+ }
+ }
+ }
+ file.close()
+ return r
+}
+
func probeIPv4Stack() bool {
- // TODO(mikio): implement this when Plan 9 supports IPv6-only
- // kernel.
- return true
+ return probe(netdir+"/ipselftab", "127.0.0.1", 128)
}
// probeIPv6Stack returns two boolean values. If the first boolean
// value is true, kernel supports basic IPv6 functionality. If the
// second boolean value is true, kernel supports IPv6 IPv4-mapping.
func probeIPv6Stack() (supportsIPv6, supportsIPv4map bool) {
- // TODO(mikio): implement this once Plan 9 gets an IPv6
- // protocol stack implementation.
- return false, false
+ // Plan 9 uses IPv6 natively, see ip(3).
+ r := probe(netdir+"/iproute", "6i", 128)
+ v := false
+ if r {
+ v = probe(netdir+"/iproute", "4b", 128)
+ }
+ return r, v
}
// parsePlan9Addr parses address of the form [ip!]port (e.g. 127.0.0.1!80).
コアとなるコードの解説
fd_plan9.go
におけるnetFD
の変更
fdmu fdMutex
の追加:netFD
構造体にfdmu
というfdMutex
型のフィールドが追加されました。これは、このnetFD
がラップするファイルディスクリプタ(ctl
とdata
)へのアクセスを同期するための鍵となります。netdir
グローバル変数:sysInit()
関数で/net
に初期化されるnetdir
変数が導入され、ファイルパスの構築が抽象化されました。これにより、/net/
がハードコードされていた箇所がnetdir + "/"
に置き換えられ、より柔軟な設定が可能になります。newFD
関数の変更:newFD
関数は、netFD
の初期化時にnetdir
を使用するように変更され、またエラーを返すようになりました。- 参照カウントとロックメカニズム:
incref()
とdecref()
メソッドは、netFD
の参照カウントを管理します。incref()
は参照を増やし、decref()
は参照を減らします。参照がゼロになり、かつクローズが要求されている場合にdestroy()
が呼び出され、ファイルディスクリプタが閉じられます。readLock()
、readUnlock()
、writeLock()
、writeUnlock()
メソッドは、fdMutex
を利用して読み書き操作の排他制御を行います。これにより、Read
やWrite
メソッドが並行して呼び出された際の競合状態が防止されます。Close()
メソッドは、fdmu.IncrefAndClose()
を呼び出すことで、安全にクローズ処理を開始します。これにより、進行中の読み書き操作が完了するまでファイルディスクリプタのクローズが遅延され、データ破損を防ぎます。
Read
およびWrite
メソッドの変更: これらのメソッドは、実際のファイル操作の前にreadLock()
またはwriteLock()
を呼び出し、操作後にreadUnlock()
またはwriteUnlock()
をdefer
で呼び出すように変更されました。これにより、ファイルディスクリプタへのアクセスが同期され、TestTCPConcurrentAccept
のような並行テストでの失敗が修正されます。
ipsock_plan9.go
におけるプローブロジックの変更
probe
ヘルパー関数の導入: 新しい汎用的なprobe
関数が追加されました。この関数は、指定されたファイル(例:/net/ipselftab
や/net/iproute
)を読み込み、特定のクエリ文字列(例: "127.0.0.1"や"6i")が存在するかどうかをチェックします。これにより、IPスタックのサポート状況を判断します。probeIPv4Stack()
の変更: これまで常にtrue
を返していたprobeIPv4Stack()
は、probe(netdir+"/ipselftab", "127.0.0.1", 128)
を呼び出すように変更されました。これは、Plan 9の/net/ipselftab
ファイルにループバックアドレスが存在するかどうかを確認することで、IPv4スタックの存在をより正確に判断します。probeIPv6Stack()
の変更: これまで常にfalse, false
を返していたprobeIPv6Stack()
は、probe(netdir+"/iproute", "6i", 128)
を呼び出してIPv6のサポートを、さらにIPv6がサポートされている場合にprobe(netdir+"/iproute", "4b", 128)
を呼び出してIPv4-mappingのサポートを判断するように変更されました。Plan 9がIPv6をネイティブに扱うという特性を考慮した実装です。
これらの変更により、Goのnet
パッケージはPlan 9環境において、より正確なネットワーク機能の検出と、並行処理における高い信頼性を提供できるようになりました。
関連リンク
- GitHub Commit: https://github.com/golang/go/commit/52125738f3ca6f30364eebf0f4d673f73a71c248
- Gerrit Code Review: https://golang.org/cl/49920048
参考にした情報源リンク
- Plan 9 Networking Model:
- Go
fdMutex
: - Go
TestTCPConcurrentAccept
failures: - Plan 9 TCP Keepalive: