[インデックス 17010] ファイルの概要
このコミットは、Go言語の net
パッケージにおけるソケットアドレスの扱いを改善し、syscall.Sockaddr
への不要な変換を削減することを目的としています。これにより、特にBSD系のOSにおけるランタイム統合型ネットワークポーラーの準備を進めています。
コミット
commit 8a7def2b3b8fc801eb4f02e58328e84ee311910e
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date: Sat Aug 3 13:32:22 2013 +0900
net: reduce unnecessary syscall.Sockaddr conversions
This CL makes IPAddr, UDPAddr and TCPAddr implement sockaddr
interface, UnixAddr is already sockaddr interface compliant, and
reduces unnecessary conversions between net.Addr, net.sockaddr and
syscall.Sockaddr.
This is in preparation for runtime-integrated network pollster for BSD
variants.
Update #5199
R=golang-dev, dave, bradfitz
CC=golang-dev
https://golang.org/cl/12010043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8a7def2b3b8fc801eb4f02e58328e84ee311910e
元コミット内容
net: reduce unnecessary syscall.Sockaddr conversions
この変更は、IPAddr
、UDPAddr
、TCPAddr
型が sockaddr
インターフェースを実装するようにし、UnixAddr
が既に sockaddr
インターフェースに準拠していることを利用して、net.Addr
、net.sockaddr
、syscall.Sockaddr
間の不要な変換を削減します。これは、BSD系のOS向けにランタイム統合型ネットワークポーラーを準備するための一環です。
関連するIssue: #5199
変更の背景
Go言語のネットワークスタックは、OSのシステムコールを抽象化して、クロスプラットフォームで一貫したネットワークプログラミングインターフェースを提供しています。この抽象化の過程で、Goの内部表現(net.Addr
や net.sockaddr
インターフェース)と、OSが要求するシステムコールレベルの表現(syscall.Sockaddr
)との間で頻繁なデータ変換が発生していました。
特に、syscall.Sockaddr
はOS固有のソケットアドレス構造体(例: syscall.SockaddrInet4
, syscall.SockaddrInet6
, syscall.SockaddrUnix
など)を抽象化するインターフェースであり、Goの net
パッケージ内でこれらの変換が繰り返されることは、パフォーマンスのオーバーヘッドやコードの複雑さを招いていました。
このコミットの主な背景は以下の点にあります。
- パフォーマンスの最適化:
net.Addr
からsyscall.Sockaddr
への頻繁な変換は、特に高負荷なネットワークアプリケーションにおいて、CPUサイクルを無駄に消費する可能性がありました。この変換を削減することで、ネットワーク処理の効率化が期待されます。 - コードの簡素化と保守性の向上: 変換ロジックが複数箇所に散らばっていると、コードの可読性が低下し、バグの温床となる可能性があります。インターフェースの実装を統一することで、コードベースがよりクリーンになり、将来的な変更やデバッグが容易になります。
- BSD系OS向けネットワークポーラーの準備: コミットメッセージに明記されているように、「runtime-integrated network pollster for BSD variants」の準備が重要な動機です。ネットワークポーラーは、複数のI/O操作を効率的に監視し、準備ができたI/Oイベントをアプリケーションに通知するメカニズムです。BSD系のOS(FreeBSD, OpenBSD, NetBSDなど)では、
kqueue
などのポーリングメカニズムが利用されます。これらのポーラーとGoランタイムをより密接に統合するためには、ソケットアドレスの表現と変換を最適化し、システムコール層との連携をスムーズにする必要がありました。不要な変換を排除することで、ポーラーが直接Goの内部アドレス表現を扱えるようになり、効率的なI/O多重化が可能になります。 - Issue #5199 の解決: このコミットは、GoのIssueトラッカーで報告されていた #5199 に関連しています。このIssueは、
net
パッケージにおけるソケットアドレスの変換効率に関するものであったと推測されます。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびネットワークプログラミングに関する基本的な概念を理解しておく必要があります。
-
net.Addr
インターフェース: Go言語のnet
パッケージで定義されている基本的なインターフェースで、ネットワークアドレスを表します。type Addr interface { Network() string // "tcp", "udp", "ip", "unix" など String() string // アドレスの文字列表現 }
TCPAddr
,UDPAddr
,IPAddr
,UnixAddr
など、具体的なアドレス型がこのインターフェースを実装しています。 -
syscall.Sockaddr
インターフェース: Go言語のsyscall
パッケージで定義されているインターフェースで、OSのシステムコールが期待するソケットアドレス構造体(例:sockaddr_in
,sockaddr_in6
,sockaddr_un
など)を抽象化します。type Sockaddr interface { // このインターフェース自体にはメソッドは定義されていないが、 // 各OS固有のSockaddr実装(例: SockaddrInet4)が // 内部的にOSのC言語構造体に対応するフィールドを持つ。 }
Goの
net
パッケージは、ネットワーク操作を行う際に、Goのnet.Addr
型をこのsyscall.Sockaddr
型に変換してシステムコールに渡す必要があります。 -
net.sockaddr
インターフェース (内部インターフェース): このコミットで言及されているnet.sockaddr
は、Goのnet
パッケージ内部で定義されている非公開(またはパッケージプライベート)のインターフェースです。これは、net.Addr
とsyscall.Sockaddr
の間の変換ロジックをカプセル化し、ソケット操作の共通部分で利用されることを意図しています。コミット前のコードでは、IPAddr
,UDPAddr
,TCPAddr
は直接sockaddr
インターフェースを実装していませんでした。 -
ソケットプログラミングの基本:
- ソケット (Socket): ネットワーク通信のエンドポイント。ファイルディスクリプタのように扱われます。
bind()
: ソケットにローカルアドレス(IPアドレスとポート番号)を割り当てるシステムコール。サーバー側で特定のポートで接続を待ち受けるために使用されます。connect()
: ソケットをリモートアドレスに接続するシステムコール。クライアント側でサーバーに接続するために使用されます。listen()
: サーバーソケットが着信接続を待ち受ける状態にするシステムコール。accept()
: サーバーソケットが着信接続を受け入れるシステムコール。syscall
パッケージ: GoプログラムからOSのシステムコールを直接呼び出すためのパッケージ。低レベルのネットワーク操作やファイル操作などに使用されます。
-
ネットワークポーラー (Network Pollster): 複数のI/Oディスクリプタ(ソケットなど)の状態変化(読み込み可能、書き込み可能など)を効率的に監視するためのメカニズム。Unix/Linuxでは
select
,poll
,epoll
が、BSD系OSではkqueue
が代表的です。Goランタイムは、これらのポーラーを利用して、ゴルーチンのスケジューリングとネットワークI/Oを非同期に処理しています。 -
IPAddr
,UDPAddr
,TCPAddr
,UnixAddr
: Goのnet
パッケージで提供される具体的なネットワークアドレス型。IPAddr
: IPアドレスのみを表す。UDPAddr
: UDP通信のためのIPアドレスとポート番号。TCPAddr
: TCP通信のためのIPアドレスとポート番号。UnixAddr
: Unixドメインソケットのためのパス。
技術的詳細
このコミットの核心は、Goの net
パッケージ内部でソケットアドレスを扱う方法の変更にあります。
変更前:
IPAddr
, UDPAddr
, TCPAddr
といった具体的なアドレス型は、直接 net
パッケージ内部の sockaddr
インターフェースを実装していませんでした。これらの型を syscall.Sockaddr
に変換する必要がある場合、toAddr()
メソッドを呼び出して net.sockaddr
インターフェース型に変換し、さらにその sockaddr
インターフェース型から sockaddr()
メソッドを呼び出して syscall.Sockaddr
に変換するという、多段階の変換プロセスが必要でした。
特に、internetSocket
関数(TCP/UDP/IPソケットの作成に使用される汎用関数)は、laddr
(ローカルアドレス) と raddr
(リモートアドレス) を sockaddr
インターフェース型として受け取っていました。関数内部でこれらの sockaddr
インターフェースから syscall.Sockaddr
への変換が行われていました。
変更後:
-
IPAddr
,UDPAddr
,TCPAddr
がsockaddr
インターフェースを直接実装: このコミットにより、IPAddr
,UDPAddr
,TCPAddr
型が、net
パッケージ内部で定義されているsockaddr
インターフェースを直接実装するようになりました。具体的には、これらの型にsockaddr(family int) (syscall.Sockaddr, error)
メソッドが追加されました。これにより、これらのアドレス型から直接syscall.Sockaddr
を取得できるようになります。UnixAddr
は元々sockaddr
インターフェースに準拠していたため、変更はありません。 -
toAddr()
メソッドの役割変更:toAddr()
メソッドは、以前は*IPAddr
や*TCPAddr
などのポインタ型をsockaddr
インターフェース型に変換するために使用されていましたが、この変更により、その役割が簡素化されました。IPAddr
,UDPAddr
,TCPAddr
自体がsockaddr
インターフェースを実装したため、toAddr()
は単にレシーバ自身をsockaddr
インターフェースとして返すようになりました。これにより、不要な型変換が削減されます。 -
internetSocket
関数の引数変更:internetSocket
関数は、laddr
とraddr
の引数として、以前はsockaddr
インターフェース型を受け取っていましたが、このコミットにより、直接IPAddr
,UDPAddr
,TCPAddr
などの具体的なアドレス型(またはそれらをラップするsockaddr
インターフェース型)を受け取るように変更されました。これにより、internetSocket
内部でのsyscall.Sockaddr
への変換ロジックが簡素化され、呼び出し元から渡されたアドレス型が直接利用されるようになります。 -
socket
関数の引数変更と内部ロジックの修正:socket
関数は、ソケットの作成と初期化を行う汎用関数です。この関数もladdr
とraddr
の引数としてsockaddr
インターフェース型を直接受け取るように変更されました。関数内部では、laddr.sockaddr(f)
やraddr.sockaddr(f)
を呼び出すことで、直接syscall.Sockaddr
を取得し、syscall.Bind
やfd.connect
に渡すようになりました。これにより、中間的なsyscall.Sockaddr
変数の宣言と代入が不要になり、コードがより直接的になります。 -
listenerSockaddr
関数の引数変更:listenerSockaddr
関数は、リスナーソケットのアドレスを準備するために使用されます。この関数もladdr
引数としてsockaddr
インターフェース型を直接受け取るように変更され、内部でladdr.sockaddr(f)
を呼び出すことでsyscall.Sockaddr
を取得するようになりました。
これらの変更により、net.Addr
の具体的な実装型(IPAddr
, UDPAddr
, TCPAddr
)が net
パッケージ内部の sockaddr
インターフェースを直接実装することで、net.Addr
-> net.sockaddr
-> syscall.Sockaddr
という多段階の変換パスが短縮され、直接 net.Addr
-> syscall.Sockaddr
の変換が可能になります。これにより、コードのパスが短くなり、変換に伴うオーバーヘッドが削減されます。
特に、BSD系のOSにおけるネットワークポーラーとの統合を考慮すると、この最適化は重要です。ポーラーは低レベルのソケット操作に密接に関わるため、アドレス変換の効率化はシステムコール呼び出しのオーバーヘッドを減らし、全体的なI/O性能を向上させることに寄与します。
コアとなるコードの変更箇所
変更は主に src/pkg/net
ディレクトリ内の以下のファイルに集中しています。
src/pkg/net/iprawsock_posix.go
src/pkg/net/ipsock_posix.go
src/pkg/net/sock_posix.go
src/pkg/net/sock_unix.go
src/pkg/net/sock_windows.go
src/pkg/net/tcpsock_posix.go
src/pkg/net/udpsock_posix.go
src/pkg/net/unixsock_posix.go
具体的な変更のパターンは以下の通りです。
-
IPAddr
,UDPAddr
,TCPAddr
にsockaddr
メソッドを追加/修正: 例:src/pkg/net/iprawsock_posix.go
--- a/src/pkg/net/iprawsock_posix.go +++ b/src/pkg/net/iprawsock_posix.go @@ -39,12 +39,15 @@ func (a *IPAddr) isWildcard() bool { } func (a *IPAddr) sockaddr(family int) (syscall.Sockaddr, error) { + if a == nil { + return nil, nil + } return ipToSockaddr(family, a.IP, 0, a.Zone) } func (a *IPAddr) toAddr() sockaddr { - if a == nil { // nil *IPAddr - return nil // nil interface + if a == nil { + return nil } return a }
sockaddr
メソッドが追加され、toAddr
メソッドが簡素化されています。 -
internetSocket
関数の引数変更: 例:src/pkg/net/ipsock_posix.go
--- a/src/pkg/net/ipsock_posix.go +++ b/src/pkg/net/ipsock_posix.go @@ -132,19 +132,8 @@ func favoriteAddrFamily(net string, laddr, raddr sockaddr, mode string) (family // Internet sockets (TCP, UDP, IP) func internetSocket(net string, laddr, raddr sockaddr, deadline time.Time, sotype, proto int, mode string, toAddr func(syscall.Sockaddr) Addr) (fd *netFD, err error) { - var la, ra syscall.Sockaddr family, ipv6only := favoriteAddrFamily(net, laddr, raddr, mode) - if laddr != nil { - if la, err = laddr.sockaddr(family); err != nil { - goto Error - } - } - if raddr != nil { - if ra, err = raddr.Sockaddr(family); err != nil { - goto Error - } - } - fd, err = socket(net, family, sotype, proto, ipv6only, la, ra, deadline, toAddr) + fd, err = socket(net, family, sotype, proto, ipv6only, laddr, raddr, deadline, toAddr) if err != nil { goto Error }
laddr
とraddr
が直接sockaddr
インターフェースとして渡され、内部でのsyscall.Sockaddr
への変換が削除されています。 -
socket
関数の引数変更と内部ロジックの修正: 例:src/pkg/net/sock_posix.go
--- a/src/pkg/net/sock_posix.go +++ b/src/pkg/net/sock_posix.go @@ -37,7 +37,7 @@ type sockaddr interface { } // Generic POSIX socket creation. -func socket(net string, f, t, p int, ipv6only bool, ulsa, ursa syscall.Sockaddr, deadline time.Time, toAddr func(syscall.Sockaddr) Addr) (fd *netFD, err error) { +func socket(net string, f, t, p int, ipv6only bool, laddr, raddr sockaddr, deadline time.Time, toAddr func(syscall.Sockaddr) Addr) (fd *netFD, err error) { s, err := sysSocket(f, t, p) if err != nil { return nil, err @@ -48,23 +48,42 @@ func socket(net string, f, t, p int, ipv6only bool, ulsa, ursa syscall.Sockaddr, return nil, err } - // This socket is used by a listener. - if ulsa != nil && ursa == nil { + // This function makes a network file descriptor for stream + // and datagram dialers, stream and datagram listeners. + // + // For dialers, they will require either named or unnamed + // sockets for their flights. We can assume that it's just a + // request from a dialer that wants a named socket when both + // laddr and raddr are not nil. A dialer will also require a + // connection setup initiated socket when raddr is not nil. + // + // For listeners and some dialers on datagram networks, they + // will only require named sockets. So we can assume that + // it's just for a listener or a datagram dialer when laddr is + // not nil but raddr is nil. + + var lsa syscall.Sockaddr + if laddr != nil && raddr == nil { // We provide a socket that listens to a wildcard - // address with reusable UDP port when the given ulsa + // address with reusable UDP port when the given laddr // is an appropriate UDP multicast address prefix. // This makes it possible for a single UDP listener // to join multiple different group addresses, for // multiple UDP listeners that listen on the same UDP // port to join the same group address. - if ulsa, err = listenerSockaddr(s, f, ulsa, toAddr); err != nil { + if lsa, err = listenerSockaddr(s, f, laddr); err != nil { + closesocket(s) + return nil, err + } + } else if laddr != nil && raddr != nil { + if lsa, err = laddr.sockaddr(f); err != nil { closesocket(s) return nil, err } } - if ulsa != nil { - if err = syscall.Bind(s, ulsa); err != nil { + if lsa != nil { + if err = syscall.Bind(s, lsa); err != nil { closesocket(s) return nil, err } @@ -75,12 +94,19 @@ func socket(net string, f, t, p int, ipv6only bool, ulsa, ursa syscall.Sockaddr, return nil, err } - // This socket is used by a dialer. - if ursa != nil { + var rsa syscall.Sockaddr + if raddr != nil { + rsa, err = raddr.sockaddr(f) + if err != nil { + return nil, err + } + } + + if rsa != nil { if !deadline.IsZero() { setWriteDeadline(fd, deadline) } - if err = fd.connect(ulsa, ursa); err != nil { + if err = fd.connect(lsa, rsa); err != nil { fd.Close() return nil, err } @@ -90,13 +116,11 @@ func socket(net string, f, t, p int, ipv6only bool, ulsa, ursa syscall.Sockaddr, } } - lsa, _ := syscall.Getsockname(s) - laddr := toAddr(lsa) - rsa, _ := syscall.Getpeername(s) - if rsa == nil { - rsa = ursa + lsa, _ = syscall.Getsockname(s) + if rsa, _ = syscall.Getpeername(s); rsa != nil { + fd.setAddr(toAddr(lsa), toAddr(rsa)) + } else { + fd.setAddr(toAddr(lsa), raddr) } - raddr := toAddr(rsa) - fd.setAddr(laddr, raddr) return fd, nil }
socket
関数の引数がsyscall.Sockaddr
からsockaddr
インターフェースに変更され、内部でladdr.sockaddr(f)
やraddr.sockaddr(f)
を呼び出すことでsyscall.Sockaddr
を取得するようになっています。また、fd.setAddr
の呼び出しロジックも変更されています。 -
listenerSockaddr
関数の引数変更: 例:src/pkg/net/sock_unix.go
--- a/src/pkg/net/sock_unix.go +++ b/src/pkg/net/sock_unix.go @@ -8,29 +8,29 @@ package net import "syscall" -func listenerSockaddr(s, f int, la syscall.Sockaddr, toAddr func(syscall.Sockaddr) Addr) (syscall.Sockaddr, error) { - a := toAddr(la) - if a == nil { - return la, nil - } - switch a := a.(type) { +func listenerSockaddr(s, f int, laddr sockaddr) (syscall.Sockaddr, error) { + switch laddr := laddr.(type) { case *TCPAddr, *UnixAddr: if err := setDefaultListenerSockopts(s); err != nil { return nil, err } + return laddr.sockaddr(f) case *UDPAddr: - if a.IP.IsMulticast() { + if laddr.IP != nil && laddr.IP.IsMulticast() { if err := setDefaultMulticastSockopts(s); err != nil { return nil, err } + addr := *laddr switch f { case syscall.AF_INET: - a.IP = IPv4zero + addr.IP = IPv4zero case syscall.AF_INET6: - a.IP = IPv6unspecified + addr.IP = IPv6unspecified } - return a.sockaddr(f) + laddr = &addr } + return laddr.sockaddr(f) + default: + return laddr.sockaddr(f) } - return la, nil }
listenerSockaddr
の引数もsyscall.Sockaddr
からsockaddr
インターフェースに変更され、内部でladdr.sockaddr(f)
を呼び出すようになっています。
コアとなるコードの解説
このコミットの主要な変更は、net
パッケージ内のソケットアドレス表現と syscall.Sockaddr
との間の変換ロジックを合理化することにあります。
-
sockaddr
インターフェースの実装の統一: 以前は、net.IPAddr
,net.UDPAddr
,net.TCPAddr
は、net
パッケージ内部のsockaddr
インターフェースを直接実装していませんでした。そのため、これらの型をsyscall.Sockaddr
に変換する際には、一度toAddr()
メソッドを介してnet.sockaddr
インターフェース型に変換し、さらにそのnet.sockaddr
インターフェース型が持つsockaddr()
メソッドを呼び出す必要がありました。このコミットでは、
net.IPAddr
,net.UDPAddr
,net.TCPAddr
が直接sockaddr
インターフェースを実装するように変更されました。具体的には、これらの型に以下のシグネチャを持つメソッドが追加されました。func (a *IPAddr) sockaddr(family int) (syscall.Sockaddr, error) func (a *UDPAddr) sockaddr(family int) (syscall.Sockaddr, error) func (a *TCPAddr) sockaddr(family int) (syscall.Sockaddr, error)
これにより、これらのアドレス型から直接
syscall.Sockaddr
を取得できるようになり、中間的な変換ステップが不要になりました。 -
toAddr()
メソッドの役割の簡素化:toAddr()
メソッドは、以前は具体的なアドレス型(例:*IPAddr
)をnet.sockaddr
インターフェース型に変換する役割を担っていました。しかし、IPAddr
などが直接sockaddr
インターフェースを実装するようになったため、toAddr()
メソッドは単にレシーバ自身をsockaddr
インターフェースとして返すだけのシンプルな実装になりました。func (a *IPAddr) toAddr() sockaddr { if a == nil { return nil } return a // a は既に sockaddr インターフェースを実装している }
これにより、
toAddr()
の呼び出しが不要になるか、あるいはその内部処理が大幅に簡素化され、不要な型アサーションやインターフェース変換が削減されます。 -
internetSocket
およびsocket
関数の引数と内部ロジックの変更:internetSocket
関数は、TCP/UDP/IPソケットの作成に使用される汎用関数です。以前はladdr
とraddr
をsockaddr
インターフェース型として受け取り、関数内部でladdr.sockaddr(family)
を呼び出してsyscall.Sockaddr
を取得していました。変更後も引数の型は
sockaddr
インターフェース型ですが、IPAddr
,UDPAddr
,TCPAddr
が直接sockaddr
インターフェースを実装したことで、呼び出し元から渡される具体的なアドレス型がそのままsockaddr
インターフェースとして扱えるようになりました。これにより、internetSocket
内部でのsyscall.Sockaddr
への変換ロジックがより直接的になり、中間変数が削減されました。同様に、
socket
関数(ソケットの作成と初期化を行う低レベル関数)も、laddr
とraddr
をsockaddr
インターフェース型として受け取るように変更されました。関数内部では、laddr.sockaddr(f)
やraddr.sockaddr(f)
を直接呼び出すことでsyscall.Sockaddr
を取得し、syscall.Bind
やfd.connect
に渡しています。これにより、コードのパスが短縮され、変換に伴うオーバーヘッドが削減されます。特に
socket
関数では、lsa, _ := syscall.Getsockname(s)
やrsa, _ := syscall.Getpeername(s)
の結果をtoAddr
でnet.Addr
に変換し、fd.setAddr
に渡すロジックも変更されています。リモートアドレスが取得できない場合(例:listen
ソケット)、以前はursa
(引数で渡されたリモートsyscall.Sockaddr
) を使用していましたが、変更後はraddr
(引数で渡されたリモートsockaddr
インターフェース) を直接使用するようになっています。これは、raddr
が既にnet.Addr
の情報を含んでいるため、より自然な流れとなります。
これらの変更は、Goのネットワークスタックにおけるソケットアドレスの内部表現とOSシステムコール間のインターフェースをより効率的かつ直接的にすることで、パフォーマンスの向上とコードの簡素化を実現しています。特に、BSD系のOSにおけるランタイム統合型ネットワークポーラーのような低レベルの最適化を可能にするための基盤となります。
関連リンク
- Go Issue 5199: https://github.com/golang/go/issues/5199 (このコミットが解決または関連するIssue)
- Go CL 12010043: https://golang.org/cl/12010043 (このコミットのChange List)
参考にした情報源リンク
- Go言語の
net
パッケージのドキュメント: https://pkg.go.dev/net - Go言語の
syscall
パッケージのドキュメント: https://pkg.go.dev/syscall - Go言語のネットワークプログラミングに関する一般的な情報源 (例: Go by Example - Sockets, Go's net package internalsなど)
- BSD系OSの
kqueue
に関する情報 (例: FreeBSD Handbook, OpenBSD man pagesなど) - Go言語のランタイムとスケジューラに関する情報 (Goの並行処理モデル、I/O多重化の仕組みなど)
- Go言語のコミット履歴とChange Listのレビュー
- Go言語のIssueトラッカー (Issue 5199の内容確認)
- ソケットプログラミングの基本概念に関する一般的なネットワークプログラミングの書籍やオンラインリソース