Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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段階のプロセスには、以下のような問題点がありました。

  1. 競合状態 (Race Condition): socket(2)呼び出しと fcntl(2)呼び出しの間に、別のスレッドやプロセスが fork(2) を実行した場合、FD_CLOEXEC フラグが設定される前に子プロセスにソケットディスクリプタが継承されてしまう可能性があります。これは、意図しないファイルディスクリプタのリークや、子プロセスが親プロセスのソケットを誤って使用するなどの問題を引き起こす可能性があります。
  2. パフォーマンスオーバーヘッド: 2つのシステムコールを連続して呼び出すことは、単一のシステムコールで同じ操作を行うよりもわずかながらオーバーヘッドが大きくなります。高負荷な環境では、この小さなオーバーヘッドも無視できません。

Linuxカーネルでは、バージョン2.6.27以降で socket(2)SOCK_NONBLOCKSOCK_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_NONBLOCKSOCK_CLOEXEC フラグがサポートされていない場合、socket(2)accept4(2) がこのエラーを返すことがあります。
  • ENOSYS (Function not implemented): システムコールがカーネルによって実装されていない場合に返されます。古いカーネルで accept4(2) が存在しない場合に返されます。
  • EPROTONOSUPPORT (Protocol not supported): 要求されたプロトコルがサポートされていない場合に返されます。FreeBSDでは、SOCK_NONBLOCKSOCK_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_NONBLOCKSOCK_CLOEXEC フラグをアトミックに設定する「高速パス」を利用するようにしたことです。

高速パスの利用

  • sysSocket 関数:

    • 変更前はLinuxのみが SOCK_NONBLOCK|SOCK_CLOEXEC フラグを syscall.Socket に直接渡していました。
    • 変更後、FreeBSDもこのフラグを直接渡すようになりました。
    • エラーハンドリングが強化され、Linuxでは syscall.EINVAL、FreeBSDでは syscall.EPROTONOSUPPORT が返された場合に、これらのフラグなしで syscall.Socket を再試行するフォールバックロジックが追加されました。これは、古いカーネルバージョンがこれらのフラグをサポートしていない場合に対応するためです。
  • accept 関数:

    • 変更前はLinuxのみが syscall.Accept4 を利用していました。
    • 変更後、FreeBSDも syscall.Accept4 を利用するようになりました。
    • エラーハンドリングが強化され、LinuxとFreeBSDの両方で syscall.ENOSYSaccept4 が存在しない場合)またはLinuxで syscall.EINVAL が返された場合に、syscall.Accept を再試行するフォールバックロジックが追加されました。

ビルドタグの変更

  • 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_NONBLOCKSOCK_CLOEXEC フラグを直接サポートしないOS向けのフォールバックロジックを実装しています。freebsd がこのファイルのビルドタグから削除されたことで、FreeBSDは高速パスを優先的に利用するようになります。

変数名の明示化

  • sysSocket および accept 関数内の引数名やローカル変数名が、より意味のある名前に変更されました(例: f, t, p -> family, sotype, protofd -> snfd -> ns)。これにより、コードの可読性と理解度が向上しています。

エラーハンドリングの差異

LinuxとFreeBSDで、socket(2)SOCK_NONBLOCKSOCK_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_NONBLOCKSOCK_CLOEXEC フラグを socket(2) および accept4(2) システムコールに直接渡すことで、ソケット作成と接続受け入れの「高速パス」を提供するプラットフォーム(Linuxなど)向けの実装です。

  1. ビルドタグの変更:

    • // +build linux から // +build freebsd linux へ変更されました。
    • これにより、FreeBSD環境でもこのファイルがコンパイルされるようになり、FreeBSDも高速パスの恩恵を受けられるようになりました。
  2. sysSocket 関数の変更:

    • 引数名の変更: f, t, p から family, sotype, proto へと、より意味が明確な名前に変更されました。
    • エラーハンドリングの強化:
      • 変更前は syscall.EINVAL (Linuxでフラグがサポートされていない場合) のみをチェックしていました。
      • 変更後、syscall.EPROTONOSUPPORT もチェックするようになりました。これは、FreeBSDで SOCK_NONBLOCKSOCK_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) で非ブロッキングモードに設定します。
  3. accept 関数の変更:

    • 引数名と変数名の変更: fd から s へ、nfd から ns へと、より意味が明確な名前に変更されました。
    • エラーハンドリングの強化:
      • 変更前は syscall.ENOSYS (Linuxで accept4 が存在しない場合) または syscall.EINVAL (Linuxでフラグがサポートされていない場合) をチェックしていました。
      • 変更後、if err == nil || (err != syscall.ENOSYS && err != syscall.EINVAL) という条件式により、エラーが発生しなかった場合、またはエラーが ENOSYSEINVAL 以外の場合には、そのまま結果を返します。
      • もし ENOSYS または EINVAL が返された場合、それはカーネルが accept4 をサポートしていないことを意味するため、syscall.Accept を再試行するフォールバックロジックが実行されます。その後、syscall.CloseOnExec(ns)syscall.SetNonblock(ns, true) を呼び出して、それぞれ FD_CLOEXEC フラグと非ブロッキングモードを別途設定します。

src/pkg/net/sys_cloexec.go の変更点

このファイルは、SOCK_NONBLOCKSOCK_CLOEXEC フラグを直接サポートしないプラットフォーム向けのフォールバックロジックを実装しています。

  1. ビルドタグの変更:

    • // +build darwin dragonfly freebsd nacl netbsd openbsd solaris から // +build darwin dragonfly nacl netbsd openbsd solaris へ変更されました。
    • freebsd がこのビルドタグから削除されたことで、FreeBSDはもはやこのファイル(低速パス)を使用せず、sock_cloexec.go(高速パス)を使用するようになりました。
  2. sysSocket および accept 関数の変更:

    • sock_cloexec.go と同様に、引数名やローカル変数名がより明示的な名前に変更されました。これは、コードベース全体での一貫性と可読性向上のための変更です。機能的な変更はありません。

これらの変更により、Go言語のネットワークパッケージはFreeBSD 10以降のシステムで、より効率的かつ安全にソケットを操作できるようになりました。特に、fork(2) を伴うプロセス生成時にソケットディスクリプタが意図せず継承されるリスクが低減され、全体的な堅牢性が向上しています。

関連リンク

参考にした情報源リンク