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

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

このコミットは、Goランタイムのネットワークポーリング(netpoll)メカニズムにおいて、epoll(Linux)およびkqueue(macOS/BSD)システムコールがエラーを返した場合に、重複するエラーメッセージが大量に出力されるのを防ぐための修正です。特に、テスト中にファイルディスクリプタが予期せず閉じられた際に発生する可能性のある「エラーメッセージの嵐」を抑制することを目的としています。

コミット

commit 94599ea745f46d2645c12481faad930084e94546
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue Mar 26 20:25:43 2013 +0400

    runtime: does not report duplicate errors in netpoll
    Prevents storm of error messages if something goes wrong.
    In the case of issue 5073 the epoll fd was closed by the test.
    Update #5073.
    
    R=golang-dev, r, rsc
    CC=golang-dev
    https://golang.org/cl/7966043
---
 src/pkg/runtime/netpoll_epoll.c  | 7 +++++--
 src/pkg/runtime/netpoll_kqueue.c | 7 +++++--
 2 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/src/pkg/runtime/netpoll_epoll.c b/src/pkg/runtime/netpoll_epoll.c
index d6ef0d1446..9b5980700e 100644
--- a/src/pkg/runtime/netpoll_epoll.c
+++ b/src/pkg/runtime/netpoll_epoll.c
@@ -57,6 +57,7 @@ runtime·netpollclose(int32 fd)
 G*
 runtime·netpoll(bool block)
 {
+	static int32 lasterr;
 	EpollEvent events[128], *ev;
 	int32 n, i, waitms, mode;
 	G *gp;
@@ -69,8 +70,10 @@ runtime·netpoll(bool block)
 retry:
 	n = runtime·epollwait(epfd, events, nelem(events), waitms);
 	if(n < 0) {
-\t\tif(n != -EINTR)\n-\t\t\truntime·printf(\"epollwait failed with %d\\n\", -n);\n+\t\tif(n != -EINTR && n != lasterr) {\n+\t\t\tlasterr = n;\n+\t\t\truntime·printf(\"runtime: epollwait on fd %d failed with %d\\n\", epfd, -n);\n+\t\t}\n \t\tgoto retry;\n \t}\n \tgp = nil;\ndiff --git a/src/pkg/runtime/netpoll_kqueue.c b/src/pkg/runtime/netpoll_kqueue.c
index ad721e293e..0ed03d31fa 100644
--- a/src/pkg/runtime/netpoll_kqueue.c
+++ b/src/pkg/runtime/netpoll_kqueue.c
@@ -71,6 +71,7 @@ runtime·netpollclose(int32 fd)
 G*
 runtime·netpoll(bool block)
 {
+	static int32 lasterr;
 	Kevent events[64], *ev;
 	Timespec ts, *tp;\n \tint32 n, i;\n@@ -88,8 +89,10 @@ runtime·netpoll(bool block)
 retry:
 	n = runtime·kevent(kq, nil, 0, events, nelem(events), tp);
 	if(n < 0) {
-\t\tif(n != -EINTR)\n-\t\t\truntime·printf(\"kqueue failed with %d\\n\", -n);\n+\t\t\tif(n != -EINTR && n != lasterr) {\n+\t\t\tlasterr = n;\n+\t\t\truntime·printf(\"runtime: kevent on fd %d failed with %d\\n\", kq, -n);\n+\t\t}\n \t\tgoto retry;\n \t}\n \tfor(i = 0; i < n; i++) {\n```

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

[https://github.com/golang/go/commit/94599ea745f46d2645c12481faad930084e94546](https://github.com/golang/go/commit/94599ea745f46d2645c12481faad930084e94546)

## 元コミット内容

Goランタイムのネットワークポーリング処理において、`epollwait`(Linux)または`kevent`(macOS/BSD)システムコールがエラーを返した場合に、重複するエラーメッセージの報告を抑制する。これにより、何らかの問題が発生した際に大量のエラーメッセージが出力される「エラーの嵐」を防ぐ。特に、`issue 5073`のケースでは、テストによって`epoll`のファイルディスクリプタが閉じられたことが原因で問題が発生した。

## 変更の背景

Goのランタイムは、効率的なネットワークI/Oを実現するために、OSが提供するイベント通知メカニズム(Linuxの`epoll`、macOS/BSDの`kqueue`など)を利用しています。これらのシステムコールは、ネットワークソケットの準備ができたことを非同期に通知する役割を担っています。

しかし、何らかの理由でこれらのシステムコールがエラーを返した場合、Goランタイムはエラーメッセージを標準出力に報告します。問題は、同じエラーが連続して発生する可能性があることです。例えば、ファイルディスクリプタが不正な状態になった場合、`epollwait`や`kevent`は繰り返し同じエラーコードを返し続ける可能性があります。

コミットメッセージに記載されている`issue 5073`は、この問題の具体的な例として挙げられています。この問題では、テストコードが`epoll`のファイルディスクリプタを閉じてしまい、その結果、`epollwait`が繰り返しエラーを返し、大量のエラーメッセージがコンソールに表示されるという事象が発生しました。このような「エラーの嵐」は、デバッグを困難にし、ログファイルを肥大化させ、システムのパフォーマンスにも影響を与える可能性があります。

このコミットは、このような状況下で、同じエラーが連続して発生しても、そのエラーメッセージを一度だけ報告し、重複する報告を抑制することで、ランタイムの堅牢性とデバッグのしやすさを向上させることを目的としています。

## 前提知識の解説

### Goランタイムのネットワークポーリング(netpoll)

Go言語は、高い並行性を特徴としており、多数のネットワーク接続を効率的に処理するために独自のI/O多重化メカニズムである`netpoll`を採用しています。GoのネットワークI/Oは、開発者からはブロッキングI/Oのように見えますが、内部的には非ブロッキングI/OとOSのイベント通知メカニズムを組み合わせて実現されています。

1.  **ブロッキングI/Oの抽象化**: Goの`net.Conn.Read()`や`net.Conn.Write()`のような操作は、あたかもブロッキングされているかのように動作します。これにより、開発者は複雑な非同期プログラミングを意識することなく、直感的にコードを書くことができます。
2.  **非ブロッキングI/Oの内部実装**: 実際には、Goランタイムはネットワークソケットを非ブロッキングモードに設定します。
3.  **netpollerの役割**: ゴルーチンがネットワークI/O操作を試み、データがすぐに利用できない場合(例: 読み取るデータがない、書き込みバッファが満杯)、`netpoller`が介入します。
    *   該当するゴルーチンは「パーク」(一時停止)され、OSスレッドから切り離されます。
    *   ファイルディスクリプタは、OSのI/O多重化機能(Linuxの`epoll`、macOS/BSDの`kqueue`、WindowsのIOCPなど)に登録されます。
    *   パークされたゴルーチンを実行していたOSスレッドは解放され、他のゴルーチンを実行できるようになります。
4.  **イベント通知とゴルーチンの再開**: OSが登録されたファイルディスクリプタがI/O可能になったことを検出すると(例: ソケットに新しいデータが到着)、`netpoller`に通知します。`netpoller`はこのイベントを受け取り、対応するパークされたゴルーチンを再開可能なキューに戻します。Goスケジューラは、利用可能なOSスレッドでそのゴルーチンを再開します。

このアーキテクチャにより、Goは少数のOSスレッドで多数の並行ネットワーク接続を管理でき、従来の「接続ごとにスレッド」モデルと比較してオーバーヘッドを大幅に削減します。`netpoller`は通常、エッジトリガー通知を使用し、状態の変化があった場合にのみ通知を受けるため、不要なシステムコールを減らし、パフォーマンスを最適化します。

### epoll (Linux)

`epoll`はLinuxカーネルが提供するI/Oイベント通知メカニズムです。多数のファイルディスクリプタ(ソケットなど)を監視し、それらのいずれかがI/O可能になったときに効率的に通知を受け取ることができます。

*   **`epoll_create`**: `epoll`インスタンスを作成し、`epoll`ファイルディスクリプタを返します。
*   **`epoll_ctl`**: `epoll`インスタンスにファイルディスクリプタを追加、変更、削除します。監視したいイベント(読み込み可能、書き込み可能など)を指定します。
*   **`epoll_wait`**: 登録されたファイルディスクリプタのいずれかでイベントが発生するのを待ちます。イベントが発生すると、イベントが発生したファイルディスクリプタのリストを返します。

`epoll`は、特に多数の同時接続を扱うサーバーアプリケーションで高いパフォーマンスを発揮します。

### kqueue (macOS/BSD)

`kqueue`はmacOS、FreeBSD、NetBSDなどのBSD系OSが提供するイベント通知メカニズムです。`epoll`と同様に、多数のファイルディスクリプタやその他のカーネルイベントを効率的に監視できます。

*   **`kqueue`**: `kqueue`インスタンスを作成し、`kqueue`ファイルディスクリプタを返します。
*   **`kevent`**: `kqueue`インスタンスにイベントフィルタ(監視したいイベント)を追加、変更、削除したり、発生したイベントを取得したりするために使用されます。`epoll_ctl`と`epoll_wait`の機能を兼ね備えています。

`kqueue`もまた、高パフォーマンスなI/O多重化を実現するために広く利用されています。

### `EINTR`エラー

`EINTR`は、システムコールがシグナルによって中断されたことを示すエラーコードです。これはエラーではないため、通常はシステムコールを再試行する必要があります。Goランタイムの`netpoll`では、`epollwait`や`kevent`が`EINTR`を返した場合、処理を中断せずに`goto retry;`でシステムコールを再試行するロジックが元々含まれています。このコミットの変更は、`EINTR`以外のエラーに焦点を当てています。

## 技術的詳細

このコミットの主要な目的は、`epollwait`または`kevent`システムコールが`EINTR`以外のエラーを返した場合に、同じエラーメッセージが繰り返し出力されるのを防ぐことです。これを実現するために、`runtime·netpoll`関数内に`static int32 lasterr;`という静的変数が導入されました。

1.  **`static int32 lasterr;`の導入**:
    *   `lasterr`は、`runtime·netpoll`関数内で最後に報告されたエラーコードを記憶するための静的変数です。静的変数であるため、関数の呼び出し間でその値が保持されます。初期値は0(または未定義動作に依存しないように初期化される)です。
    *   `src/pkg/runtime/netpoll_epoll.c`と`src/pkg/runtime/netpoll_kqueue.c`の両方にこの変数が追加されています。

2.  **エラー報告ロジックの変更**:
    *   変更前は、`n < 0`(システムコールがエラーを返した場合)かつ`n != -EINTR`(シグナルによる中断ではない場合)であれば、無条件に`runtime·printf`でエラーメッセージが出力されていました。
    *   変更後は、エラーメッセージを出力する条件に`n != lasterr`が追加されました。
        *   `if(n < 0) {`
        *   `    if(n != -EINTR && n != lasterr) {`
        *   `        lasterr = n;`
        *   `        runtime·printf(...)`
        *   `    }`
        *   `    goto retry;`
        *   `}`
    *   この新しい条件`n != lasterr`により、現在発生したエラーコード`n`が、前回報告されたエラーコード`lasterr`と異なる場合にのみ、エラーメッセージが出力されます。
    *   エラーメッセージが出力された後、`lasterr`は現在のエラーコード`n`で更新されます。

このメカニズムにより、`epollwait`や`kevent`が同じエラーコードを連続して返し続けたとしても、最初の一度だけエラーメッセージが報告され、それ以降は同じエラーが続く限りメッセージは出力されなくなります。これにより、ログの肥大化やコンソールへの大量出力が抑制され、デバッグ時の視認性が向上します。

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

### `src/pkg/runtime/netpoll_epoll.c`

```diff
--- a/src/pkg/runtime/netpoll_epoll.c
+++ b/src/pkg/runtime/netpoll_epoll.c
@@ -57,6 +57,7 @@ runtime·netpollclose(int32 fd)
 G*
 runtime·netpoll(bool block)
 {
+	static int32 lasterr;
 	EpollEvent events[128], *ev;
 	int32 n, i, waitms, mode;
 	G *gp;
@@ -69,8 +70,10 @@ runtime·netpoll(bool block)
 retry:
 	n = runtime·epollwait(epfd, events, nelem(events), waitms);
 	if(n < 0) {
-\t\tif(n != -EINTR)\n-\t\t\truntime·printf(\"epollwait failed with %d\\n\", -n);\n+\t\tif(n != -EINTR && n != lasterr) {\n+\t\t\tlasterr = n;\n+\t\t\truntime·printf(\"runtime: epollwait on fd %d failed with %d\\n\", epfd, -n);\n+\t\t}\n \t\tgoto retry;\n \t}\n \tgp = nil;\n```

### `src/pkg/runtime/netpoll_kqueue.c`

```diff
--- a/src/pkg/runtime/netpoll_kqueue.c
+++ b/src/pkg/runtime/netpoll_kqueue.c
@@ -71,6 +71,7 @@ runtime·netpollclose(int32 fd)
 G*
 runtime·netpoll(bool block)
 {
+	static int32 lasterr;
 	Kevent events[64], *ev;
 	Timespec ts, *tp;\n \tint32 n, i;\n@@ -88,8 +89,10 @@ runtime·netpoll(bool block)
 retry:
 	n = runtime·kevent(kq, nil, 0, events, nelem(events), tp);
 	if(n < 0) {
-\t\tif(n != -EINTR)\n-\t\t\truntime·printf(\"kqueue failed with %d\\n\", -n);\n+\t\t\tif(n != -EINTR && n != lasterr) {\n+\t\t\tlasterr = n;\n+\t\t\truntime·printf(\"runtime: kevent on fd %d failed with %d\\n\", kq, -n);\n+\t\t}\n \t\tgoto retry;\n \t}\n \tfor(i = 0; i < n; i++) {\n```

## コアとなるコードの解説

両方のファイル(`netpoll_epoll.c`と`netpoll_kqueue.c`)において、`runtime·netpoll`関数内に以下の変更が加えられています。

1.  **`static int32 lasterr;`**:
    *   `runtime·netpoll`関数のスコープ内で、`lasterr`という名前の静的整数型変数が宣言されています。
    *   `static`キーワードにより、この変数はプログラムの実行期間中メモリに保持され、`runtime·netpoll`が呼び出されるたびに初期化されることはありません。これにより、前回の関数呼び出しで発生したエラーコードを記憶しておくことが可能になります。

2.  **エラー報告条件の変更**:
    *   システムコール(`epollwait`または`kevent`)が負の値`n`を返した場合(エラーを示す)、エラー処理ブロックに入ります。
    *   変更前の条件は`n != -EINTR`でした。これは、シグナルによる中断(`EINTR`)ではない場合にエラーを報告するという意味です。
    *   変更後の条件は`n != -EINTR && n != lasterr`です。
        *   `n != -EINTR`: 以前と同様に、シグナルによる中断ではないことを確認します。
        *   `n != lasterr`: **新しく追加された条件**です。現在発生したエラーコード`n`が、前回報告されたエラーコード`lasterr`と異なる場合にのみ、この条件が真となります。
    *   この二つの条件が両方とも真である場合にのみ、以下の処理が実行されます。
        *   `lasterr = n;`: 現在のエラーコード`n`を`lasterr`に保存し、次回以降の比較のために更新します。
        *   `runtime·printf(...)`: エラーメッセージを標準出力に出力します。`epoll`の場合は`"runtime: epollwait on fd %d failed with %d\\n"`、`kqueue`の場合は`"runtime: kevent on fd %d failed with %d\\n"`という形式で、ファイルディスクリプタとエラーコードが表示されます。

このロジックにより、同じエラーが連続して発生しても、最初の1回だけメッセージが出力され、それ以降は同じエラーが続く限りメッセージは抑制されます。これにより、ログのスパムを防ぎ、重要なエラーメッセージを見つけやすくする効果があります。

## 関連リンク

*   Go言語の公式Issueトラッカー: [https://github.com/golang/go/issues](https://github.com/golang/go/issues) (コミットメッセージに記載されている`issue 5073`は、Goの公式Issueトラッカーでは直接見つかりませんでしたが、内部的な追跡番号であるか、非常に古いIssueである可能性があります。)
*   Goのコードレビューシステム (Gerrit): [https://golang.org/cl/7966043](https://golang.org/cl/7966043) (このコミットの変更リストへのリンク)

## 参考にした情報源リンク

*   The Go runtime's netpoll: [https://goperf.dev/blog/go-netpoll](https://goperf.dev/blog/go-netpoll)
*   Go's netpoller: [https://morsmachine.dk/go-scheduler](https://morsmachine.dk/go-scheduler)
*   Understanding Go's Netpoller: [https://stackademic.com/blog/understanding-gos-netpoller](https://stackademic.com/blog/understanding-gos-netpoller)
*   Go Concurrency: The Netpoller: [https://medium.com/@juliensalinas/go-concurrency-the-netpoller-101-c71222222222](https://medium.com/@juliensalinas/go-concurrency-the-netpoller-101-c71222222222)
*   epoll(7) - Linux man page: [https://man7.org/linux/man-pages/man7/epoll.7.html](https://man7.org/linux/man-pages/man7/epoll.7.html)
*   kqueue(2) - FreeBSD man page: [https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2](https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2)
*   EINTR - Wikipedia: [https://en.wikipedia.org/wiki/EINTR](https://en.wikipedia.org/wiki/EINTR)