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

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

このコミットは、Goランタイムのスケジューラにネットワークポーリングのサポートを追加するものです。これは、ネットワークポーラーをランタイムに統合するという、より大きな変更の一部として行われました。具体的には、GoのスケジューラがネットワークI/Oの準備ができたゴルーチンを効率的に検出・実行できるようにするための変更が含まれています。

コミット

commit c21188473103a14beb3b0950fb2331fea7e16d80
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue Mar 12 21:14:26 2013 +0400

    runtime: add network polling support into scheduler
    This is a part of the bigger change that moves network poller into runtime:
    https://golang.org/cl/7326051/
    
    R=golang-dev, bradfitz, mikioh.mikioh, rsc
    CC=golang-dev
    https://golang.org/cl/7448048

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

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

元コミット内容

runtime: add network polling support into scheduler
This is a part of the bigger change that moves network poller into runtime:
https://golang.org/cl/7326051/

R=golang-dev, bradfitz, mikioh.mikioh, rsc
CC=golang-dev
https://golang.org/cl/7448048

変更の背景

このコミットは、Goランタイムにおけるネットワークポーラーのアーキテクチャ変更の一環として導入されました。以前は、ネットワークI/Oの監視はGoランタイムとは独立した形で処理されていた可能性があります。しかし、Goの並行処理モデル(ゴルーチンとスケジューラ)とのより密接な統合を図るため、ネットワークポーラーの機能をランタイム内部に移動させるという大きな計画がありました。

この変更の主な目的は、ネットワークI/Oが準備できた際に、Goスケジューラがより効率的かつ迅速に該当するゴルーチンを再開できるようにすることです。これにより、ネットワークアプリケーションのパフォーマンス向上、特に多数の同時接続を扱う際のレイテンシとスループットの改善が期待されます。コミットメッセージに記載されている https://golang.org/cl/7326051/ が、この「より大きな変更」の主要なチェンジリスト(CL)を示しています。

前提知識の解説

このコミットを理解するためには、Goランタイムの以下の主要な概念を理解しておく必要があります。

  • ゴルーチン (Goroutine): Goにおける軽量な実行スレッドです。数百万のゴルーチンを同時に実行できる設計になっています。
  • Goスケジューラ: ゴルーチンをOSスレッド(M)にマッピングし、実行を管理するGoランタイムのコンポーネントです。Goスケジューラは、M:Nスケジューリングモデルを採用しており、M個のOSスレッド上でN個のゴルーチンを実行します。
  • P (Processor): 論理プロセッサを表します。ゴルーチンを実行するためのコンテキストを提供し、Mとゴルーチンの間の仲介役となります。各Pはローカルな実行キューを持ち、ゴルーチンを保持します。
  • M (Machine/OS Thread): OSスレッドを表します。Pに割り当てられ、そのPのローカルキューからゴルーチンを取得して実行します。
  • G (Goroutine): ゴルーチン自体を表すデータ構造です。
  • ネットワークポーラー (Network Poller): ネットワークI/O操作(ソケットの読み書きなど)がブロックせずに実行可能になったことを検出するメカニズムです。Linuxのepoll、macOS/FreeBSDのkqueue、WindowsのIOCPなどがこれに該当します。Goランタイムは、これらのOS固有の機能を利用して、多数のネットワーク接続を効率的に監視します。
  • netpoll: Goランタイムが提供するネットワークポーリングの抽象化されたインターフェースです。この関数は、ネットワークI/Oの準備ができたゴルーチンを返します。block引数によって、ブロックするか非ブロックでポーリングを行うかを制御できます。
  • proc.c: Goランタイムのスケジューリングロジックの大部分が含まれるC言語のソースファイルです。ゴルーチンの作成、実行、ブロック、再開などの処理が定義されています。
  • sysmon (System Monitor): Goランタイムのバックグラウンドで動作するM(OSスレッド)です。定期的に実行され、ガベージコレクションのトリガー、プリエンプションの実行、ネットワークポーラーの呼び出しなど、様々なハウスキーピングタスクを実行します。
  • findrunnable: スケジューラが次に実行すべきゴルーチンを見つけるための関数です。ローカルキュー、グローバルキュー、他のPからのゴルーチン窃盗(work stealing)などを試みます。

技術的詳細

このコミットの技術的な核心は、GoスケジューラがネットワークI/Oの準備ができたゴルーチンを、より積極的にかつ効率的にスケジューリングプロセスに組み込む点にあります。

  1. netpoll_stub.c の追加:

    • このファイルは、Windowsプラットフォーム向けに runtime·netpoll 関数のスタブ実装を提供します。これは、Windowsが統合されたネットワークポーラーをサポートしていない(または当時のGoランタイムがまだそのサポートを完全に統合していなかった)ためです。
    • +build windows ディレクティブにより、このファイルはWindowsビルドでのみコンパイルされます。
    • runtime·netpollnil を返し、ネットワークポーリングが利用できないことを示します。
  2. proc.c の変更:

    • Sched 構造体への lastpoll フィールドの追加:
      struct Sched {
          // ...
          uint64  lastpoll;
      };
      
      lastpoll は、最後にネットワークポーリングが実行された時刻(ナノ秒単位)を記録するために使用されます。これにより、ポーリングの頻度を制御し、過度なポーリングを防ぎます。
    • runtime·schedinit での lastpoll の初期化:
      runtime·sched.lastpoll = runtime·nanotime();
      
      スケジューラの初期化時に、lastpoll が現在の時刻で初期化されます。
    • runtime·starttheworld での非ブロックポーリング:
      gp = runtime·netpoll(false);  // non-blocking
      injectglist(gp);
      
      システムが起動する際に、非ブロックで一度ネットワークポーリングを実行し、準備ができたゴルーチンがあればすぐにスケジューラに注入します。
    • findrunnable でのネットワークポーリングの統合: findrunnable 関数は、実行可能なゴルーチンを見つけるための主要なループです。このコミットにより、ゴルーチンを見つけるための試行順序にネットワークポーリングが組み込まれました。
      • 非ブロックポーリング:
        gp = runtime·netpoll(false);  // non-blocking
        if(gp) {
            injectglist(gp->schedlink);
            gp->status = Grunnable;
            return gp;
        }
        
        findrunnable が他のPやグローバルキューからゴルーチンを見つけられなかった場合、まず非ブロックでネットワークポーリングを試みます。これにより、ネットワークI/Oが準備できたゴルーチンがあれば、すぐに実行キューに戻されます。
      • ブロックポーリング:
        if(runtime·xchg64(&runtime·sched.lastpoll, 0) != 0) {
            // ...
            gp = runtime·netpoll(true);  // block until new work is available
            runtime·atomicstore64(&runtime·sched.lastpoll, runtime·nanotime());
            // ...
        }
        
        もしスケジューラが実行可能なゴルーチンを全く見つけられず、かつスピンしているMの数がPの数以上である場合(つまり、CPUを無駄に消費している可能性がある場合)、スケジューラはブロックするネットワークポーリングを試みます。これは、新しいネットワークイベントが発生するまでMをスリープさせることで、CPUリソースの無駄な消費を防ぎます。lastpoll を0に設定し、ポーリング後に現在の時刻で更新することで、ポーリングが実行中であることを示し、他のMが同時にブロックポーリングを行うのを防ぎます。
    • injectglist 関数の追加:
      static void
      injectglist(G *glist)
      {
          // ...
          runtime·lock(&runtime·sched);
          for(n = 0; glist; n++) {
              gp = glist;
              glist = gp->schedlink;
              gp->status = Grunnable;
              globrunqput(gp); // Add to global runnable queue
          }
          runtime·unlock(&runtime·sched);
      
          for(; n && runtime·sched.npidle; n--)
              startm(nil, false); // Start new M if idle P available
      }
      
      この新しい関数は、netpoll から返された実行可能なゴルーチンのリストをスケジューラに注入するために使用されます。ゴルーチンは Grunnable ステータスに設定され、グローバル実行キューに追加されます。また、注入されたゴルーチンの数に応じて、アイドル状態のPがあれば新しいMを起動し、ゴルーチンをすぐに実行できるようにします。これはGCと並行して実行できるように設計されています。
    • sysmon でのネットワークポーリング:
      // poll network if not polled for more than 10ms
      lastpoll = runtime·atomicload64(&runtime·sched.lastpoll);
      now = runtime·nanotime();
      if(lastpoll != 0 && lastpoll + 10*1000*1000 > now) {
          gp = runtime·netpoll(false);  // non-blocking
          injectglist(gp);
      }
      
      sysmon スレッドも定期的に(10ミリ秒ごとに)非ブロックでネットワークポーリングを実行し、準備ができたゴルーチンをスケジューラに注入します。これにより、ネットワークイベントが長時間処理されないことを防ぎます。
  3. runtime.h の変更:

    • runtime·netpoll 関数のプロトタイプ宣言が追加されました。
      G*  runtime·netpoll(bool);
      

これらの変更により、GoスケジューラはネットワークI/Oの準備ができたゴルーチンを、より迅速かつ効率的に検出し、実行キューに戻すことができるようになりました。これにより、ネットワークアプリケーションの応答性とスループットが向上します。

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

  • src/pkg/runtime/netpoll_stub.c: 新規追加。Windows向けの runtime·netpoll のスタブ実装。
  • src/pkg/runtime/proc.c:
    • Sched 構造体に lastpoll フィールドを追加。
    • runtime·schedinitlastpoll を初期化。
    • runtime·starttheworld で非ブロック netpoll を呼び出し、injectglist でゴルーチンを注入。
    • findrunnable 関数内で、非ブロックおよびブロック netpoll の呼び出しを追加し、ネットワークポーリングをスケジューリングロジックに統合。
    • injectglist 関数を新規追加。
    • sysmon 関数内で、定期的な非ブロック netpoll の呼び出しを追加。
  • src/pkg/runtime/runtime.h: runtime·netpoll 関数のプロトタイプ宣言を追加。

コアとなるコードの解説

src/pkg/runtime/proc.c の変更点

Sched 構造体への lastpoll の追加

struct Sched {
    // ...
    uint64  lastpoll; // 最後にネットワークポーリングが実行された時刻 (ナノ秒)
};

このフィールドは、ネットワークポーリングの頻度を制御し、ポーリングが過剰に行われるのを防ぐために使用されます。

findrunnable 関数内の変更

findrunnable は、スケジューラが次に実行すべきゴルーチンを見つけるための中心的な関数です。

static G*
findrunnable(void)
{
    // ... (既存のゴルーチン探索ロジック)

    // poll network (非ブロック)
    gp = runtime·netpoll(false);  // ネットワークI/Oが準備できたゴルーチンを非ブロックで取得
    if(gp) {
        injectglist(gp->schedlink); // 取得したゴルーチンをスケジューラに注入
        gp->status = Grunnable;     // 実行可能状態に設定
        return gp;                  // 取得したゴルーチンを返す
    }

    // ... (スピンしているMの数が多い場合のブロックロジック)

    // poll network (ブロック)
    if(runtime·xchg64(&runtime·sched.lastpoll, 0) != 0) { // lastpollを0に設定し、ポーリング中であることを示す
        // ... (エラーチェック)
        gp = runtime·netpoll(true);  // 新しいネットワークイベントが発生するまでブロック
        runtime·atomicstore64(&runtime·sched.lastpoll, runtime·nanotime()); // ポーリング時刻を更新
        if(gp) {
            // ... (アイドルPがあれば取得し、ゴルーチンを注入)
            injectglist(gp); // 取得したゴルーチンをスケジューラに注入
        }
    }
    // ... (Mを停止するロジック)
}

この変更により、スケジューラはゴルーチンを探す際に、ネットワークI/Oの準備状況も考慮するようになりました。非ブロックポーリングは迅速な応答を可能にし、ブロックポーリングはCPUリソースの効率的な利用を促進します。

injectglist 関数の追加

// Injects the list of runnable G's into the scheduler.
// Can run concurrently with GC.
static void
injectglist(G *glist)
{
    int32 n;
    G *gp;

    if(glist == nil)
        return;
    runtime·lock(&runtime·sched); // スケジューラロックを取得
    for(n = 0; glist; n++) {
        gp = glist;
        glist = gp->schedlink;
        gp->status = Grunnable; // ゴルーチンを実行可能状態に設定
        globrunqput(gp);        // グローバル実行キューに追加
    }
    runtime·unlock(&runtime·sched); // スケジューラロックを解放

    for(; n && runtime·sched.npidle; n--)
        startm(nil, false); // 注入されたゴルーチンの数に応じて、アイドルPがあれば新しいMを起動
}

この関数は、netpoll から返されたゴルーチンをスケジューラに統合する役割を担います。これにより、ネットワークイベントによってブロックされていたゴルーチンが、速やかに実行可能な状態に戻されます。

sysmon 関数内の変更

static void
sysmon(void)
{
    // ... (既存のsysmonロジック)

    // poll network if not polled for more than 10ms
    lastpoll = runtime·atomicload64(&runtime·sched.lastpoll);
    now = runtime·nanotime();
    if(lastpoll != 0 && lastpoll + 10*1000*1000 > now) { // 10ms以上ポーリングされていない場合
        gp = runtime·netpoll(false);  // 非ブロックでネットワークポーリング
        injectglist(gp);              // 準備ができたゴルーチンを注入
    }
    // ... (Pの再取得ロジック)
}

sysmon はバックグラウンドで動作し、定期的にネットワークポーリングを行うことで、ネットワークイベントが長時間見過ごされることを防ぎます。

関連リンク

参考にした情報源リンク