Read OSS

マルチプロセスアーキテクチャとIPCシステム

上級

前提知識

  • 第1回:アーキテクチャ概要(マルチプロセスモデルの紹介)
  • プロセス間通信の基本概念(メッセージパッシング、シリアライゼーション)への理解
  • C++の仮想ディスパッチパターンとテンプレートメタプログラミングの基礎

マルチプロセスアーキテクチャとIPCシステム

WebKit2のマルチプロセスアーキテクチャは、オリジナルのKHTMLフォーク以来、このプロジェクトで最も重要なアーキテクチャ上の革新です。Webコンテンツを独立したプロセスで動作させることで、WebKitは次の3点を保証します。侵害されたWebページがユーザーのファイルシステムにアクセスできないこと、クラッシュしたタブがブラウザ全体を巻き込まないこと、そしてGPUドライバのバグがセキュリティ脆弱性へとエスカレートしないことです。

しかし、マルチプロセスアーキテクチャの価値はIPCシステムによって左右されます。この記事では、.messages.in定義ファイルからコード生成、実行時のディスパッチに至るまで、WebKitのカスタムメッセージパッシングフレームワークを詳しく見ていきます。また、ページロードがプロセス境界をまたいでどのように行われるかを追い、全体の仕組みを把握しましょう。

4つのプロセスタイプ

第1回で紹介したように、WebKit2は4種類のプロセスタイプを使用します。ここではその実装クラスを確認してみましょう。

flowchart TD
    subgraph UI["UI Process"]
        APP["Host App (Safari)"]
        WPP["WebPageProxy<br/>4,000+ LOC"]
        NPP["NetworkProcessProxy"]
        GPP["GPUProcessProxy"]
    end
    
    subgraph WC["WebContent Process"]
        WP["WebPage<br/>wraps WebCore::Page"]
        WCORE["WebCore engine"]
        JSC_R["JSC runtime"]
    end
    
    subgraph NP["Network Process"]
        NETP["NetworkProcess"]
        NETC["NetworkConnectionToWebProcess"]
    end
    
    subgraph GP["GPU Process"]
        GPUP["GPUProcess"]
        GPUC["GPUConnectionToWebProcess"]
    end
    
    WPP <-->|"IPC::Connection"| WP
    NPP <-->|"IPC::Connection"| NETP
    GPP <-->|"IPC::Connection"| GPUP
    WP <-->|"IPC::Connection"| NETC
    WP <-->|"IPC::Connection"| GPUC

UIプロセスはアプリケーション本体です。WebPageProxyを保持しています。これはWebページの状態をミラーリングし、ユーザーのアクションをIPCで転送するプロキシオブジェクトです。WebPageProxyIPC::MessageReceiverを実装し、WebContentプロセスからのレスポンスを処理します。

WebContentプロセスWebPageを実行します。これはWebCore::Page(レンダリングエンジン)をラップし、IPC::MessageReceiverIPC::MessageSenderの両方を実装しています。通常、タブごとに独自のWebContentプロセスが割り当てられます。

ネットワークプロセスはHTTPネットワーキングをすべて集中管理します。すべてのネットワークリクエストを単一のプロセスを通じてルーティングすることで、WebKitはWebContentプロセスに直接ネットワークアクセスを与えることなく、クッキーポリシーの適用、キャッシュの管理、ストレージの処理を実現しています。

GPUプロセスはGPUアクセラレートされた処理を担当します。歴史的にセキュリティ脆弱性の主要な発生源であったGPUドライバコードを、WebContentサンドボックスとUIプロセスの両方から切り離す役割を果たします。

AuxiliaryProcess:統一されたプロセスライフサイクル

すべての子プロセス(WebContent、Network、GPU)はAuxiliaryProcessを継承しています。

classDiagram
    class IPC_Connection_Client {
        <<interface>>
        +didReceiveMessage()
        +didClose()
    }
    class IPC_MessageSender {
        <<interface>>
        +send()
        +sendSync()
        #messageSenderConnection() Connection*
    }
    class AuxiliaryProcess {
        +initialize()
        +disableTermination()
        +enableTermination()
        +addMessageReceiver()
        +removeMessageReceiver()
    }
    class WebProcess {
        -WebPage pages
    }
    class NetworkProcess {
        -NetworkSession sessions
    }
    class GPUProcess {
        -GPUConnectionToWebProcess connections
    }
    
    IPC_Connection_Client <|-- AuxiliaryProcess
    IPC_MessageSender <|-- AuxiliaryProcess
    AuxiliaryProcess <|-- WebProcess
    AuxiliaryProcess <|-- NetworkProcess
    AuxiliaryProcess <|-- GPUProcess

ここで重要な設計上の判断は、AuxiliaryProcessIPC::Connection::Client(メッセージ受信用)とIPC::MessageSender(メッセージ送信用)の両方を継承していることです。これにより、初期化・メッセージの受信と処理・終了という統一されたライフサイクルが確立されます。

addMessageReceiverremoveMessageReceiverメソッドにより、プロセスは異なるメッセージタイプのハンドラを動的に登録できます。メッセージはReceiverName(どのサブシステムがメッセージを処理すべきかを識別するenum)によってルーティングされ、オプションでデスティネーションID(特定のWebページなど、具体的なインスタンスを識別するもの)によってもルーティングされます。

IPCフレームワーク:Connection、Sender、Receiver

IPCのコアインフラはSource/WebKit/Platform/IPC/にあります。3つのクラスが骨格を形成しています。

IPC::Connectionは、プラットフォームプリミティブ(macOSではMachポート、LinuxではUnixドメインソケット)上に構築された、スレッドセーフな参照カウント付きIPCチャネルです。メッセージのシリアライズ、デシリアライズ、ディスパッチを担当します。

IPC::MessageSendersend()sendSync()メソッドを提供するインターフェースです。送信側はコネクションの詳細を知る必要はなく、send(SomeMessage { args... })を呼び出すだけで、フレームワークがシリアライズと配信を処理してくれます。

IPC::MessageReceiverは受信側のカウンターパートです。仮想メソッドdidReceiveMessage()didReceiveMessageWithReplyHandler()didReceiveSyncMessage()を定義しています。各receiverは受信したDecoderをパースし、適切なハンドラにディスパッチします。

ヒント: IPCの問題をデバッグする際は、Connection.hを出発点にしましょう。メッセージディスパッチスレッド、同期メッセージのタイムアウト動作、そして接続が中断された場合のエラー処理が定義されています。

メッセージ定義とコード生成

WebKitはIPCディスパッチコードを手書きしません。代わりに、カスタムIDLを使った.messages.inファイルにメッセージを定義し、Pythonスクリプトがその C++ディスパッチコードを生成します。

NetworkProcess.messages.inを見てみましょう。

[
    DispatchedFrom=UI,
    DispatchedTo=Networking,
    ExceptionForEnabledBy
]
messages -> NetworkProcess : AuxiliaryProcess WantsAsyncDispatchMessage {
    InitializeNetworkProcess(struct WebKit::NetworkProcessCreationParameters ...) -> ()
    CreateNetworkConnectionToWebProcess(...) -> (...) AllowedWhenWaitingForSyncReply
    ...
}

いくつかのポイントを確認しておきましょう。

  1. ヘッダアノテーション(DispatchedFrom=UIDispatchedTo=Networking)がメッセージのフロー方向を明記しています。
  2. -> ()構文がリプライメッセージ(デフォルトでは非同期)を定義します。
  3. AllowedWhenWaitingForSyncReplyは、プロセスが同期的なリプライ待ちでブロックされている間も処理できるメッセージを示します。
  4. プラットフォーム固有のメッセージは#if USE(SOUP) / #if USE(CURL)ガードで囲まれています。
flowchart LR
    MSG["NetworkProcess.messages.in"] --> PARSER["webkit/parser.py"]
    PARSER --> MODEL["webkit/model.py<br/>(AST of messages)"]
    MODEL --> GEN["generate-message-receiver.py"]
    GEN --> HEADER["NetworkProcessMessages.h<br/>(message enums, structs)"]
    GEN --> RECV["NetworkProcessMessageReceiver.cpp<br/>(dispatch switch)"]

generate-message-receiver.pyスクリプトはwebkit.parserを使って.messages.inファイルをパースし、次にwebkit.messagesでC++コードを生成します。生成されたreceiverには大きなswitch文が含まれており、各メッセージタイプをデシリアライズして対応するハンドラメソッドを呼び出します。

このコード生成アプローチが重要な理由は3つあります。型安全なシリアライゼーションを保証すること(コンパイラがメッセージの引数型の一致を確認する)、ボイラープレートをなくすこと、そして.messages.inファイルを編集するだけで新しいメッセージを簡単に追加できることです。

プロキシパターン:WebPageProxy ↔ WebPage

WebKit2のアーキテクチャを象徴する例が、WebPageProxy(UIプロセス)とWebPage(WebContentプロセス)のミラーリングです。ユーザーが開始したページロードを追ってみましょう。

sequenceDiagram
    participant User
    participant WPP as WebPageProxy<br/>(UI Process)
    participant IPC as IPC::Connection
    participant WP as WebPage<br/>(WebContent Process)
    participant WC as WebCore::Page
    
    User->>WPP: loadURL("https://example.com")
    WPP->>WPP: Update navigation state
    WPP->>IPC: send(LoadURL { url, ... })
    IPC->>WP: didReceiveMessage(LoadURL)
    WP->>WC: mainFrame().loader().load(request)
    WC-->>WP: didStartProvisionalNavigation
    WP->>IPC: send(DidStartProvisionalLoadForFrame { ... })
    IPC->>WPP: didReceiveMessage(DidStartProvisionalLoad)
    WPP->>WPP: Update UI (show loading indicator)
    
    Note over WC: Network process fetches HTML...
    
    WC-->>WP: didCommitNavigation
    WP->>IPC: send(DidCommitLoadForFrame { ... })
    IPC->>WPP: didReceiveMessage(DidCommitLoad)
    WPP->>WPP: Update URL bar, title

ここでは複数のことが同時に起きています。

  1. 状態のミラーリングWebPageProxyはナビゲーション状態(現在のURL、タイトル、読み込み進捗)の独自のコピーを保持しているため、UIプロセスはIPCのラウンドトリップなしにそれを参照できます。

  2. デフォルトで非同期 — ほとんどのメッセージは非同期です。UIプロセスはLoadURLを送信した後も処理を続け、ページの読み込み完了を待ってブロックされることはありません。

  3. 双方向メッセージングWebPageProxyWebPageにコマンドを送り、WebPageはイベントをWebPageProxyに返します。両側がMessageReceiverを実装しています。

  4. WebCoreの分離 — WebCoreのPageオブジェクトはWebContentプロセス内でのみ操作されます。UIプロセスがWebCore APIに直接アクセスすることはありません。

このプロキシパターンはWebKit2全体で繰り返されています。NetworkProcessProxyNetworkProcessGPUProcessProxyGPUProcessなど、多くの専門化されたプロキシペアが存在します。パターンが一貫しているため、一組を理解すればすべてを読み解けるようになります。

ヒント: あるコンポーネントのすべてのIPCメッセージを調べるには、.messages.inファイルを検索しましょう:find Source/WebKit -name "*.messages.in"。数十のファイルが見つかり、それらはIPCのサーフェスエリア全体のインデックスとして機能します。

次回予告

ここまで、メモリ管理からDOMレンダリング、マルチプロセスIPCに至るアーキテクチャの各層を見てきました。第5回ではJavaScriptCore — JavaScriptエンジン — を深く掘り下げます。ソースコードがインタープリテーションから最適化JITまで4つのコンパイルティアをどのように移行するか、B3コンパイラバックエンドがどのようにマシンコードを生成するか、そしてRiptideガベージコレクタがオブジェクトのライフタイムをどのように管理するかを追っていきましょう。この記事で説明したIPCパターンは、JSCのガベージコレクタがWebCoreの参照カウントオブジェクトとどのように連携するかを議論する際に再び登場します。