エンジョイC051回目newproc関数が1を返す仕組み(2021-04-18)

1
ほえほえ@スプシマン @hoehoe1234

エンジョイC051回目newproc関数が1を返す仕組み(2021-04-18) プロセス切り替えの中心であるswtchの仕組みを理解できなければforkシステムコール中で使用されているnewproc関数が1を返す理由はわかりません。今回は仮想記憶にさかのぼってその理由を解説しました。

2021-04-22 15:28:04
ほえほえ@スプシマン @hoehoe1234

仮想記憶の復習からです。プログラムからは仮想アドレスとPARにしかアクセスできません。直接物理アドレスを扱うことはできないんですね。pdp11では仮想アドレスにPARの加算をして物理アドレスをハードウエアが生成しますがPARの値は64倍して加算されるところがポイントです。 pic.twitter.com/6LEy23pZgb

2021-04-22 16:35:40
拡大
ほえほえ@スプシマン @hoehoe1234

言葉を変えると、PARに1を設定すると物理アドレスは64になります。これはいわば、「下駄」に相当します。仮想アドレスにたしての物理アドレス中での下駄なんですね。この下駄を調整することにより物理アドレスのどこでも仮想アドレスにマッピングすることができます。

2021-04-22 16:37:28
ほえほえ@スプシマン @hoehoe1234

注意してほしいのはこの下駄(PAR)は8本あることです。仮想アドレスの上位3ビットを使ってどの下駄を使用するのかを選びます。16ビットは64ですから8本の下駄にわけるので8K単位で任意の物理空間にマッピングすることができます。

2021-04-22 16:38:53
ほえほえ@スプシマン @hoehoe1234

もうひとつ違った言い方をすれば、仮想アドレスを利用するプログラムは、自分内では連続したアドレス ①000 11~11 ②001 00~00 (①と②は仮想空間では連続した値) でも上位3ビットで下駄が変わってくるんですね。これは「仮想空間を8分割して任意の場所に再配置できる」とも言えます。

2021-04-22 16:41:16
ほえほえ@スプシマン @hoehoe1234

ユーザプロセスは、物理メモリ内では |PPDA | |-------------------| |プログラム本体 | という形で格納されています。 PPDAは更にu構造体とカーネルスタックに分けられます。 | u構造体 | | カーネルスタック |

2021-04-22 17:02:17
ほえほえ@スプシマン @hoehoe1234

各ユーザプロセスは管理情報としてu構造体とカーネルスタックを持っているんですね。u構造体はカーネル内ではC000(OCT 140000)番地に配置されています。カーネルもスケジューラ(proc[0])として動くので自分用のu構造体も必要です。

2021-04-22 17:04:33
ほえほえ@スプシマン @hoehoe1234

仮想記憶をつかって「反対」のことを考えます。各ユーザプロセスのPPDAの先頭にはそのプロセスのu構造体が配置されています。カーネルは各プロセスのu構造体にアクセスしたいんですよね。どういう操作をすればよいでしょうか?

2021-04-22 17:06:37
ほえほえ@スプシマン @hoehoe1234

カーネルプログラム内で定義したu構造体のアドレスはC000でした。2進数で表すと先頭は1100になります。先頭の3ビットでPARを選びますのでこれは8本あるPARのゼロから数えて6本目になります。

2021-04-22 17:09:03
ほえほえ@スプシマン @hoehoe1234

ここまでを整理すると ①PARは仮想空間の上位3ビットに対応して8本ある ②PARは物理アドレスの下駄として働く。64倍される。 ③u構造体のアドレスはC000でありこの変数にアクセスするとPAR6が下駄として使われる。 ④PARを選択する以外の仮想アドレスはすべてゼロである。 となります。

2021-04-22 17:11:11
ほえほえ@スプシマン @hoehoe1234

この整理をもとに「ユーザプロセスのu構造体をカーネルがみる」ためにはPAR6を適正な値を設定すればよいことになります。仮想アドレスはC000なのですから逆算してPAR6を設定すればよい。という発想になります。

2021-04-22 17:12:34
ほえほえ@スプシマン @hoehoe1234

PARを選択するためのビット(仮想アドレスの上位3ビット)以外はゼロですので、PARは物理アドレス計算時に64倍されますので、64倍して物理アドレスになる値を設定すればよいということになります。

2021-04-22 17:13:44
ほえほえ@スプシマン @hoehoe1234

この値がまさに、プロセス構造体が持っているp_addrの値となります。ですからカーネルは、u構造体にアクセスしたいプロセスのp_addrをPAR6に設定すればよい。ということになります。これがまさにプロセスの切り替えであり「カレントプロセスの定義」となります。 ここまでが前提です。

2021-04-22 17:15:23
ほえほえ@スプシマン @hoehoe1234

もう少し前提条件がありました。C言語から関数を呼び出すときの呼び出し規約、および、スタックの状態です。カーネルではスタックを操作しますのでJSR PC, dstとJSR R5, detの違いを理解する必要があります。実際にコードからアセンブラを出力したいのですがまだその環境が整っていません。

2021-04-22 17:22:12
ほえほえ@スプシマン @hoehoe1234

ですから黒本にあるcsv/cretをつかった方式と、x86系でみた方式の両方でスタックの状態を書いてみました。x86系のほうが簡単なので以後はその呼び出し方法で記載します。アセンブラコードが見れる環境が整えば改めて記載します。

2021-04-22 17:25:10
ほえほえ@スプシマン @hoehoe1234

csv/cret関数はこのようになっています。スタックフレームを生成(mov sp, r5)の後にレジスタをスタックに保存した後にRTS PCを「しません」。スタックはそのままに「JSR PC, (R0)」を行います。 pic.twitter.com/2q2Lr5UHof

2021-04-22 17:39:43
拡大
ほえほえ@スプシマン @hoehoe1234

JSRで飛ぶ飛び先はどこでしょうか?そうです、csv関数を呼び出した(JSR PC, csv)次の行なのです。これは簡単にいうとcsv関数を呼び出した行の次の行になります。呼び出した関数からはcsv関数を呼び出してcsv関数から返ってきたように見えます。

2021-04-22 17:41:02
ほえほえ@スプシマン @hoehoe1234

C言語の世界では関数を呼び出すとスタックフレームが作られ、関数が終了すると元の関数に戻ってきますがこれは単に「そうみえる」だけにすぎません。スタック上にJSR PC, dstでプッシュされた戻りアドレス(JSR命令の次のアドレス)にジャンプしているだけなのです。

2021-04-22 17:42:57
ほえほえ@スプシマン @hoehoe1234

この巧妙なスタック操作を通じてOSはプロセスを切り替えます。今回はnewprocからどうして1が返ってくるのか?の話でした。質問とその時の状況がどうなっているのかをまずは整理します。整理した図はこの通りです。 pic.twitter.com/kryflmH2kc

2021-04-22 17:45:21
拡大
ほえほえ@スプシマン @hoehoe1234

①swtch関数で新しく生成された子プロセスが選択されます。retu関数はカーネルのPAR6の値を設定します。この行為がすなわち、カレントプロセスとなりユーザモードで動作するプロセスを選択していることになります。 出典 黒本76P pic.twitter.com/FpQzD6dc6G

2021-04-22 17:51:55
拡大
ほえほえ@スプシマン @hoehoe1234

retuになります。この関数がプロセス切り替えをおこなっています(10行目)。これは引き数の値をKISA6(カーネルPAR6)に設定することによりカーネルからみた場合のC000から始まる8Kの仮想アドレスの下駄を設定しています。引き数は選択したプロセスのp_addrとなります。 pic.twitter.com/6AnidhMk4D

2021-04-22 17:56:28
拡大
ほえほえ@スプシマン @hoehoe1234

これによりメモリ上には、一度も稼働したことはないですが親プロセスの実行状態(カーネルスタック)まで含めてコピーされた子プロセスがカーネル(スケジューラ)により選択されたことになります。

2021-04-22 17:57:39
ほえほえ@スプシマン @hoehoe1234

次にこのときの子プロセスのカーネルスタックの状態を書いてみます。アセンブラが見れないので通常のC言語規約(x86系)に従って書いています。重要なのはBP(newprocのBP)とBPの下にある戻りアドレス(PC)になります。 pic.twitter.com/kHlEfikoAp

2021-04-22 18:03:15
拡大
ほえほえ@スプシマン @hoehoe1234

forkからnewprocを呼び出すと ①jsr pc, newproc この時点でpcはjsrの次の行を指しておりスタックにプッシュされます。 ②newprocの先頭では push bp mov sp, bp が行われ、bpはnewprocのBP、旧BP(forkのBP)はスタック上に保管されます。これがforkからnewprocを呼び出したときの状況です。

2021-04-22 18:06:52
ほえほえ@スプシマン @hoehoe1234

もしここでnewprocがreturn nを行うと mov n, r0 rsr pc が行われてnewprocを呼び出したfork関数に戻ります。もちろんそんなことはなくて話は続きます。

2021-04-22 18:08:55