UNIX V6のswtchでプロセス切り替えを理解する
カーネルスタックを切り替えてもコードの実行が続く理由
Copyright © 2026 LWP 山中 一弘
本資料は、出典を明記いただければ、商用・非商用を問わず、ご自由に複製・改変・再配布していただけます。なお、著作権表示は改変せず、そのまま記載してご利用くださいますようお願いいたします。
記事要約
UNIX V6のプロセス切り替えを読むとき、最初に混乱しやすいのは「スタックを切り替えたのに、なぜ関数の実行が続くのか」という点です。通常のC言語の関数呼び出しでは、関数は同じスタック上で呼び出され、同じスタック上の戻りアドレスに戻ります。しかしカーネル内のプロセス切り替えでは、swtch の途中でカーネルスタックが別プロセスのものに切り替わります。
この仕組みを理解するには、ユーザプロセスから見えるカーネルと、実際のカーネルの構造を分けて考える必要があります。ユーザプロセスから見ると、自分専用のカーネル空間があるように見えます。実際には、カーネルコードは全プロセスで共有され、プロセスごとに切り替わるのは主にPPDAとカーネルスタックです。
この記事では、エンジョイC 47回目のOSコードリーディング素材をもとに、fork、newproc、savu、retu、swtch の関係を、プロセス文脈とスタック文脈に分けて整理します。
本記事の対象とゴール
想定読者
UNIX V6や古典的UNIXカーネルのコードリーディングをしている人
fork 後の子プロセスがどこから動き始めるのかで迷っている人
swtch、savu、retu の役割を文脈で理解したい人
C言語の関数呼び出しと、カーネルのスタック切り替えの違いを整理したい人
本記事で得られること
カーネルコードは共有され、プロセスごとに切り替わるのはカーネルスタックとPPDAであると整理できます。
newproc が親プロセスの文脈で子プロセスのメモリイメージを作る流れを追えます。
swtch の中でカーネルスタックが切り替わっても、コード実行が継続する理由を説明できます。
子プロセス側で newproc から1が戻ったように見える理由を、戻りアドレスとレジスタの観点で理解できます。
本記事で扱わないこと
UNIX V6の全ソースコード解説
PDP-11アセンブラ命令の完全なリファレンス
現代OSのコンテキストスイッチ実装
スケジューリングポリシーの詳細
先に結論
UNIX V6のプロセス切り替えでは、カーネルのコードそのものが切り替わるわけではありません。カーネルコードは全プロセスで共有され、切り替わるのは、その時点でカーネルが参照するPPDAとカーネルスタックです。
そのため、swtch の途中で別プロセスのカーネルスタックへ切り替わっても、命令列としての swtch は続きます。ただし、スタックを基準に参照される戻りアドレスや保存されたBP/SPは、切り替え先プロセスのものになります。
子プロセス生成の流れを粗く書くと、次のようになります。
fork(c) fork(sys) newproc savu親プロセスは newproc の中で自分自身の実行状態を材料にして子プロセスを作ります。このとき、子プロセス側には、親プロセスからコピーされたカーネルスタックが残ります。
後で swtch が子プロセスを選び、retu によって子プロセスのカーネルスタックへ切り替えると、そこに保存されていた戻りアドレスへ戻ります。結果として、子プロセスでは newproc を呼び出した直後から再開したように見えます。
1. ユーザプロセスから見えるカーネルと実際のカーネル
ユーザプロセスから見ると、システムコールでカーネルに入ったとき、自分専用のカーネルが応答しているように見えます。
この見え方は、プロセスごとに専用のカーネルスタックやPPDAがあるためです。ユーザプロセスは、自分のシステムコールに対応するカーネル内処理を、自分の文脈として観察します。
しかし、実際のカーネルコードはプロセスごとに複製されているわけではありません。カーネルのコードセグメントや主要なデータ領域は共有されます。
ここで分けるべきものは次の3つです。
全プロセスで共有されるカーネルコード
現在のプロセスに対応するPPDA
現在のプロセスに対応するカーネルスタック
プロセス切り替えでは、1番目のカーネルコードは同じままです。2番目と3番目が切り替わります。
2. カーネルスタックを切り替えてもコードは続く
通常のC言語の関数呼び出しでは、スタックは同じものとして扱われます。
呼び出し元 呼び出し先 return 呼び出し元へ戻るこの流れでは、戻りアドレス、ローカル変数、保存されたBPなどが同じスタック上にあります。
一方、カーネルのプロセス切り替えでは、実行中の命令列は同じカーネルコード上にありながら、途中で参照するスタックが変わります。
ここが直感に反するところです。スタックが変わると、ローカル変数や戻りアドレスの意味が変わります。しかし、CPUが次に実行する命令は、カーネルコード上の次の命令です。
つまり、次の2つを分けて考えます。
命令列としてのコードは継続するスタック上の文脈は切り替わるこの分離が分かると、swtch の読みにくさがかなり減ります。
3. newprocは親プロセスの文脈で子プロセスを作る
fork の流れでは、親プロセスがシステムコールを通じてカーネルに入り、最終的に newproc で子プロセスを作ります。
概念的には、次の呼び出し関係になります。
fork(c) fork(sys) newprocこの時点では、まだ実行しているのは親プロセスの文脈です。カーネルスタックも親プロセス側のPPDAにあるものです。
newproc の仕事は、親プロセスの状態をもとにして子プロセスのメモリイメージやプロセス管理情報を作ることです。子プロセスはここでメモリ上に作られますが、作られた瞬間にユーザコードを実行し始めるわけではありません。
重要なのは、子プロセス側にもカーネルスタックのコピーができることです。そこには、親プロセスが fork(sys) から newproc を呼び出したときの戻りアドレスやBP/SPの状態が反映されています。
4. savuはBPとSPを保存する
savu は、プロセスの再開に必要なスタック情報を保存するための低レベルルーチンです。ここでは単純化して、「呼び出し元関数のBPとSPを指定された場所へ保存するもの」と考えれば十分です。
保存先は、通常はPPDAの先頭付近にある領域です。
PPDA saved_sp saved_bp ...newproc の中で savu が呼ばれると、その時点の親プロセスのスタック状態が保存されます。ただし、この保存は親プロセスの実行を直接止めるものではありません。savu は保存するだけで、親プロセス側の処理はそのまま続きます。
ここで大切なのは、保存されたBP/SPが、後でそのプロセスのカーネルスタックを再開するための入口になることです。
5. 親プロセス側ではnewprocは普通に戻る
親プロセスの文脈では、newproc は通常の関数呼び出しとして実行されます。
fork(sys) newproc 子プロセス情報を作る savuでBP/SPを保存する return親プロセスから見れば、newproc は呼び出され、処理を行い、呼び出し元へ戻っただけです。
このとき、親プロセス側の fork(sys) は newproc の戻り値を見て、親プロセスとしての処理を続けます。元素材では、親側では newproc の戻り値ゼロの場合の分岐へ進む、という点が説明されています。
ここまでは、通常のC言語の関数呼び出しに近い読み方で追えます。
6. swtchは一度スケジューラを経由する
プロセス切り替えの中心にいるのが swtch です。
swtch は、単に現在のプロセスから次のプロセスへ直接飛ぶだけの関数ではありません。いったんスケジューラ側のスタックへ切り替え、そこで次に動かすプロセスを選びます。
大まかには、次のように考えます。
現在のプロセスのカーネルスタック swtch retuでスケジューラスタックへ切り替え 次のプロセスを選ぶ retuで選ばれたプロセスのカーネルスタックへ切り替え swtchの処理は続くポイントは、retu によってスタックが切り替わっても、命令列としての swtch は継続することです。
7. retuはスタック文脈を切り替える
retu は、指定されたプロセスの保存済みスタック情報を使って、カーネルスタックの文脈を切り替える低レベル処理です。
swtch の中では、概念的に2種類の retu が出てきます。
現在のプロセスのスタックからスケジューラ側のスタックへ切り替える
スケジューラが選んだプロセスのカーネルスタックへ切り替える
2つ目の retu が、子プロセスを初めて実行可能にするときに重要です。
子プロセスのPPDAがカレントとして割り当てられ、子プロセスの保存済みSP/BPが復元されると、以後のスタック参照は子プロセスのカーネルスタックを基準に行われます。
ただし、カーネルコードは共有されています。そのため、CPUは swtch の続きの命令を実行しています。ここで「コードは続くが、スタックは子プロセスのもの」という状態が生まれます。
8. swtchがローカル変数を使わない意味
元素材では、swtch がローカル変数を使わないことにも触れています。これは重要です。
C言語のローカル変数は、通常、スタック上に置かれます。多くの場合、BP相対で参照されます。
もし swtch がスタック上のローカル変数に依存していたら、retu でカーネルスタックが切り替わった瞬間に、参照するローカル変数の中身が別プロセス側のものになってしまいます。
つまり、次のような危険があります。
retuの前: 親プロセス側のスタック上の変数を読んでいるretuの後: 子プロセス側のスタック上の同じ位置を読んでしまうプロセス切り替えの途中でスタックそのものが変わる以上、swtch はその前提で書かれていなければなりません。
9. 子プロセスはどこから動き始めるのか
ここまでの道具立てがそろうと、子プロセスがどこから動き始めるのかを説明できます。
親プロセスが newproc を呼び出した時点で、親のカーネルスタックには newproc から戻るための戻りアドレスがあります。子プロセスは親のコピーとして作られるため、子プロセス側のカーネルスタックにも同じ構造が残ります。
後で swtch が子プロセスを選び、子プロセスのカーネルスタックへ切り替えると、保存されていたBP/SPが復元されます。
その後、swtch が戻るとき、C言語の規約に従って戻り値がレジスタに設定されます。元素材では、この値が1として説明されています。
戻り先は関数名ではありません。スタック上に積まれている戻りアドレスです。
そのため、子プロセス側では次のように見えます。
fork(sys) newprocnewprocを呼び出した次の行へ戻るただし、これは親プロセスの続きではありません。子プロセスのカーネルスタックを使って、同じ戻りアドレスの位置へ戻っているのです。
10. なぜ子プロセスではnewprocから1が戻ったように見えるのか
親プロセス側では、newproc は普通に戻ります。その戻り値は親プロセス用の分岐に使われます。
一方、子プロセス側では、後から swtch によってカーネルスタックが選ばれ、newproc を呼び出した直後の戻りアドレスへ戻ります。そのとき、戻り値レジスタには子プロセス側としての値が入ります。
結果として、ソースコード上は同じ newproc 呼び出しの次の行を実行しているように見えますが、親と子で戻り値が違います。
これは、C言語だけを見ていると不思議に見えます。しかし、戻り先が関数名ではなくスタック上の戻りアドレスであり、戻り値がレジスタで渡されることを考えると、筋道が見えてきます。
整理すると、次のようになります。
親プロセス 親のカーネルスタックでnewprocを呼ぶ newprocは通常のreturnで戻る 親用の戻り値で分岐する子プロセス 親からコピーされたカーネルスタックを持つ swtchで子のカーネルスタックが選ばれる 保存された戻りアドレスへ戻る 子用の戻り値で分岐するこのため、子プロセスでは newproc から1が戻ってきたように見えます。
11. 「いまどの文脈で動いているか」を確認する
カーネルコードを読むときは、コードの行だけを追うと迷います。同じコード行でも、どのプロセスのカーネルスタックを使っているかによって意味が変わるためです。
特に確認すべきなのは、次の3点です。
いま実行している命令列はどのカーネルコードか。
いまのPPDAはどのプロセスのものか。
いま参照しているカーネルスタックはどのプロセスのものか。
この3つを分けると、swtch の読み方が変わります。
単に「プロセスが切り替わる」と言うと、すべてが一瞬で別物になるように感じます。しかし実際には、共有されたカーネルコードの中で、PPDAとカーネルスタックが切り替わり、その結果として次に戻る場所や戻り値の意味が変わります。
12. 通常のプロセス切り替えでも同じ見え方になる
元素材では補足として、通常のプロセス切り替えにも触れています。
通常の切り替えでは、swtch の中で現在のプロセスの状態を savu で保存します。その後、スケジューラを経由して別プロセスが選ばれます。
ユーザプロセスから見ると、途中で別プロセスが動いていたとしても、後で自分が再開されたときには「swtch を呼び出して、swtch から戻ってきた」ように見えます。
これは、保存されたスタック文脈へ戻るからです。プロセス切り替えの間に何が起きていても、そのプロセス自身から見れば、保存された戻りアドレスの続きから再開されます。
まとめ
UNIX V6の swtch を理解するには、C言語の関数呼び出しだけでは足りません。C言語の関数呼び出しは重要な前提ですが、カーネルではその途中でスタック文脈そのものが切り替わるからです。
押さえるべきポイントは次の通りです。
カーネルコードは共有される。
プロセスごとにPPDAとカーネルスタックがある。
savu は再開に必要なBP/SPを保存する。
retu は保存された文脈へカーネルスタックを切り替える。
swtch はスタックを切り替えても命令列としては継続する。
戻り先は関数名ではなく、スタック上の戻りアドレスである。
子プロセスが newproc の呼び出し直後から始まるように見えるのは、コピーされたカーネルスタックと、保存された戻りアドレスを使って再開するためです。
プロセス切り替えを読むときは、「どのコードを実行しているか」だけでなく、「どのプロセスのスタックで実行しているか」を常に横に置いて読む必要があります。ここが分かると、fork、newproc、swtch のつながりがかなり見通しやすくなります。
出典メモ
元リンクファイル: C:\Users\hoehoe\マイドライブ\LWP記事\03過去記号\エンジョイC\エンジョイC047回目プロセスの切り替(2021-03-21) - Togetter.URL
元URL: https://togetter.com/li/1692150
移行後URL: https://posfie.com/@hoehoe1234/p/qPryeLr
元タイトル: エンジョイC047回目プロセスの切り替(2021-03-21)
公開日: 2021-04-03
更新日: 2022-02-01
取得日: 2026-05-24
備考: Togetterからposfieへ移行されたページの本文を取得し、元投稿の要点をLWP公開記事として再構成しました。
