[インデックス 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.gosrc/pkg/net/ipsock_posix.gosrc/pkg/net/sock_posix.gosrc/pkg/net/sock_unix.gosrc/pkg/net/sock_windows.gosrc/pkg/net/tcpsock_posix.gosrc/pkg/net/udpsock_posix.gosrc/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の内容確認)
- ソケットプログラミングの基本概念に関する一般的なネットワークプログラミングの書籍やオンラインリソース