TECHNOLOGY LINUX KERNEL

【第5回】元東大教員から学ぶLinuxカーネル「プロセス管理」

2022年9月7日

本記事の信頼性

  • リアルタイムシステムの研究歴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社で自分に合うスクールを見つけましょう.後悔はさせません!

今回のテーマはプロセス管理です.

プロセス管理により,プロセスのライフサイクルがわかります.

以下の記事を理解していることを前提とします.

プロセス

プロセスはシステムで現在実行されているプログラムのことです.

プロセスは以下の情報で構成されています.

  • CPUのレジスタ
  • プログラムコード(テキストセクション)
  • メモリセグメントの状態(データ,スタック,その他)
  • カーネルリソース(開いているファイル,保留中のシグナルなど)
  • スレッド

また,プロセスはプロセッサとメモリを仮想化します.

ユーザ空間からプロセスは以下の関数で操作します.

  • fork関数:呼び出したプロセスを複製して新しいプロセスを作成する.
  • execファミリー関数:現在のプロセスイメージを新しいプロセスイメージに置き換える.
  • wait関数:呼び出し元プロセスの子プロセスの状態変化(終了,シグナルにより停止または再開)を待つ.

親子プロセスの実行フローは下図になります.

execution flow of parent and child processes

上記のページにあるfork関数のコードfork.cを実行してみましょう!

実行結果は以下になります.

システムコールやシグナルをトレースするstraceコマンドで実行してみましょう!

2行目でexecve関数,9行目でclone関数を呼び出していることがわかります.

プロセスディスクリプタ「task_struct」

task_struct構造体

プロセスディスクリプタ「task_struct」は,プロセス情報を管理する構造体です.

linux/include/linux/sched.hでtask_structは構造体として定義されています.

※長いので一部省略しています.

Kernel Stack

古いカーネルでは,task_struct(Linuxカーネルのバージョン4.9まではthread_info)は各プロセスのカーネルスタックの一番下に割り当てられています(上図).

現在のtask_structを取得するには,スタックポインタの最下位13ビットをマスクするだけです.

Linuxカーネルのバージョン4.9以降,カーネルスタックをオーバーフローさせると悪用される可能性があるため,task_structはヒープで動的に確保されます.

現在のtask_structに効率的にアクセスするために,カーネルはcurrent_taskというCPUごとの変数を保持します.

x86-64とARM64のget_current関数とcurrentマクロの実装は以下になります.

currentマクロを利用してcurrent_taskを取得していることがわかります.

プロセス識別子(PID)

プロセスの管理は,プロセス識別子(PID)で行います.

PIDの型はpid_tになります.

  • x86は最大32768(多くの場合はint型)
  • x86-64では約400万まで増加可能
  • 最大値に達すると折り返される

PIDの最大値を知りたいあなたは,以下のコマンドを実行しましょう.

私のPC環境ではPIDの最大値は4194304です.

また,システム全体でのスレッド数の上限の確認方法は以下になります.

私のPC環境ではシステム全体でのスレッド数の上限は62906です.

プロセスの状態(task->state)

プロセスの状態(task->state)を紹介します.

  • TASK_RUNNING
    • タスクが実行可能である(実行状態またはCPUごとのスケジューラ実行キューにある(実行可能状態)).
    • タスクはユーザ空間またはカーネル空間にある.
  • TASK_INTERRUPTIBLE
    • プロセスは何らかの条件を待ってスリープしている.
    • 待機条件が成立するか,シグナルを受信するとTASK_RUNNINGに切り替わる.
  • TASK_UNINTERRUPTIBLE
    • シグナルで起床しないこと以外は,TASK_INTERRUPTIBLEと同じである.
  • __TASK_TRACED
    • 別プロセス(例:デバッガ)によるトレース中である.
  • __TASK_STOPPED
    • プロセスを一時停止させるためのシグナル(SIGSTOP等)を受信した結果,実行も待機もしない状態である.

※__TASK_TRACEDと__TASK_STOPPEDは通常とは異なる状態遷移であることに注意して下さい.

下図にプロセスの状態遷移図を示します.

task state

プロセスコンテキストとプロセス家系図

カーネルはプロセスコンテキストまたは割り込みコンテキストで実行できます.

currentは,カーネルがシステムコールの実行などプロセスコンテキストで実行されるときのみ意味があります.

割り込みは独自のコンテキストを持ちます.

プロセスは家系図として管理され,initプロセスが全てのプロセスの親プロセス(root)になります.

initプロセスは,ブートプロセスの最終段階としてカーネルによって起動されるプロセスです.

システムのinitscriptsを読み,さらにプログラムを実行します.

デーモンなどのプログラムを実行し,最終的にブートプロセスを完了させます.

initプロセスのPIDは1で,そのtask_sturctはinit_taskというグローバル変数としてlinux/init/init_task.cで以下のように定義されています.

※長いので一部省略しています.

psコマンドを実行するとinitプロセスがPID1で実行していることがわかります.

プロセスのツリーを表示するpstreeコマンドの実行結果は以下になります.

systemdという名前のinitプロセスが実行していることがわかります.

※systemdを含むinitプロセスの詳細はこちらがわかりやすいです.

プロセスの生成

プロセスの生成は,fork関数ベースで実行されます.

currentとの関係は以下になります.

  • currentの親タスク: current->parent
  • currentのタスク: current->children
  • currentの親タスクの下にある兄弟タスク: current->siblings
  • currentのシステム内の全タスクのリスト: current->tasks

また,linux/include/linux/sched/signal.hで定義されているnext_task(t),for_each_process(t)マクロ等で簡単に調査できます.

Linuxでは,何もないところからタスクを生成する機能(例:Unixのspawn関数やWindowsのCreateProcess関数)は実装されていません.

fork関数とexec関数の組み合わせて新しいプロセスを実行します.

fork関数は親プロセスのコピーである子プロセスを生成します.

親プロセスと子プロセスの違いは,PID,PPID,いくつかのリソースや統計情報のみです.

exec関数は,プロセスのアドレス空間に新しい実行ファイルをロードします.

Copy-on-Write(CoW)は,「データの書き込み時にコピーする」機構です.

fork関数で,Linuxは親ページテーブルを複製し,新しいプロセスディスクリプタを生成します.

ページテーブルのアクセスビットを読み取り専用に変更します.

ページがデータの書き込み時にアクセスされると,そのページがコピーされ,対応するページテーブルのエントリが読み取り/書き込みに変更されます.

この機構がCoWとなります.

fork関数はデータのコピーを遅らせるか,完全に防ぐことで高速化することができます.

また,fork関数は,読み取り専用ページを子孫間で共有することにより,メモリを節約することができます.

fork関数はcloneシステムコールで実装されています.

sys_clone関数は,linux/kernel/fork.cにあるkernel_clone関数(旧do_fork関数)を呼び出し,kernel_clone関数はcopy_process関数を呼び出して新しいタスクを開始します.

copy_process関数では,以下の操作をします

  • カーネルスタック,task_struct,thread_infoを複製するdup_task_struct関数を呼び出します.
  • プロセス数の制限をオーバーフローしないようにチェックします.
  • task_struct構造体の様々なメンバ変数をクリアします.
  • sched_for関数を呼び出し,子タスクの状態をTASK_NEWに設定します.
  • ファイル,シグナルハンドラなどの親情報をコピーします.
  • alloc_pid関数を使用して新しいPIDを取得します.
  • 新しい子プロセスのtask_struct構造体へのポインタを返します.

最後に,kernel_clone関数(旧_do_fork関数)がwake_up_new_task関数を呼び出します.

新しい子タスクはTASK_RUNNING状態になります.

スレッド

Linuxカーネルにおけるスレッドの定義

スレッドは,同じアドレス空間を共有し,同じプログラムに属する同時実行のフローです.

Linuxカーネルにおけるスレッドの定義を紹介します.

Linuxカーネルにはスレッドという概念がありません.

スレッド固有のスケジューリングはないです.

Linuxではすべてのスレッドを標準プロセスとして実装しています.

つまり,スレッドは他のプロセスと情報を共有しているだけのプロセスなので,各スレッドは独自のtask_struct構造体を持ちます.

スレッドは,cloneシステムコールで共有することを示す特定のフラグを付けて生成されます.

カーネルスレッド

カーネルスレッドは,カーネルでバックグラウンド処理を行うために利用されます.

ユーザ空間のスレッドに非常によく似ていて,スケジューリング可能なエンティティ(通常のプロセスのようなもの)です.

ただし,独自のアドレス空間は持ちません.(task_structのmmはNULLになります.)

カーネルスレッドはすべてkthreaddカーネルスレッド(PID 2)からフォークされたものです.

「ps --ppid 2」でカーネルスレッド一覧を以下に表示します.

カーネルスレッドとして,ワークキュー(kworker)やCPU間のロードバランシング(migration)があることがわかります.

カーネルスレッドの生成

カーネルスレッドを生成するには,linux/include/linux/kthread.hにあるkthread_createマクロ(実体はlinux/kernel/kthread.cにあるkthread_create_on_node関数)を利用します.

kthread_create関数で作成された場合,スレッドは実行可能状態ではありません.

スレッドを実行可能状態にするには以下のどちらの関数を呼び出す必要があります.

他のスレッドはlinux/kernel/kthread.cにあるkthread_stop関数を利用してカーネルスレッドに停止を要求できます.

カーネルスレッドはlinux/kernel/kthread.cにあるkthread_should_stop関数を呼び出して,継続するか停止するかを決定する必要があります.

プロセスの終了

プロセスはexitシステムコールの呼び出しにより終了します.

コンパイラがmain関数から戻るときに暗黙的に挿入することができます.

sys_exitシステムコールはlinux/kernel/exit.cにあるdo_exit関数を呼び出します.

do_exit関数は以下を実行します.

  • task_structにPF_EXITINGフラグを立てるexit_signals関数を呼び出します.
  • 親プロセスから取得するtask_structのexit_codeフィールドに終了コードを設定します.
  • exit_mm関数をコールして,タスクのmm_structを解放します.
  • exit_sem関数を呼び出します.プロセスがセマフォ待ちのキューに入っている場合,ここでキューを解除します.
  • exit_files関数およびexit_fs関数を呼び出して,それぞれファイルディスクリプタおよびファイルシステムデータの参照カウンタをデクリメントします.参照カウンタが0になると,そのオブジェクトはもはやどのプロセスからも利用されなくなり,破棄されます.

また,do_exit関数はexit_notify関数とdo_task_dead関数を呼び出します.

exit_notify関数は以下を実行します.

  • 親にシグナルを送ります.
  • 子プロセスをスレッドグループ内の他のスレッドまたはinitプロセスに再保存します.
  • task_structのexit_stateをEXIT_ZOMBIEに設定します.

do_task_dead関数は以下を実行します.

  • タスクの状態をTASK_DEADに設定します.
  • 新しいプロセスに切り替えるためにschedule関数を呼び出します.プロセスはスケジュールされなくなったので,do_exit関数は決して返りません.

この時点で残っているtask_struct,thread_info,カーネルスタックは,wait関数により親プロセスに情報を提供するために必要です.

親プロセスが情報を取得した後,プロセスが保持していた残りのメモリが解放されます.

クリーンアップ処理はwait関数の実装から呼ばれるlinux/kernel/exit.cにあるrelease_task関数で実装されています.

タスクリストからタスクを削除し,残りのリソースを解放します.

ゾンビプロセス(親がない孤児プロセス)について解説します.

ゾンビプロセスは親タスクが子タスクより先に終了した場合に,小タスクがなる状態のことです.

この場合,子タスクは再親化(reparent)されなければなりません.

exit_notify関数はforget_original_parent関数を呼び出し,find_new_reaper関数を呼び出します.

スレッドグループに別のタスクが存在する場合はそのタスクのtask_structを返し,そうでない場合はinitを返します.

そして,現在死につつあるタスクの全ての子タスク(ゾンビプロセス)がreaper(刈り手)に再親化されます.

まとめ

今回はプロセス管理を紹介しました.

具体的には,プロセス,プロセスディスクリプタ「task_struct」,プロセスの生成,スレッド,プロセスの終了を解説しました.

プロセス管理を深く理解したいあなたは,以下の記事を読みましょう!

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

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

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

友だち追加

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

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

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