[インデックス 18722] ファイルの概要
このコミットは、Go言語のネットワークパッケージにおいて、FreeBSD上でのソケット作成および接続受け入れ処理の高速化と堅牢化を目的としたものです。具体的には、SOCK_NONBLOCK
および SOCK_CLOEXEC
フラグを socket(2)
および accept4(2)
システムコールに直接渡すことで、アトミックなソケット操作を可能にし、競合状態のリスクを低減しています。また、変数名をより明示的にすることでコードの可読性を向上させています。
コミット
commit 1d086e39b09e6fa3b5e87da5dd6ed8154319f1db
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date: Tue Mar 4 09:28:09 2014 +0900
net: enable fast socket creation with close-on-exec flag on freebsd
Also makes variable names explicit.
Fixes #7186.
LGTM=iant
R=golang-codereviews, gobot, iant, bradfitz
CC=golang-codereviews
https://golang.org/cl/69100043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1d086e39b09e6fa3b5e87da5dd6ed8154319f1db
元コミット内容
このコミットの元の内容は以下の通りです。
- 件名:
net: enable fast socket creation with close-on-exec flag on freebsd
(net: FreeBSDでclose-on-execフラグによる高速ソケット作成を有効化) - 本文:
Also makes variable names explicit.
(変数名も明示的にする。) - 関連するIssue:
Fixes #7186.
(Issue #7186を修正。)
このコミットは、FreeBSD環境におけるソケット操作のパフォーマンスと信頼性を向上させることを主眼としています。
変更の背景
Go言語のネットワークパッケージは、クロスプラットフォームで効率的なネットワーク通信を提供することを目指しています。ソケットの作成や接続の受け入れといった基本的な操作は、アプリケーションのパフォーマンスに直結します。特に、多数の接続を扱うサーバーアプリケーションでは、これらの操作のオーバーヘッドを最小限に抑えることが重要です。
従来のソケット作成プロセスでは、socket(2)
システムコールでソケットディスクリプタを作成した後、別途 fcntl(2)
を用いて O_NONBLOCK
(非ブロッキング) や FD_CLOEXEC
(close-on-exec) フラグを設定する必要がありました。この2段階のプロセスには、以下のような問題点がありました。
- 競合状態 (Race Condition):
socket(2)
呼び出しとfcntl(2)
呼び出しの間に、別のスレッドやプロセスがfork(2)
を実行した場合、FD_CLOEXEC
フラグが設定される前に子プロセスにソケットディスクリプタが継承されてしまう可能性があります。これは、意図しないファイルディスクリプタのリークや、子プロセスが親プロセスのソケットを誤って使用するなどの問題を引き起こす可能性があります。 - パフォーマンスオーバーヘッド: 2つのシステムコールを連続して呼び出すことは、単一のシステムコールで同じ操作を行うよりもわずかながらオーバーヘッドが大きくなります。高負荷な環境では、この小さなオーバーヘッドも無視できません。
Linuxカーネルでは、バージョン2.6.27以降で socket(2)
に SOCK_NONBLOCK
と SOCK_CLOEXEC
フラグが導入され、バージョン2.6.28以降で accept4(2)
システムコールが導入されました。これらのシステムコールは、ソケットの作成や接続受け入れと同時に非ブロッキングおよびclose-on-execフラグを設定できるアトミックな操作を提供します。これにより、上記の競合状態とパフォーマンスオーバーヘッドの問題が解決されます。
このコミット以前のGoのネットワークパッケージは、Linuxに対してはこれらの高速パスを利用していましたが、FreeBSDに対しては利用していませんでした。FreeBSD 10以降のカーネルでも同様の機能(SOCK_NONBLOCK
, SOCK_CLOEXEC
フラグ、accept4(2)
)が導入されたため、このコミットではFreeBSDでもこれらの高速パスを利用できるように拡張されました。
前提知識の解説
1. ファイルディスクリプタ (File Descriptor, FD)
Unix系OSにおいて、ファイルやソケット、パイプなどのI/Oリソースは「ファイルディスクリプタ」と呼ばれる整数値で識別されます。プログラムはファイルディスクリプタを通じてこれらのリソースにアクセスします。
2. socket(2)
システムコール
新しいソケットを作成するためのシステムコールです。
int socket(int domain, int type, int protocol);
domain
: アドレスファミリ (例:AF_INET
for IPv4,AF_INET6
for IPv6)type
: ソケットのタイプ (例:SOCK_STREAM
for TCP,SOCK_DGRAM
for UDP)protocol
: プロトコル (通常は0で、type
に応じたデフォルトプロトコルが選択される)
3. accept(2)
および accept4(2)
システムコール
接続指向ソケット(TCPソケットなど)で、クライアントからの接続要求を受け入れるためのシステムコールです。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
accept4(2)
は accept(2)
の拡張版で、flags
引数を通じてソケットのプロパティをアトミックに設定できます。
4. SOCK_NONBLOCK
フラグ
socket(2)
や accept4(2)
の type
または flags
引数に指定できるフラグです。このフラグが設定されたソケットは「非ブロッキングモード」で動作します。
- ブロッキングモード: I/O操作(例:
read
,write
,accept
)が完了するまで、呼び出し元のスレッドがブロック(停止)します。 - 非ブロッキングモード: I/O操作がすぐに完了しない場合でも、呼び出し元のスレッドはブロックされず、エラー(通常は
EAGAIN
またはEWOULDBLOCK
)を返してすぐに制御を返します。これにより、単一のスレッドで複数のI/O操作を効率的に処理できるようになります(例:select
,poll
,epoll
などのI/O多重化メカニズムと組み合わせて使用)。
5. SOCK_CLOEXEC
フラグ (Close-on-exec)
socket(2)
や accept4(2)
の type
または flags
引数に指定できるフラグです。このフラグが設定されたファイルディスクリプタは、execve(2)
などの exec
ファミリのシステムコールによって新しいプログラムが実行される際に、自動的に閉じられます。
- なぜ重要か:
fork(2)
システムコールで子プロセスが作成されると、親プロセスのファイルディスクリプタは子プロセスに継承されます。しかし、子プロセスがexec
を実行して別のプログラムに切り替わる場合、親プロセスから継承されたファイルディスクリプタが不要になることがほとんどです。FD_CLOEXEC
フラグ(またはSOCK_CLOEXEC
)を設定することで、これらの不要なファイルディスクリプタが子プロセスにリークするのを防ぎ、リソースの無駄遣いやセキュリティ上の潜在的な問題を回避できます。
6. syscall.EINVAL
, syscall.ENOSYS
, syscall.EPROTONOSUPPORT
これらはGo言語の syscall
パッケージで定義されているエラーコードです。
EINVAL
(Invalid argument): システムコールに無効な引数が渡された場合に返されます。古いカーネルでSOCK_NONBLOCK
やSOCK_CLOEXEC
フラグがサポートされていない場合、socket(2)
やaccept4(2)
がこのエラーを返すことがあります。ENOSYS
(Function not implemented): システムコールがカーネルによって実装されていない場合に返されます。古いカーネルでaccept4(2)
が存在しない場合に返されます。EPROTONOSUPPORT
(Protocol not supported): 要求されたプロトコルがサポートされていない場合に返されます。FreeBSDでは、SOCK_NONBLOCK
やSOCK_CLOEXEC
フラグがサポートされていない場合に、socket(2)
がこのエラーを返すことがあります。これはLinuxのEINVAL
と同様の状況で発生しますが、エラーコードが異なります。
7. // +build
タグ
Go言語のビルドシステムで使用される特殊なコメント行です。Goのソースファイルは、このタグによって特定の環境(OS、アーキテクチャなど)でのみコンパイルされるように制御できます。
例: // +build linux
はLinuxでのみコンパイルされることを意味します。// +build freebsd linux
はFreeBSDとLinuxの両方でコンパイルされることを意味します。
8. syscall.ForkLock
Goランタイム内部で使用されるロック機構です。fork(2)
システムコールは、子プロセスが親プロセスのメモリ空間をコピーするため、fork(2)
の実行中に親プロセスがファイルディスクリプタを操作すると、子プロセスで不整合が生じる可能性があります。ForkLock
は、このような競合を防ぐために、fork(2)
の前後でファイルディスクリプタの操作を同期させる役割を担います。
技術的詳細
このコミットの主要な技術的変更点は、FreeBSD環境において、ソケット作成 (sysSocket
) および接続受け入れ (accept
) の際に、SOCK_NONBLOCK
と SOCK_CLOEXEC
フラグをアトミックに設定する「高速パス」を利用するようにしたことです。
高速パスの利用
-
sysSocket
関数:- 変更前はLinuxのみが
SOCK_NONBLOCK|SOCK_CLOEXEC
フラグをsyscall.Socket
に直接渡していました。 - 変更後、FreeBSDもこのフラグを直接渡すようになりました。
- エラーハンドリングが強化され、Linuxでは
syscall.EINVAL
、FreeBSDではsyscall.EPROTONOSUPPORT
が返された場合に、これらのフラグなしでsyscall.Socket
を再試行するフォールバックロジックが追加されました。これは、古いカーネルバージョンがこれらのフラグをサポートしていない場合に対応するためです。
- 変更前はLinuxのみが
-
accept
関数:- 変更前はLinuxのみが
syscall.Accept4
を利用していました。 - 変更後、FreeBSDも
syscall.Accept4
を利用するようになりました。 - エラーハンドリングが強化され、LinuxとFreeBSDの両方で
syscall.ENOSYS
(accept4
が存在しない場合)またはLinuxでsyscall.EINVAL
が返された場合に、syscall.Accept
を再試行するフォールバックロジックが追加されました。
- 変更前はLinuxのみが
ビルドタグの変更
-
src/pkg/net/sock_cloexec.go
:- 変更前:
// +build linux
- 変更後:
// +build freebsd linux
- これにより、このファイル(高速パスを実装している)がFreeBSD環境でもコンパイルされるようになりました。
- 変更前:
-
src/pkg/net/sys_cloexec.go
:- 変更前:
// +build darwin dragonfly freebsd nacl netbsd openbsd solaris
- 変更後:
// +build darwin dragonfly nacl netbsd openbsd solaris
- このファイルは、
SOCK_NONBLOCK
やSOCK_CLOEXEC
フラグを直接サポートしないOS向けのフォールバックロジックを実装しています。freebsd
がこのファイルのビルドタグから削除されたことで、FreeBSDは高速パスを優先的に利用するようになります。
- 変更前:
変数名の明示化
sysSocket
およびaccept
関数内の引数名やローカル変数名が、より意味のある名前に変更されました(例:f, t, p
->family, sotype, proto
、fd
->s
、nfd
->ns
)。これにより、コードの可読性と理解度が向上しています。
エラーハンドリングの差異
LinuxとFreeBSDで、socket(2)
に SOCK_NONBLOCK
や SOCK_CLOEXEC
フラグを渡した際に、それらがサポートされていない場合に返されるエラーコードが異なる点に対応しています。
- Linux:
EINVAL
(Invalid argument) - FreeBSD:
EPROTONOSUPPORT
(Protocol not supported) この差異を吸収することで、両OSで適切なフォールバック処理が行われるようになります。
コアとなるコードの変更箇所
src/pkg/net/sock_cloexec.go
--- a/src/pkg/net/sock_cloexec.go
+++ b/src/pkg/net/sock_cloexec.go
@@ -5,7 +5,7 @@
// This file implements sysSocket and accept for platforms that
// provide a fast path for setting SetNonblock and CloseOnExec.
-// +build linux
+// +build freebsd linux
package net
@@ -13,18 +13,20 @@ import "syscall"
// Wrapper around the socket system call that marks the returned file
// descriptor as nonblocking and close-on-exec.
-func sysSocket(f, t, p int) (int, error) {
- s, err := syscall.Socket(f, t|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC, p)
- // The SOCK_NONBLOCK and SOCK_CLOEXEC flags were introduced in
- // Linux 2.6.27. If we get an EINVAL error, fall back to
- // using socket without them.
- if err == nil || err != syscall.EINVAL {
+func sysSocket(family, sotype, proto int) (int, error) {
+ s, err := syscall.Socket(family, sotype|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC, proto)
+ // On Linux the SOCK_NONBLOCK and SOCK_CLOEXEC flags were
+ // introduced in 2.6.27 kernel and on FreeBSD both flags were
+ // introduced in 10 kernel. If we get an EINVAL error on Linux
+ // or EPROTONOSUPPORT error on FreeBSD, fall back to using
+ // socket without them.
+ if err == nil || (err != syscall.EPROTONOSUPPORT && err != syscall.EINVAL) {
return s, err
}
// See ../syscall/exec_unix.go for description of ForkLock.
syscall.ForkLock.RLock()
- s, err = syscall.Socket(f, t, p)
+ s, err = syscall.Socket(family, sotype, proto)
if err == nil {
syscall.CloseOnExec(s)
}
@@ -41,12 +43,14 @@ func sysSocket(f, t, p int) (int, error) {
// Wrapper around the accept system call that marks the returned file
// descriptor as nonblocking and close-on-exec.
-func accept(fd int) (int, syscall.Sockaddr, error) {
- nfd, sa, err := syscall.Accept4(fd, syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC)
- // The accept4 system call was introduced in Linux 2.6.28. If
- // we get an ENOSYS or EINVAL error, fall back to using accept.
+func accept(s int) (int, syscall.Sockaddr, error) {
+ ns, sa, err := syscall.Accept4(s, syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC)
+ // On Linux the accept4 system call was introduced in 2.6.28
+ // kernel and on FreeBSD it was introduced in 10 kernel. If we
+ // get an ENOSYS error on both Linux and FreeBSD, or EINVAL
+ // error on Linux, fall back to using accept.
if err == nil || (err != syscall.ENOSYS && err != syscall.EINVAL) {
- return nfd, sa, err
+ return ns, sa, err
}
// See ../syscall/exec_unix.go for description of ForkLock.
@@ -54,16 +58,16 @@ func accept(fd int) (int, syscall.Sockaddr, error) {
// because we have put fd.sysfd into non-blocking mode.
// However, a call to the File method will put it back into
// blocking mode. We can't take that risk, so no use of ForkLock here.
- nfd, sa, err = syscall.Accept(fd)
+ ns, sa, err = syscall.Accept(s)
if err == nil {
- syscall.CloseOnExec(nfd)
+ syscall.CloseOnExec(ns)
}
if err != nil {
return -1, nil, err
}
- if err = syscall.SetNonblock(nfd, true); err != nil {
- syscall.Close(nfd)
+ if err = syscall.SetNonblock(ns, true); err != nil {
+ syscall.Close(ns)
return -1, nil, err
}
- return nfd, sa, nil
+ return ns, sa, nil
}
src/pkg/net/sys_cloexec.go
--- a/src/pkg/net/sys_cloexec.go
+++ b/src/pkg/net/sys_cloexec.go
@@ -5,7 +5,7 @@
// This file implements sysSocket and accept for platforms that do not
// provide a fast path for setting SetNonblock and CloseOnExec.
-// +build darwin dragonfly freebsd nacl netbsd openbsd solaris
+// +build darwin dragonfly nacl netbsd openbsd solaris
package net
@@ -13,10 +13,10 @@ import "syscall"
// Wrapper around the socket system call that marks the returned file
// descriptor as nonblocking and close-on-exec.
-func sysSocket(f, t, p int) (int, error) {
+func sysSocket(family, sotype, proto int) (int, error) {
// See ../syscall/exec_unix.go for description of ForkLock.
syscall.ForkLock.RLock()
- s, err := syscall.Socket(f, t, p)
+ s, err := syscall.Socket(family, sotype, proto)
if err == nil {
syscall.CloseOnExec(s)
}
@@ -33,22 +33,22 @@ func sysSocket(f, t, p int) (int, error) {
// Wrapper around the accept system call that marks the returned file
// descriptor as nonblocking and close-on-exec.
-func accept(fd int) (int, syscall.Sockaddr, error) {
+func accept(s int) (int, syscall.Sockaddr, error) {
// See ../syscall/exec_unix.go for description of ForkLock.
// It is probably okay to hold the lock across syscall.Accept
// because we have put fd.sysfd into non-blocking mode.
// However, a call to the File method will put it back into
// blocking mode. We can't take that risk, so no use of ForkLock here.
- nfd, sa, err := syscall.Accept(fd)
+ ns, sa, err := syscall.Accept(s)
if err == nil {
- syscall.CloseOnExec(nfd)
+ syscall.CloseOnExec(ns)
}
if err != nil {
return -1, nil, err
}
- if err = syscall.SetNonblock(nfd, true); err != nil {
- syscall.Close(nfd)
+ if err = syscall.SetNonblock(ns, true); err != nil {
+ syscall.Close(ns)
return -1, nil, err
}
- return nfd, sa, nil
+ return ns, sa, nil
}
コアとなるコードの解説
src/pkg/net/sock_cloexec.go
の変更点
このファイルは、SOCK_NONBLOCK
と SOCK_CLOEXEC
フラグを socket(2)
および accept4(2)
システムコールに直接渡すことで、ソケット作成と接続受け入れの「高速パス」を提供するプラットフォーム(Linuxなど)向けの実装です。
-
ビルドタグの変更:
// +build linux
から// +build freebsd linux
へ変更されました。- これにより、FreeBSD環境でもこのファイルがコンパイルされるようになり、FreeBSDも高速パスの恩恵を受けられるようになりました。
-
sysSocket
関数の変更:- 引数名の変更:
f, t, p
からfamily, sotype, proto
へと、より意味が明確な名前に変更されました。 - エラーハンドリングの強化:
- 変更前は
syscall.EINVAL
(Linuxでフラグがサポートされていない場合) のみをチェックしていました。 - 変更後、
syscall.EPROTONOSUPPORT
もチェックするようになりました。これは、FreeBSDでSOCK_NONBLOCK
やSOCK_CLOEXEC
フラグがサポートされていない場合に返されるエラーコードです。 if err == nil || (err != syscall.EPROTONOSUPPORT && err != syscall.EINVAL)
という条件式により、エラーが発生しなかった場合、またはエラーがEPROTONOSUPPORT
(FreeBSD) やEINVAL
(Linux) 以外の場合には、そのまま結果を返します。- もし
EPROTONOSUPPORT
またはEINVAL
が返された場合、それはカーネルがこれらのフラグをサポートしていないことを意味するため、syscall.ForkLock.RLock()
でロックを取得し、フラグなしでsyscall.Socket
を再試行するフォールバックロジックが実行されます。その後、syscall.CloseOnExec(s)
を呼び出してFD_CLOEXEC
フラグを別途設定し、syscall.SetNonblock(s, true)
で非ブロッキングモードに設定します。
- 変更前は
- 引数名の変更:
-
accept
関数の変更:- 引数名と変数名の変更:
fd
からs
へ、nfd
からns
へと、より意味が明確な名前に変更されました。 - エラーハンドリングの強化:
- 変更前は
syscall.ENOSYS
(Linuxでaccept4
が存在しない場合) またはsyscall.EINVAL
(Linuxでフラグがサポートされていない場合) をチェックしていました。 - 変更後、
if err == nil || (err != syscall.ENOSYS && err != syscall.EINVAL)
という条件式により、エラーが発生しなかった場合、またはエラーがENOSYS
やEINVAL
以外の場合には、そのまま結果を返します。 - もし
ENOSYS
またはEINVAL
が返された場合、それはカーネルがaccept4
をサポートしていないことを意味するため、syscall.Accept
を再試行するフォールバックロジックが実行されます。その後、syscall.CloseOnExec(ns)
とsyscall.SetNonblock(ns, true)
を呼び出して、それぞれFD_CLOEXEC
フラグと非ブロッキングモードを別途設定します。
- 変更前は
- 引数名と変数名の変更:
src/pkg/net/sys_cloexec.go
の変更点
このファイルは、SOCK_NONBLOCK
や SOCK_CLOEXEC
フラグを直接サポートしないプラットフォーム向けのフォールバックロジックを実装しています。
-
ビルドタグの変更:
// +build darwin dragonfly freebsd nacl netbsd openbsd solaris
から// +build darwin dragonfly nacl netbsd openbsd solaris
へ変更されました。freebsd
がこのビルドタグから削除されたことで、FreeBSDはもはやこのファイル(低速パス)を使用せず、sock_cloexec.go
(高速パス)を使用するようになりました。
-
sysSocket
およびaccept
関数の変更:sock_cloexec.go
と同様に、引数名やローカル変数名がより明示的な名前に変更されました。これは、コードベース全体での一貫性と可読性向上のための変更です。機能的な変更はありません。
これらの変更により、Go言語のネットワークパッケージはFreeBSD 10以降のシステムで、より効率的かつ安全にソケットを操作できるようになりました。特に、fork(2)
を伴うプロセス生成時にソケットディスクリプタが意図せず継承されるリスクが低減され、全体的な堅牢性が向上しています。
関連リンク
- Go Issue #7186: https://github.com/golang/go/issues/7186
- Go CL 69100043: https://golang.org/cl/69100043
参考にした情報源リンク
socket(2)
man page (Linux): https://man7.org/linux/man-pages/man2/socket.2.htmlaccept4(2)
man page (Linux): https://man7.org/linux/man-pages/man2/accept4.2.htmlsocket(2)
man page (FreeBSD): https://www.freebsd.org/cgi/man.cgi?query=socket&sektion=2accept(2)
man page (FreeBSD): https://www.freebsd.org/cgi/man.cgi?query=accept&sektion=2fcntl(2)
man page (Linux): https://man7.org/linux/man-pages/man2/fcntl.2.htmlexecve(2)
man page (Linux): https://man7.org/linux/man-pages/man2/execve.2.html- Go build constraints: https://pkg.go.dev/cmd/go#hdr-Build_constraints
- Go
syscall
package documentation: https://pkg.go.dev/syscall - FreeBSD 10 Release Notes (mentioning
accept4
andSOCK_CLOEXEC
): https://www.freebsd.org/releases/10.0R/relnotes/ (具体的なセクションはリリースノート内で検索が必要) - Linux kernel history for
SOCK_NONBLOCK
andSOCK_CLOEXEC
: https://lwn.net/Articles/272909/ (LWN.netの記事など) - Linux kernel history for
accept4
: https://lwn.net/Articles/280777/ (LWN.netの記事など) EPROTONOSUPPORT
on FreeBSD: https://www.freebsd.org/cgi/man.cgi?query=errno&sektion=2 (errno man page)EINVAL
on Linux: https://man7.org/linux/man-pages/man3/errno.3.html (errno man page)ENOSYS
on Linux: https://man7.org/linux/man-pages/man3/errno.3.html (errno man page)ForkLock
in Go: Goのソースコード内のコメントや、Goの並行処理に関するドキュメントで言及されることがあります。