TECHNOLOGY LINUX KERNEL

【第6回】元東大教員から学ぶLinuxカーネル「プロセススケジューリング」

本記事の信頼性

  • リアルタイムシステムの研究歴12年.
  • 東大教員の時に,英語でOS(Linuxカーネル)の授業.
  • 2012年9月~2013年8月にアメリカのノースカロライナ大学チャペルヒル校(UNC)コンピュータサイエンス学部で客員研究員として勤務.C言語でリアルタイムLinuxの研究開発.
  • プログラミング歴15年以上,習得している言語: C/C++PythonSolidity/Vyper,Java,Ruby,Go,Rust,D,HTML/CSS/JS/PHP,MATLAB,Assembler (x64,ARM).
  • 東大教員の時に,C++言語で開発した「LLVMコンパイラの拡張」,C言語で開発した独自のリアルタイムOS「Mcube Kernel」GitHubにオープンソースとして公開
  • 2020年1月~現在はアメリカのノースカロライナ州チャペルヒルにあるGuarantee Happiness LLCのCTOとしてECサイト開発やWeb/SNSマーケティングの業務.2022年6月~現在はアメリカのノースカロライナ州チャペルヒルにあるJapanese Tar Heel, Inc.のCEO兼CTO.
  • 最近は自然言語処理AIイーサリアムに関する有益な情報発信に従事.
    • (AI全般を含む)自然言語処理AIの論文の日本語訳や,AIチャットボット(ChatGPT,Auto-GPT,Gemini(旧Bard)など)の記事を50本以上執筆.アメリカのサンフランシスコ(広義のシリコンバレー)の会社でプロンプトエンジニア・マネージャー・Quality Assurance(QA)の業務委託の経験あり.
    • (スマートコントラクトのプログラミングを含む)イーサリアムや仮想通貨全般の記事を200本以上執筆.イギリスのロンドンの会社で仮想通貨の英語の記事を日本語に翻訳する業務委託の経験あり.

こういった私から学べます.

前回を読んでいない方はこちらからどうぞ.

Linuxカーネルの記事一覧はこちらからどうぞ.

LinuxカーネルはC言語で書かれています.

私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.

私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!

友だち追加

独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!

今回のテーマはプロセススケジューリングです.

Linuxカーネルがどのようにタスクの実行順序を制御しているかがわかります.

プロセススケジューリングとは

プロセススケジューリングとは,次にどのプロセスを,いつ,どれだけの時間実行するかを決定する機構のことです.

プロセススケジューリングの目的は,プロセッサ(CPU)を最大限に活用することです.

例えば,以下のような要件があります.

  • 待機中のプロセスのためにCPUサイクルを浪費したくない
  • 優先度の高いプロセスに高い優先度を与えたい
  • 低優先度プロセスを飢餓状態にしたくない

マルチタスクは,複数プロセスをインターリーブに実行することです.

シングルコアのCPUスケジューラでは,同時に1プロセスしか実行できないため,複数のプロセスが並行に実行します(並列ではない).

マルチコアのCPUスケジューラでは,同時に複数プロセスを実行できるため,真の並列処理が可能になります.

マルチタスクOSの種類は以下になります.

スケジューリングポリシー

スケジューリングポリシーでは,I/OバウンドなタスクとCPUバウンドなタスクの処理を考慮します.

  • I/Oバウンドなプロセス
    • ディスク,ネットワーク,キーボード,マウス等のI/O待ちでほとんどの時間を消費する.
    • 短時間しか実行されない.
    • レスポンスタイムが重要である.
  • CPUバウンドなプロセス
    • 科学計算等の処理でCPUを多く利用する.
    • 長時間動作させるとキャッシュが熱くなる(多くヒットする).

また,スケジューリングポリシーはタスクの優先度を考慮します.

  • プロセスの価値とプロセッサ時間の必要性に基づいてランク付けを行う.
  • 優先度の高いプロセスが優先度の低いプロセスより先に実行される.

Linuxには2つの優先順位範囲があります.

  • nice値:範囲は-20から+19まで(デフォルトは0)
    • niceの値が高いほど優先度が低い.
  • リアルタイム優先度:範囲は0~99
    • 値が高いほど優先度が高い.
    • リアルタイム・プロセスは常に標準的な(nice)プロセスより先に実行されます.

Linuxでnice値とリアルタイム優先度のプロセスを知りたいあなたは,以下のコマンドを実行しましょう!

PIDがプロセスID,NIがnice値,RTPRIOがリアルタイム優先度,CMDがプロセス名になります.

linux process priority

上図にユーザ空間とカーネル空間から見たLinuxのプロセス優先度を示します.

ユーザ空間の優先度はリアルタイムは99~0(100レベル),nice値のノンリアルタイム優先度は-20~19(40レベル)になります(合計140レベル).

ユーザ空間のリアルタイムの優先度は数値が大きいほど高くなりますが(99が最高優先度),ノンリアルタイムの優先度は数値が小さいほど高くなる(-20が最高優先度)になることに注意して下さい.

※topコマンドを実行する場合,リアルタイムの優先度は-100~-1,nice値の優先度は0~39になります.

これに対して,task_struct構造体のprioメンバの変数が管理するカーネル空間の優先度の範囲0~139(140レベル)で,0が最高優先度になります.

また,task_struct構造体のrt_prorityメンバ変数が管理するリアルタイムの優先度の範囲は0~99で,99が最高優先度になります(いわゆるユーザ空間のリアルタイムの優先度).

スケジューリングポリシーはタイムスライスを考慮します.

タイムスライスとは,プロセスがプリエンプションされるまでに実行すべき時間のことである.

デフォルトのタイムスライスを絶対的に定義するのは難しい.

長すぎる場合はインタラクティブ性能が低下し,短すぎる場合はコンテキストスイッチのオーバーヘッドが大きくなります.

LinuxのCompletely Fair Scheduler(CFS)

LinuxのCompletely Fair Scheduler(CFS)を紹介します.

CFSはLinuxカーネルのバージョン2.6.23から採用されたプロセススケジューラで,現在まで利用されています.

※バージョン2.6.0~の2.6.22(2003~2007年)はO(1)スケジューラが採用されていました(解説動画は以下).

Linux CFSは絶対的なタイムスライスを利用しません.

プロセスが受け取るタイムスライスは,システムの負荷(CPUの割合)の関数です.

さらに,そのタイムスライスはプロセスの優先度によって重み付けされます.

あるプロセスPが実行可能状態になる場合,PがCよりも小さな割合でCPUを消費していれば,Pは現在実行中のプロセスCを横取りします.

以下の2つのタスクのスケジュール例を紹介します.

  • テキストエディタ:I/Oバウンド,レイテンシ重視(インタラクティブ型)
  • ビデオエンコーダ:CPUバウンド,バックグラウンドジョブ

スケジューリング目標は以下になります.

  • テキストエディタ:実行準備ができたら,ビデオエンコーダを先行して実行し,インタラクティブ性能を向上させたい.
  • ビデオエンコーダ:CPUキャッシュのヒット率を上げるため,できるだけ長く実行したい.

UNIXのスケジューリングポリシーでは,テキストエディタに高い優先度を与えます.

この理由として,多くのCPUを必要とするからではなく,必要なときに常にCPUを利用できるようにしたいからです.

これに対して,Linux CFSのスケジューリングポリシーでは,各プログラムが実際に使用したCPU時間を記録することで,テキストエディタに特定の割合のCPU時間を保証します.

例えば,「テキストエディタ:ビデオエンコーダ=50%:50%」の割合になります.

テキストエディタはユーザの入力待ちのためほとんどスリープし,ビデオエンコーダはプリエンプションされるまで動作し続けます.

テキストエディタが起動したとき,Linux CFSはテキストエディタの方がビデオエンコーダよりCPU利用率が低いことを確認し,テキストエディタがビデオエンコーダをプリエンプションします.

下図にLinux CFSにおけるテキストエディタとビデオエンコーダのスケジュール例を示します.

Linux CFSでは,I/Oバウンドなタスク(テキストエディタのキーボード入力)のインタラクティブ性能と,バックグラウンドで動作するCPUバウンドなタスク(ビデオエンコーダ)の性能を両立できていることがわかります.

example of linux cfs

Linux CFSの設計について紹介します.

Linux CFSは,Rotating Staircase Deadline Scheduler(RSDL)の進化系です.

各時点で,同じ優先順位の各プロセスは,全く同じ量のCPU時間を受け取っています.

CPU上でn個のタスクを並列に実行できる場合,それぞれにCPUの処理能力の1/nを与えます.

CFSは,あるプロセスをある時間実行した後,最も実行時間の短い実行可能なプロセスと交換します.

デフォルトのタイムスライスはなく,CFSは実行可能なプロセス数に応じてプロセスの実行時間を計算します.

CFSの動的なタイムスライスは,プロセスの優先度によっていい感じに重み付けされます.

具体的には,「タイムスライス = タスクの重み / 実行可能なタスクの総重量」になります.

実際のタイムスライスを計算するために,CFSはターゲットレイテンシを設定します.

ターゲットレイテンシとは,実行可能なすべてのプロセスが少なくとも一度はスケジューリングされるべき期間のことで,最小粒度は1msのフロア(デフォルト)になります.

同じ優先度(下図の上部)と異なる優先度(下図の下部)のプロセスのスケジュール例は以下になります.

同じ優先度の場合は全てのプロセスが同じ実行時間になり,異なる優先度の場合は優先度の高いプロセスAがプロセスBより多くの時間を実行していることがわかります.

example of linux cfs with same and different priorities

Linuxのスケジューリングクラスの設計と実装

Linuxのスケジューリングクラスの設計と実装を紹介します.

Linuxのスケジューラはモジュール化されており,スケジューリングアルゴリズムのプラグイン可能なインタフェースを提供します.

異なるスケジューリングアルゴリズムが共存し,それぞれのタイプのプロセスをスケジューリングすることができます.

スケジューリングクラスはスケジューリングアルゴリズムの一つです.

各スケジューリングクラスにはスケジューリングポリシー(SCHED_DEADLINE,SCHED_FIFO,SCHED_RR,SCHED_OTHER等)があります.

※SCHED_OTHERはユーザ空間の名前,SCHED_NORMALはカーネル空間の名前で同じ実装を指します.

スケジューラの基本コードは,以下の2つのケースでスケジューラを起動します.

  • タイマ割り込みを処理する時(割り込みハンドラからscheduler_tick関数を呼び出し)
  • カーネルがschedule関数を呼び出した時

scheduler_tick関数とschedule関数の実装はlinux/kernel/sched/core.cにあります.

また,次に実行するタスクを選択するpick_next_task関数も同じくlinux/kernel/sched/core.cにあります.

Linux CFSの実装はSCHED_OTHERポリシーになります.

カーネルコード内ではSCHED_NORMALと記載されていて,実装はlinux/kernel/sched/fair.cにあります.

また,リアルタイムスケジューリングポリシーは以下の3つになります.

  • SCHED_FIFO : 先入れ先出しスケジューリング
  • SCHED_RR : ラウンドロビンスケジューリング
  • SCHED_DEADLINE : 散発的タスク(Sporadic Task)モデルによるデッドラインスケジューリング

※散発的タスクとは最短到着間隔(いわゆる最短周期)が保証されているタスクのことです.

linux/kernel/sched/sched.hにあるsched_class構造体は以下になります.

sched_class構造体は,すべてのスケジューリングクラスの抽象ベースクラスになります.

linux/kernel/sched/fair.cにCFSの実装があります.

各スケジューリングクラスは独自の機能を実装しています.

CFSの実装はlinux/kernel/sched/fair.cにあります.

Linux CFSの動作

Linux CFSの動作を紹介します.

Linux CFSでは,各々のプロセスは実行した時間を「仮想ランタイム(Virtual Runtime)」として保持します.

include/linux/sched.hにあるtask_struct構造体にsched_entityメンバ変数,sched_entity構造体に仮想ランタイムであるvruntimeメンバ変数(単位はns)(23行目)があることがわかります.

CFSでは,linux/kernel/sched/fair.cにあるupdate_curr関数でタイマ割り込みごとにタスクの実行時間を計算します.

CFSではスケジューラのデータ構造として赤黒木を利用しています.

赤黒木を知りたいあなたはこちらからどうぞ.

CFSはvruntimeでインデックスされたタスクの赤黒木(rbtree)を保持します(すなわちランキュー(runqueue)).

常にvruntimeが最も小さいタスク(一番左のノード)を選びます.

CFSの動作は以下の動画がわかりやすいです.

また,CFS VisualizerでCFSの動作を確認してみましょう.

CFSが次に実行する実行可能なプロセスを選択する必要がある場合,vruntimeが最も小さいプロセスが選択されます.

つまり,赤黒木の一番左のノードになります.

この操作はlinux/kernel/sched/fair.cにある__pick_first_entity関数で行われます.

タスクが起動またはマイグレーションすると,ランキューに追加されます.

この操作は,linux/kernel/sched/fair.cにあるenqueue_entity関数で行われます.

タスクがスリープまたはマイグレーションすると,ランキューから削除されます.

この操作はlinux/kernel/sched/fair.cにあるdequeue_entity関数で行われます.

タスクがスリープする理由は,指定された時間,I/O待ち,ミューテックスでのブロック等になります.

スリープまでの手順は以下になります.

  1. タスクにスリープマークをつける.
  2. タスクをwaitqueueに入れる.
  3. 実行可能状態のプロセスの赤黒木からタスクをデキューする.
  4. タスクはschedule関数を呼び出して,実行する新しいプロセスを選択する.

プロセスの起床はスリープの逆の手順になります.

スリープ状態に関連する以下の2つの状態があります.

  • TASK_INTERRUPTIBLE:シグナルによりスリープ状態のタスクを起床する.
  • TASK_UNINTERRUPTIBLE:起床までシグナルの送信を遅延する.

waitqueueはイベント発生を待っているプロセスのリストを保持します.

waiequeueはpthreadの条件変数と似たような概念です.

waitqueueの定義はlinux/inlcude/linux/wait.hにあるwait_queue_entry構造体とwait_queue_head構造体になります.

waitqueueにプロセスを挿入するために,linux/include/linux/wait.hにある以下のマクロを利用します.

  • wait_event_interruptibleマクロ:TASK_INTERRUPTIBLE状態でwaitqueueにプロセスを挿入する.
  • wait_eventマクロ,wait_event_timeoutマクロ,wait_event_lock_irqマクロ等:特定の条件を満たすまでTASK_UNINTERRUPTIBLE状態でwaitqueueにプロセスを挿入する.

プロセスの起床は,wake_up関数が担当します.

デフォルトでは,waitqueueにある全てのプロセスを起床します.

WQ_FLAG_EXCLUSIVEフラグを設定する排他タスクは,linux/kernel/sched/wait.cにあるprepare_to_wait_exclusive関数で追加します.

waitqueueからプロセスを起床するためには,linux/kernel/sched/core.cにある以下の関数を呼び出ます.

  • default_wake_function関数がtry_to_wake_up関数を呼び出す.
  • try_to_wake_up関数がttwu_queue関数を呼び出す.
  • ttwu_queue関数がttwu_do_activate関数を呼び出す.
  • ttwu_do_activate関数がttwu_do_wakeup関数を呼び出す.
  • ttwu_do_wakeup関数でタスクの状態をTASK_RUNNINGに設定する.

ttwu_do_wakeup関数のコードは以下になります.

8行目のWRITE_ONCEマクロでタスクの状態をTASK_RUNNINGに設定していることがわかります.

マルチコアプロセッサにおけるCFSでは,共有データ構造への高価なアクセスを回避するために,CPU単位でrunqueueを持ちます.

ランキューはバランスを保つ必要があります.

例えば,デュアルコアで,優先度の高いプロセスが長いランキューにある場合と,優先度の低いプロセスが短いランキューにある場合を考えます.

この場合,優先度の高いプロセスは優先度の低いプロセスより少ないCPU時間になってしまいます.

この逆転現象を回避するために,ロードバランサーが優先順位とCPU使用率に基づいて定期的に実行します.

プリエンプションとコンテキストスイッチ

コンテキストスイッチとは,現在CPU上で動作しているプロセスを別のプロセスに切り替える動作のことです.

linux/kernel/sched/core.cにある__schedule関数で呼び出されるcontext_switch関数によって実行されます.

※schedule関数は__schedule関数を呼び出すので,schedule関数->__schedule関数->context_switch関数という呼び出し順序になります.

context_switch関数では以下の関数を呼び出します

  • membarrier_switch_mm関数:アドレス空間を切り替えます.
  • switch_to関数:CPUの状態(レジスタ)を切り替えます.

context_switch関数は以下になります.

33行目でmembarrier_switch_mm関数,56行目でswitch_to関数を呼び出していることがわかります.

タスクは,schedule関数を呼び出すことで,自発的にCPUを放棄することができます.

また,以下の場合,現在のタスクはプリエンプションされる必要があります.

  • 十分な時間実行された (すなわち,そのvruntimeが最小でなくなった) 場合
  • より高い優先度を持つタスクが起床した場合

need_reschedは,再スケジュールする必要があるかどうかを表すフラグです.

nee_reschedフラグはアーキテクチャ毎に固有のフラグTIF_NEED_RESCHEDとして定義されています.

x86-64とARM64の定義は以下になります.

x86-64ではTIF_NEED_RESCHEDは3,ARM64ではTIF_NEED_RESCHEDは1とアーキテクチャ毎に数値が異なります.

need_reschedフラグはlinux/include/linux/sched.hにある以下の関数で操作します.

  • set_tsk_need_resched関数:need_reschedフラグを設定する.
  • clear_tsk_need_resched関数:need_reschedフラグをクリアする.
  • test_tsk_need_resched関数:need_reschedフラグか設定されているどうか(再スケジュールが必要かどうか)判定する.

need_reschedフラグが設定されている場合,scheduler_tick関数とtry_to_wake_up関数を呼び出します.

need_reschedフラグは以下のタイミングでチェックされます.

  • (システムコールや割込みから)ユーザ空間に戻った時
  • 割り込みから復帰した時

need_reschedフラグがセットされている場合,schedule関数が呼び出されます.

UNIX系OSの多くカーネルはノンプリエンプティブですが,Linuxカーネルはプリエンプティブです.

プリエンプティブスケジューラはカーネルのプリエンプションで対処します.

タスクをロックせずに安全な状態で実行すれば,カーネル内でプリエンプションできます.

thread_info構造体のpreempt_countが現在のロック深度を示します.

「need_resched && !preempt_count」の場合,プリエンプションしても安全です.

このチェックは,割り込みからカーネルに戻る時やロック解放時に行われます.

カーネルプリエンプションは以下の場合に発生します.

  • 割り込みから復帰した時
  • カーネルコードが再びプリエンプティブになった時
  • カーネル内のタスクがブロックした時(例:mutex)

linux/include/linux/preempt.hの以下のマクロでプリエンプションの無効と有効を設定します.

  • preempt_disableマクロ:プリエンプションを無効にする.
  • preempt_enableマクロ:プリエンプションを有効にする.

リアルタイムスケジューリングクラス

Linuxカーネルには,リアルタイムスケジューリングクラスがあります.

schedule関数->__schedule関数->pick_next_task関数でfor_each_classマクロを呼び出してスケジューリングクラスを反復します.

リアルタイムスケジューリングクラスは,ノンリアルタイムスケジューリングクラスより高い優先度で実行します.

つまり,リアルタイムスケジューリングクラスはノンリアルタイムスケジューリングクラスより前の位置に配置されるという意味になります.

リアルタイムスケジューリングクラスを含む全てのクラスの優先度は高い順に以下になります.

  • stop_sched_class
    • CONFIG_SMPが有効の場合,最高優先度のクラス
    • ロードバランサーやCPUのホットプラグを実行するためにCPUを停止するスケジューリングクラス
    • 操作関数はlinux/kernel/sched/stop_task.cで定義
  • dl_sched_class
    • CONFIG_SMPが無効の場合(ユニプロセッサの場合),最高優先度のクラス
    • 動的優先度のリアルタイムスケジューリングクラス
    • SCHED_DEADLINEポリシーで利用
    • 操作関数はlinux/kernel/sched/deadline.cで定義
  • rt_sched_class
    • 固定優先度のリアルタイムスケジューリングクラス
    • SCHED_FIFO,SCHED_RRポリシーで利用
    • 操作関数はlinux/kernel/sched/rt.cで定義
  • fair_sched_class
    • Linux CFSのSCHED_OTHER(SCHED_NORMAL)ポリシーで利用するクラス
    • ノンリアルタイムで低優先度のバックグランドジョブ(バッチプロセス)のスケジューリングSCHED_BATCHポリシーでも利用
    • 操作関数はlinux/kernel/sched/fair.cで定義
  • idle_sched_class
    • ノンリアルタイムで非常に優先度の低いジョブのSCHED_IDLEポリシーで利用するクラス
    • 操作関数はlinux/kernel/sched/idle.cで定義

これらのスケジューリングクラスは,linux/include/asm-generic/vmlinux.lds.hでアーキテクチャ固有のリンカスクリプト内で定義されています.

バージョン5.8までは操作関数と同じコードでスケジューリングが定義されていましたが,pick_next_task関数の最適化のため,バージョン5.9でリンカスクリプトの実装に変更しました(参考情報は以下).

SCHED_FIFOは,バージョン2.2.x(2000年頃)に実装されたリアルタイムスケジューリングポリシーです.

SCHED_FIFOでは,タスクをブロックまたは解放するまで実行されます.

優先度の高いリアルタイムタスクのみがプリエンプション可能で,同じ優先度のタスクはラウンドロビン方式で実行します(下図).

sched_fifo

SCHED_RRは,SCHED_FIFOと同時期に実装されたリアルタイムスケジューリングポリシーです.

SCHED_RRは,タイムスライスが固定であること以外は,SCHED_FIFOと同じです(下図).

sched_rr

SCHED_DEADLINEは,バージョン3.14でメインライン化されたリアルタイムポリシーで,予測可能なリアルタイムスケジューリングが可能になります.

SCHED_DEADLINEは,各タスクの起動期間と最悪実行時間(WCET:Worst Case Execution Time)に基づくEarliest Deadline First(EDF)スケジューリングとConstant Bandwidth Server(CBS)を統合したリアルタイムスケジューリングです.

まずはEDFとCBSを知りたいあなたはこちらからどうぞ.

SCHED_DEADLINEの詳細を知りたいあなたは,「Deadline Task Scheduling」の記事を読みましょう!

SCHED_DEADLINEがEDF + CBSで実装している理由を説明します.

EDFスケジューリングは,一時的なオーバーロードが発生した際,ドミノ倒しが発生してしまう問題があります.

下図は,通常のEDFスケジューリング(上部)と,タスク1が一時的なオーバーロード(Overrun)が発生してタスク2がドミノ倒しでデッドラインミスしているスケジューリング(下部)です.

problem of edf scheduling

この問題を解決するために,SCHED_DEADLINEでは,EDF + CBSでTemporal Isolationを採用しています.

下図の上部は先程のドミノ倒しの例ですが,下部はTemporal Isolationを採用することで,タスク2はドミノ倒しでデッドラインミスをしないことがわかります.

ただし,タスク1はOverrunによりデッドラインミスが発生していることに注意して下さい.

temporal isolation of edf cbs scheduling

SCHED_FIFOを利用したRMスケジューリング,SCHED_DEADLINEを利用したEDFスケジューリングの実装を知りたいあなたはこちらからどうぞ.

参考:Nestスケジューラ

2022年9月12日にLPC 2022で発表されたNestは,マルチコア(メニーコア)プロセッサでタスクの実行間隔を短くするウォームコアを考慮したスケジューラです.

Nestは,できる限りウォームコアを利用することで,CPUキャッシュのヒット率を向上させることができます.

Intel 6130(16コア/32スレッド)の4ソケット(合計64コア/128スレッド)のシステムにおいてNestはCFSと比較して最大200%性能向上しています.

将来的にNestはCFSを置き換えるかもしれませんね.

Nestの講演動画はこちらです(スライドはこちら).

まとめ

今回はプロセススケジューリングを紹介しました.

具体的には,Linux CFSとリアルタイムスケジューリングを解説しました.

プロセススケジューリングやリソース管理の詳細を知りたいあなたは,以下の記事を読みましょう!

LinuxカーネルはC言語で書かれています.

私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.

私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!

友だち追加

独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!

次回はこちらからどうぞ.

-TECHNOLOGY, LINUX KERNEL
-, , , , , , ,