このページの目次
task_struct とプロセスのライフサイクル
カバー範囲: task_struct の主要フィールドと進化 → プロセス状態マシン → fork/exec/exit の完全パス → カーネルスレッド カーネルバージョン: 2.6 ~ 6.x、5.x 以降の変更点を重点的に注釈
概要
Linux では「すべてがプロセス」です。カーネルスレッドもプロセスであり、ユーザー空間スレッドもプロセスです。その違いは、clone() の際に何を共有するかという点だけです。この統一されたモデルが機能しているのは、task_struct という巨大な構造体が、スケジューリング、メモリ、ファイル、シグナルなどのサブシステムを結びつけているおかげです。
本稿では、task_struct の主要フィールドから始め、プロセスが fork() で誕生し exit() で消滅するまでの完全なライフサイクルを追跡し、各ステップでカーネルが何を行い、データ構造がどのように変化するかを解説します。
task_struct:プロセス記述子
include/linux/sched.h で定義されており、カーネル内で最も複雑な構造体の一つです(現在のメインラインでは約 700 行)。初回閲覧時にすべてのフィールドを暗記する必要はありません。階層的な所属関係を理解すれば十分です。
flowchart LR
subgraph TS["task_struct (プロセス記述子)"]
ID["🆔 身元識別<br/>pid, tgid<br/>comm (プロセス名)"]
SCHED["⏱️ 状態とスケジューリング<br/>__state, prio<br/>se (CFS) / rt / dl"]
MM["🧠 メモリ管理<br/>mm / active_mm<br/>vmacache"]
FS["📂 ファイルと IO<br/>files (fd テーブル)<br/>fs (root/pwd)"]
REL["👥 親族関係<br/>parent / children<br/>sibling"]
SIG["📡 シグナル<br/>signal (共有記述子)<br/>sighand (処理テーブル)"]
STACK["📚 カーネルスタック<br/>stack → thread_info"]
STAT["📊 統計と監査<br/>start_time, utime/stime<br/>sum_exec_runtime"]
end
classDef ts fill:#e3f2fd,stroke:#1565c0
classDef cat fill:#fff3e0,stroke:#ef6c00
class ID,SCHED,MM,FS,REL,SIG,STACK,STAT cat
なぜスレッドもプロセスなのか?
Linux は「プロセス」と「スレッド」を区別しません。両方とも task_struct です。違いは clone() のフラグにあります。
// fork(): 新規プロセス
;
// pthread_create(): アドレス空間を共有する「スレッド」
;
// vfork(): 子プロセスが exec/exit するまで親プロセスをブロック(COW による fork の普及によりほぼ置き換え済み)
;
主要なフラグ:
| フラグ | 効果 |
|---|---|
CLONE_VM | mm_struct を共有(アドレス空間をコピーしない) |
CLONE_FS | fs_struct を共有(root/pwd が一致) |
CLONE_FILES | files_struct を共有(fd テーブルを共有) |
CLONE_SIGHAND | sighand_struct を共有(シグナル処理関数を共有) |
CLONE_THREAD | 同一スレッドグループ(tgid が親プロセスと同じ) |
CLONE_VFORK | 子プロセスが exec/exit するまで親プロセスをブロック |
getpid() が返すのは tgid(スレッドグループ ID)であり、pid ではありません。同じプロセス内のすべてのスレッドは同じ tgid を持ちますが、それぞれが独立した pid を持っています。これが ps -eLf で同じ PID の下に複数の LWP が表示される理由です。
task_struct はどのようにして見つかるのか?
// 現在実行中のプロセスの task_struct (x86_64 で最速)
// 実装: カーネルスタックポインタの下位ビットマスクから thread_info を取得し、そこから task を取得
static __always_inline struct task_struct *
5.x 以降、task_struct * は直接 per-CPU 変数 pcpu_hot.current_task に格納され、カーネルスタックポインタからの間接計算は行われなくなりました。
プロセス状態マシン
stateDiagram-v2
[*] --> TASK_RUNNING : fork()
TASK_RUNNING --> TASK_INTERRUPTIBLE : wait_event_*()
TASK_RUNNING --> TASK_UNINTERRUPTIBLE : io_schedule()
TASK_RUNNING --> TASK_STOPPED : SIGSTOP / ptrace
TASK_RUNNING --> EXIT_ZOMBIE : do_exit()
TASK_INTERRUPTIBLE --> TASK_RUNNING : シグナル / wake_up()
TASK_UNINTERRUPTIBLE --> TASK_RUNNING : IO 完了 / wake_up()
TASK_STOPPED --> TASK_RUNNING : SIGCONT
EXIT_ZOMBIE --> EXIT_DEAD : 親プロセス wait()
EXIT_DEAD --> [*] : RCU grace period
各状態の詳細
TASK_RUNNING (R 状態) — CPU 上で実行中、または runqueue でスケジューリングを待機中。ps/top で表示される R 状態のプロセスは、すべてが現在実行中であるとは限りません。CPU を待っている可能性があります。
|
TASK_INTERRUPTIBLE (S 状態) — 割込み可能なスリープ。待機状態の大多数はこの状態です:パイプのデータ待ち、ソケット待ち、セマフォ待ちなど。シグナルによってウェイクアップ可能(-EINTR を返す)。
TASK_UNINTERRUPTible (D 状態) — 割込み不可能なスリープ。典型的なシナリオはディスク IO の完了待ちです。この状態が存在する理由は、一部の IO パスがシグナルによって中断できないためです(例:ページキャッシュのライトバック時の lock_page())。D 状態が長引くと、top や負荷表示で「D state hang」と表示されます。
5.x 以降、TASK_KILLABLE が導入されました。これは NFS などの低速 IO 時に使用できる新しいスリープモードです。UNINTERRUPTIBLE との違いは、SIGKILL で終了できる点です。これにより、「NFS がダウンしてプロセスが D 状態で永遠に終了しない」という古典的な問題が解決されました。実装は wait_event_killable() マクロで行われ、内部で fatal_signal_pending() をチェックします。
TASK_STOPPED (T 状態) — SIGSTOP/SIGTRAP/ptrace による一時停止。プロセスはプロセステーブル内に残っており、SIGCONT によって再開できます。
TASK_TRACED (t 状態) — ptrace によるデバッグ中。STOPED との違いは、シグナルの伝達と ptrace イベントの処理方法が異なる点です。
EXIT_ZOMBIE (Z 状態) — プロセスは死亡しましたが、task_struct はまだ残っており、親プロセスが wait() して終了コードを読み取るのを待機中。ゾンビはメモリを占有しません(mm は解放済み)が、PID と task_struct は占有します。
EXIT_DEAD — 親プロセスが wait() した後、task_struct は RCU grace period の終了後に実際に回收されます。これは、他の CPU がまだ rcu_dereference 参照を持っている可能性を防ぐためです。
状態の保存の進化
// 旧 (2.6 ~ 5.12): long state
task->state = TASK_UNINTERRUPTIBLE;
// 新 (5.14+): WRITE_ONCE/READ_ONCE を通じてアクセス
;
__state に変更された目的は、WRITE_ONCE/READ_ONCE マクロの使用を強制し、コンパイラによる書き込みの破断(tearing)や順序違反による状態の不整合を防ぐことです。
fork:プロセスの誕生
エントリポイントとパス
// glibc: fork() → clone(SIGCHLD, ...)
// カーネル (5.x 統一エントリ):
→
→
kernel_clone() (kernel/fork.c) は統一された clone エントリポイントです(5.x でリファクタリングされ、以前は _do_fork() と呼ばれていました)。渡されるフラグによって、どのリソースを共有するかが決まります。
copy_process — 中核(約 1500 行)
Copy-on-Write が fork を高速にする秘密
// kernel/fork.c: copy_page_range() → copy_p4d_range() → ... → copy_pte_range()
static inline int
後続のページフォールト時に初めて実際にコピーが行われます:
親または子のいずれかが書き込み → ページフォールト → do_wp_page()
→ wp_page_copy() → alloc_page() → copy_user_highpage() → 各自が書き込み可能に
これが fork() が <1ms で完了でき、実際のメモリオーバーヘッドが書き込み時にのみ発生する理由です。
vfork の微妙な点
vfork() は、元々 fork がページテーブルをコピーするオーバーヘッドを避けるために存在しました(COW の登場以前)。現在は COW によってほぼ置き換えられていますが、posix_spawn() パスで使用され続けています。親プロセスは CLONE_VFORK によってブロックされ、子プロセスが exec するまで待機します。これにより、無意味な COW フォールトを回避します。カーネルは completion メカニズムを使用して、親プロセスのブロック待機を実現しています。
exec:生まれ変わり
// fs/exec.c
→ → →
exec は新しいプロセスを作成せず、現在実行中のプロセスのアドレス空間全体を置き換えます。PID は変わりませんが、実行中のコード、データ、スタックはすべて新しくなります。
実行フロー
setuid/setgid と資格情報 (credential)
ELF ファイルに suid ビットが設定されている場合、exec は資格情報も置き換えます。
// fs/exec.c
if
これはカーネル内の権限モデルの中核であり、prepare_binprm() で完了します。
exit:プロセスの消滅
do_exit パス
// kernel/exit.c
void __noreturn
ゾンビプロセスの詳細
孤児プロセスの養子縁組:親プロセスが子プロセスよりも先に終了した場合、exit_notify() は find_new_reaper() を呼び出して、子プロセスを以下に引き渡します。
- 同一スレッドグループ内の他のスレッド
- 同一 PID ネームスペースの
init(PID 1)
これが、PID 1 がゾンビを溜め込まない理由です。init プロセスは積極的に循環 wait() を行います。
ゾンビプロセスのデバッグ
# ゾンビプロセスの検索
|
# ゾンビが占有するリソース
# ゾンビの親プロセス
|
# 親プロセスが wait() を行わない場合、親プロセスを kill する必要があります
# その後、init がゾンビを養子縁組し、自動的に回収します
カーネルスレッド
カーネルスレッドは、ユーザー空間を持たない特殊なプロセスです(mm == NULL)。カーネルモードで実行され、ユーザープロセスとは異なる方法で作成および管理されます。
// include/linux/kthread.h
struct task_struct *;
void ;
int ;
bool ;
// 簡易版: 作成 + 起動を一度に行う
kthreadd (PID 2)
すべてのカーネルスレッドの親プロセスです。起動パス:
start_kernel() → rest_init() → kernel_thread(kernel_init, ...)
└─ kernel_thread(kthreadd, ...)
kthreadd は kthread_create_list を循環処理します。リクエストを取り出し → create_kthread() を呼び出し → kernel_thread() で新しいスレッドを作成します。
メモリとスケジューリングの特徴
// カーネルスレッドは独自の mm を持たず、直前のプロセスの active_mm を借用する
task->mm = NULL;
task->active_mm = &init_mm; // グローバルカーネルアドレス空間への参照
// コンテキストスイッチ時:
// if (prev->mm == NULL) // 直前のプロセスがカーネルスレッドの場合
// enter_lazy_tlb(prev->active_mm, next);
// ページテーブルは切り替えられない(カーネルアドレス空間は共有されているため)
これにより、カーネルスレッドのコンテキストスイッチが極めて高速になる理由が説明できます。TLB のフラッシュは不要です(x86: CR3 を書き込まない)。
一般的なカーネルスレッド
| スレッド | 役割 |
|---|---|
ksoftirqd/<N> | softirq の処理(ネットワーク受信、タイマーなど) |
kworker/<N>:<name> | workqueue ワーカー |
migration/<N> | スケジューラの負荷分散 |
rcu_gp / rcu_par_gp | RCU grace period の管理 |
jbd2/<dev>-<N> | ext4 ジャーナルのコミット |
kcompactd<N> | メモリ断片化の解消 |
# すべてのカーネルスレッドを表示
| |
# kthreadd の子プロセスツリー
主要な /proc インターフェース
# /proc/<pid>/stat — プロセス状態(純粋な数字、高速解析用)
# フィールド: pid comm state ppid pgrp session tty_nr tpgid flags minflt cminflt majflt cmajflt utime stime cutime cstime ...
# /proc/<pid>/status — 人間が読みやすいバージョン
# 含まれるもの: Name, State, Tgid, Pid, PPid, Uid, VmSize, VmRSS, Threads, voluntary_ctxt_switches, nonvoluntary_ctxt_switches
# /proc/<pid>/task/<tid>/ — スレッドディレクトリ
# /proc/sys/kernel/pid_max — PID の上限(デフォルト 32768、4194304 まで調整可能)
参考文献と拡張
- カーネルドキュメント:
Documentation/filesystems/proc.rst,Documentation/scheduler/sched-design-CFS.rst - LWN 深度記事:
- 「The task_struct and process creation」シリーズ
- 「clone(), fork(), and vfork()」(lwn.net/Articles/176929/)
- 「TASK_KILLABLE」(lwn.net/Articles/288056/)
- ソースファイル:
include/linux/sched.h— task_struct の定義kernel/fork.c— copy_process(), kernel_clone()fs/exec.c— do_execve(), exec_binprm(), load_elf_binary()kernel/exit.c— do_exit(), release_task()kernel/kthread.c— kthreadd の管理
キーワード: task_struct, fork, COW, exec, zombie, kernel_clone, TASK_KILLABLE, カーネルスレッド, kthreadd