<style> u { text-decoration-color: gray; text-decoration-style: wavy; /* 波線 */ } </style> # トランザクション 論文におけるトランザクションは、**スケジューリングの決定をカーネルに伝達する手段**として紹介されている。実際、ghost-kernelにはそれを実現するように実装されているが、より範囲を広げて、agentがCPUをYieldしたり、他のagentへPingを送ったり、といった処理もトランザクションの1つとして実装されている。 大まかに分けると、ghost-kernelにおけるトランザクションは以下の4種類になる。 * commit txn * sync-group commit txn * local yield * ping **commit txn**は、スケジューリングの決定を行う。1つのトランザクションをコミットする実装と、複数のトランザクションをまとめてコミットする実装がある。 **sync-group commit txn**もスケジューリングの決定を行うが、commit txnと異なる点は、まとめて処理をしたトランザクションは**すべて成功する**か、**すべて失敗する**か、しかない点。 **local yield**は、agentがCPUを手放す。他のsched_classのタスクのためにCPUを明け渡すときなどに実行されるもの。 **ping**は、remoteのagentを起こすというもの。 トランザクションは、<u>やり取りの効率化のために共有メモリを介してデータが渡される</u>。共有メモリ上のデータ構造は[ghost_txn](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/include/uapi/linux/ghost.h#L660-L679)構造体によって定義されており、ここにトランザクションの内容を記述した後、**トランザクション用のシステムコール**を発行することで行われる。 :::warning :dart: yieldとpingもghost_txnを介して行うことができるが、それとは別にGHOST_IOC_RUNシステムコールによって行うこともできる。ghost-userspaceではこちらの方法を採用しているため、yieldとpingに関してはGHOST_IOC_RUNの方をまとめる。 ::: # [ghost_txn](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/include/uapi/linux/ghost.h#L660-L679) トランザクションの具体的な内容はこのデータ構造を介して渡される。このデータ構造はCPUごとに存在する共有メモリを介して、カーネルとagentの両方からアクセス可能である。 :::warning ghost_txnが含まれる共有メモリは、ghOStFSの /sys/fs/ghost/enclave_\<n>/cpu_data を mmap することでメモリにマップすることができる(詳しくは「ghOSt FS」を参照)。 ::: 実装は以下のようになっている。 ```c struct ghost_txn { int32_t version; int32_t cpu; // 紐付いているCPUの番号 _ghost_txn_state_t state; // txnの状態(後で詳しくまとめる) uint32_t agent_barrier; uint32_t task_barrier; uint16_t run_flags; uint8_t commit_flags; uint8_t unused; int64_t gtid; // 最後にカーネルがトランザクションをコミットしたときのUnix時間 // 「コミット」とは、処理を正常に完了した場合も、失敗した場合も含まれる。 int64_t commit_time; uint64_t cpu_seqnum; union { // ここはアトミックなuint32_t型のデータ _ghost_txn_owner_t sync_group_owner; } u; } __ghost_cacheline_aligned; ``` 以下、代表的なメンバから順番に紹介していく。 ## gtid 基本的にはtargetのgtidを指定する。コミットが正常に終了したとき、そのCPU上で実行状態になるようなタスクのgtidである。 しかし、特別な値を指定することも可能。特別な値とは、以下の3つである。 * GHOST_NULL_GTID(0) * GHOST_AGENT_GTID(-1) * GHOST_IDLE_GTID(-2) **GHOST_AGENT_GTID**を指定すると、そのトランザクションに対応するagentへ**Ping**が送られる。つまり、対象のagentをブロックから目覚めさせてIPIを送信する、といったことを行う。 **GHOST_IDLE_GTID**を指定すると、nextタスクにはIDLEタスクが設定される。これは、後半で紹介する**LocalYield**とやっていることは同じである。agentがCPUを他のタスクに明け渡したいときに、このトランザクションを行う。 **GHOST_NULL_GTID**を指定すると、そのトランザクションのCPU上で**再スケジューリング**が行われる。カーネル側の処理は[ここ](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/kernel/sched/ghost.c#L814-L816)。 :::warning :dart: なお、このような特殊な値によってできることは、GHOST_IOC_RUNシステムコールでも実装されている。ghost-userspaceでもそっちのAPIを利用してPingやLocalYieldの実装がなされており、わざわざトランザクショによって行う必要はないように思える。 ::: ## state stateメンバは**アトミックなint型**として定義される。 このメンバが取りうる値は列挙体で定義されており([ソースコード](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/include/uapi/linux/ghost.h#L615-L639))、大まかな分類がある。 | 分類 | |-| | **int型の最小値**の[GHOST_TXN_COMPLETE](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/include/uapi/linux/ghost.h#L616)($\texttt{=-2147483648}$)は、トランザクションがカーネルによって正常にコミットされた状態を表す。 | [GHOST_TXN_COMPLETE以外の**負の値**](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/include/uapi/linux/ghost.h#L617-L631)は、トランザクションが正常にコミットできなかった状態を表す。 | $\texttt{0, 1, ..., NR_CPUS-1}$ の値のときは**CPU番号**を意味しており、その番号に対応するagentがトランザクションの処理を行っている最中であることを示す。 | **intの最大値**の[GHOST_TXN_READY](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/include/uapi/linux/ghost.h#L638)($\texttt{=2147483647}$)は、これからトランザクションを行うときに**agentが設定する値**である。 なお、状態には正の値と負の値があるが、正の値の状態は**カーネルでコミット処理をやっている最中の状態**を表し、負の値の状態は**コミット処理が終了したときの状態**を表す。 これらの状態を駆使して、複数のagentがトランザクションを同時に行わないようにする、などの制御が行われている。 これらの状態遷移について詳しく見ていく。 まず、agentは**GHOST_TXN_READY**を書き込み、そのあとにトランザクションを発行する。もし、状態をREADYとせずにトランザクションを発行した場合、そのトランザクションはコミットされない([カーネル側の処理](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/kernel/sched/ghost.c#L6884-L6885)ではトランザクションが無視されている)。 以降の状態遷移はカーネル内部で行われる。次に状態が遷移する先は**CPUの番号**である。<u>カーネルはすぐにコミット処理を行うのではなく、いったんトランザクションを請求する、というフェーズを挟む</u>。**正常にトランザクションの請求に成功したもののみ、コミット処理を行うことができる**、というわけである(トランザクションの請求は[ghost_claim_txn](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/kernel/sched/ghost.c#L5929-L5962)という関数で行われる)。 正常に請求が行われると、そのCPUはそのトランザクションのコミット処理を行う権利を得る。**コミット処理を行い**、エラーが発生すればそのエラーの内容を意味する値を書き込み、正常に終了すればGHOST_TXN_COMPLETEを書き込む。 コミット処理終了後、システムコールから戻った**agentはトランザクションの状態を確認し**、コミットが成功したのか失敗したのかを確認して**フィードバックする**。 状態の概略図。 ![image](https://hackmd.io/_uploads/BkKQMAM3p.png) stateには以下のものがある(これで全部ではない)。数値を書いているのは、ghost-userspaceでスケジューラを実行しているとエラー内容が数値でしか出力されないため。 | state | 値 | 内容 |-|-|-| | **COMPLETE** | $\texttt{-2147483648}$ | 正常にコミット完了。 | **TARGET_ONCPU** | $\texttt{-2147483646}$ | targetは既に実行状態にある。ALLOW_TASK_ONCPUが指定されていないと、このエラーメッセージが送られてくることがある。 | **TARGET_STALE** | $\texttt{-2147483645}$ | task_barrierの値が古い。つまり、targetに関する最新のメッセージを処理していない、というエラーメッセージ。 | **TARGET_NOT_ FOUND** | $\texttt{-2147483644}$ | 指定されたgtidに対応するタスクが存在しない。 | **TARGET_NOT_ RUNNABLE**| $\texttt{-2147483643}$ | 指定されたgtidに対応するタスクがrq内に存在しない(つまり、タスクが実行可能状態ではない)。 | **AGENT_STALE** | $\texttt{-2147483642}$ | agentの知識が古い。例えば、agentのbarrierが古かったり、明らかに古い情報を使用していると分かる場合。 | **CPU_UNAVAIL** | $\texttt{-2147483640}$ | CPUがより高い優先度のsched_classのタスクを 実行中。 | **POISONED** | $\texttt{-2147483633}$ | syncコミットに失敗。 | ## commit_flags このメンバは、**コミットがどのように制御されるのか**、を指定するフラグである。 トランザクションをコミットするときに**のみ**重要なものを、このフラグで表現する。 使用可能なフラグは以下のように定義されていた(なお、説明をしやすいように並べ替えたりしている:[ソースコード](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/include/uapi/linux/ghost.h#L577-L592))。 ### [COMMIT_AT\_\*](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/include/uapi/linux/ghost.h#L578-L579) まず、COMMIT_AT\_\*というフラグは**コミットを行うタイミング**を指定する。現実装では、2つのフラグが定義されており、<u>この2つのフラグのセットのされ方によってカーネルがいつコミット処理を行うのかが決定される</u>。 ```c #define COMMIT_AT_SCHEDULE (1 << 0) /* commit when oncpu task schedules */ #define COMMIT_AT_TXN_COMMIT (1 << 1) /* commit in GHOST_COMMIT_TXN op */ #define COMMIT_AT_FLAGS (COMMIT_AT_SCHEDULE | COMMIT_AT_TXN_COMMIT) ``` [COMMIT_AT_SCHEDULE](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/include/uapi/linux/ghost.h#L578)は、**sched_classのbalanceが呼ばれたときにコミット処理を行う**、というもの。コミット処理を行うタイミングを限定したいときに指定する。なお、ghost-userspaceでは、このフラグを使用している実装が見当たらなかった、、。[ここのコメント](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/kernel/sched/ghost.c#L6891-L6896)を参照。 [COMMIT_AT_TXN_COMMIT](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/include/uapi/linux/ghost.h#L579)は、**COMMIT_TXNシステムコールの内部でコミット処理を行う**、というもの。そのため、システムコールが完了したら、各トランザクションは終了していることが保証される。 この**どちらのフラグも指定されていないとき**は、カーネル処理の**至るところ**でトランザクションのコミットが試みられる。このようなコミットを**Greedy Commit**と呼ぶ。 ### ALLOW_TASK_ONCPU ```c #define ALLOW_TASK_ONCPU (1 << 2) /* If task is running on a remote cpu * then let continue running there. */ ``` nextタスクが**リモートのCPU上で実行中だった場合その実行を継続させる**、というフラグ。 このフラグをセットしなかった場合、agentはtargetが非実行状態であることを保証する必要がある。もし、フラグをセットせずにtargetが実行中のままコミットを行おうとした場合、GHOST_TXN_TARGET_ONCPUエラーとなる。 なお、注意点として、このフラグが効果を持つのは、<u>targetを実行中のCPUがトランザクションのCPUと同じ時のみである</u>。もし、トランザクションのCPUとは違う別のCPU上でtargetが実行中だった場合は、無条件でエラーとなるので注意。 ### ELIDE_AGENT_BARRIER_INC ghost-userspaceではまったく使われていない。  ```c #define ELIDE_AGENT_BARRIER_INC (1 << 3) /* Do not increment the agent * barrier (ie. on successfully * latching the task). */ ``` ### INC_AGENT_BARRIER_ON_FAILURE コミット処理に失敗したときに、そのトランザクションのCPUのagentのbarrierをインクリメントするように設定するフラグ。これも、ghost-userspaceではまったく使われていない。 関連するカーネルの処理は[ここ](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/kernel/sched/ghost.c#L6433-L6440)。 ```c #define INC_AGENT_BARRIER_ON_FAILURE (1 << 4) /* Increment agent_barrier * on transaction failure. */ ``` ## run_flags ghost_runを実行時に使用されるフラグを表すメンバ。 タスクがoncpuになったときやoffcpuになったときの副作用を指定する。 ```c /* flags accepted by ghost_run() */ #define RTLA_ON_PREEMPT (1 << 0) /* Return To Local Agent on preemption */ #define RTLA_ON_BLOCKED (1 << 1) /* Return To Local Agent on block */ #define RTLA_ON_YIELD (1 << 2) /* Return To Local Agent on yield */ #define RTLA_ON_IDLE (1 << 5) /* Return To Local Agent on idle */ #define NEED_L1D_FLUSH (1 << 6) /* Flush L1 dcache before entering guest */ #define NEED_CPU_NOT_IDLE (1 << 7) /* Notify agent when a non-idle task is * scheduled on the cpu. */ #define ELIDE_PREEMPT (1 << 9) /* Do not send TASK_PREEMPT if we preempt * a previous ghost task on this cpu */ #define SEND_TASK_ON_CPU (1 << 10) /* Send TASK_LATCHED at commit time */ /* After the task is latched, don't immediately preempt it if the cpu local * agent is picked to run; wait at least until the next sched tick hits * (assuming the agent is still running). This provides a good tradeoff between * avoiding spurious preemption and preventing an unbounded blackout for the * latched task while the agent is runnable. */ #define DEFER_LATCHED_PREEMPTION_BY_AGENT (1 << 11) #define DO_NOT_PREEMPT (1 << 12) /* Do not preempt running tasks */ ``` 実際に使われているフラグを以下にまとめる。 | RUNフラグ | 内容 | | --------- | ---- | | **RTLA_ON_IDLE** | CPUがIDLE状態になったときに、agentに処理を戻すように要請する。 | | **SEND_TASK_ON_CPU** | ラッチされたタスクが実行状態になったときに、TASK_ON_CPUメッセージを送信するように要請する。上のコメントには TASK_LATCHED メッセージを送信する、と書いてあるが、このメッセージは既に無いので注意。 | ### DO_NOT_PREEMPT nextタスクはプリエンプトできないように指定するフラグ。 もし、ターゲットCPU上で他のタスクが実行中だった場合、このトランザクションは失敗する。 このフラグが使われているカーネルの箇所は[ここ](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/kernel/sched/ghost.c#L6254-L6259)。 なお、ghost-userspaceではまったく使われていない。 ## task_barrier taskの最新のbarrier値を指定するフィールド。この値が古かった場合、トランザクションはTARGET_STALE状態となって失敗する。 ## agent_barrier agentのステータスワードから読み出したbarrier値を指定するフィールド。 このフィールドは、ローカルなコミットのときのみ参照されるため、リモートコミットのときは未指定でもよい。 ## u.sync_group_owner Sync Commit で使われるフィールド。 kSyncGroupNotOwned (= -1) のときは所有されていないことを示す。 0 以上の値のときは、所有しているCPUの番号を意味する。 # 用語 これからトランザクションについて詳しく説明していくが、説明を簡単にするために以下の用語を使うことにする。 | | | | -------- | -------- | | this cpu   | このトランザクションを実行中のCPU。トランザクション要求を出したagentに紐付いているCPU。 | | run cpu   | トランザクションのCPU。this cpu == run cpu のときは、ローカルコミットとなるし、this cpu != run cpu のときは、リモートコミットとなる。 | | target cpu  | target(txn->gtid に対応するタスク)が所属していたCPU。targetとは、次に実行状態にしたいタスクのことである。target cpu != run cpu のときは、targetのマイグレーション処理が行われたりする。 :::warning :dart: Per-CPU型の場合、this cpu == run cpu が常に成り立つ。タスクのマイグレーションを行うときは run cpu != target cpu となる。 :dart: Centralized型の場合、これら3つがすべて異なる可能性もある。 ::: また、トランザクション関連のシステムコールはghOStFSにおけるenclave_dir/ctlへのioctlで実現されている。毎回、そのシステムコールについて言及するのを避けるため、以下の用語を使うことにする。 ||| |-|-| | COMMIT | GHOST_IOC_COMMIT_TXNに対応するシステムコール | SYNC_COMMIT | GHOST_IOC_SYNC_GROUP_TXNに対応するシステムコール | RUN | GHOST_IOC_RUNに対応するシステムコール # COMMIT COMMITシステムコールは、**スケジューリングの決定**、**LocalYield**、**Ping**、を行うことができる。どの取引が行われるのかは、ghost_txn.gtidの値によって決まる。LocalYieldとPingに関してはRUNシステムコールの方で紹介するため、ここでは割愛する。 以降、COMMITシステムコールを使ってスケジューリングの決定(次に実行状態にするタスクをカーネルに伝える)を行う仕組みや方法についてまとめていく。 まず、COMMITシステムコールの引数のデータ構造は[ghost_ioc_commit_txn](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/include/uapi/linux/ghost.h#L129-L137)構造体で定義される。実装は以下のようになっていて、**処理するトランザクションの集合をCPUのリスト(bitmask)として渡す**ことになっている。 ```c struct ghost_ioc_commit_txn { #ifdef __KERNEL__ ulong *mask_ptr; // カーネル側に見えているメンバ #else cpu_set_t *mask_ptr; // ユーザー側に見えているメンバ #endif uint32_t mask_len; // cpuマスクの長さ int flags; // 現在使用可能なフラグはない。 }; ``` COMMITによってトランザクションを処理する大まかな手順は以下の通り。 1. 対象CPUの[ghost_txn](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/include/uapi/linux/ghost.h#L660-L679)にトランザクションの内容を記述する。複数のCPUのトランザクションをまとめて処理する場合は、このときにまとめて設定する。 2. COMMITシステムコールを発行する。 3. COMMITシステムコールのカーネル側の処理では、[ioctl_ghost_commit_txn](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/kernel/sched/ghost.c#L6828-L6964)が実行される。この関数内でコミット処理を行い、[ghost_txn.state](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/include/uapi/linux/ghost.h#L663)にコミットの結果を書き込んでagentに戻る。**なお、トランザクションが正常に行われた場合は、このシステムコールから返ってくるときにコンテキストスイッチが行われる**。 4. agentはその結果を読み取り、コミットが成功したのか失敗したのかを知る。結果に応じて行う処理を変える。 ![E9740939-5AFC-43E4-AC94-1566B1D8653C](https://hackmd.io/_uploads/Hykz4GQha.jpg) 各ステップの詳細を見ていく。 1.について、ghost_txnに書き込む必要のある項目は以下の通り。 ||| |-|-| | state | GHOST_TXN_READYを書き込む。 | gtid | 次にrun_cpuで実行状態にしたいghOStスレッドのgtidを指定する。 | agent_barrier | agentのステータスワードから読み出した最新のbarrier値を書き込む。なお、いつそのbarrier値を読み出したのかはかなり重要になってくる(後述)。 | task_barrier | targetの最新のbarrier値を設定する。これはメッセージに付属してカーネルから渡される値である。 | run_flags | 詳しくは[run_flags](#run_flags)を参照。 | commit_flags | 詳しくは[commit_flags](#commit_flags)を参照。 3.について、COMMITシステムコールから返ってきたとき、そのトランザクションが成功したのか、失敗したのか、でカーネル内部で行われた処理はまったく異なる。その違いを以下の図に示す。トランザクションが成功した場合は、**COMMITシステムコールから戻るのは次のスケジューリング契機**になる。 ![0C8159DC-8BA9-44A9-88CE-56482BFD8347](https://hackmd.io/_uploads/HJ5bKfmh6.jpg) 4.について、トランザクションの結果はghost_txn.stateの値を見ることで分かる。ここの値とその意味については[state](#state)を参照。 # SYNC_COMMIT SYNC_COMMITシステムコールも、COMMITシステムコールと同様、スケジューリングの決定を行う。COMMITと異なる点は、**コミット処理はすべて成功するかすべて失敗するか**、のどちらかしか取りえない、という点である。COMMITシステムコールの場合、まとめて複数のトランザクションを処理しようとした場合、一部のトランザクションのみ正常に終了する、といった状況を許しているが、SYNC_COMMITではそのような状況は生まれないことが保証されている。 SYNC_COMMITの処理の概要はCOMMITと同じである。システムコールの引数も、同じデータ構造[ghost_ioc_commit_txn](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/include/uapi/linux/ghost.h#L129-L137)が使われる。そのため、特に問題なく利用することができるが、1つSYNC_COMMIT特有の現象があるので、抑えておく必要がある。 SYNC_COMMITでは、カーネル側の実装の都合上、**トランザクションの状態が一部COMPLETEとなっている**場合がある。一部のトランザクションのみが成功するようなケースはありえない、と上述したが、このようなケースが生じてしまうのはおかしい、と思うだろう。**SYNC_COMMITでは、1つでも他に失敗したトランザクションが存在した場合、たとえトランザクションがCOMPLETEとなっていても、実際にtargetが実行状態にならないように制御されている**。そのため、SYNC_COMMIT自体が失敗した場合は、すべてのコミット処理が**実質的に**失敗していると考えてよいのである。 なお、ghost-userspaceではSYNC_COMMITが使われているスケジューラはなかった。 # RUN RUNシステムコールは、**LocalYield**と**Ping**を行うことができる。なお、RUNに関してはghost_txnを介さずに、必要なデータはシステムコールの引数で渡してしまうことになっている。 このシステムコールで渡される引数のデータ構造は[ghost_ioc_run](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/include/uapi/linux/ghost.h#L152-L158)である。この構造体の各フィールドの説明は以下の通り。 ||| |-|-| | gtid | 具体的なタスクのgtidを指定するのではなく、特別な値を指定する。この値によって、ghost_runが行う処理が定まる。[GHOST_NULL_GTID](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/include/uapi/linux/ghost.h#L604) を指定すればローカルCPUのYieldを意味し、[GHOST_AGENT_GTID](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/include/uapi/linux/ghost.h#L605) を指定すればリモートCPUへのPingを意味する。 | agent_barrier | LocalYieldの場合はagentのバリア値を渡す。Pingの場合はこのフィールドに特に意味はないので、0でも渡しておけばいい。 | task_barrier | 特に意味はなし。0を渡しておけばいい。 | run_cpu | LocalYieldの場合はthis cpuを指定する(さもなくばエラー)。PingのときはPingを送る先のcpuの番号を指定する。 | run_flags | ghost_runで使えるフラグを指定する。指定できるフラグはRTLA_ON_IDLEのみ。 このシステムコールが発行されると呼び出されるカーネル側の関数は[ghost_run](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/kernel/sched/ghost.c#L5432-L5587)である。 それぞれの処理について見ていく。 ## LocalYield まず、LocalYieldから。まず、重要な点として、LocalYieldを実行できるのは**そのスレッドがAgentスレッドのときのみ**、という制約がある(参考:[カーネル側の処理](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/kernel/sched/ghost.c#L5476-L5479))。 LocalYieldを実行すると、それを実行したagentスレッドはghost_runの中で停止しているように見える。LocalYieldから復帰してくるタイミングには、以下が考えられる。 * 他のスレッドからPingが送られてきたとき * メッセージキューに新しいメッセージがpushされて、そのメッセージキューの起こす対象に自分自身が含まれているとき(ghost_queue.notifierを参照) LocalYieldを行うタイミングとしては色々考えられるが、Per-CPU型の場合はagentに優先度boostがかかっているとき、などが挙げられる。Centralized型の場合は、satellite agentが常にLocalYieldし続ける、といった実装になる。 カーネル部分の処理は以下の部分である([ソースコード](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/kernel/sched/ghost.c#L5520-L5582))。 ```c // agentのblocked_in_runをセットしたあとにbarrier値を確認する。 // もし、barrier値が最新の値でなければエラーとなり-ESTALEを返す。 smp_store_mb(rq->ghost.blocked_in_run, true); if (unlikely(agent_barrier_get(agent) != agent_barrier)) { rq->ghost.blocked_in_run = false; rq_unlock_irq(rq, &rf); error = -ESTALE; goto done; } /* * Reuse the latched task in case it was updated by a remote agent * (before this local yield). */ ghost_set_pnt_state(rq, rq->ghost.latched_task, run_flags); rq_unlock_irq(rq, &rf); sched_preempt_enable_no_resched(); // 実際にCPUを明け渡す。 schedule(); ... // scheduleからの復帰は、agentの状態が!blocked_in_runとなったことを意味する。 VM_BUG_ON(rq->ghost.blocked_in_run); ``` ## Ping Pingについて。 Pingはリモートagentを起こすために使われる。Pingは(LocalYieldとは異なり)agentプロセスに含まれるスレッドであれば誰でも送ることができるようになっている。 Pingのカーネル側の重要な処理部分は以下の部分である([ソースコード](https://github.com/google/ghost-kernel/blob/edd5f9490d82df24c16f90a62f7be05c6c389867/kernel/sched/ghost.c#L5500-L5518))。 ```c ... // `agent`はPingを送る先のagentのtask_struct rq = cpu_rq(run_cpu); agent = rq->ghost.agent; ... /* ping agent */ if (gtid == GHOST_AGENT_GTID) { // 現在のスレッドとPingを送る先のスレッドが同じプロセス上にいる必要がある。 // 逆に、同じアドレス空間を共有しているスレッドであれば、 // 誰でもPingを送信することができる。 if (same_process(agent, current)) { // Pingを送る先のagentのバリアをインクリメントする。 // これはPingとLocalYieldで同期をとるのにかなり重要っぽい。 agent_barrier_inc(rq); // agentを!bolocked_in_run状態にして再スケジューリング要求を // 出す。TODO: schedule_agent(rq, true); } else { // プロセス空間を共有していない場合はエラー error = -EINVAL; } rq_unlock_irq(rq, &rf); goto done; } ... ``` Centralized型のFIFOスケジューラの実装を見てみると、agent_barrierの読み出すタイミングが重要っぽい。 ユーザー側のAPIには、色々なクラスで実装されているが、実装のメイン部分はGhost::Runである。 # ユーザー空間ライブラリの実装 ここまで、ghost-kernelが提供するトランザクションの仕組みについてまとめてきた。ghost-userspaceでは、これらトランザクションを行う専用のクラスを[RunRequest](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/lib/enclave.h#L282-L407)という名前で定義している。このクラスはghost_txnのデータ構造のラッパーとなり、扱いやすい形でトランザクションを抽象化している。 このクラスを利用してトランザクションを発行するまでの処理を見ていくことにする。例として、FIFOスケジューラの実装から引用する([リンク](https://github.com/google/ghost-userspace/blob/9ca0a1fb6ed88f0c4b0b40a5a35502938efa567f/schedulers/fifo/per_cpu/fifo_scheduler.cc#L262-L300))。 ```cpp // cpuに対応するトランザクションのRunRequestを取得する。 // RunRequestインスタンスへのポインタはEnclaveインスタンスを介して受け取ることができる。 RunRequest* req = enclave()->GetRunRequest(cpu); // 次に実行状態にしたいタスクが存在する場合 if (next) { ... // トランザクションの内容を書き込む。 // Openの内部でghost_txnの各フィールドへの設定が行われる。 req->Open({ .target = next->gtid, .target_barrier = next->seqnum, .agent_barrier = agent_barrier, .commit_flags = COMMIT_AT_TXN_COMMIT, }); // COMMITシステムコールを発行する。 // trueが返ってきたらCOMMITに成功したことを意味する。 if (req->Commit()) { /* コミットに成功したときの処理 */ } else { /* コミットに失敗したときの処理 */ } } else { // 次に実行状態にしたいタスクが存在しない場合はCPUをYieldする。 // COMMITと同様、RunRequestによって行われているが、内部で実行されている // システムコールは別のものである。 ... req->LocalYield(agent_barrier, flags); } ``` ここらへんの詳細な説明に関しては、「libディレクトリ」を参照してほしい。