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

[インデックス 14435] ファイルの概要

このコミットは、Go言語の標準ライブラリである net パッケージ内の src/pkg/net/tcpsock_posix.go ファイルに対する変更です。このファイルは、POSIX互換システム(Linux, macOSなど)におけるTCPソケットの動作、特にTCPリスナーが新しい接続を受け入れる (AcceptTCP) 際の低レベルな処理を定義しています。

コミット

このコミットは、net パッケージにおける fd.sysfd のデータ競合(data race)を修正することを目的としています。具体的には、TCPListenerAcceptTCP メソッド内で fd.sysfd < 0 のチェックを削除することで、accept() システムコール中にファイルディスクリプタが閉じられた場合の競合状態を解消しています。これにより、AcceptTCP がより堅牢になり、予期せぬエラーやクラッシュを防ぎます。

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/c9856e7d2244670a9c8cd8a4e8aa361b7667575d

元コミット内容

commit c9856e7d2244670a9c8cd8a4e8aa361b7667575d
Author: Dave Cheney <dave@cheney.net>
Date:   Mon Nov 19 06:53:58 2012 +1100

    net: fix data race on fd.sysfd
    
    Fixes #4369.
    
    Remove the check for fd.sysfd < 0, the first line of fd.accept() tests if the fd is open correctly and will handle the fd being closed during accept.
    
    R=dvyukov, bradfitz
    CC=golang-dev
    https://golang.org/cl/6843076

変更の背景

この変更は、Go言語の net パッケージにおいて、fd.sysfd という内部フィールドに対するデータ競合の問題を解決するために行われました。データ競合は、複数のゴルーチン(Goの軽量スレッド)が同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生するプログラミング上のバグです。これにより、プログラムの動作が予測不能になったり、クラッシュしたりする可能性があります。

具体的には、TCPListenerAcceptTCP メソッドが新しい接続を受け入れる際に、内部のファイルディスクリプタ (fd) の sysfd フィールドが、別のゴルーチンによって同時に閉じられる(無効な値になる)可能性がありました。従来のコードでは、l.fd.sysfd < 0 というチェックがありましたが、このチェックと実際の accept() システムコール呼び出しの間に、fd が閉じられてしまう時間差が存在しました。この時間差がデータ競合の窓となり、無効なファイルディスクリプタに対して操作を行おうとして syscall.EINVAL などのエラーが発生したり、より深刻な問題を引き起こす可能性がありました。

コミットメッセージにある Fixes #4369 は、GoのIssue Trackerにおける特定のバグ報告に対応していることを示しています。このIssueでは、まさにこのデータ競合が報告され、修正が求められていました。

前提知識の解説

1. データ競合 (Data Race)

データ競合は、並行プログラミングにおける一般的なバグの一種です。以下の3つの条件がすべて満たされたときに発生します。

  • 複数のゴルーチン(またはスレッド)が同時に同じメモリ位置にアクセスする。
  • 少なくとも1つのアクセスが書き込み操作である。
  • アクセスが同期メカニズム(ミューテックス、チャネルなど)によって保護されていない。

データ競合が発生すると、プログラムの動作が非決定論的になり、デバッグが非常に困難になります。Go言語は、go run -race コマンドでデータ競合検出器を有効にすることができ、開発中にこれらの問題を特定するのに役立ちます。

2. ファイルディスクリプタ (File Descriptor, FD) と sysfd

ファイルディスクリプタは、Unix系OSにおいてファイルやソケットなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる非負の整数です。ソケット通信では、リスニングソケットや接続されたソケットはそれぞれファイルディスクリプタによって参照されます。

Go言語の net パッケージ内部では、これらのOSレベルのファイルディスクリプタを抽象化し、fd 構造体(またはそれに類するもの)で管理しています。fd.sysfd は、この抽象化された fd 構造体の中に保持されている、実際のOSレベルのファイルディスクリプタの値を指します。sysfd が負の値(特に -1)である場合、それはファイルディスクリプタが無効であるか、閉じられていることを示す慣例的な方法です。

3. accept() システムコール

accept() は、サーバープログラムがクライアントからの新しい接続要求を受け入れるために使用するシステムコールです。リスニングソケット(サーバーが接続を待機しているソケット)に対して accept() を呼び出すと、カーネルはキューにある次の接続要求を取り出し、その接続のための新しいソケット(および新しいファイルディスクリプタ)を作成して返します。元のリスニングソケットは引き続き新しい接続を受け入れることができます。

4. syscall.EINVAL

syscall.EINVAL は、Unix系システムコールが返す可能性のあるエラーコードの一つで、「無効な引数 (Invalid argument)」を意味します。このエラーは、システムコールに渡された引数が無効である場合(例えば、無効なファイルディスクリプタを渡した場合など)に返されます。

5. Goの net パッケージの構造

Goの net パッケージは、ネットワークI/Oのプリミティブを提供します。内部的には、OS固有のシステムコール(socket, bind, listen, accept, read, write など)を抽象化し、クロスプラットフォームで一貫したAPIを提供しています。TCPListenerTCPConn といった型は、これらの低レベルなソケット操作をGoのインターフェースとしてラップしたものです。

技術的詳細

このコミットの技術的詳細は、TCPListener.AcceptTCP() メソッドにおける l.fd.sysfd < 0 のチェックが、データ競合の根本原因となっていた点に集約されます。

従来のコードでは、AcceptTCP メソッドの冒頭で、リスナーの内部ファイルディスクリプタ l.fd が有効であり、かつそのOSレベルのファイルディスクリプタ l.fd.sysfd が負の値でないことを確認していました。

if l == nil || l.fd == nil || l.fd.sysfd < 0 {
    return nil, syscall.EINVAL
}

このチェックは、accept() システムコールを呼び出す前に、ソケットがまだ開いていることを確認するためのものでした。しかし、問題は、このチェックが実行された直後、かつ実際の l.fd.accept(sockaddrToTCP) が呼び出されるまでの間に、別のゴルーチン(例えば、リスナーを閉じるゴルーチン)が l.fd を閉じてしまい、l.fd.sysfd を無効な値(例えば -1)に設定する可能性があったことです。

もしこのような競合が発生した場合、l.fd.sysfd < 0 のチェックはパスしますが、その直後の l.fd.accept() 呼び出し時には l.fd.sysfd が既に無効な状態になっているため、accept() システムコールが失敗し、syscall.EINVAL などのエラーを返すか、より深刻な未定義の動作を引き起こす可能性がありました。

このコミットの修正は、l.fd.sysfd < 0 のチェックを削除するという非常にシンプルなものです。

if l == nil || l.fd == nil { // l.fd.sysfd < 0 のチェックが削除された
    return nil, syscall.EINVAL
}

この変更の根拠は、コミットメッセージにも明記されているように、「fd.accept() の最初の行が、fdが正しく開いているかをテストし、accept 中にfdが閉じられた場合を処理する」という点にあります。つまり、l.fd.accept() メソッド自体が、内部で sysfd の有効性を適切にチェックし、もし sysfd が無効になっていれば、それに応じたエラーハンドリングを行う責任を負っているということです。

これにより、AcceptTCP メソッドの呼び出し元は、l.fd.sysfd の状態を事前にチェックする必要がなくなり、l.fd.accept() に処理を委ねることで、データ競合の窓をなくすことができます。l.fd.accept() は、OSの accept システムコールを呼び出す前に、必要なロックやチェックを適切に行うことで、安全にファイルディスクリプタを扱います。もし accept 呼び出し中にファイルディスクリプタが閉じられたとしても、fd.accept() はそれを検出し、適切なエラー(例えば net.OpErrorsyscall.EINVAL をラップしたもの)を返すことで、プログラムがクラッシュすることなく、エラーを適切に処理できるようになります。

この修正は、Goの並行処理モデルにおいて、共有状態へのアクセスを最小限にし、責任を適切なコンポーネントに委譲するという設計原則に沿ったものです。

コアとなるコードの変更箇所

--- a/src/pkg/net/tcpsock_posix.go
+++ b/src/pkg/net/tcpsock_posix.go
@@ -231,7 +231,7 @@ type TCPListener struct {
 // AcceptTCP accepts the next incoming call and returns the new connection
 // and the remote address.
 func (l *TCPListener) AcceptTCP() (c *TCPConn, err error) {
-	if l == nil || l.fd == nil || l.fd.sysfd < 0 {
+	if l == nil || l.fd == nil {
 		return nil, syscall.EINVAL
 	}
 	fd, err := l.fd.accept(sockaddrToTCP)

コアとなるコードの解説

変更は src/pkg/net/tcpsock_posix.go ファイル内の TCPListener 型の AcceptTCP メソッドにあります。

元のコード:

func (l *TCPListener) AcceptTCP() (c *TCPConn, err error) {
	if l == nil || l.fd == nil || l.fd.sysfd < 0 {
		return nil, syscall.EINVAL
	}
	fd, err := l.fd.accept(sockaddrToTCP)
	// ... (後続の処理)
}

修正後のコード:

func (l *TCPListener) AcceptTCP() (c *TCPConn, err error) {
	if l == nil || l.fd == nil {
		return nil, syscall.EINVAL
	}
	fd, err := l.fd.accept(sockaddrToTCP)
	// ... (後続の処理)
}

変更点は、if 文の条件式から || l.fd.sysfd < 0 の部分が削除されたことです。

  • l == nil: TCPListener オブジェクト自体が nil であるかどうかのチェック。これは、メソッドが nil レシーバで呼び出された場合のパニックを防ぐための基本的なガードです。
  • l.fd == nil: TCPListener が内部で保持するファイルディスクリプタの抽象化オブジェクト fdnil であるかどうかのチェック。これは、リスナーが適切に初期化されていない場合や、既に閉じられている場合などに fdnil になる可能性があるためです。

削除された || l.fd.sysfd < 0 のチェックは、fd オブジェクトが存在する場合に、その内部のOSレベルのファイルディスクリプタ sysfd が有効な値(非負)であるかを直接確認していました。このチェックが削除されたことで、AcceptTCP メソッドは、l.fdnil でない限り、l.fd.accept() メソッドにソケットの受け入れ処理を委ねるようになりました。

この変更の意図は、fd.sysfd の状態チェックと実際の accept システムコール呼び出しの間に発生しうるデータ競合の窓をなくすことです。l.fd.accept() メソッド自体が、sysfd の有効性を内部で安全に(例えば、適切なロックを使用して)確認し、無効な場合はエラーを返す責任を持つため、AcceptTCP の呼び出し元で二重にチェックする必要がなくなりました。これにより、コードが簡素化され、並行処理における潜在的なバグが解消されました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (netパッケージ): https://pkg.go.dev/net
  • Go言語におけるデータ競合の検出: https://go.dev/doc/articles/race_detector
  • Unix系システムコール accept() のマニュアルページ (例: man 2 accept)
  • ファイルディスクリプタに関する一般的な情報 (例: Wikipedia, Linux man pages)
  • Go言語のソースコード (netパッケージの内部実装)