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

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

このコミットは、Goランタイムのネットワークポーリングメカニズム、特にBSD系のシステムで利用されるkqueueに関するバグ修正です。src/pkg/runtime/netpoll_kqueue.cファイル内のruntime·netpoll()関数が、単一のイベントに対してruntime·netpollready()を複数回呼び出す可能性があった問題を解決しています。

コミット

commit 82ef961af59fdabc1a956ba6d7bc0c0b961172b2
Author: Bill Neubauer <wcn@golang.org>
Date:   Tue May 28 05:03:10 2013 +0800

    runtime: fix runtime·netpoll() to call runtime·netpollready() only once per event.
    
    R=golang-dev, minux.ma
    CC=golang-dev
    https://golang.org/cl/9808043

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

https://github.com/golang/go/commit/82ef961af59fdabc1a956ba6d7bc0c0b961172b2

元コミット内容

このコミットは、Goランタイムのネットワークポーリング処理におけるバグを修正するものです。具体的には、runtime·netpoll()関数が、kqueueから受け取った単一のイベント(例えば、読み込みと書き込みの両方が可能になったソケット)に対して、runtime·netpollready()関数を複数回呼び出してしまう問題を修正しています。これにより、不要な処理の重複や、それに伴うパフォーマンスの低下、あるいは競合状態の発生を防ぎます。

変更の背景

Goランタイムは、効率的なI/O処理のためにノンブロッキングI/Oとイベント駆動型モデルを採用しています。これは、ネットワーク接続などのI/O操作が完了するのを待つ間、他のゴルーチンが実行できるようにするためです。このI/O多重化を実現するために、OSが提供するイベント通知メカニズム(Linuxではepoll、macOS/BSDではkqueue、WindowsではI/O Completion Portsなど)を利用しています。

kqueueは、ファイルディスクリプタの状態変化(読み込み可能、書き込み可能など)を効率的に監視するためのシステムコールです。runtime·netpoll()関数は、このkqueueからのイベントを待ち受け、イベントが発生した際に、そのイベントに対応するゴルーチンをスケジューラにキューイングするためにruntime·netpollready()を呼び出します。

元の実装では、kqueueから返された単一のイベントが、例えばEVFILT_READ(読み込み可能)とEVFILT_WRITE(書き込み可能)の両方のフィルターに一致する場合、runtime·netpollready()がそれぞれのフィルターに対して個別に呼び出されていました。これは、同じPollDesc(ポーリング対象のディスクリプタを表現する構造体)に対して、実質的に同じイベント処理を二重にトリガーしてしまうことを意味します。このような重複呼び出しは、パフォーマンスのオーバーヘッドを引き起こすだけでなく、場合によっては競合状態や不正確な状態遷移を引き起こす可能性がありました。

このコミットの目的は、この冗長な呼び出しを排除し、単一のイベントに対してruntime·netpollready()が一度だけ、かつ正確なモード(読み込み、書き込み、またはその両方)で呼び出されるようにすることです。

前提知識の解説

Goランタイムとスケジューラ

Goプログラムは、Goランタイム上で動作します。Goランタイムは、ゴルーチン(軽量なスレッド)のスケジューリング、メモリ管理(ガベージコレクション)、チャネル通信、そしてI/O処理など、Go言語の並行処理モデルを支える重要な役割を担っています。 Goスケジューラは、M(マシン、OSスレッド)、P(プロセッサ、論理CPU)、G(ゴルーチン)という3つのエンティティを使ってゴルーチンを効率的にOSスレッドにマッピングし、実行します。I/O操作などでゴルーチンがブロックされる場合、スケジューラはそのゴルーチンを待機状態にし、他のゴルーチンを実行することで、OSスレッドの有効活用と高い並行性を実現します。

ネットワークポーリングとノンブロッキングI/O

GoのネットワークI/Oは、基本的にノンブロッキングで行われます。これは、read()write()のようなシステムコールが、データが準備できていない場合でもすぐに制御を返し、ゴルーチンがブロックされるのを防ぐことを意味します。 データが準備できていない場合、GoランタイムはOSのイベント通知メカニズム(kqueue, epollなど)にそのファイルディスクリプタを登録し、データが準備できるまでゴルーチンを待機させます。データが準備できたというイベントがOSから通知されると、ランタイムはそのゴルーチンを「実行可能」状態に戻し、スケジューラがそのゴルーチンを再開します。このプロセス全体を「ネットワークポーリング」と呼びます。

kqueue (Kernel Queue)

kqueueは、FreeBSD、macOS、NetBSD、OpenBSDなどのBSD系UNIXシステムで利用される、高性能なイベント通知インターフェースです。ファイルディスクリプタの状態変化(読み込み可能、書き込み可能、エラーなど)を効率的に監視するために設計されています。 kqueueを使用する基本的な流れは以下の通りです。

  1. kqueue()システムコールでカーネルキューを作成する。
  2. kevent()システムコールを使って、監視したいイベント(例: 特定のファイルディスクリプタの読み込みイベント)を登録する。
  3. kevent()システムコールを再度呼び出し、登録したイベントが発生するのを待つ(ブロッキングまたはノンブロッキング)。
  4. イベントが発生すると、kevent()は発生したイベントのリストを返す。 イベントはkevent構造体で表現され、filterフィールド(EVFILT_READ, EVFILT_WRITEなど)やudataフィールド(ユーザーが関連データを格納できるポインタ)などを含みます。

PollDesc

Goランタイム内部では、ネットワークI/Oを扱うファイルディスクリプタごとにPollDescという構造体が関連付けられています。この構造体は、そのディスクリプタの状態(読み込み可能か、書き込み可能かなど)、関連するゴルーチン、およびその他のポーリング関連のメタデータを管理します。runtime·netpollready()関数は、このPollDescを使って、イベントが発生したディスクリプタに関連するゴルーチンを特定し、実行可能状態に移行させます。

技術的詳細

このコミットの技術的な核心は、kqueueから返される単一のkeventが複数のフィルター(例えばEVFILT_READEVFILT_WRITE)に同時に一致する場合のruntime·netpollready()の呼び出しロジックの最適化です。

元のコードでは、runtime·netpoll()関数内でkqueueからイベントの配列eventsを受け取った後、各イベントevに対して以下のように処理していました。

 	for(i = 0; i < n; i++) {
 		ev = &events[i];
 		if(ev->filter == EVFILT_READ)
 			runtime·netpollready(&gp, (PollDesc*)ev->udata, 'r');
 		if(ev->filter == EVFILT_WRITE)
 			runtime·netpollready(&gp, (PollDesc*)ev->udata, 'w');
 	}

このロジックの問題点は、もしev->filterEVFILT_READEVFILT_WRITEの両方の条件を満たすようなイベント(これはkqueueの設計上はありえませんが、論理的なフローとして)や、あるいは同じudata(つまり同じPollDesc)を持つイベントがevents配列内に複数回現れる場合に、runtime·netpollready()が複数回呼び出されてしまうことです。特に、kqueueは一つのkevent構造体で複数のイベントフラグを表現するのではなく、filterによってイベントの種類を区別するため、このコードはEVFILT_READEVFILT_WRITEが同時に発生した場合に、それぞれ独立したkeventとして返されることを想定しているように見えます。しかし、もし何らかの理由で同じPollDescに対して読み書き両方のイベントが同時に発生し、それが別々のkeventとして返されたとしても、runtime·netpollreadyは同じPollDescに対して複数回呼び出されることになります。

修正後のコードでは、この問題を解決するためにmodeという新しい変数を導入しています。

 	for(i = 0; i < n; i++) {
 		ev = &events[i];
+		mode = 0; // 各イベントの処理開始時にmodeを初期化
 		if(ev->filter == EVFILT_READ)
-			runtime·netpollready(&gp, (PollDesc*)ev->udata, 'r');
+			mode += 'r'; // 読み込みイベントであれば'r'のASCII値を加算
 		if(ev->filter == EVFILT_WRITE)
-			runtime·netpollready(&gp, (PollDesc*)ev->udata, 'w');
+			mode += 'w'; // 書き込みイベントであれば'w'のASCII値を加算
+		if(mode) // modeが0でなければ(つまり、何らかのイベントが発生していれば)
+			runtime·netpollready(&gp, (PollDesc*)ev->udata, mode); // 一度だけ呼び出す
 	}

この変更により、各keventに対して、そのfilterEVFILT_READまたはEVFILT_WRITEのいずれか、あるいは両方に該当する場合でも、mode変数にそれぞれの文字のASCII値を加算し、最終的にmodeが非ゼロであれば(つまり、何らかのイベントが検出された場合)、runtime·netpollready()を一度だけ呼び出すようになります。 'r''w'のASCII値を加算するという方法は、Goランタイム内部でこれらの文字が特定の意味を持つフラグとして扱われていることを示唆しています。例えば、'r'は読み込み可能、'w'は書き込み可能を意味し、両方が加算された値は「読み書き両方可能」という複合的な状態を示すために使われると考えられます。これにより、runtime·netpollready()は、そのイベントが読み込み、書き込み、またはその両方のいずれに対応するのかを正確に一度の呼び出しで受け取ることができます。

この修正は、runtime·netpollready()の呼び出し回数を最適化し、ランタイムの効率性と正確性を向上させます。

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

変更はsrc/pkg/runtime/netpoll_kqueue.cファイルに集中しています。

--- a/src/pkg/runtime/netpoll_kqueue.c
+++ b/src/pkg/runtime/netpoll_kqueue.c
@@ -74,7 +74,7 @@ runtime·netpoll(bool block)
  	static int32 lasterr;
  	Kevent events[64], *ev;
  	Timespec ts, *tp;
- 	int32 n, i;
+ 	int32 n, i, mode; // mode変数の追加
  	G *gp;
  
  	if(kq == -1)
@@ -97,10 +97,13 @@ retry:
  	}
  	for(i = 0; i < n; i++) {
  	 	ev = &events[i];
+\t\tmode = 0; // modeの初期化
  	 	if(ev->filter == EVFILT_READ)
-\t\t\truntime·netpollready(&gp, (PollDesc*)ev->udata, 'r');
+\t\t\tmode += 'r'; // 'r'をmodeに加算
  	 	if(ev->filter == EVFILT_WRITE)
-\t\t\truntime·netpollready(&gp, (PollDesc*)ev->udata, 'w');
+\t\t\tmode += 'w'; // 'w'をmodeに加算
+\t\tif(mode) // modeが非ゼロの場合のみ
+\t\t\truntime·netpollready(&gp, (PollDesc*)ev->udata, mode); // 一度だけ呼び出し
  	}
  	if(block && gp == nil)
  	 	goto retry;

コアとなるコードの解説

  • int32 n, i, mode;: modeという新しいint32型の変数が宣言されました。この変数は、現在のイベントが読み込み可能か、書き込み可能か、またはその両方かを示すフラグを保持するために使用されます。
  • mode = 0;: 各イベントの処理ループの開始時に、mode変数が0に初期化されます。これにより、前のイベントの状態が次のイベントの処理に影響を与えないようにします。
  • if(ev->filter == EVFILT_READ) mode += 'r';: イベントのフィルターがEVFILT_READ(読み込みイベント)である場合、mode変数に文字'r'のASCII値が加算されます。
  • if(ev->filter == EVFILT_WRITE) mode += 'w';: イベントのフィルターがEVFILT_WRITE(書き込みイベント)である場合、mode変数に文字'w'のASCII値が加算されます。
  • if(mode) runtime·netpollready(&gp, (PollDesc*)ev->udata, mode);: mode変数が0でない場合(つまり、EVFILT_READまたはEVFILT_WRITEのいずれか、あるいは両方が検出された場合)、runtime·netpollready()関数が一度だけ呼び出されます。この際、第三引数には計算されたmodeの値が渡されます。これにより、runtime·netpollready()は、イベントの種類(読み込み、書き込み、またはその両方)を正確に一度の呼び出しで受け取ることができます。

この変更により、単一のPollDescに関連するイベントが複数回発生した場合でも、runtime·netpollready()が冗長に呼び出されることがなくなり、ランタイムの効率性と正確性が向上します。

関連リンク

  • Go言語の公式ドキュメント: https://golang.org/
  • Goのソースコード(GitHub): https://github.com/golang/go
  • kqueueに関するmanページ (FreeBSD): https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2
  • Goのネットワークポーラーに関する議論やドキュメント(Goの内部実装に関するより深い理解のため):
    • GoのI/Oスケジューリングに関するブログ記事やドキュメントは多数存在しますが、公式のものはGoのソースコード内のコメントやデザインドキュメントに散見されます。

参考にした情報源リンク