UNIX V6のnewprocが1を返す仕組み

UNIX V6のnewprocが1を返す仕組み

fork後の子プロセスが「関数呼び出しの途中から戻ってくる」理由を、仮想記憶とカーネルスタックで整理する

Copyright © 2026 LWP 山中 一弘

本資料は、出典を明記いただければ、商用・非商用を問わず、ご自由に複製・改変・再配布していただけます。なお、著作権表示は改変せず、そのまま記載してご利用くださいますようお願いいたします。

記事要約

 UNIX V6の fork を読むと、newproc が親プロセス側では通常の関数呼び出しとして見える一方、子プロセス側では newproc が 1 を返したように見える場面があります。単純なC言語の関数呼び出しだけで考えると、まだ動いたことのない子プロセスが、なぜ関数の戻り値を受け取れるのかが分かりにくくなります。

 この記事では、newproc が 1 を返す仕組みを、PDP-11/UNIX V6の仮想記憶、PAR、PPDA、u構造体、カーネルスタック、retu、そしてスワップ復帰時に出てくる aretu の関係として整理します。ポイントは、子プロセスが新しく最初から実行を始めるのではなく、親プロセスの実行状態をコピーされ、その状態へ切り替えられることです。

 この記事で最も重要な見方は、fork 関数から見たときの「見かけ」と、実際に戻っている場所を分けることです。親プロセスでは、fork が呼んだ newproc が普通に戻ってきます。一方、子プロセスでは、fork から見ると newproc から戻ってきたように見えますが、実際に戻り値 1 を作っているのは、後で子プロセスを選んだ swtch の return(1) です。この「newproc から戻ったように見えるが、実体は swtch から戻っている」というすり替わりが、本記事の中心です。

 板書で扱われていた内容も本文に取り込みます。PARは仮想アドレスを物理アドレスへ写すための「下駄」であり、u構造体を見るにはPAR6へ対象プロセスの p_addr を入れます。さらに、関数呼び出しでは戻りアドレスや旧BPがスタック上に積まれているため、カーネルスタックごとコピーされた子プロセスは、swtch に選ばれた後、親が止まっていた場所の続きから戻ってきたように見えます。

本記事の対象とゴール

想定読者

  • UNIX V6の fork、newproc、swtch、retu の関係を追っている人

  • newproc がなぜ子プロセス側で 1 を返したように見えるのかを整理したい人

  • PDP-11の仮想アドレス、PAR、u構造体、カーネルスタックの関係を実行状態として理解したい人

本記事で得られること

  1. PAR6へ p_addr を設定する意味が分かります。

  2. PPDA、u構造体、カーネルスタックがプロセス切り替えでなぜ重要か分かります。

  3. 子プロセスが newproc の戻り値として 1 を受け取る流れを、関数呼び出しとスタック復帰から説明できます。

  4. fork 関数から見ると newproc から戻ったように見えるのに、実際には swtch の return(1) が子プロセス側の戻り値になっている、という一番重要な読み方を説明できます。

本記事で扱わないこと

  • UNIX V6の全ソースコードを網羅的に読むこと

  • PDP-11アセンブリの命令仕様を細部まで解説すること

  • 現代OSの fork 実装とUNIX V6を同一視すること

先に結論

 newproc が子プロセス側で 1 を返すように見える理由は、子プロセスが親のメモリとカーネルスタックをコピーされ、そのコピーされた実行状態へ swtch によって切り替えられるからです。

 親が fork から newproc を呼んだ時点で、スタックには newproc から戻るための情報が積まれています。子プロセスは、そのスタック状態まで含めて作られます。まだ一度もユーザ処理として走っていなくても、カーネルから見れば「戻るべきスタック」を持っています。

 プロセス切り替えでは、対象プロセスの p_addr をPAR6へ設定し、そのプロセスのu構造体とカーネルスタックがカーネルの固定仮想アドレスから見えるようにします。その後、保存されていたスタックから復帰すると、子プロセスは newproc 呼び出しの続きへ戻ったように動きます。

 ここで重要なのは、V6のソース上で newproc が単純に return(1) を実行しているわけではない、という点です。swtch の末尾には return(1); があり、newproc のコメントには、子プロセス側では実際に走るコードは swtch であり、その戻り値が newproc の呼び出し元から見える、と説明されています。つまり「newproc が1を返す」とは、コピーされた実行状態と swtch の戻り値が合わさって、Cの呼び出し元からそう見える、という意味です。

 fork 関数を読んでいる読者の視点では、親も子も newproc 呼び出しの直後へ戻ってくるように見えます。しかし、親は本当に newproc の return(0) から戻ります。子は、あとで swtch に選ばれ、swtch の return(1) から戻ります。それでも子のカーネルスタックは fork が newproc を呼んでいた途中の状態なので、fork から見ると newproc が1を返したように見えるのです。

ソースコードで先に確認する要点

 図や板書が途中で欠けていると、どの操作が決定的なのかが見えにくくなります。UNIX V6のソースコードに沿って読むなら、先に次の4点を押さえると全体がつながります。

  1. swtch() は、いったん現在の戻り場所を savu(u.u_rsav) で保存し、スケジューラ用のスタックへ retu(proc[0].p_addr) で切り替えてから、次に走らせるプロセスを探します。

  2. 走らせるプロセスが決まると、swtch() は retu(rp->p_addr); によって、そのプロセスのu領域とカーネルスタックを見えるようにし、続けて sureg(); でユーザ空間側のセグメントレジスタを整えます。

  3. 通常のメモリ上にいるプロセスであれば、その後 swtch() の末尾の return(1); に到達します。この 1 が、子プロセス側で newproc の呼び出し元に見える値になります。

  4. aretu(u.u_ssav) は、SSWAP が立っているときのスワップ復帰用の特別な経路です。newproc が1を返す基本の説明では、中心は retu(rp->p_addr) と swtch() の return(1) に置くと読みやすくなります。

 ここで混乱しやすいのは、「newproc の呼び出し元」と「実際に return している関数」が一致しないことです。ソースコードを読むと、fork は newproc を呼び、その戻り値を見て親側と子側を分けます。そのため、fork のソース上では、親も子も newproc から戻ったように読めます。

 しかし、子プロセスでその場面を作っている実体は newproc の return ではありません。子プロセスは親のカーネルスタックをコピーされているため、fork が newproc を呼んでいた途中の戻り先を持っています。そこへ swtch が return(1) するので、fork から見ると newproc が1を返したように見えます。

第1章(仮想アドレスを物理アドレスへ写す下駄)

 この章では、板書の最初に出ていたPARの考え方を整理します。newproc の戻り値を理解する前に、カーネルが「いま見ているプロセスのu構造体」を切り替える仕組みを押さえる必要があります。

1.1(PARは物理アドレスへ足す下駄である)

 PDP-11のメモリ管理では、仮想アドレスをそのまま物理アドレスとして使いません。仮想アドレスの一部を使ってPARを選び、そのPARの値を物理アドレス計算の基準にします。

 板書では、PARに 1 を設定すると物理アドレスの基準が 64 になる、と説明されていました。これは、PARの値が物理アドレス計算時に64倍されるためです。つまりPARは、仮想アドレスを物理メモリのどこへ載せるかを決める下駄です。

 この下駄を差し替えることで、同じ仮想アドレスを使っていても、実際に参照する物理メモリ位置を変えられます。

1.2(PARは8本あり、上位3ビットで選ばれる)

 板書では、PARが8本あることが強調されていました。仮想アドレスの上位3ビットを使って、どのPARを使うかを選びます。

 16ビットの仮想アドレス空間を8分割するため、1本のPARが担当する範囲は8K単位になります。仮想アドレス上では連続して見える領域でも、上位3ビットが変わると別のPARが使われます。そのため、物理メモリ上では別の場所へ配置できます。

 板書の意味を文章にすると、仮想空間を8つの窓に分け、それぞれの窓に別々の下駄を履かせる、ということです。

1.3(u構造体のC000はPAR6を使う)

 カーネル内でu構造体は C000、8進では 140000 番地に置かれるものとして扱われます。C000 を2進数で見ると、先頭側のビットは 110 になります。

 上位3ビットでPARを選ぶため、この仮想アドレスはPAR6を使います。さらに、PARを選ぶための上位ビット以外の部分は、u構造体の先頭を見る場面ではゼロになります。

 したがって、カーネルが対象プロセスのu構造体を見たいなら、PAR6へ適切な下駄を設定すればよい、という発想になります。

第2章(p_addrをPAR6へ入れる意味)

 この章では、プロセス構造体の p_addr と、カーネルから見えるu構造体の関係を整理します。

2.1(ユーザプロセスはPPDAを持つ)

 ユーザプロセスは、物理メモリ上でPPDAとプログラム本体を持ちます。PPDAの中には、u構造体とカーネルスタックがあります。

 本文元では、物理メモリ内の配置は、PPDAがあり、その中にu構造体とカーネルスタックがあり、その後ろにプログラム本体がある、という形で説明されていました。

 ここで重要なのは、u構造体とカーネルスタックがプロセスごとに存在することです。プロセスを切り替えるとは、単に実行する命令列を変えるだけではありません。カーネルから見えるu構造体とカーネルスタックも切り替える必要があります。

2.2(カーネルは同じC000から別プロセスのu構造体を見る)

 カーネルのコードは、u構造体を固定された仮想アドレスで参照します。しかし、実際の物理メモリ上では、各プロセスのu構造体はそれぞれ別の場所にあります。

 この矛盾を解決するのがPAR6です。カーネルは、いま見たいプロセスのPPDA先頭を指す値をPAR6へ入れます。すると、同じ仮想アドレス C000 を参照しても、物理メモリ上では別プロセスのu構造体を見ることになります。

 この「同じ仮想アドレスから、違う物理メモリを見る」ことが、カレントプロセスを切り替えるうえでの土台です。

2.3(p_addrはPAR6へ入れるための値である)

 プロセス構造体が持つ p_addr は、対象プロセスのPPDAを指すための値です。物理アドレスそのものというより、PARへ入れて物理アドレス計算に使うための値として考えると理解しやすくなります。

 retu(rp->p_addr) という呼び出しは、この意味で読むべきです。rp が次に走らせるプロセスなら、rp->p_addr を使って、そのプロセスのu構造体とカーネルスタックが見えるようにPAR6を切り替えます。

 板書では、この操作を「プロセスの切り替え」であり「カレントプロセスの定義」として整理していました。どのプロセスのu構造体をC000から見ているかが、カレントプロセスの実体になるからです。

第3章(Cの関数呼び出しはスタック上の錯覚である)

 この章では、newproc の戻り値を理解するために、関数呼び出しとスタックフレームを見ます。

3.1(関数から戻るとは戻りアドレスへジャンプすること)

 C言語では、関数を呼び出すと関数に入り、return すると呼び出し元へ戻るように見えます。しかし、機械語のレベルでは、呼び出し元へ戻るためのアドレスをスタックに積み、最後にそのアドレスへジャンプしているだけです。

 板書では、JSR PC, csv を例に、飛び先から戻る場所は csv を呼び出した行の次の行である、と説明していました。つまり、呼び出された関数から見れば、csv を呼び出し、その csv から戻ってきたように見えます。

 この「そう見えるだけ」という理解が重要です。関数呼び出しは、スタックに積まれた戻りアドレスとレジスタの復帰によって作られる見かけです。

3.2(スタックフレームには戻るための情報がある)

 関数に入ると、戻りアドレス、旧BP、引数、局所変数などがスタック上に置かれます。板書では、x86系の書き方も使いながら、push bp、mov sp, bp によって旧BPを保存し、新しいBPを作る流れが示されていました。

 UNIX V6そのものの呼び出し規約はPDP-11ですが、ここで理解したいことは共通です。スタック上に「どこへ戻るか」「どのフレームを親とするか」が保存されています。

 したがって、スタックを丸ごとコピーすれば、関数呼び出しの途中の状態もコピーされます。これは newproc の話に直結します。

3.3(newproc呼び出し時点のスタックもコピー対象になる)

 fork から newproc を呼び出すと、newproc へ入るための戻りアドレスや、fork 側のフレーム情報がスタック上に置かれます。

 板書では、fork から newproc を呼び出した時点で、PCは jsr の次の行を指し、その値がスタックへ積まれる、と整理されていました。さらに、newproc の先頭では旧BPがスタック上に保存され、newproc 用のBPが作られます。

 この時点のスタック状態を含めて子プロセスが作られるため、子プロセスは「newproc の途中から戻るための材料」を最初から持つことになります。

第4章(swtchが選ぶと子プロセスが途中から戻る)

 この章では、子プロセスがまだ走っていないにもかかわらず、newproc の戻り値を受け取るように見える理由をまとめます。

4.1(子プロセスは親の実行状態を含んで作られる)

 newproc は、親プロセスのメモリだけでなく、実行状態に関わるカーネルスタックも含めて子プロセスを作ります。

 そのため、子プロセスはまだユーザプロセスとして一度も稼働していなくても、親がカーネル内で fork、newproc と進んできた状態のコピーを持っています。

 板書では、これにより「一度も稼働したことはないが、親プロセスの実行状態、つまりカーネルスタックまで含めてコピーされた子プロセスが、カーネルにより選択されたことになる」と説明されていました。

4.2(retuは対象プロセスのu領域へ切り替える)

 swtch が次に動かすプロセスを選ぶと、retu(rp->p_addr) によって対象プロセスのu領域へ切り替えます。

 板書画像では、retu(rp->p_addr); の行が強調され、その後に sureg()、さらにスワップ状態の場合の aretu(u.u_ssav) へ続く流れが示されていました。これは、まず対象プロセスのPPDAを見えるようにしてから、必要な復帰処理へ進むという読み方になります。

 別の板書では、_retu がスタック上の値を KISA6 へ入れる箇所が強調されていました。KISA6 はPAR6に対応するカーネル側のレジスタとして読めます。ここへ対象プロセスのアドレスを入れることで、カーネルのC000番地から見えるu構造体とカーネルスタックが切り替わります。

 V6の m40.s では、_retu は呼び出し元へ戻るアドレスを取り出した後、引数として渡された値を KISA6 に入れ、_u から sp と r5 を復帰してから、その戻りアドレスへジャンプします。Cの関数呼び出しの見え方では retu から戻っただけに見えますが、戻った先で使っているスタックは、すでに別プロセスのu領域内のスタックです。

 ここで aretu と混同しないことが大切です。aretu も sp と r5 を復帰して戻りますが、KISA6 を新しく設定する役割は retu 側にあります。swtch の通常経路では、まず retu(rp->p_addr) が対象プロセスのu領域へ切り替え、その後に return(1) へ進む、と読むのが素直です。

4.3(復帰するとnewprocが1を返したように見える)

 切り替え後、保存されていたカーネルスタックから復帰すると、子プロセスは newproc 呼び出しの続きへ戻ったように動きます。

 親プロセス側では、newproc は通常の関数呼び出しとして進みます。一方、子プロセス側では、コピーされたスタックから復帰するため、newproc が別の戻り値を返したように見えます。

 V6のコメントは、この点をかなり直接的に書いています。新しいプロセスは、親と同じ newproc 呼び出しの中で実行を始めたように見えるが、実際に走るコードは swtch である、という趣旨です。そして swtch が返す値が、子プロセス側の newproc 呼び出し元から見える値になります。

 したがって、「戻り値レジスタに1が入るように調整されている」とだけ考えるよりも、swtch() の return(1) が、コピーされたスタックを通じて newproc の呼び出し元へ見える、と考える方がソースに沿っています。Cのソースだけを見ると不思議に見えますが、実体はスタック、u領域、レジスタ復帰、そして swtch の戻り値の組み合わせです。

4.4(forkから見た見かけと実際の戻りは違う)

 ここがこの記事で一番大切なところです。fork 関数を読んでいると、親プロセスでも子プロセスでも、newproc の呼び出しが終わって fork の続きへ戻ってきたように見えます。つまり、ソースコード上の見かけはどちらも「newproc から戻った」です。

 ただし、実際の戻り方は親と子で違います。親プロセスは、newproc の末尾にある return(0); から普通に戻ります。これは通常のC関数呼び出しです。

 一方、子プロセスは、後でスケジューラに選ばれたときに swtch の中で復帰します。swtch は retu(rp->p_addr) によって子のu領域とカーネルスタックへ切り替え、その状態で通常経路の return(1); に到達します。この return(1) が、コピー済みのカーネルスタック上では、fork が呼んだ newproc の戻り値として見えます。

 そのため、fork から見た説明は次のようになります。

  1. 親では、newproc が本当に return(0) して、fork に戻る。

  2. 子では、swtch が return(1) するが、コピー済みのスタックのせいで、fork には newproc が1を返したように見える。

 つまり、「子では newproc から戻る」のではなく、「子では swtch から戻るのに、fork からは newproc から戻ったように見える」と言う方が、仕組みを正確に表しています。

4.5(swtchのreturn(1)が目印になる)

 V6の swtch() は、実行可能なプロセスを選ぶと、retu(rp->p_addr);、sureg(); の順に進みます。ここまでで、カーネルが見ているu領域と、その上のカーネルスタックは、選ばれたプロセスのものになっています。

 その直後に SSWAP の判定があります。これは、プロセスがスワップアウトに伴う特別な復帰を必要とする場合だけ、aretu(u.u_ssav) に進むための分岐です。通常のメモリ上のプロセスなら、この分岐を通らず、swtch() の return(1); へ進みます。

 newproc() のソース上のコメントは、この return(1) の「微妙な含意」が newproc の戻り値の説明になる、と明示しています。つまり、板書の「return 1」という言葉は、newproc の本文に return(1) があるという意味ではなく、swtch が1を返し、その1が子プロセス側の newproc 呼び出し元に見える、という意味で読むと正確です。

第5章(newprocが1を返す仕組みを一つの流れにする)

 ここまでの内容を、fork から子プロセスが戻ってくるまでの流れとしてまとめます。

5.1(親がforkからnewprocを呼ぶ)

 まず親プロセスが fork を実行し、その中で newproc を呼びます。この時点で、スタックには newproc から戻るための戻りアドレスや旧BPが積まれます。

 通常のC言語の見え方では、newproc が終われば fork の中へ戻ります。親プロセス側ではこの理解で問題ありません。

5.2(newprocが子プロセスの実行状態を作る)

 newproc は、子プロセス用のプロセス構造体、PPDA、u構造体、カーネルスタックを作ります。ここで重要なのは、カーネルスタックまでコピーされることです。

 カーネルスタックには、fork から newproc を呼び出している途中の状態が残っています。したがって、子プロセスは、まだ実行されたことがなくても「戻るべき場所」を持ちます。

5.3(swtchが子プロセスを選び、PAR6を切り替える)

 スケジューラが子プロセスを選ぶと、retu(rp->p_addr) によってPAR6が子プロセスのPPDAへ向くように設定されます。

 これにより、カーネルがC000番地から見るu構造体とカーネルスタックは、子プロセスのものになります。ここがカレントプロセスの切り替わりです。

 この時点で「命令をどこから実行するか」だけを見ていると迷います。実際には、retu によって sp と r5 も対象プロセスのu領域から復帰されます。つまり、Cの呼び出し関係を作っている足場そのものが、子プロセスのコピー済みスタックへ差し替わります。

5.4(保存されたスタックから復帰して1を返す)

 子プロセスのカーネルスタックへ切り替わった後、復帰処理によって、コピーされていた関数呼び出しの続きへ戻ります。

 その戻り方は、C言語の関数呼び出しの見え方としては newproc が戻ってきたように見えます。ただし、子プロセス側で実際に戻り値を作っている中心は、swtch() の return(1) です。そのため、fork 側から見ると「子では newproc が1を返した」と読めます。

 つまり、newproc が普通に return 1; を実行していると考えるのではなく、コピーされたスタック、retu によるu領域とスタックの切り替え、そして swtch の戻り値によって、そう見える状態が作られていると理解するのが正確です。

 このとき、fork のソース行だけを見ると、子プロセスも newproc 呼び出しの直後から処理を続けているように見えます。しかし、その見かけを作っている戻り命令は newproc のものではありません。子プロセスでは、swtch が戻っているのに、コピーされたスタック上の戻り先が newproc 呼び出し直後なので、fork には newproc が戻ったように見えるのです。

5.5(親側はreturn(0)、子側はswtchの1に見える)

 ここまでを親と子に分けると、さらに読みやすくなります。

 親プロセス側では、newproc() の末尾にある return(0); へ普通に進みます。これは、親が子プロセスを作った後、そのまま現在のカーネルスタック上で関数から戻る経路です。

 一方、子プロセス側では、親のスタックをコピーした状態から後で選ばれます。子が選ばれると、swtch() が retu(rp->p_addr) で子のu領域へ切り替え、通常経路では return(1); へ到達します。この戻り値が、コピー済みの呼び出し関係を通じて、子側の newproc 呼び出し元に見えます。

 そのため、同じ fork の中で newproc を呼んでいても、親は0、子は1という分岐が成立します。ここがV6の newproc を読むときの一番大きな山場です。

 関数名で言い切るなら、次の対比になります。

| 視点 | 親プロセス | 子プロセス |

| --- | --- | --- |

| fork のソース上の見え方 | newproc から戻る | newproc から戻るように見える |

| 実際に戻り値を作る場所 | newproc の return(0) | swtch の return(1) |

| なぜそう見えるか | 通常の関数呼び出しだから | 親のカーネルスタックがコピーされ、戻り先が newproc 呼び出し直後だから |

第6章(板書内容の読み取り)

 今回の元記事には板書画像が含まれていました。画像そのものは転載せず、本文に次のように取り込みました。

6.1(PARとC000の板書)

 最初の板書では、仮想アドレスの上位ビットでPARを選ぶ図、PAR値を64倍して物理アドレスの下駄にする図、C000番地がPAR6を使うことが描かれていました。

 この板書から読み取るべき点は、u構造体の仮想アドレスが固定でも、PAR6を変えれば別プロセスのu構造体を同じC000から見られる、ということです。

6.2(newprocが1を返す理由の板書)

 別の板書では、「newprocがなぜ1を返すのか」という問いに対して、PPDAがコピーされること、現在のスタック状態もコピーされること、スケジューラが子プロセスを選ぶこと、復帰時に戻り値が r0 に入ることが整理されていました。

 この板書の要点は、子プロセスが新規に関数先頭から走るのではなく、親の実行状態をコピーされたうえで、保存された戻り先へ復帰するという点です。

 画像の一部が欠けていても、読む順番は次のように補えます。まず、親プロセスのPPDAが子プロセス用にコピーされます。PPDAの中にはu構造体だけでなく、その時点のカーネルスタックも含まれます。次に、子プロセスが実行可能なプロセスとして登録され、後で swtch に選ばれます。選ばれた時点で retu(rp->p_addr) によってPAR6が子のPPDAへ向き、子のu領域とカーネルスタックが現在のものになります。最後に、swtch の戻り値 1 が、コピーされた呼び出し関係を通じて newproc の呼び出し元へ見えます。

 板書中の r0 は、PDP-11でC関数の戻り値を受け渡すレジスタとして読むと理解しやすくなります。ただし本文では、単に「どこかで r0 に1を入れる」という説明に閉じず、ソース上の swtch() の return(1) と結び付けて理解します。

6.3(retuとaretuのコード断片)

 コード画像では、retu(rp->p_addr); が強調されていました。また、_retu 側ではスタック上の値を KISA6 へ移す処理が示されていました。

 これは、対象プロセスの p_addr を使って、カーネルが見ているu領域を切り替える操作として読めます。_retu は KISA6 を変更したうえで、_u から sp と r5 を復帰します。ここで、Cの関数呼び出しを支えているスタックそのものが、選ばれたプロセスのものへ変わります。

 aretu は保存されたスタック状態へ復帰する処理ですが、swtch の中では SSWAP が立っているときだけ使われる特別な経路です。通常の「子プロセスが newproc の呼び出し元へ1を返したように見える」説明では、retu(rp->p_addr) と swtch() の return(1) を中心に読めば十分です。

6.4(欠けた図を本文で補うための読み替え)

 今回の板書は、写真としては一部が読み取りにくい箇所があります。そこで、記事としては図の欠けた部分を次の文章に置き換えて読むと、流れが途切れません。

  1. 親が fork の中で newproc を呼ぶ。

  2. その時点のu領域とカーネルスタックを含むPPDAが、子プロセス用にコピーされる。

  3. 子プロセスは、まだユーザ処理を実行していないが、戻り先を含むカーネルスタックを持っている。

  4. swtch が子プロセスを選び、retu(rp->p_addr) でPAR6を子のPPDAへ切り替える。

  5. retu によって、子のu領域から sp と r5 が復帰される。

  6. 通常経路では swtch() が return(1); へ進む。

  7. その 1 が、コピー済みの呼び出し関係を通じて、子側の newproc 呼び出し元に見える。

 この7段階で読むと、板書の図が途中で欠けていても、「なぜまだ走っていない子が戻り値を持っているのか」という疑問に答えられます。答えは、子が最初から空の状態で始まるのではなく、戻り先を含む実行状態のコピーとして作られるからです。

まとめ

 newproc が 1 を返す仕組みは、C言語の return だけで考えると分かりません。鍵になるのは、fork から見た見かけと、実際に戻り値を作っている場所を分けることです。

 親プロセスでは、newproc が本当に return(0) して fork に戻ります。子プロセスでは、fork から見ると newproc から戻ったように見えますが、実際に戻り値 1 を作っているのは swtch の return(1) です。

 この見かけを成立させているのが、仮想記憶、PAR6、PPDA、u構造体、カーネルスタック、retu によるu領域の切り替えです。

 カーネルはPAR6へ対象プロセスの p_addr を入れることで、C000番地から見えるu構造体とカーネルスタックを切り替えます。子プロセスは、親の実行状態をコピーされたカーネルスタックを持っているため、swtch に選ばれると、newproc 呼び出しの続きへ戻ったように動きます。

 その復帰時に、通常経路では swtch() の return(1) が使われます。V6のソースコメントが述べている通り、子プロセスでは実際に動くコードは swtch ですが、その戻り値が newproc の呼び出し元に見えます。これが、子プロセス側で newproc が 1 を返したように見える理由です。

 これは魔法ではなく、スタックとレジスタと仮想記憶が協調して作っている見え方です。図が欠けていても、p_addr がPAR6へ入り、u領域とカーネルスタックが切り替わり、swtch の return(1) がコピー済みの呼び出し関係へ返る、と追えば、読者は素直に理解できます。

出典メモ

  • 元Togetter URL: https://togetter.com/li/1703539

  • リダイレクト後URL: https://posfie.com/@hoehoe1234/p/FgPha4T

  • 元まとめタイトル: エンジョイC051回目newproc関数が1を返す仕組み(2021-04-18)

  • 取得日: 2026-05-16

  • 原型として使った範囲: posfieで公開表示された本文と板書・コード画像。広告、ランキング、関連まとめ、コメント欄は記事素材に含めていません。

  • 画像の扱い: 板書画像は、PAR6、C000、p_addr、PPDA、u構造体、カーネルスタック、retu/aretu、KISA6、戻り値 r0 の関係を本文で説明するために読み取りました。画像そのものは転載していません。

  • UNIX V6参照ソース: sys/ken/slp.c の swtch() と newproc()。swtch() の retu(rp->p_addr);、sureg();、SSWAP 時の aretu(u.u_ssav);、末尾の return(1);、および newproc() 直前のコメントを確認しました。参照URL: https://raw.githubusercontent.com/lsahn-org/unix-v6/master/sys/ken/slp.c

  • UNIX V6参照ソース: sys/conf/m40.s の _savu、_retu、_aretu。_retu が KISA6 を設定し、_u から sp と r5 を復帰する箇所を確認しました。参照URL: https://github.com/lsahn-org/unix-v6/blob/master/sys/conf/m40.s