[読書メモ] オペレーティング システム: 3 つの簡単なオペレーティング システム入門
第 4 章、抽象化: プロセス
4.1 プロセスとは何ですか?
- プログラムを実行するためにオペレーティング システムによって提供される抽象化
- プロセスがアクセスできるメモリ (アドレス空間と呼ばれる) は、そのプロセスの一部です。
- プロセスのマシン状態のもう 1 つの部分はレジスタです。
- たとえば、プログラム カウンター (PC) (命令ポインター、または IP とも呼ばれます) は、プログラムが現在実行している命令を示します。同様に、スタック ポインターと関連するフレーム ポインターは、関数パラメーター スタック、ローカル変数、および戻りアドレスを管理するために使用されます。
4.2 プロセス API:
作成 ( create
): オペレーティング システムには、新しいプロセスを作成するためのメソッドが含まれている必要があります。シェルにコマンドを入力するか、アプリケーション アイコンをダブルクリックすると、オペレーティング システムが起動され、指定されたプログラムを実行する新しいプロセスが作成されます。
Destroy ( destroy
): プロセスを作成するためのインターフェイスがあるため、システムはプロセスを強制的に破棄するためのインターフェイスも提供します。もちろん、多くのプロセスは実行が終了すると自動的に終了します。ただし、ユーザーはプロセスが終了しない場合にプロセスを終了したい場合があるため、暴走プロセスを停止するインターフェイスが便利です。
wait( wait
): プロセスの実行が停止するまで待機すると便利な場合があるため、何らかの待機インターフェイスが提供されることがよくあります。 その他の制御 (その他の制御): プロセスの強制終了または待機に加えて、その他の
制御が存在する場合もあります。たとえば、ほとんどのオペレーティング システムには、プロセスを一時停止 (一定期間実行を停止) してから再開 (実行を継続) する何らかの方法が用意されています。
status ( statu
): 通常、プロセスの実行時間や現在の状態など、プロセスに関するステータス情報を取得するためのインターフェイスもいくつかあります。
その他の制御 (その他の制御): プロセスの強制終了または待機に加えて、他の制御がある場合があります。たとえば、ほとんどのオペレーティング システムでは、プロセスを一時停止 (一定期間実行を停止) してから再開 (実行を継続) する何らかの方法が提供されています。
4.3 プロセスの作成: 詳細
- オペレーティング システムはどのようにしてプログラムを起動し、実行するのでしょうか? プロセス作成は実際にどのように行われるのでしょうか?
- プログラムを実行するためにオペレーティング システムが最初に行う必要があるのは、コードと静的データ (初期化された変数など) をメモリにロードすることです。
- プログラムは最初、何らかの実行可能形式でディスク (ディスク、または最新のシステムではフラッシュ ベースの SSD 上) に存在します。したがって、プログラムと静的データをメモリにロードするプロセスでは、オペレーティング システムがディスクからそれらのバイトを読み取り、メモリのどこかに配置する必要があります。
4.4 プロセスのステータス
実行中: 実行状態では、プロセスがプロセッサ上で実行されています。これは、命令を実行していることを意味します。
準備完了 (準備完了): 準備完了状態では、プロセスを実行する準備ができていますが、何らかの理由でオペレーティング システムが現時点では実行しないことを選択しています。
ブロック済み: ブロック状態では、プロセスは何らかの操作を実行しますが、他のイベントが発生するまで実行する準備ができていません。一般的な例としては、プロセスがディスクへの I/O 要求を行うと、他のプロセスがプロセッサを使用できるようにブロックされます。
- IO によりプロセスがブロックされる
- オペレーティング システムは、リソースの使用率を向上させるために CPY をビジー状態に保つために多くの決定を下す必要があります。
4.5 データ構造
- state 状態と init zomibe...
第 5 章 説明: プロセス API
5.1 fork() システムコール
システムコール fork() は、新しいプロセスを作成するために使用されます。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
printf("hello world (pid:%d)\n", (int) getpid());
int rc = fork();
if (rc < 0) {
// fork failed; exit
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) {
// child (new process)
printf("hello, I am child (pid:%d)\n", (int) getpid());
} else {
// parent goes down this path (original process)
printf("hello, I am parent of %d (pid:%d)\n",
rc, (int) getpid());
}
return 0;
}
运行这段程序(p1.c),将看到如下输出:
prompt> ./p1
hello world (pid:29146) hello, I am parent of 29147 (pid:29146)
hello, I am child (pid:29147)
- 子プロセスと I は親プロセスの完全なコピーです。具体的には、独自のアドレス空間 (つまり、独自のプライベート メモリ)、レジスタ、プログラム カウンタなどを持っていますが、fork() から返される値は異なります。親プロセスが取得する戻り値は新しく作成した子プロセスのPIDとなり、子プロセスが取得する戻り値は0となります。
- 他のケースでは、子プロセスが最初に実行される可能性があり、CPU のスケジュールに応じてさまざまな状況が発生します。
5.2 wait() システムコール
man
詳細についてはマニュアルをご利用ください- プロセスは wait を呼び出すとすぐに自身をブロックし、現在のプロセスの子プロセスが終了したかどうかを自動解析し、ゾンビ化した子プロセスを見つけた場合は子プロセスの情報を収集して完全に破棄してリターンし、子プロセスが見つからない場合は子プロセスが現れるまでここでブロックします。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[]) {
printf("hello world (pid:%d)\n", (int) getpid());
int rc = fork();
if (rc < 0) {
// fork failed; exit
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) {
// child (new process)
printf("hello, I am child (pid:%d)\n", (int) getpid());
sleep(1);
} else {
// parent goes down this path (original process)
int wc = wait(NULL);
printf("hello, I am parent of %d (wc:%d) (pid:%d)\n",
rc, wc, (int) getpid());
}
return 0;
}
- 子プロセスが①の実行を終了するまでシステムコールは戻りません。したがって、親プロセスが先に実行された場合でも、親プロセスは子プロセスの実行が終了するまで丁寧に待ち、その後 wait() が戻り、その後親プロセスが独自の情報を出力します。
5.3 最後に、exec() システムコール
- このシステムコールにより、子プロセスは親プロセスと同じプログラムを実行できるようになります。たとえば、p2.c で fork() を呼び出す人は、同じプログラムのコピーを実行したい人にのみ役立ちます。ただし、同じプログラムを実行したいことがよくあり、exec() がまさにそれを実行します (
- exec() には、execl()、execle()、execlp()、execv()、execvp() など、いくつかのバリエーションがあります。詳細については、マニュアルページを参照してください。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int
main(int argc, char *argv[])
{
printf("hello world (pid:%d)\n", (int) getpid());
int rc = fork();
if (rc < 0) {
// fork failed; exit
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) {
// child (new process)
printf("hello, I am child (pid:%d)\n", (int) getpid());
char *myargs[3];
myargs[0] = strdup("wc"); // program: "wc" (word count)
myargs[1] = strdup("p3.c"); // argument: file to count
myargs[2] = NULL; // marks end of array
execvp(myargs[0], myargs); // runs word count
printf("this shouldn't print out");
} else {
// parent goes down this path (original process)
int wc = wait(NULL);
printf("hello, I am parent of %d (wc:%d) (pid:%d)\n",
rc, wc, (int) getpid());
}
return 0;
}
- fork()、wait()、および exec() を使用する (p3.c)
5.4 API がこのように設計されている理由
- fork() と exec() を分離すると、シェルで多くの便利な関数を簡単に実装できるようになります。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <assert.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int rc = fork();
if (rc < 0) {
// fork failed; exit
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) {
// child: redirect standard output to a file
close(STDOUT_FILENO);
open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);
// now exec "wc"...
char *myargs[3];
myargs[0] = strdup("wc"); // program: "wc" (word count)
myargs[1] = strdup("p4.c"); // argument: file to count
myargs[2] = NULL; // marks end of array
execvp(myargs[0], myargs); // runs word count
} else {
// parent goes down this path (original process)
int wc = wait(NULL);
assert(wc >= 0);
}
return 0;
}
prompt> wc p3.c > newfile.txt
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
-- p4 我实调用了 fork 来创建新的子进程,之后调用 execvp()来执行 wc。
-- 屏幕上谁有看到输出, 是由于结果被重我向到文件 p4.output。
- 補足: RTF(Friendly)M - マニュアルを読んでください。
仕事
第 6 章 メカニズム: 制限付き直接実行
- OS は、何らかの方法で多くのタスクが物理 CPU を共有する必要があります。
- 1 つのプロセスをしばらく実行してから、別のプロセスを実行する、というように繰り返します。このようにCPUをタイムシェアリングすることで仮想化が実現されます。ただし、問題はパフォーマンス (追加のオーバーヘッドなし) と制御 (アクセス許可) です。
6.1 基本テクニック: 制限付き直接実行
-
プログラムをできるだけ高速に実行するために使用される手法は、制約付き直接実行と呼ばれ
(limited direct execution) LDE
、単純にプログラムを CPU 上で直接実行します。 -
通常の呼び出しを使用してプログラムにジャンプし
main()
、後でカーネルに戻ります。
-
実はそんなに単純ではなく、もしプログラムの実行に制限がなければOSは何も制御できないので「ただのライブラリ」になってしまいます。
6.2 問題 1: 操作の制限 (権限の問題)
- ハードウェアとオペレーティング システムの問題: 重要な質問: 制限された操作を実行するにはどうすればよいですか??
ヒント: Protected Control Transfer
Hardware を使用すると、さまざまな実行モードを提供してオペレーティング システムを支援します。以下では用户模式(user mode)
、アプリケーションは完全访问
ハードウェア リソースを持つことができません。以下では内核模式(kernel mode)
、オペレーティング システムはマシンの にアクセスできます全部资源
。陷入
(トラップ) カーネルおよびユーザー モード プログラムへの (トラップからの復帰) に対する特別な命令も提供されます陷阱返回
。また、オペレーティング システムがトラップ テーブルがメモリ内のどこにあるかをハードウェアに伝えるための命令も提供されます。
私たちが採用するアプローチは、新しいプロセッサ モードを導入することです。
ユーザーモード
ユーザー モードで実行されるコードは次のようになります受到限制
。たとえば、プロセスはユーザー モードで実行中に I/O リクエストを発行できません。これを行うと、プロセッサが例外をスローし、オペレーティング システムがプロセスを終了する可能性があります。
カーネルモード
オペレーティング システム (またはカーネル) はこのモードで実行されます。このモードでは、実行中のコードは、たとえば、I/O リクエストの発行や、あらゆる種類の制限された命令の実行など、好きなことを実行できます特权操作
。
システムコール
系统调用
カーネルが、关键功能
ファイルシステムへのアクセス、プロセスの作成と破棄、他のプロセスとの通信、メモリの追加割り当てなど、特定のことをユーザー プログラムに慎重に公開できるようにします。。
ユーザーが何か特权操作
(ディスクからの読み取りなど) を実行したい場合は、ハードウェアが提供する機能を使用できます系统调用
。
システムコールを実行するには、プログラムは特別な陷阱
(トラップ)命令を実行する必要があります。この命令は同時にカーネルにジャンプし、特権レベルを に昇格します内核模式
。カーネルに入ると、システムは特权操作
呼び出しプロセスに必要な作業を実行するために必要なことはすべて実行できます (許可されている場合)。それが完了すると、オペレーティング システムは特別な从陷阱返回
(トラップからのリターン) 命令を呼び出します。ご想像のとおり、この命令は呼び出し元のユーザー プログラムに戻り、特権レベルをユーザー モードに下げます。
トラップを実行するとき、ハードウェアは、オペレーティング システムがトラップからの復帰命令を発行するときに使用できる十分な呼び出し元レジスタが格納されていることを確認する必要があるため、注意が必要です正确返回
。
トラップは
OS
どのコードを内部で実行するかをどのようにして知るのでしょうか? カーネルは、ブート時にトラップ テーブルを設定することでこれを行います。
トラップテーブル
カーネルは起動時に陷阱表
トラップアドレスの初期化(トラップテーブル)を設定することで実現します。
マシンが起動すると、システムは特権 (カーネル) モードで実行されるため、必要に応じてマシンのハードウェアを自由に構成できます。オペレーティング システムが最初に行うことの 1 つは、異常なイベントが発生したときにどのコードを実行するかをハードウェアに指示することです。たとえば、ハードディスク割り込みが発生したとき、キーボード割り込みが発生したとき、またはプログラムがシステム コールを呼び出したときに、どのようなコードを実行する必要があるでしょうか。通常、オペレーティング システムは、特別な命令を通じてこれらのトラップ ハンドラーの場所をハードウェアに通知します。ハードウェアは、通知を受けると、次回マシンを再起動するまでこれらのハンドラーの場所を記憶し、システム コールやその他の異常なイベントが発生したときに何をすべきか(つまり、どのコード部分にジャンプするか) を認識します。セキュリティを向上させましょう!!
問題 2: プロセス間の切り替え
重要な質問: CPU の制御を取り戻す方法
オペレーティング システムはどのようにして CPU の制御を取り戻し、プロセス間を切り替えることができるのでしょうか?
連携方法:システムコール待ち
- 長時間実行されるプロセスは定期的に CPU を放棄すると想定されます
- システムコール(例:yield)
非協調モード: クロック割り込み
时钟中断
(タイマー割り込み)。クロック デバイスは、数ミリ秒ごとに割り込みを生成するようにプログラムできます。割り込みが生成されると、現在実行中のプロセスが停止し、中断处理程序
オペレーティング システムに事前に設定された (割り込みハンドラー) が実行されます。この時点で、オペレーティング システムは CPU の制御を取り戻すため、現在のプロセスを停止し、別のプロセスを開始するなど、必要な操作を行うことができます。
硬件
特に割り込みが発生した場合には、トラップ命令からのその後の復帰で実行中のプログラムを適切に再開できるように、実行中のプログラムの十分な状態を保存する責任があることに注意してください发生中断
。この操作は、明示的なシステム コールと非常によく似た暗黙的な操作と考えることができます。
コンテキストの保存と復元
-
オペレーティング システムが上記の 2 つの方法で制御を取得すると、プロセスを切り替えるかどうかを決定できますが、この決定はスケジューラによって行われます。
-
オペレーティング システムがプロセスを切り替えることを決定した場合、最初にプロセスを切り替える必要があります
上下文切换
(コンテキスト スイッチ)。つまり、現在実行中のプロセス保存一些寄存器的值
(たとえば、カーネル スタックへ) と、間もなく実行されるプロセス恢复一些寄存器的值
(カーネル スタックから) に対して行う必要があります。このようにして、オペレーティング システムは、トラップからの復帰命令が最終的に実行されるときに、以前に実行されていたプロセスに戻らず、別のプロセスの実行を継続することを保証できます。
コンテキストの切り替えには、レジスタの保存と復元だけでなく、ページ テーブルの切り替えなどの他の操作も含まれます。
- オペレーティング システムは、実行中のプロセス A からプロセス B への切り替えを決定します。この時点で、
switch()
現在のレジスタ値を (A のプロセス構造に) 慎重に保存し、レジスタを (プロセス構造から) プロセス B に復元して、コンテキスト () を切り替えるルーチンを呼び出します。具体的には、スタック ポインタを変更して (A の代わりに) B のカーネル スタックを使用しますswitch context
。最後に、オペレーティング システムはトラップから戻り、B のレジスタを復元し、実行を開始します。
xv6 的上下文切换代码 :
OS_CPU_PendSVHandler:
CPSID I @ Prevent interruption during context switch
MRS R0, PSP @ PSP is process stack pointer
CMP R0, #0
BEQ OS_CPU_PendSVHandler_nosave @ equivalent code to CBZ from M3 arch to M0 arch
@ Except that it does not change the condition code flags
SUBS R0, R0, #0x10 @ Adjust stack pointer to where memory needs to be stored to avoid overwriting
STM R0!, {
R4-R7} @ Stores 4 4-byte registers, default increments SP after each storing
SUBS R0, R0, #0x10 @ STM does not automatically call back the SP to initial location so we must do this manually
LDR R1, =OSTCBCur @ OSTCBCur->OSTCBStkPtr = SP;
LDR R1, [R1]
STR R0, [R1] @ R0 is SP of process being switched out
@ At this point, entire context of process has been saved
問題の原因:
コードが最適化されると、rbuf_len はレジスタ r8 に保存されます。コンテキストが切り替わると、r8 レジスタは保存されず、その結果、r8 レジスタの値が他のプロセスによって変更されます。現在のプロセスに戻った後、r8 の値を復元することはできません。
考察: 同時実行性が割り込みに与える影響
別の処理中に割り込みが発生するとどうなりますか?
1 つは割り込み処理中禁止中断
(割り込みを無効にする) です。これにより、1 つの割り込みが処理されている間、他の割り込みが CPU に渡されないことが保証されます。もちろん、オペレーティング システムはこれを行う際に注意する必要があります。割り込みを無効にする時間が長すぎると、割り込みが失われる可能性があり、これは (技術的に) 好ましくありません。
ヒント: 再起動は便利です。最初に OS を再起動した後 (起動時)、トラップ ハンドラーを設定し、クロック割り込みを開始し、制限モードでのみプロセスを実行します。OS は、プロセスが効率的に実行できることを確認し、特権操作のみを実行するか、プロセスが CPU を長時間独占しすぎて切り替える必要がある場合、OS の介入が必要になります。