<style> u { text-decoration-color: orange; text-decoration-style: wavy; /* 波線 */ } </style> # ユーザー空間ライブラリ ユーザー空間で動くagentを実装するためのライブラリは [ghost-userspace/lib](https://github.com/google/ghost-userspace/tree/main/lib) 配下に実装されている。ここでは、そのライブラリで定義されているクラスや関数をまとめる。 # channel.h 主にメッセージ関連のデータ構造が定義されている。Channelとはメッセージキューのことを意味する。 ## [Message](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/channel.h#L21-L73) やり取りされるメッセージのクラス。 実際にやり取りされる生のデータ構造 [ghost_msg](ghost_msg) のラッパークラスと考えればよい。実際、以下のように構造体の各フィールド値を受け取るメンバ関数が実装されている。 | ghost_msg のフィールド | Message クラスのメンバ関数 | |-|-| | type | type() | | length | length() | | seqnum | seqnum() | | payload | payload() | 他に便利なメンバ関数としては、 ||| |-|-| | [gtid()](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/channel.h#L47-L50) | <u>タスク関連のメッセージのときにのみ</u>呼び出し可能。そのメッセージがどのタスクからのメッセージなのかを解析し、そのタスクの**gtid**を返す。 | [cpu()](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/channel.h#L38-L41) | <u>CPU関連のメッセージのときにのみ</u>呼び出し可能。そのメッセージがどのCPUからのメッセージなのかを解析し、**CPUの番号**を返す。 msgは **operator<<** が実装されており、std::coutなどによって標準出力に出力可能である。 クラスの特別な値として[**kEmpty**](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/channel.h#L72)が定義されており、これは空のメッセージを表す。メッセージを取り出そうとしたけどメッセージがキューに存在していなかったときなどに、この値が返される。 ## [Channel](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/channel.h#L72) メッセージキューを抽象化した抽象基底クラス。後述するLocalChannelが具象クラスとして実装される。 :::success このクラスが抽象基底クラスとして実装されている理由が[ここのコメント](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/channel.h#L97-L109)に書かれている。 ::: ### Peek & Consume 現在のキューから1つのメッセージを読み出すメソッド。 **Peek**は、先頭のメッセージを読み出し、 * メッセージが存在すれば、そのメッセージのMessageオブジェクトを返す。 * メッセージが存在しなければ、空のメッセージを指すkEmptyオブジェクトを返す。 **Consume**は、先頭のメッセージをキューから削除する。 :::success これら2つのメンバ関数は以下のような典型的な使われ方があり、Scheduler クラスのメッセージ処理の部分で利用される。 ```cpp Message msg; Channel *channel = [ ... ]; // メッセージを読み出したいChannelへのポインタをセット while (!(msg = channel->Peek()).empty()) { DispatchMessage(msg); channel->Consume(msg); } ``` 例えばFIFOスケジューラの実装は[こちら](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/schedulers/fifo/per_cpu/fifo_scheduler.cc#L319-L323)。 ::: ### AssociateTask ```cpp bool AssociateTask(Gtid gtid, int barrier, int* status) const; ``` スレッドをChannelに関連付ける。 スレッドをChannelに関連付けることで、**そのスレッドのメッセージをこのChannelに送るように設定**できる。そのため、ロードバランシングなどを行うときに使われたりする。 引数の意味は以下の通り。 ||| |-|-| | gtid | 関連付けを行うスレッドのgtid。 | barrier | そのスレッドの最新のbarrier値。 | status | システムコールの詳細な結果を格納するためのポインタ。特に必要なければ、**nullptr**を設定すればいい。 返り値は、処理に成功すればtrueを、失敗すればfalseを返す。 ++この処理は失敗することも全然ある++。例えば、スレッドに関連付けられたChannelを変更するとき、**そのスレッドが発行したメッセージがもとのChannelに残ったままであれば、失敗する**ことになっている。その場合は、もとのChannelに溜まっているメッセージの処理を行った後で、再度この関数を呼び出すようにする。 :::success 内部では、[GHOST_IOC_ASSOC_QUEUE](https://hackmd.io/@cwtjjkgpTyeVG_KcZmF3LQ/HJNj83_ma/https%3A%2F%2Fhackmd.io%2F%40cwtjjkgpTyeVG_KcZmF3LQ%2FS1aXXxTZa#GHOST_IOC_ASSOC_QUEUE) システムコールが発行されている。 agentスレッドもghOStスレッドも同様に関連付けることが可能だが、それぞれ意味合いが少し変わってくる。詳しくは[メッセージキューの関連付けとWAKEUPリスト](https://hackmd.io/@cwtjjkgpTyeVG_KcZmF3LQ/HJNj83_ma/https%3A%2F%2Fhackmd.io%2F%40cwtjjkgpTyeVG_KcZmF3LQ%2FHy2Oq2mmp#%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%82%AD%E3%83%A5%E3%83%BC%E3%81%AE%E9%96%A2%E9%80%A3%E4%BB%98%E3%81%91%E3%81%A8WAKEUP%E3%83%AA%E3%82%B9%E3%83%88)を参考。 ::: ### SetEnclaveDefault ```cpp bool SetEnclaveDefault() const; ``` 自身をデフォルトのキューとしてシステムに設定する関数。 この関数はEnclaveの初期化処理で呼び出されるため([ソースコード](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/enclave.cc#L45))、この関数を直接呼び出す必要はない。<u>ただし、初期化処理の中で [Scheduler::GetDefaultChannel](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/scheduler.h#L125-L126) を使うため、Scheduler::GetDefaultChannel は実装しておく必要がある</u>。 ## [LocalChannel](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/channel.h#L112-L145) Channelの具象クラス。 必要な仮想関数の実装が定義されている。 スケジューラの実装では、Schedulerクラスが管理するように設計するとよい。 [コンストラクタ](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/channel.cc#L26-L39)では3つの引数をとり、実際にメッセージキューを開設して、ラップする。 | 引数 || |-|-| | nelems | メッセージキューで使うリングバッファのサイズ。バッファのサイズは sizeof(struct ghost_msg) を1つの単位として指定する。 | node | NUMAノードの情報。 | cpulist | 新しいメッセージがpushされたときに起こすagent。CpuListで指定したのは、今の実装では2つまで指定することができるから。cpulistで指定したものは、コンストラクタ内部で [Ghost::ConfigQueueWakeup](#ConfigQueueWakeup) を呼び出すことで設定している。Per-CPU型の場合は対応するCPUを、Centralized型の場合は空リストを渡しておけばよさそう。空リストを渡した場合、メッセージキューに新しいメッセージがpushされてきても、どのagentスレッドも起こさないので、Centralized型には丁度いい。 それぞれのメンバ関数の実装へのリンクを貼っておく。説明はChannelの方を参照。 * [Peek](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/channel.cc#L69-L86) * [Consume](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/channel.cc#L55-L67) * [AssociateTask](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/channel.cc#L46-L53) * [SetEnclaveDefault](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/channel.cc#L88-L90) # base.h ## Gtid ++**g**host++ **t**hread **id**entifierの略。 * ghOStでスレッドを管理するときの識別子。 * /proc/\<pid\>/ghost/gtid から実際の値を見ることができる。 ghOStでスレッドを区別するときは64bitすべてを使う(LinuxのTIDは22bitしか使わない:参考:https://www.yunabe.jp/docs/maximum_number_of_threads.html)。 ```cpp // Returns the raw GTID (ghOSt thread identifier) of the calling thread. // // A ghost tid is a 64-bit identifier for a task (as opposed to a TID // that can be up to 22 bits). The extra bits allow an agent to distinguish // between incarnations of the same tid (due to tid reuse). It is always // possible to get the linux TID associated with a GTID and interact with // it using traditional linux tools (e.g. ps, top, gdb, /proc/<pid>). // // Most callers should never need to call this function (preferring // Gtid::Current() since it caches the gtid in a thread-local var). absl::StatusOr<int64_t> GetGtid(); ``` Linuxカーネルではtidを64bit整数で管理しているが、この値が取ることのできる上限も定めている。値は以下のコマンドで確認できる。 ``` $ sudo cat /proc/sys/kernel/pid_max_max 4194304 ``` 手元の環境では `4194304(=0x400000)` であった。つまり、tidは22bitで表すことのできる値となる。ghost tidは、この tid と seqnum(スレッドが生成されるたびにインクリメントされていく値)を内部に含む以下のようなフォーマットで管理される。 ![](https://hackmd.io/_uploads/By1k8B8f6.jpg) 最上位ビットを0としたのは、gtidが負の場合に特別な意味をもたせるためである。 gtid が正の値のときは上のようなフィールドを持つのに対し、gtid が負の値のときで特別な意味を持つものを以下に示す。 ## [Notification](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/base.h#L312-L367) スレッド間で同期を取るために使われるクラス。 主に、Notifyメンバ関数とWaitForNotificationメンバ関数が使われる。 これらのメンバ関数の使われ方としては、<u>あるスレッドがWaitForNotificationを実行した場合、他のスレッドがNotifyを実行するまで処理がブロックされる</u>、といった感じだ。 [Agentクラス](#Agent)などで使われたりする。 ## [ForkedProcess](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/base.h#L374-L414) fork周りのプロセスの管理を行うクラス。 コンストラクタにラムダ式を渡すと、そのラムダ式で定義された処理を子プロセスで行う。 [AgentProcessクラス](#AgentProcess)などで使われたりする。 # agent.h このヘッダーで定義されているのは、AgentプロセスやAgentスレッドを管理するためのクラスが実装されている。Agentプロセスとは、Agentスレッドを他のスレッドから隔離して実行するためのプロセスのことである。 ## [Agent](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L38-L123) Per-CPU型のagentスレッド用の抽象基底クラス。 LocalAgentに継承される。 AgentクラスのインスタンスはFullAgentクラスによって管理される。 Agentクラスがもつメンバは以下の通り。 ```cpp class Agent { [ ... ] protected: Enclave* const enclave_; // 所属しているenclaveへのポインタ virtual void ThreadBody() = 0; // agentスレッドの本体 Gtid gtid_; // agentスレッドのgtid Cpu cpu_; // 対応するCPU Notification ready_, finished_, enclave_ready_, do_exit_; // 以下参照 std::thread thread_; // agentスレッド }; ``` [Notificationクラス](#Notification)のメンバ変数がいくつか実装されているが、これらは**mainスレッドとagentスレッドの間の通信を行うためのもの**である。 ready_とenclave_ready_は初期化処理の同期に使われる。 以下に初期化処理の簡単な概略図を示す。Agentスレッドの各メンバ関数を見た後にまた戻ってくると理解しやすい。 ![image](https://hackmd.io/_uploads/S1AFZnWjT.png) この図の簡単な説明をすると、 * まず、mainスレッドはagentスレッドを生成した後([StartBegin()](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.cc#L20))、ready_による通知を待つ([StartComplete()](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.cc#L22))。==ここらへんの処理は[FullAgent::StartAgentTasks](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L471-L482)で行われている。== * agentスレッドは、生成されると初期化処理を行い、完了するとready_に通知を送る([SignalReady()](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L92))。その後、enclave_ready_による通知を待つ([WaitForEnclaveReady()](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L95))。 * ready_の通知によってenclaveは処理の続きを実行できるようになり、初期化処理の続きを行う。enclaveの初期化が完了すると、enclave_ready_に通知を送る([EnclaveReady()](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L105))。 * enclave_ready_から通知を受け取ったagentスレッドは、すべての初期化処理が完了したことを知り、スケジューリングを開始する(つまり、初期化処理の完了)。 finished_とdo_exit_は終了処理の同期に使われる。 スケジューラはagentプロセスやagentスレッドなどを生成しているため、正常に終了するためには適切に処理を行う必要がある。 以下に終了処理の概略図を示す。 ![D0A972C5-1D4B-4D30-A0C1-D25E2FFB2D25](https://hackmd.io/_uploads/BkgYCnWoa.jpg) この図の簡単な説明をすると、 * まず、スケジューラを終了するような命令をmainスレッドで検知すると、finished_変数に通知を送り、agentスレッドを起こす。 * agentスレッドはfinished_に通知が来ていることを確認すると、スケジューリング処理をやめ、終了処理に移行する([FIFOスケジューラのソースコード](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/schedulers/fifo/per_cpu/fifo_scheduler.cc#L395))。 * do_exit_による通知が到着するまで、agentスレッドは待機する。 * mainスレッドは立て続けに、do_exit_に通知を送り、agentスレッドをjoinする。 * do_exit_からの通知を受け取ったagentスレッドは、スレッドを終了する。 ### ThreadBody agentスレッドの本体。 純粋仮想関数で、LocalAgentで実装されている。詳しくはLocalAgentの説明を参照。 この関数は、StartBeginで使われている([ソースコード](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.cc#L20))。 ### AgentThread(override必須) agentスレッドのメイン部。 ThreadBodyの中から実行される([ソースコード](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.cc#L60))。 スケジューラごとに実装を分ける。例えば、FIFOスケジューラの実装は[ここ](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/schedulers/fifo/per_cpu/fifo_scheduler.cc#L385-L408)で定義されている。 ### [AgentScheduler](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L98)(override必須) Agentクラスの派生クラスは、それぞれSchedulerクラスの派生クラスへのポインタを持っておく必要がある。この関数は、そのようなSchedulerインスタンスへのポインタを返す関数である。 ### [StartBegin](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.cc#L20) ```cpp void Agent::StartBegin() { thread_ = std::thread(&Agent::ThreadBody, this); } ``` Agent::ThreadBodyをスレッドとして生成する関数。 次に説明するStartCompleteと合わせて初期化処理に使われる関数である。 ### [Ping](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.cc#L64-L67) ```cpp bool Ping(); ``` agentスレッドにpingを送る関数。 関数の実装はRunRequest::Pingを実行しているだけ。 詳細は、RunRequest::Pingを参照せよ。 ### [cpu](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L71) このagentスレッドが紐付けられたCPUの番号を返す関数。 ### [gtid](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L78) このagentスレッドのgtidを返す関数。 ### [status_word](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L85) このagentスレッドのステータスワードを返す関数。 status_wordに関連していくつかの便利な関数も実装されている。 * cpu_avail * boosted_priority * barrier ### [boosted_priority](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L82) ```cpp bool boosted_priority() const { return status_word().boosted_priority(); } ``` Agentの優先度がboostされているときtrue、boostされていないときfalseを返す関数。 **優先度がboostされている**とは、<u>同CPU上に(ghOStよりも優先度の高い)他のスケジューリングポリシーに属すスレッドが実行可能状態として存在している状況のこと</u>をいう。 もしtrueが返されたならば、そのCPUを明け渡さなければならない。 優先度boostについて詳しくは[ステータスワード](https://hackmd.io/@cwtjjkgpTyeVG_KcZmF3LQ/HJNj83_ma/https%3A%2F%2Fhackmd.io%2F%40cwtjjkgpTyeVG_KcZmF3LQ%2FS1dcusZEa#GHOST_SW_BOOST_PRIO)の方で解説している。 ### [barrier](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L83) ```cpp BarrierToken barrier() const { return status_word().barrier(); } ``` agentのステータスワードからbarrier値を読み出して返す関数。 やっていることは、status_word().barrier()と同じ。 ### [StartComplete](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.cc#L22) agentスレッドの初期化処理が終了するまで待機する関数。 StartBeginを合わせて、[FullAgent::StartAgentTasks](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L471-L482)で実行される。 ### [SignalReady](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L92) agentスレッド内部で実行される関数。 mainスレッドに対し、agentスレッドの初期化処理が完了したことを通知する。 ### [WaitForEnclaveReady](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L95) enclaveの初期化処理が完了するまで待機する関数。 よく、SignalReadyと一緒に使われる。 例えば、FIFOスケジューラの実装では、[この部分](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/schedulers/fifo/per_cpu/fifo_scheduler.cc#L390-L391)で実行されている。 ### [EnclaveReady](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L95) enclaveの初期化処理が完了したことを通知する関数。 [enclaveの初期化処理内部](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/enclave.cc#L94)で呼び出されている。 この関数によって通知が発行されると、agentはスケジューリングを開始する。 ### [TerminateBegin](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.cc#L69-L76) finished_に通知を送った後にdo_exit_に通知を送る関数。 終了処理の最初に実行される関数。 ### [TerminateComplete](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.cc#L78-L92) agentスレッドをjoinする関数。 この関数は、TerminateBeginを合わせて[FullAgent::TerminateAgentTasks](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L485-L503)から呼び出される。 どちらの関数も、agentスレッドから実行されるのではなく、mainスレッドから実行される。 ### [Finished](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L66) finished_から通知が届いているのかを確認する関数。 基本的な使い方としては、agentスレッドのloopの条件に使う。 例えば、FIFOスケジューラの実装では、agentスレッドのメイン処理部分では以下のように使われている。 ```cpp void FifoAgent::AgentThread() { ... // finished_に通知が送られてくるまでループを続ける。 while (!Finished() || ...) { scheduler_->Schedule(...); ... } } ``` ## [LocalAgent](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L125-L136) Agentクラスを継承したクラス。 スケジューラを実装するときには、このクラスの派生クラスを実装する必要がある(e.g. [FifoAgent](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/schedulers/fifo/per_cpu/fifo_scheduler.h#L157-L167), [CfsAgent](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/schedulers/cfs/cfs_scheduler.h#L652-L662), ...)。 派生クラスを実装するときは、AgentThreadとAgentSchedulerをoverrideしておく必要があることに注意。 ### コンストラクタ コンストラクタはEnclaveへのポインタとcpuを受け取る。 cpuには、そのagentが紐付けられるCPUが指定される。 ### [ThreadBody](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.cc#L24-L62) agentスレッドの本体。 主にagentスレッドの初期化処理などを行う。 :::info ※ agentスレッドのメイン部分(スケジューリングを行うところ)は、AgentThread関数が担っている。 ::: この関数の処理を以下に示す。 ```cpp void LocalAgent::ThreadBody() { int queue_fd; Scheduler* s = AgentScheduler(); if (!s) { // !sとなるのは少数のテストケースのときのみ。 // 基本的にはAgentSchedulerからは適切なスケジューラへのポインタが返ってくる。 queue_fd = -1; } else { // agentが使用するメッセージキューのfdを取得する。 queue_fd = s->GetAgentChannel(cpu_).GetFd(); } ... gtid_ = Gtid::Current(); ... // このスレッドをagentスレッドとしてシステムに登録する。 // このとき、agentスレッドとメッセージキューが関連付けられるので、 // cpu_に関連するメッセージはこのメッセージキューに送られてくるようになる。 int ret; do { ret = GhostHelper()->SchedAgentEnterGhost(enclave_->GetCtlFd(), cpu_, queue_fd); } while (ret && errno == EBUSY); // ステータスワードを読み出しておく。 status_word_ = LocalStatusWord(StatusWord::AgentSW{}); // enclaveのagentリスト(Enclave::agents_)にthisをpushしておく。 enclave_->AttachAgent(cpu_, this); // agentスレッドのメイン処理部分! AgentThread(); // 終了処理 WaitForExitNotification(); } ``` ## AgentRpcBuffer ## FullAgent 1つのenclaveとひも付き、複数のagentをまとめて管理するクラス。 ghOStのユーザー側を実装するには、このFullAgentの派生クラスを実装する必要がある。 このクラスはEnclaveとAgentを管理する。 ```cpp template <class EnclaveType = LocalEnclave, class AgentConfigType = AgentConfig> class FullAgent {  ... public: EnclaveType enclave_; // Enclaveインスタンス protected: std::vector<std::unique_ptr<Agent>> agents_; // Agentインスタンス(CPUの数だけ) }; ``` クラスのテンプレート引数にはEnclaveの型とAgentConfigの型を指定することができる。基本的にはLocalEnclaveを使うので、特に他の選択肢を気にする必要はないが、AgentConfigに関しては独自の実装を用意することもできる(e.g. [CfsConfig](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/schedulers/cfs/cfs_scheduler.h#L664-L677), ...)。 このクラスは[AgentProcessに管理され](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L692)、他のクラス(Enclave, Agent, Scheduler)を全て管理する。例えば、FullFifoAgentとその他のクラスの間の関係図は以下のようになっている。 ```mermaid classDiagram AgentProcess *-- FullFifoAgent FullFifoAgent *-- LocalEnclave FullFifoAgent *-- FifoAgent FullFifoAgent *-- FifoScheduler ``` ### [コンストラクタ](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L437-L439) コンストラクタでは、LocalEnclaveのコンストラクタの呼び出し、[GhostHelper->InitCore()](#InitCore)の実行、を行う。ここで、ghOStFSのマウント処理など、必要な処理を行っている。 FullAgentの派生クラスを作ったら、最初にFullAgentのコンストラクタを呼び出す必要がある。なお、FullAgentの派生クラスの実装は<u>どれも同じような感じ</u>なので、他のスケジューラの実装を参考にするとよい(e.g [FullFifoAgent](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/schedulers/fifo/per_cpu/fifo_scheduler.h#L172-L177), ...)。 ### [デストラクタ](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L440-L445) デストラクタではagentスレッドの終了処理などを行う。 [ここのコメント](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L440-L445)にもあるが、派生クラスのデストラクタでTerminateAgentTasksを呼び出すことになっている(e.g. [~FullFifoAgent()](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/schedulers/fifo/per_cpu/fifo_scheduler.h#L179-L181), [~FullCfsAgent()](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/schedulers/cfs/cfs_scheduler.h#L691) ...)。これこそ決まった形式になっているので、忘れずに実装しておくこと。 ### MakeAgent ```cpp virtual std::unique_ptr<Agent> MakeAgent(const Cpu& cpu) = 0; ``` Agentの派生クラスのインスタンスを生成する関数。 これは純粋仮想関数になっていて、派生クラスでの実装が必須となっている。次に紹介するStartAgentTasksの内部で呼び出される。 [FIFOスケジューラの実装](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/schedulers/fifo/per_cpu/fifo_scheduler.h#L183-L185)を参考にするとよい。 ### StartAgentTasks agentスレッドを生成し、最初の初期化STEPを完了するところまでを行う関数。 派生クラスのコンストラクタからthis->StartAgentTasks()のようにして呼び出されることを想定したメンバ関数([FIFOスケジューラの例](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/schedulers/fifo/per_cpu/fifo_scheduler.h#L175))。 関数の実装は以下のようになっている。 ```cpp void StartAgentTasks() { for (const Cpu& cpu : *enclave_.cpus()) { // MakeAgentによってAgentインスタンスを作成 agents_.push_back(MakeAgent(cpu)); // agentスレッドをspawn(spawnされたagentスレッドでは初期化処理が行われる) agents_.back()->StartBegin(); } for (auto& agent : agents_) { // agentスレッドの初期化処理が完了するまで待機 agent->StartComplete(); } } ``` ### [TerminateAgentTasks](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L485-L503) agentスレッドを正常に終了させる関数。 派生クラスのデストラクタから呼ばれる関数。 ### RpcHandler ```cpp virtual void RpcHandler(int64_t req, const AgentRpcArgs& args, AgentRpcResponse& response) = 0; ``` 親プロセスからRPCが要求されたときに呼び出される関数。 この関数は純粋仮想関数になっているので、FullAgentの派生クラスは必ず実装しなければならない。 使われ方を見てみると、デバッグ用途などに使われている(FullFifoAgent などを参考)。 ## AgentProcess ```cpp template <class FullAgentType, class AgentConfigType> class AgentProcess { ... protected: // fork後のプロセスを管理するためのクラス std::unique_ptr<ForkedProcess> agent_proc_; // 子プロセスでインスタンス化されるFullAgent std::unique_ptr<FullAgentType> full_agent_; std::unique_ptr<SharedBlob> sb_ ABSL_GUARDED_BY(rpc_mutex_); private: absl::Mutex rpc_mutex_; // 共有メモリ用のミューテックス } ``` agentスレッドを実行するためのプロセスを生成&管理するクラス。 <u>このクラスの狙いは**agentスレッドを他のスレッドから隔離すること**である</u>。スケジューリングに関係のないスレッドがagentスレッドを邪魔する可能性を減らすため(資源の競合など)、メモリ空間を分けて、agentスレッドを実行するメモリ空間を隔離するようにする。 コンストラクタでforkを行い、子プロセスをagentプロセスとして管理する。**agentプロセスでは、FullAgentのインスタンスを作成し、agentスレッドを開始する**。 親プロセスと子プロセスは共有メモリを介して互いに通信してコミュニケーションを取ることができる(RPC)。この仕組みを利用し、**親プロセスが終了したときにagentプロセスの終了処理を適切に実行する**ことができる。 ### [コンストラクタ](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L591-L643) コンストラクタの処理について、親プロセスと子プロセスそれぞれについて見ていく。 親プロセスの場合は以下のようになる。 ```cpp explicit AgentProcess(AgentConfigType config) { // ① 共有メモリを作成(このメモリ領域を介してIPCする) sb_ = std::make_unique<SharedBlob>(); // ② fork agent_proc_ = std::make_unique<ForkedProcess>(config.stderr_fd_); if (!agent_proc_->IsChild()) { // ③ 親プロセスはagent_ready_で子プロセスの初期化を待ち終了。 sb_->agent_ready_.WaitForNotification(); return; } ... } ``` 子プロセスの場合は以下のようになる。 ```cpp explicit AgentProcess(AgentConfigType config) { // ① 共有メモリを作成(このメモリ領域を介してIPCする) sb_ = std::make_unique<SharedBlob>(); // ② fork agent_proc_ = std::make_unique<ForkedProcess>(config.stderr_fd_); if (!agent_proc_->IsChild()) { // ここには到達しない } ... // ③ FullAgentのコンストラクタを呼び出す full_agent_ = std::make_unique<FullAgentType>(config); CHECK_EQ(prctl(PR_SET_NAME, "ap_child"), 0); // ④ 各種シグナルを無効化する GhostSignals::IgnoreCommon(); // ⑤ 親プロセスから送られてくるRPCハンドラを実行する用のCFSスレッドを生成 auto rpc_handler = std::thread([this]() { CHECK_EQ(prctl(PR_SET_NAME, "ap_rpc"), 0); for (;;) { sb_->rpc_pending_.WaitForNotification(); sb_->rpc_pending_.Reset(); if (full_agent_->enclave_.IsOnline()) { sb_->rpc_res_ = AgentRpcResponse(); // FullAgentのRpcHandlerを実行している full_agent_->RpcHandler(sb_->rpc_req_, sb_->rpc_args_, sb_->rpc_res_); } else { sb_->rpc_res_.response_code = -ENODEV; } sb_->rpc_done_.Notify(); } }); rpc_handler.detach(); // ⑥ 親プロセスにagentプロセスの初期化が完了したことを通知する sb_->agent_ready_.Notify(); // ⑦ 親プロセスからkillが送られてくるまで待機 //   この通知はAgentProcessのデストラクタから呼ばれる sb_->kill_agent_.WaitForNotification(); // ⑧ full_agent_のデストラクタを呼び、agentスレッドなどを始末する full_agent_ = nullptr; // ⑨ agentプロセスの終了 _exit(0); } ``` ### [デストラクタ](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L645-L648) AgentProcessのデストラクタは以下のようになっていて、kill_agent_に対し通知を送るようになっている。つまり、親プロセス上でAgentProcessのインスタンスに対してデストラクタが呼ばれたら、きちんとagentプロセスなどの後片付けもやってくれるのである。 ```cpp virtual ~AgentProcess() { sb_->kill_agent_.Notify(); agent_proc_->WaitForChildExit(); } ``` ### SharedBlob ```cpp class SharedBlob { public: ... Notification agent_ready_; // child to parent Notification kill_agent_; // parent to child int64_t rpc_req_; // RPCリクエストの種類を表す値 AgentRpcArgs rpc_args_; AgentRpcResponse rpc_res_; Notification rpc_pending_; // parent to child Notification rpc_done_; // child_to_parent private: GhostShmem* blob_; }; ``` クラス内定義されたプロセス間通信を実現するためのヘルパークラス。 ### Rpc ```cpp int64_t Rpc(uint64_t req, const AgentRpcArgs& args = AgentRpcArgs()); ``` 親プロセスから子プロセスに対してRPCを行うメンバ関数。 reqにはプロセス間で**予め決められた番号**を渡す。子プロセス(agent プロセス)側がこれらの情報を受け取り、子プロセス上で処理を実行して返り値を送り返す。この関数自体の返り値は、RPCの返り値である。 ![041799E9-421E-4354-A161-5075B625B6E6](https://hackmd.io/_uploads/HJTYYtuVa.jpg) 子プロセス側(つまりAgentプロセス側)でRPCを待ち受けるスレッドは以下のように実装されている([ソースコード](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L609-L629))。 ```cpp auto rpc_handler = std::thread([this]() { CHECK_EQ(prctl(PR_SET_NAME, "ap_rpc"), 0); for (;;) { // ① rpc_pending_を待つ sb_->rpc_pending_.WaitForNotification(); // ② AgentProcess::Rpcから通知が来た! sb_->rpc_pending_.Reset(); if (full_agent_->enclave_.IsOnline()) { // ③ enclaveが有効ならRpcHandlerを実行する sb_->rpc_res_ = AgentRpcResponse(); // Reset the response. full_agent_->RpcHandler(sb_->rpc_req_, sb_->rpc_args_, sb_->rpc_res_); } else { // ③' enclaveが存在しないのでハンドラは実行せず-ENODEVを返す sb_->rpc_res_.response_code = -ENODEV; } // ④ RPCの終了の通知を送る sb_->rpc_done_.Notify(); } }); ``` # scheduler.h ## [Seqnum](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/scheduler.h#L44-L69) atomic処理をラップした**シーケンス番号**のクラス。 シーケンス番号は**32bit**整数。 このクラスはTaskクラスの[seqnum](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/scheduler.h#L92)フィールドでのみ使われるクラスである。 [load()](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/scheduler.h#L58-L60) と [store()](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/scheduler.h#L63-L65) を用いたアクセスではメモリバリアが効いているが、それ以外の代入などのアクセスではメモリバリアが効いていない。 :::success 例えば、メモリ操作の順序保証がいらないときは以下のように使えばよい。このような使い方をしたときは、メモリへのアクセス順序は前後が入れ替わる可能性がある。 ```cpp uint32_t seqnum = task->seqnum; task->seqnum = seqnum; ``` メモリ操作の順序保証が必要なときは以下のように使う。このような使い方をしたときは、メモリへのアクセス順序は前後が入れ替わる可能性がある。 ```cpp // ここのメモリ操作は必ず先行して行われる ... task->seqnum.store(seqnum); uint32_t seqnum = task->seqnum.load(); ... // ここのメモリ操作は必ず上の処理が完了してから行われる ``` ::: ## Task タスクの情報をまとめて管理するためのクラス。 これは構造体で定義されているので、各フィールドに直接アクセスできる。 この構造体が管理するフィールドは、 | フィールド || |-|-| | gtid | タスクのgtid | status_word | タスクのステータスワード | seqnum | タスクのメッセージが到着するたびに更新されるシーケンス番号。BasicDispatchScheduler::DispatchMessage で更新される([ソースコード](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/scheduler.h#L616))。そのため、BasicDispatchScheduler::TaskNew などの**メッセージコールバックで値を更新する必要はない**。 :::warning ++Q. seqnumは [status_word.barrier()](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/ghost.h#L429) から直接読み出すこともきるはずだが、Taskのメンバとして管理している目的って何?++ A. seqnumが管理している値は直近で処理した[メッセージに付属していたseqnumの値](#Message)である。この値をトランザクションで指定することで、スケジューラが処理したメッセージをカーネルに伝える役割を担っている。status_word.barrier() は読み出し時点でのカーネルの値を示しており、言うなれば**直近でカーネルがメッセージをpushしたこと**を表す値であり、使われる目的が異なる。 ::: スケジューラを実装するときは、**スケジューリングに必要な情報を追加した派生クラスを実装**して使う。特に**タスクの状態**とタスクがスケジュールされている**CPUの情報**はスケジューラを実装する上でほぼ必須と考えてよさそう。 以下にいくつかの派生クラスの例を示す。 ### [FifoTask](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/schedulers/fifo/per_cpu/fifo_scheduler.h#L30-L58) 以下のフィールドが追加されている。 | フィールド || |-|-| | run_state | タスクの状態 | cpu | タスクがスケジュールされるCPU | preempted | タスクが直近でプリエンプトされたか? | prio_boost | 優先度ブーストが起きたか? ## TaskAllocator TaskType(Task\<>の派生クラス)のインスタンスの管理を行うクラス。 ```cpp template <typename TaskType> class TaskAllocator { ... virtual TaskType* GetTask(Gtid gtid) = 0; virtual std::tuple<TaskType*, bool> GetTask(Gtid gtid, ghost_sw_info sw_info) = 0; virtual void FreeTask(TaskType* task) = 0; typedef std::function<bool(Gtid gtid, const TaskType* task)> TaskCallbackFunc; virtual void ForEachTask(TaskCallbackFunc func) = 0; } ``` 抽象基底クラスであり、スレッドセーフに実装した $\tt ThreadSafeMallocTaskAllocator$ と、シングルスレッドからのアクセスのみを想定して排他処理を省略して実装した $\tt SingleThreadMallocTaskAllocator$ の2つの具象クラスに継承される。 **$\tt TaskAllocator$ の目的は $\texttt{Task<>}$ インスタンスのライフタイムの管理である**。スケジューラは、TASK_NEWメッセージによって新しいタスクを認識すると、そのタスクに対応したデータ構造を作成して管理する必要がある。また、TASK_DEPARTEDメッセージが届くと、そのタスクのデータ構造を消去する必要がある。そこらへんの面倒な処理はライブラリの実装に含めちゃおう、ということである。 このクラスは、key = gtid, value = Taskインスタンス として管理する。 :::info 要するに **(key, val) = (gtid, Task obj)** としたmapのような働きをしてくれるもの。Allocatorという名前がついているのは、Taskオブジェクトがこのクラスの中で勝手に生成されて、解放されるから。 ::: ### 🌑 GetTask(gtid) gtidに対応するタスクのTaskインスタンスを取得する。 ### 🌑 GetTask(gtid, sw_info) sw_infoを引数に追加すると、もしgtidに対応するタスクを管理していなかったら、それを新しく作成する、ことまで行う。 :::warning 引数の違いで処理が変わるので注意。 ::: ### 🌑 FreeTask(task) 引数で指定した Task<> インスタンスを削除する。 ### 🌑 ForEachTask 管理下にある全てのタスクに対して同じ処理を行うときに呼び出す。 ## SingleThreadMallocTaskAllocator Centralized型などでは実際に稼働しているagentは全体で1つだけなので、タスクアロケータへのアクセスが競合することはない。そのため、排他制御などを行わずに軽量な操作で実装されたのがこのクラス。 ```cpp template <typename TaskType> class SingleThreadMallocTaskAllocator : public TaskAllocator<TaskType> { [ ... ] private: absl::flat_hash_map<int64_t, TaskType*> task_map_; }; ``` :::info TaskType インスタンスはハッシュマップで管理されている。 ::: ## ThreadSafeMallocTaskAllocator Per-CPU型のスケジューラの場合、複数のagentスレッドがSchedulerクラスにアクセスすることになる。そのため、排他制御が必要になる。このクラスはそういった使用のために使われるスレッドセーフなタスクアロケータである。 SingleThreadMallocTaskAllocator の派生クラスで、GetTaskなどのスレッド安全でない関数をスレッド安全な関数実装に override している(毎回ロックを取得する)。 ## [Scheduler](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/scheduler.h#L105-L154) このクラスはスケジューラに必要な最小限の機能を実装している。「最小限」というのは、問題なく初期化を完了できる、という意味である。このクラスは抽象基底クラスなので、スケジューラを実装するユーザーは、このクラスを継承した派生クラスを実装する必要がある。 ```cpp class Scheduler { [ ... ] private: Enclave* const enclave_; CpuList cpus_; // 管理するCPU }; ``` <u>**ユーザー空間上**で動作するスケジューラを実装する場合は、Scheduler を直接継承するのではなく、BasicDispatchScheduler を継承するようにすべきである</u>。なぜなら、メッセージ通信のラッパー関数などが Scheduler には用意されておらず、不便だから。 一方で、<u>**eBPFプログラムとして**動作するスケジューラを実装する場合は、Scheduler を直接継承してもいい</u>。なぜなら、メッセージがユーザー空間に送られてくることはなくeBPFで完結するため、ユーザー空間上のプログラムは高度な処理をする必要がないから。 :::warning :dart: SchedulerクラスとEnclaveクラスとAgentクラスは、初期化処理が互いに依存しあっている。eBPFスケジューラを実装するときには「ユーザー空間側のスケジューラなんていらないよ」となるかもしれないが、初期化処理を完了するためにも最小限の実装はしなければならない、、。 ::: ### 🌕 [コンストラクタ](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/scheduler.h#L111-L115) ```cpp Scheduler(Enclave* enclave, CpuList cpus); ``` Enclaveへのポインタと管理するCPUのリストを引数に渡す。 BasicDispatchSchedulerのコンストラクタから呼ばれる。 基本的には、FullAgentのコンストラクタで生成するように実装する([FullFifoAgentの例](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/schedulers/fifo/per_cpu/fifo_scheduler.h#L173-L174))。 ### 🌕 [GetAgentChannel](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/scheduler.h#L131-L133) ```cpp virtual Channel& GetAgentChannel(const Cpu& cpu); ``` cpuに紐付いたagentが使っているChannelを返す関数。 [デフォルト実装](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/scheduler.h#L131-L133)はあるものの、常にDefaultChannel()を返すだけの実装となっているため、派生クラスでagent用のChannelを返す実装をしてあげたほうがいい。 この関数は[Agent::ThreadBody](#ThreadBody)の中で呼び出され、[Ghost::SchedAgentEnterGhost](#SchedAgentEnterGhost)で使われる。つまり、**この関数の返り値(Channel型)にそのagentが関連付けられる**ことになる。 :::success FIFOスケジューラの実装では、GetAgentChannelの実装をサボっている代わりに、初期化関数内でagentとChannelをそれぞれ関連付ける処理を記述している([ソースコード](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/schedulers/fifo/per_cpu/fifo_scheduler.cc#L63-L66))。 ::: ### 🌕 [GetDefaultChannel](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/scheduler.h#L126) ```cpp virtual Channel& GetDefaultChannel() = 0; ``` 新しいghOStスレッドが生成されたときに最初に関連付けられるChannelのことを**デフォルトチャンネル**という。全てのスケジューラはデフォルトチャンネルを1つ持っていなければならない。 なお、この関数は純粋仮想関数であり、**派生クラスで実装しなければならない**。この関数は[Enclave::Readyの中](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/enclave.cc#L45)で呼び出され、システムにデフォルトチャンネルとして登録されることになっている。 ### 🌕 [EnclaveReady](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/scheduler.h#L120) 初期化処理の一番最後の方で呼び出される関数。[Enclave::Readyのここ](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/enclave.cc#L88)で呼び出される。この時点で、EnclaveとAgentの初期化処理は完了していると考えていい。 ここには初期化の一番最後にやっておきたい処理を記述する。例えば、MSG_CPU_TICKメッセージの発行を有効にしたり、agentとChannelの関連付けをしたり。 ### 🌕 [enclave()](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/scheduler.h#L141) ```cpp Enclave* enclave() const { return enclave_; } ``` Schedulerが参照しているEnclaveへのポインタを返す関数。protected関数なので派生クラスのなかからしか呼び出すことができない。 ### 🌕 [cpus()](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/scheduler.h#L149) ```cpp const CpuList& cpus() const { return cpus_; } ``` スケジューラが管理するCPUのリストを返す。これもprotected関数。 ## [BasicDispatchScheduler](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/scheduler.h#L188-L404) ```cpp template <typename TaskType> class BasicDispatchScheduler : public Scheduler { [ ... ] private: std::shared_ptr<TaskAllocator<TaskType>> const allocator_; }; ``` Schedulerの派生クラス。**ユーザー空間上のスケジューラに共通する処理をまとめた便利なクラス**なので、(eBPF以外の)スケジューラを実装するときはこのクラスを継承すべし。 :::success :dart: BasicDispatchSchedulerを継承しないスケジューラとしては、例えばBPFを使用したスケジューラなどが考えられる。BPFプログラムでスケジューリングの決定を行う場合、カーネルからのメッセージはagentに届かないので、そもそもBasicDispatchSchedulerの実装が必要ない、というわけである。 ::: このクラスの便利な点は、 * メッセージ処理のデコードをやってくれるので、派生クラスはコールバックを実装するだけでいい。 * Taskインスタンスの管理をやってくれるので、派生クラスでは必要な処理に注力できる。 メッセージのコールバックは、[ここ](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/scheduler.h#L376-L386)と[ここ](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/scheduler.h#L392-L398)で定義されている仮想関数たちである。これらの仮想関数をoverrideすることで、メッセージごとの処理を設定できる。 メッセージは[DispatchMessage](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/scheduler.h#L200)の内部で振り分けられて、適切なコールバックへ渡されることになっている。 コールバックには純粋仮想関数のものと、仮想関数のものが実装されている。純粋仮想関数の方は、**送られてくる可能性が常にあるメッセージを対象としており、派生クラスでの実装が必須**となっている。一方、仮想関数の方は、デフォルトではメッセージが送られて来ず、**設定をすると送られてくるようになるメッセージを対象としているため、派生クラスでの実装はオプショナル**となっている。 ### 🌕 [DispatchMessage](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/scheduler.h#L496-L618) ```cpp template <typename TaskType> void BasicDispatchScheduler<TaskType>::DispatchMessage(const Message& msg); ``` Channelから取り出したメッセージmsgを処理するメソッド。内部では色々なことが行われているが、最終的にはメッセージタイプに対応するコールバックにmsgが渡される。 :::info 例えば、MSG_TASK_NEWタイプのメッセージが到着したら、TaskNew()が呼ばれる。 ::: この関数はスケジューラがメッセージキューに溜まっているメッセージを処理するときに呼び出される。[FIFOスケジューラの実装](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/schedulers/fifo/per_cpu/fifo_scheduler.cc#L319-L323)などを見ると理解がしやすいと思う。 処理の流れを以下に示す。 1. CPU関連のメッセージであれば先に処理して終了。 2. そうでない場合はタスク関連のメッセージになる。処理を行う対象のタスクのTaskオブジェクトをTaskAllocatorから取得しておく(TaskAllocator->GetTask)。 * **MSG_TASK_NEWが渡された場合は、この時点でTaskオブジェクトのアロケートも行っておく**。 3. メッセージタイプの種類に合わせて、関数を呼び出す。 4. **タスクのシーケンス番号をインクリメントする**。 詳しくは実装を参照。 <u>DispathMessage内部で[Task.seqnum](#Seqnum)の値は最新の値に更新されるので、コールバック内部で更新処理を記述する必要はない!</u> # ghost.h ## [Ghost](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/ghost.h#L93-L390) ghost-kernel が提供している機能をまとめたクラス。特に ghOStFS の機能をラップしており、**ghOSt のシステムコールのラッパー**をまとめたもの。クラスの内部で必要な処理をやってくれたりするので、ghOStFSの存在をあまり感じずにghost-kernelとやりとりすることができる。 シングルトンで設計されており、[GhostHelper()](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/ghost.cc#L407-L410) 関数でインスタンスにアクセスできるようになっている。そのため、 ```cpp GhostHelper()->AssociateQueue(fd_, GHOST_TASK, gtid.id(), barrier, 0); ``` のように使うことができる。 Ghostクラスには**グローバルなEnclave**を1つ設定する。そのため、Ghostクラスが管理するのは ghOStFS 全体ではなく、ghOStFS に含まれる1つの enclave ディレクトリになる。 ```cpp class Ghost { [ ... ] private: int gbl_ctl_fd_ = -1; int gbl_dir_fd_ = -1; }; ``` * gbl_ctl_fd_:グローバルenclaveのコントロールファイル(/sys/fs/enclave_n/ctl)のfdを指す。 * gbl_dir_fd_:グローバルenclaveのディレクトリ(/sys/fs/enclave_n/)のfdを指す。 :::info ghOStクラスはシングルトンのような実装になっていて、`GhostHelper` という関数によって間接的にアクセスされる。そのため、グローバルなenclaveもプログラム内で1つしか存在できない。 ```cpp // Returns the Ghost helper instance for this machine. The pointer is never null // and is owned by the `GhostHelper` function. The pointer lives until the // process dies. Ghost* GhostHelper(); ``` ここで `LocalEnclave` の `CommitInit` というメンバ関数内のコメントを見てみると、以下のようなことが書いてあった。 ```cpp ![](https://hackmd.io/_uploads/rJ9ZD0Pfa.jpg) void LocalEnclave::CommonInit() { // Bug out if we already have a non-default global enclave. We shouldn't have // more than one enclave per process at a time, at least not until we have // fully moved away from default enclaves. [ ... ] } ``` 1つのプロセス内に2つ以上のenclaveは同時刻に存在してはならない、とある。 つまり、システム上に異なるスケジューリングポリシーを混在させるときは、それぞれのポリシーごとにenclaveを用意して、別々のプロセスで実行するということである。 ::: ### [MountGhostfs](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/ghost.cc#L147-L156) ghOStFS を /sys/fs/ghost にマウントする。 これを最初にやっておかないと /sys/fs/ghost/ctl などへアクセスできないので注意。 なお、このメソッドは GetSupportedVersions から呼ばれるため、ライブラリを利用していれば特に気にしなくても勝手に実行してくれるようになっている。 #### ++Q. ライブラリを使用したとき ghOSt FS のマウントはいつ行われるのか?++ **A. main 関数が呼ばれる前のスタートアップの時点でマウントされる。** これは Agent::kVersionCheck という静的メンバ変数の初期化が以下のように行われるためである。 ```cpp class Agent { [ ... ] // The purpose of this variable is to ensure 'CheckVersion' runs even if it is // not called directly. Calling 'CheckVersion' automatically on process // startup fixes this issue as the static method will fail on the 'CHECK_EQ' // and the process will crash if the versions do not match. static const bool kVersionCheck; }; const bool Agent::kVersionCheck = Ghost::CheckVersion(); ``` 上のコメントにもあるが、このように設計した理由は ghOSt のバージョンがカーネルとユーザーとで一致しない場合は、いかなる処理よりも先にプログラムをクラッシュさせるためである。Ghost::CheckVersion() 関数の内部を追っていくと ghOSt FS のマウント処理も行われているので、これもまた何よりも先に実行されることが保証されている。 ### CheckVersion カーネル側のghOStのバージョンとユーザー側のghOStのバージョンがマッチしているかどうかを検証する関数。この関数を呼び出すと自動で MountGhostfs が実行されるため、ghOStFS がシステムにマウントされていない状況でも問題なく処理される。 ### [InitCore](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/ghost.cc#L181-L190) ユーザー空間のABIバージョンがカーネル空間のABIバージョンと等しいかどうか、などの確認を行う処理。基本的には、ghOStを利用する前に実行しておくべき関数である。 なお、この関数はFullAgentのコンストラクタで呼ばれる([ソースコード](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/agent.h#L438))ため、普通にスケジューラを実装している場合は明示的に呼び出す必要はない。 ### [CreateQueue](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/ghost.h#L140-L149) ```cpp int CreateQueue(int elems, int node, int flags, uint64_t& mapsize); ``` ioctl の引数に GHOST_IOC_CREATE_QUEUE を指定し、enclaveに対しメッセージキューを作るように要請する。 詳しくは[こちら](https://hackmd.io/@cwtjjkgpTyeVG_KcZmF3LQ/HJNj83_ma/https%3A%2F%2Fhackmd.io%2F%40cwtjjkgpTyeVG_KcZmF3LQ%2FS1aXXxTZa#GHOST_IOC_CREATE_QUEUE)。 ### [ConfigQueueWakeup](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/ghost.h#L161-L182) ```cpp int ConfigQueueWakeup(int queue_fd, const CpuList& cpulist, int flags); ``` キューに新しいメッセージがプッシュされたときに起こすagentを設定する。 cpulistで指定したagentが起こす対象となる。 詳しくは[こちら](https://hackmd.io/@cwtjjkgpTyeVG_KcZmF3LQ/HJNj83_ma/https%3A%2F%2Fhackmd.io%2F%40cwtjjkgpTyeVG_KcZmF3LQ%2FS1aXXxTZa#GHOST_IOC_CONFIG_QUEUE_WAKEUP)。 ### SchedTaskEnterGhost この関数は /sys/fs/ghost/enclave_1/tasks ファイルに pid を書き込む。 このファイルに write システムコールが発行されたときに実行されるのが、カーネル側の [`gf_tasks_write`](https://github.com/google/ghost-kernel/blob/ghost-v5.11/kernel/sched/ghost.c#L7919) 関数である。 ```cpp // Moves the specified thread to the ghOSt scheduling class, using the enclave // dir_fd. `pid` may be a pid_t or a raw gtid. If dir_fd is -1, this will use // the enclave dir_fd previously set with SetGlobalEnclaveFds(). virtual int SchedTaskEnterGhost(int64_t pid, int dir_fd); virtual int SchedTaskEnterGhost(const Gtid& gtid, int dir_fd); ``` ### [SchedAgentEnterGhost](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/ghost.cc#L220-L240) ```cpp int SchedAgentEnterGhost(int ctl_fd, const Cpu& cpu, int queue_fd); ``` **<u>この関数を呼び出した</u>スレッドをagentとしてシステムに登録する関数**。 ghOStFSの enclave_\<id>/ctl に `become agent <cpu> <qfd>` と書き込む処理のラッパー。 引数は、 ||| |-|-| | ctl_fd | enclaveのctlファイルのfd | cpu | agentが紐付けられるCPU | queue_fd | agentが関連付けられるメッセージキューのファイル記述子。cpuで発生したメッセージは、ここで指定されたメッセージキューにpushされることになる。 この呼び出しを行ったときのカーネル側の処理を別にまとめ中。 ### [SchedGetAffinity](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/ghost.h#L349-L360) ```cpp int SchedGetAffinity(const Gtid& gtid, CpuList& cpulist); ``` gtidで指定したタスクのCPUアフィニティをcpulistに書き込む関数。 成功すれば0、失敗すれば-1を返す。 内部では sched_getaffinity が呼ばれている。 ### [SchedSetAffinity](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/ghost.h#L365-L368) ```cpp int SchedSetAffinity(const Gtid& gtid, const CpuList& cpulist); ``` gtidで指定したタスクのCPUアフィニティをcpulistに設定する。 内部では sched_setaffinity が呼ばれている。 ### [SchedGetScheduler](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/ghost.h#L371-L373) ```cpp int SchedGetScheduler(const Gtid& gtid); ``` gtidで指定したタスクのスケジューリングクラスを返す。 内部では sched_getscheduler が呼び出されている。 ## GhostThread ghOStまたはCFSのスケジューラで管理されるスレッドを生成するクラス。 予めenclaveを生成しておき、ghOStによるタスク管理ができるようにしておく。 以下のようにして使う。 ```cpp ghost::GhostThread gt(ghost::GhostThread::KernelScheduler::kGhost, []() { std::cout << "Hello World From ghOSt thread!" << std::endl; }); CHECK(gt.Joinable()); gt.Join(); ``` メンバ変数 ```cpp // The thread's TID (thread identifier). int tid_; // The thread's GTID (Google thread identifier). Gtid gtid_; // The kernel scheduling class the thread is running in. KernelScheduler ksched_; // This notification is notified once the thread has started running. Notification started_; // The thread. std::thread thread_; ``` コンストラクタの実装。 * kschedにはクラス(kCfsまたはkGhost)を指定 * workはスレッドで実行する手続き * dir_fdにはenclaveディレクトリのfdを渡す * この引数はkGhostのときのみ意味を持つ。 * それ以外のときはデフォルト引数の-1を指定する。 ```cpp GhostThread::GhostThread(KernelScheduler ksched, std::function<void()> work, int dir_fd) : ksched_(ksched) { // よく分からん GhostThread::SetGlobalEnclaveFdsOnce(); // `dir_fd` must only be set when the scheduler is ghOSt. CHECK(ksched == KernelScheduler::kGhost || dir_fd == -1); // workスレッドを生成する thread_ = std::thread([this, w = std::move(work), dir_fd] { tid_ = GetTID(); gtid_ = Gtid::Current(); // スレッドが始まったことを通知 started_.Notify(); // 最初はスレッドはCFSで管理される if (ksched_ == KernelScheduler::kGhost) { // 引数で指定したプロセスをghOStの管理下へ移動させる // pid=0とすることで、currentのPIDが指定される const int ret = GhostHelper()->SchedTaskEnterGhost(/*pid=*/0, dir_fd); CHECK_EQ(ret, 0); } std::move(w)(); }); // スレッドが実際にスタートするまで待機 started_.WaitForNotification(); } ``` ## [LocalStatusWord](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/ghost.h#L485-L503) StatusWordの派生クラス。コンストラクタなどが実装されただけ。 LocalStatusWordとStatusWordの実装を分けた理由がいまいちわからん。 ## StatusWordTable ```cpp class StatusWordTable { [ ... ] protected: // Empty constructor for subclasses. StatusWordTable() {} int fd_ = -1; size_t map_size_ = 0; ghost_sw_region_header* header_ = nullptr; ghost_status_word* table_ = nullptr; }; ``` *status words* が格納された共有メモリを扱うためのクラス。 以降、**SWテーブル**と呼ぶことにする。 SWテーブルの構造は以下のようになっている。 header_ は、この領域の先頭を指すことになっている。 ![](https://hackmd.io/_uploads/SyTzPRwzT.jpg) このクラスは抽象クラスであり、次の LocalStatusWordTable に継承される。 ### ForEachTaskStatusWord ```cpp void ForEachTaskStatusWord( const std::function<void(ghost_status_word* sw, uint32_t region_id, uint32_t idx)> l) ``` SW領域内の全てのタスクのSWに対して処理を行う関数(AgentのSWに対しては行わない)。 引数にはSWに対して呼び出されるコールバックを指定する。 ## LocalStatusWordTable StatusWordTable の実装に{コン,デ}ストラクタの実装が追加されただけである。 ```cpp class LocalStatusWordTable : public StatusWordTable { public: LocalStatusWordTable(int enclave_fd, int id, int numa_node); ~LocalStatusWordTable() final; }; ``` コンストラクタの実装を見ていく。 コンストラクタでは、新しくSW領域を作成するか、既存のSW領域にアタッチするか、のどちらかを行う。SW領域を作成する場合は enclave にSW領域を作成するように要請し、そのSW領域を共有メモリとしてmmapする。ユーザー空間から enclave に指定できるのは、SW領域のIDとNUMAノードの番号である。 ```cpp LocalStatusWordTable::LocalStatusWordTable(int enclave_fd, int id, int numa_node) { // ① enclave_n/ctlファイルを読み書き可でopenする int ctl = openat(enclave_fd, "ctl", O_RDWR); // ② "create sw_region"コマンドを書き込み、SW領域を作成するようにカーネルに要請する std::string cmd = absl::StrCat("create sw_region ", id, " ", numa_node); ssize_t ret = write(ctl, cmd.c_str(), cmd.length()); //   コマンドが成功したときは新しくファイルが作成され、すでに同じIDのSW領域が存在している //  場合には、errnoにEEXISTがセットされることになっている。 CHECK(ret == cmd.length() || errno == EEXIST); close(ctl); // ③ 作成したSW領域をユーザー空間にマップする //   上記の処理終了後にはenclave_n/sw_regions/sw_<id>というファイルが //   存在しているはずである(Read−Only)。このファイルをmmapする。 fd_ = openat(enclave_fd, absl::StrCat("sw_regions/sw_", id).c_str(), O_RDONLY); ... map_size_ = GetFileSize(fd_); header_ = static_cast<ghost_sw_region_header*>( mmap(nullptr, map_size_, PROT_READ, MAP_SHARED, fd_, 0)); ... // ④ SW領域はヘッダとSWの配列で構成されている。 //   配列へのオフセットがヘッダに記述されているので、それをもとにSW配列の先頭アドレスを //   table_メンバにセットする。 table_ = reinterpret_cast<ghost_status_word*>( reinterpret_cast<intptr_t>(header_) + header_->start); } ``` ## BarrierToken バリアの型。Status Word のフィールドなどで使われる。 ```cpp typedef uint32_t BarrierToken; ``` ##