14例外的な制御フローの例外と処理(例外的な制御フローの例外と処理)

例外制御フロー

異常な制御フローが発生する場所:
例外的な制御フロー (ECF) は、プログラム実行中のいくつかの特別なイベントまたは条件によって引き起こされる制御フローの変化です。異常な制御フローは通常、次の状況で発生します。

  • ハードウェア例外と割り込み: ハードウェア例外は、ゼロ除算、不正なメモリ アドレスへのアクセス、浮動小数点オーバーフローなど、プロセッサによって検出される潜在的なエラー状態です。ハードウェア割り込みは、処理されるイベントをプロセッサに通知するために外部デバイス (マウス、キーボード、ネットワーク インターフェイス カードなど) によって送信される信号です。このような場合、プロセッサは現在実行中の一連の命令を一時停止し、代わりに事前定義された例外ハンドラまたは割り込みハンドラを実行します。
  • オペレーティング システム カーネル: オペレーティング システム カーネルは、システム コールに応答するときに制御フローを変更する場合があります。たとえば、プロセスがファイルを読み取るシステム コールを発行する場合、オペレーティング システムは制御フローをユーザー プロセスからカーネル モードに切り替え、ファイル読み取り要求を処理してから、制御フローをユーザー プロセスに戻す必要がある場合があります。 。
  • シグナル: シグナルは、プロセス間で通知を配信するために使用されるソフトウェア例外制御フローです。プロセスがシグナルを受信すると、オペレーティング システムは現在のプロセスの実行を中断し、代わりにシグナルに関連付けられた信号処理関数を実行します。信号は、プロセスが不正な操作 (不正なメモリ アドレスへのアクセスなど) を実行した場合などに、他のプロセスによって送信されたり、オペレーティング システムによって生成されたりすることがあります。
  • 例外処理ステートメント: 高級プログラミング言語では、例外制御フローは通常、例外処理ステートメント (try-catch-finally ステートメントなど) によって表されます。プログラマーはこれらのステートメントを使用して、ファイル読み取りエラー、NULL ポインター参照、ネットワーク接続の中断など、実行時に発生する可能性のある例外をキャッチして処理できます。
  • スレッドの同期とスケジューリング: マルチスレッド環境では、スレッドのスケジューリングと同期によって制御フローの異常が発生する可能性もあります。あるスレッドが別のスレッドがミューテックスまたはリソースを解放するのを待機している場合、スレッド スケジューラは、リソースが使用可能になるまで制御フローを他のスレッドに切り替えることがあります。この場合、例外制御流体はスレッドを切り替えて同期します。
    例外制御フローの問題解決:
    例外制御フロー (Exception Control Flow) は、プログラム実行中の異常イベントを処理するためのメカニズムです。例外イベントは、ゼロによる除算、配列の範囲外、ファイル読み取りエラーなど、プログラムの動作中に発生する可能性のある異常または予期しない状況です。プログラムをより安定して実行できるようにするために、開発者は例外制御フローを通じてこれらの例外を検出し、処理する必要があります。
    問題解決における例外制御フローの利点:
    (1) プログラムの安定性の向上: 例外をキャッチして処理することにより、エラーが発生してもプログラムは直接クラッシュせずに実行を継続できます。
    (2) プログラムの保守性の向上: 例外の集中処理によりコードが読みやすく保守しやすくなり、潜在的な問題の特定に役立ちます。
    (3) ユーザー エクスペリエンスの向上: 例外処理により、プログラムはエラー発生時に、より分かりやすく詳細なエラー情報をユーザーに提供できます。

例外定義

異常とは通常、通常とは異なる、予期せぬ、異常な状況や出来事を指します。コンピューター プログラミングでは、例外とは通常、システム クラッシュ、データ損失、入力エラーなど、プログラムの実行中に発生するエラーまたは予期せぬ状態を指します。

例外テーブル

例外テーブルとは、コンピュータプログラムの実行中に発生する可能性のある異常事態とその対処方法をまとめたテーブルを指します。例外には通常、次の側面が含まれます。

  • 例外タイプ: NULL ポインター例外、配列範囲外例外、ファイルが見つからない例外など、考えられる例外タイプをリストします。
  • 例外情報: 例外の原因や考えられるシナリオなど、各例外タイプを詳細に説明します。
  • 処理方法: 例外の種類ごとに、例外のキャッチとプロンプト情報の提供、例外の再スロー、ログ記録など、プログラムが例外を処理する方法をリストします。
  • コード例: 例外をキャッチして処理する方法を示すコードをいくつか提供します。

例外テーブルは、開発プロセスにおけるプログラマにとって非常に重要なツールであり、例外を識別して処理し、プログラムの安定性と堅牢性を確保するのに役立ちます。

異常な分類方法

例外は、次のようなさまざまな方法で分類できますが、これらに限定されません。

  • 例外の種類に応じて: 実行時例外、チェック時例外、システム例外など。
  • 例外処理方法に応じて: 例外のキャッチ、例外のスロー、例外の処理など。
  • 例外発生のタイミングに応じて:同期例外、非同期例外など。

同期例外と非同期例外は、例外が発生するタイミングによって分類されます。同期例外とは、プログラムの実行中に発生するエラーや例外を指し、プログラムが停止して例外がスローされるため、直ちに処理する必要があります。たとえば、除算演算の分母がゼロの場合、プログラムはその点に到達すると停止し、例外メッセージをスローします。例外は直ちに処理する必要があります。非同期例外とは、プログラムの実行中にエラーまたは例外が発生しても、プログラムは引き続き実行できる状況を指します。例外情報はすぐにはスローされませんが、特定の時点でシステムまたはプログラムによって自動的に処理されます。一瞬。たとえば、ネットワーク接続の問題により、ネットワーク要求により非同期例外が発生する可能性があり、プログラムは再送信を試行するか、送信する前にネットワーク応答を待ちます。

割り込みは例外を処理するもう 1 つの方法です。割り込みは非同期例外です。コンピュータが特定のタスクを処理しているときに、ハードウェア障害やユーザー入力など、タスクに関係のないイベントが発生することを意味します。コンピュータは現在のタスクを一時停止し、イベントの処理に切り替えます。そして元のタスクに戻ります。割り込みは通常、システム内のハードウェア デバイスまたはプログラムによって開始され、割り込みハンドラーによって処理できます。割り込み処理方式により、コンピュータは複数のタスクを同時に処理できるようになり、コンピュータ システムの効率と信頼性が向上します。

同期例外、トラップ、およびシステムコール

同期例外、トラップ、システム コールはオペレーティング システムの 3 つの重要な概念であり、すべてコンピュータ プログラムの実行プロセスに関連しています。

同期例外とは、プログラムの実行中にエラーまたは異常な状況が発生し、プログラムの実行が停止し、すぐに処理する必要がある例外がスローされることを意味します。たとえば、ゼロで除算したり、不正なメモリ アドレスにアクセスしたりすると、同期例外が発生します。同期例外は通常、ハードウェアまたはソフトウェアによって自動的に検出され、処理されます。たとえば、オペレーティング システムは例外が発生したことを通知する信号をプロセスに送信し、プロセスは信号の種類に応じてそれを処理する必要があります。

システム コールは、ユーザー プログラムがオペレーティング システムにサービスを要求するためのインターフェイスであり、オペレーティング システムが外部サービスを提供するためのインターフェイスでもあります。ユーザープログラムは、ファイルの読み取りと書き込み、プロセスの作成、ネットワーク通信などのいくつかの操作を実行する必要があります。ただし、これらの操作はオペレーティング システムによって完了する必要があります。したがって、ユーザー プログラムはシステム コールを通じてオペレーティング システムへの要求を開始し、オペレーティング システムがユーザー プログラムに代わって対応するサービスを実行できるようにする必要があります。システム コールは通常、ソフトウェア割り込みを通じて実装されます。つまり、ユーザー プログラムはトラップ命令を通じてカーネル状態に切り替わり、対応するシステム コール命令を実行します。オペレーティング システムは、カーネル状態で対応するサービスを完了し、結果をカーネル状態に返します。ユーザープログラム。

一般に、同期例外、トラップ、およびシステムコールは、コンピュータプログラムの実行中の例外メカニズムおよび呼び出しメソッドであり、オペレーティングシステムにおいて重要な役割を果たします。

欠陥、ページ欠落、保護欠陥

フォールト、ページミス、および保護フォールトは、コンピューターのオペレーティング システムで見られる一般的なタイプの例外です。
フォールトとは、不正なオペランド、不正な命令、不正なメモリ アクセスなど、プログラムの実行時に発生する例外を指します。これらの例外は通常、ハードウェアの検出によって発生し、オペレーティング システムはこれらの例外を処理する必要があります。フォールトはエラーとは異なります。エラーは通常、0 による除算やスタック オーバーフローなど、プログラム ロジックまたは設計の問題によって発生します。

ページ欠落とは、プログラムがアクセスする必要があるページがメモリ内にない場合に発生する異常な状況を指します。オペレーティング システムは、アクセスする必要があるページをディスクからメモリにロードし、プログラムがそのページにアクセスできるようにします。ページ。メモリ内でページが変更されていない場合は、ページ欠落例外がスローされます。プログラムがページにアクセスできるように、オペレーティング システムはディスクからページをメモリに読み取り、ページ テーブルなどのデータ構造を更新する必要があります。

保護エラーは、プログラムが十分なアクセス許可を取得せずに、カーネル モード リソースや他のプロセスのメモリなどの保護されたリソースにアクセスしようとしたときに発生し、例外が発生します。オペレーティング システムはプログラムのアクセス権限をチェックする必要があり、権限が不十分な場合は保護エラーが発生します。

一般に、フォールト、ページ ミス、および保護フォールトは、オペレーティング システムにおける一般的なタイプの例外です。オペレーティング システムは、システムの安定性とセキュリティを確保するために、これらの例外を検出して処理する必要があります。
次に、無効なメモリ参照の例を示します。整数の配列へのポインタがあるが、そのポインタが未割り当てのメモリ アドレスを指していると仮定します。

#include <stdio.h>
#include <stdlib.h>
int main() {
    int *ptr;
    *ptr = 10; // 无效内存引用
    printf("%d\n", *ptr);
    return 0;
}

このプログラムでは、整数配列へのポインタ ptr を宣言しますが、それにメモリ空間を割り当てません。次に、ポインタが指すメモリ アドレスに整数 10 を割り当てようとします。これにより、無効なメモリ参照フォールトが発生します。最後に、プログラムはポインタが指すメモリ アドレスの値を読み取って出力しようとしますが、そのメモリ アドレスが割り当てられていないため、プログラムはクラッシュして終了します。
このタイプの失敗を回避するには、ポインタが常に割り当てられたメモリ アドレスを指していることを確認し、ポインタを使用する前に初期化する必要があります。このタイプの障害は、malloc() 関数を使用してヒープ上にメモリのセクションを割り当てることで回避できます。次に例を示します。

#include <stdio.h>
#include <stdlib.h>
int main() {
    int *ptr = (int *)malloc(sizeof(int));
    *ptr = 10; // 正确的内存引用
    printf("%d\n", *ptr);
    free(ptr);
    return 0;
}

このプログラムでは、malloc() 関数を使用して int 型のメモリ空間をヒープ上に割り当て、ポインタ ptr がメモリ アドレスを指します。次に、整数 10 をメモリ アドレスに割り当て、値を出力します。最後に、free() 関数を使用してメモリ空間を解放します。これにより、無効なメモリ参照エラーが回避されます。

失敗の例、無効なメモリ参照

障害とは、プログラムが無効な操作を実行しようとしたり、無効なメモリ アドレスにアクセスしようとしたりして、オペレーティング システムがプログラムに例外信号を送信する状況を指します。無効なメモリ参照は、プログラムが未割り当てのメモリ アドレスまたは解放されたメモリ アドレスにアクセスしようとしたときに発生する一般的な障害でもあります。

終了

終了とは、プログラムまたはプロセスが終了するか、強制的に停止されるプロセスを指します。

プログラムの実行の最後には、いくつかの段階を経ます。まず、オペレーティング システムはプログラムをメモリにロードし、それにリソースまたはアクセス許可を割り当てます。その後、プログラムはタスクを完了するか例外が発生するまで実行を開始します。プログラムの実行が完了すると、プログラムは自動的に終了して終了し、占有されていたリソースと権限が解放されます。プログラムでエラーや未処理の例外などの異常な状況が発生した場合、プログラムは自動的に終了して終了します。

プロセスの終了は正常終了と異常終了に分けられます。通常、正常終了とは、プロセスがタスクを完了して終了し、オペレーティング システムがプロセスのリソースと権限を再利用することを意味します。異常終了とは、プロセスが、不正なメモリ アドレスへのアクセス、ゼロによる除算など、処理できない異常な状況に遭遇したことを意味します。オペレーティング システムは、システムの安定性とセキュリティを確保するために、プロセスを強制的に終了し、リソースを再利用する必要があります。 。

一般に、終了とは、プログラムまたはプロセスが終了する、または強制的に停止されるプロセスです。これはオペレーティング システムの非常に重要な部分であり、システムの安定性と効率を確保するために、オペレーティング システムはプログラムとプロセスを正常に終了し、占有しているリソースとアクセス許可を速やかに回復する必要があります。

exit
Unix/Linux では、exit() システム コールを呼び出すことでプロセス自体を終了できます。exit() 関数は、プロセスの終了時にいくつかのクリーンアップ操作を実行し、プロセスの終了ステータスを親プロセスに渡します。プロセスの終了ステータスは、通常、プロセスの終了理由または実行結果を示すために使用されます。
C 言語では、exit() 関数は stdlib.h ヘッダー ファイルで定義されており、そのプロトタイプは次のとおりです。

void exit(int status);

このうち、status はプロセスの終了ステータスを示す整数です。status が 0 の場合は、プロセスが正常に終了したことを意味します。他の終了ステータスを使用して、エラー コードやその他の情報を表すことができます。

exit() 関数はプロセスを直接終了するのではなく、プロセスの終了ステータスを親プロセスに渡し、親プロセスがプロセスを終了するかどうかを決定することに注意してください。親プロセスが子プロセスの終了ステータスを待たない場合、子プロセスは「ゾンビプロセス」になる可能性があり、wait() や waitpid() などの関数を使用して子の終了ステータスを待つ必要があります。その資源を処理し、リサイクルします。
以下は、exit() 関数を使用してプロセスを終了する方法を示す簡単なプログラム例です。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    printf("before exit()\n");
    exit(0);  // 终止进程,并返回状态码0
    printf("after exit()\n");  // 这行代码不会被执行
    return 0;
}

このサンプルプログラムでは、exit()関数を呼び出すと、プロセスは即座に終了し、ステータスコード0を返します。したがって、printf() 関数の出力は「exit() 前」に限定され、「exit() 後」は実行されません。

Linux/x86-64 で一般的に使用されるシステム コール

Linux/x86-64 は、ユーザー プログラムが使用する多くのシステム コールを提供する一般的なオペレーティング システムおよびコンピューター アーキテクチャです。以下は、Linux/x86-64 で一般的に使用されるシステム コールです。

read(): ファイル記述子からデータを読み取ります。
write(): ファイル記述子にデータを書き込みます。
open(): ファイルを開きます。
close(): ファイルを閉じます。
creat(): ファイルを作成します。
unlink(): ファイルを削除します。
mkdir(): ディレクトリを作成します。
rmdir(): ディレクトリを削除します。
chdir(): 現在の作業ディレクトリを変更します。
getpid(): 現在のプロセスのプロセス ID を取得します。
fork(): 新しいプロセスを作成します。
execve(): 新しいプログラムを実行します。
wait(): 子プロセスが終了するのを待ちます。
Pipe(): パイプを作成します。
dup(): ファイル記述子を複製します。
select(): ファイル記述子のセットで I/O イベントを待ちます。
socket(): ソケットを作成します。
bind(): ソケットをアドレスにバインドします。
listen(): ソケットをリッスンします。
accept(): クライアント接続を受け入れます。
connect(): リモートホストに接続します。

これらのシステムコールは、C言語の標準ライブラリ関数またはシステムコールライブラリ関数を通じて呼び出すことができます。これらは、ユーザー プログラムがファイル操作、プロセス管理、ネットワーク通信、その他の機能を実行できる一連の基本的なオペレーティング システム サービスを提供します。

システムコール、オープン

syscall は、Linux オペレーティング システムによって提供されるシステム コール メカニズムで、ユーザー プログラムがオペレーティング システム カーネル内の関数を直接呼び出すことができるようにします。Linux/x86-64 では、syscall 命令を使用してシステム コールをトリガーします。この命令は、システム コール番号とパラメータをオペレーティング システム カーネルに渡します。カーネルは、システム コール番号とパラメータに基づいて対応する操作を実行し、結果を返します。結果をユーザープログラムに送信します。
open は、ファイルを開くために一般的に使用されるシステム コールです。Linux/x86-64 では、open システム コールのシステム コール番号は 2 で、パラメータは 3 つあります。
(1) const char *pathname: オープンするファイルのパス名。
(2)int flags: ファイルを開くためのメソッドとフラグ ビット (O_RDONLY、O_WRONLY、O_RDWR など)。
(3)mode_t モード: ファイルのパーミッション (たとえば、0666) は、ファイルが読み取り可能および書き込み可能であることを意味します。

ユーザー プログラムは、C 言語の標準ライブラリ関数 open() を介して open システム コールを呼び出すことができます。次に例を示します。

#include <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);

この関数は、後続のファイルの読み取りおよび書き込み操作のためのファイル記述子を返します。たとえば、test.txt という名前のファイルを開いてデータを書き込むには、次のようにプログラムを作成します。

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
    int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd == -1) {
        perror("open");
        return 1;
    }
    write(fd, "Hello, World!", 13);
    close(fd);
    return 0;
}

プログラムは、open() 関数を使用して test.txt ファイルを開き、書き込みモードで開きました。ファイルを開くことができない場合、プログラムはエラー メッセージを出力して終了します。開くことが成功すると、プログラムは write() 関数を通じて「Hello, World!」文字列をファイルに書き込み、ファイル記述子を閉じます。

プロセス

プロセスは、コンピューター上で実行されているプログラムのインスタンスです。オペレーティング システムでは、プロセスはリソースの割り当てとスケジューリングの基本単位であり、コンピュータ内でのプログラムの逐次実行とみなすこともできます。プロセスには複数のスレッドを含めることができ、各スレッドはプログラムのタスクの一部を実行します。

プロセスには独自のメモリ空間、レジスタ セット、プログラム カウンタ、オープン ファイル、その他のシステム リソースがあり、それらは互いに独立しており、相互に干渉しません。オペレーティング システムは、プロセス スケジューリング アルゴリズムを通じてプロセスの実行を管理し、複数のタスクの同時実行を実現するために CPU タイム スライスを割り当てます。

各プロセスには一意のプロセス識別子 (プロセス識別子、PID) があり、異なるプロセスを区別するために使用されます。プログラムが開始されると、オペレーティング システムはそのプログラム用に新しいプロセスを作成し、それに一意の PID を割り当てます。

プロセスのステータスは、実行中、準備完了、ブロック済みの 3 種類に分類できます。プロセスが実行中の場合、プロセスは実行状態になります。プロセスが入力および出力の待機、リソース割り当ての待機など、特定のイベントの発生を待機している場合はブロック状態になります。プロセスは実行の準備ができていますが、CPU タイム スライスをまだ取得していないため、準備完了状態にあります。

プロセスは、プロセス間通信メカニズム (パイプ、共有メモリ、メッセージ キューなど) を通じて通信し、データを共有できます。同時に、オペレーティング システムは、プロセスの作成、プロセスの破棄、プロセスの終了の待機など、プロセス管理のためのいくつかのシステム コールも提供します。

ここに画像の説明を挿入します
ここに画像の説明を挿入します
ステータスの詳細:

  • 実行状態---プロセスが CPU に割り当てられ、実行を開始すると、実行状態になります。このとき、プロセスは CPU リソースを使用して命令を実行しており、CPU タイム スライスをめぐって他のプロセスと競合する可能性があります。実行状態では、プロセスは、ファイルの読み取りと書き込み、ネットワーク要求の送信など、ユーザー モードまたはカーネル モードで実行できるあらゆる操作を実行できます。
  • ブロック状態-----プロセスが特定のイベントの発生を待機しており、実行を続行できない場合、プロセスはブロック状態になります。たとえば、プロセスが IO 操作の完了を待っているとき、ロックまたはセマフォを待っているとき、別のプロセスがメッセージを送信するのを待っているときなど、プロセスはブロッキング状態になります。ブロッキング状態では、プロセスは CPU リソースを使用せず、CPU タイム スライスをめぐって競合しません。
  • 終了状態------プロセスが実行タスクを完了して終了すると、プロセスは終了状態になります。この状態では、プロセスは、CPU 時間、メモリ、ファイル記述、ロックなど、使用しているすべてのリソースを解放します。終了したプロセスの情報は、親プロセスまたは他のプロセスのプロセス テーブルに残され、プロセスの終了ステータス情報を表示します。これら 3 つの状態は、
    オペレーティング システムにおけるプロセスの基本的な状態であり、それらの変換と管理はオペレーティング システムのプロセスであり、スケジューリングとリソース管理の重要な部分です。

プロセスの 2 つの抽象化

  • 専用 CPU とレジスタ
    プロセスの 2 つの抽象概念は、オペレーティング システムにおけるプロセスの 2 つの最も基本的な抽象概念、つまりプロセスの実行ステータスとプロセスのアドレス空間を指します。
    最初の抽象化は、プロセスが実行時に CPU とレジスタを排他的に占有し、独立した実行ユニットとみなすことができることを意味します。プロセスの実行中は CPU のタイムスライスを占有し、CPU を切り替えることでマルチタスクの同時実行を実現できます。プロセスの実行中、CPU とレジスタはプロセスによって排他的に占有され、プロセスはこれらのリソースを使用して独自のコードを実行したり、独自のデータを読み書きしたりできます。

  • 独自のアドレス空間を持つ
    2 番目の抽象化は、各プロセスがコード、データ、スタックなどを含む独自のアドレス空間 (仮想アドレス空間とも呼ばれる) を持つことです。各プロセスは実行時に独自のコード、データ、スタックなどの内容を持ち、各プロセスのアドレス空間は互いに独立しており、相互に干渉しません。オペレーティング システムは、仮想メモリ管理メカニズムを使用してアドレス空間を割り当て、保護します。これにより、各プロセスが独立して実行できるようになり、システムのセキュリティと安定性が確保されます。

プロセスのアドレス空間は仮想です。つまり、プロセスは物理メモリ全体に排他的にアクセスできると考えていますが、実際には、オペレーティング システムによって割り当てられたメモリの一部にしかアクセスできません。これは、最新のオペレーティング システムでは、物理メモリを同じサイズの多数のページに分割する仮想メモリ テクノロジが採用されているためです。各ページには、仮想アドレスと物理アドレスがあります。オペレーティング システムは、マッピング テーブルを通じて仮想アドレスと物理アドレスの関係を管理します。対応関係。各プロセスには独自のマッピング テーブルがあるため、プロセスはメモリにアクセスするときに自分のアドレス空間にのみアクセスできます。

プロセスのアドレス空間には通常、次の部分が含まれます。
1. コード セグメント: プログラムの実行可能コードを格納します。
2. データ セグメント: プログラムで定義されたグローバル変数と静的変数を格納します。
3. ヒープ: 動的に割り当てられたメモリ (malloc 関数を使用して割り当てられたメモリなど) を格納します。
4. スタック: 関数呼び出し構成内のローカル変数と関数パラメータを格納します。

プロセスを表示するための top コマンド
Linux システムでは、top コマンドを使用して、システム内のプロセスのステータスとリソースの使用状況をリアルタイムで監視できます。このコマンドは、CPU 使用率、メモリ使用量、および各プロセスの実行時間を表示する対話型ターミナル インターフェイスを提供します。プロセスやその他の情報。

top コマンドを使用するには、ターミナルに「top」と入力して Enter キーを押します。top コマンドのインターフェースでは、システム内で実行中のすべてのプロセスのリストが、各プロセスが 1 行で表示されます。各行にはプロセスのPID(プロセス識別子)、プロセスが属するユーザー、プロセスのCPU使用率、メモリ使用量などの情報が表示されます。

上部のコマンド インターフェイスでは、いくつかのショートカット キーを使用して、
P: CPU 使用率による並べ替え、
M: メモリ使用量による並べ替え、
k: プロセスの強制終了、
H: スレッド情報の表示などの操作を実行できます。

さらに、top -p コマンドを使用して、指定した PID のプロセス情報を表示することもできます。たとえば、PID 123 のプロセスに関する情報を表示するには、ターミナルに「top -p 123」と入力します。

マルチプロセス管理の共有、コンテキストの切り替えはアドレス空間とレジスタの変更です。
オペレーティング システムでは、複数のプロセスが同時に実行されることがあります。これらのプロセスは、メモリやファイルなどの一部のリソースを共有する必要がある場合があります。セキュリティとプロセス間の独立性を確保するために、オペレーティング システムはマルチプロセス管理メカニズムを採用しています。

マルチプロセス管理メカニズムでは、各プロセスは独自の独立したアドレス空間とレジスタを持ちます. アドレス空間とは、コード\データ\スタックなどを含むプロセスが使用できるメモリ空間を指します. レジスタはCPU内部のレジスタです. used CPU が命令を実行するために必要なデータを保存するために使用されます。

オペレーティング システムがプロセスを切り替える必要がある場合、現在のプロセスのアドレス空間とレジスタ ステータスを保存します。次に、新しいプロセスのアドレス空間とレジスタ ステータスをロードします。このプロセスはコンテキスト スイッチングと呼ばれます。コンテキスト スイッチングでは、オペレーティング システムが
(1) 現在のプロセスの状態を保存する。オペレーティング システムは、後で現在のプロセスの実行状態に復元できるように、現在のアドレス空間とレジスタの状態を保存する必要があります。(2 )
新しいプロセスの状態をロードします。CPU が新しいプロセスのコードの実行を開始できるように、オペレーティング システムは新しいプロセスのアドレス空間とレジスタ状態をロードする必要があります。

マルチプロセス管理機構では、異なるプロセス間のアドレス空間やレジスタの状態が独立しているため、プロセス間のデータが干渉することがなく、システムのセキュリティと安定性が向上します。コンテキストスイッチング 大量の状態情報を保存および復元する必要があるため、一定のパフォーマンスのオーバーヘッドが生じます。

コンテキスト スイッチングのプロセス
コンテキスト スイッチングとは、オペレーティング システム内の複数のプロセスまたはスレッドによる CPU リソースの共有を指します。プロセスまたはスレッドが CPU リソースを放棄する必要がある場合、オペレーティング システムは現在のプロセスまたはスレッドのコンテキスト情報を保存し、次のプロセスまたはスレッドのコンテキスト情報に切り替えてから、CPU を再起動して新しいプロセスを実行する必要があります。またはスレッド。

コンテキスト切り替えのプロセスには通常、次の手順が含まれます。
(1) 現在のプロセスまたはスレッドは、I/O 操作などの外部イベントの発生を待つ必要がある操作を実行するか、時間の終わりに操作を実行します。スライスのため、CPU リソースを放棄する必要があるため、システムはコンテキスト スイッチをトリガーします。
(2) オペレーティング システムは、CPU レジスタ、プログラム カウンター、スタック ポインタなどの値を含む、現在のプロセスまたはスレッドのコンテキスト情報を保存します。この情報は、プロセスまたはスレッドの制御ブロック (PCB/TCB) に保存されます。オペレーティング システム カーネルの。
(3) オペレーティング システムは、プロセスまたはスレッドのスケジューリング アルゴリズムに基づいて、次に実行するプロセスまたはスレッドを選択し、そのコンテキスト情報を CPU にロードします。
(4) オペレーティング システムは CPU 制御を次のプロセスまたはスレッドに渡し、CPU は新しいプロセスまたはスレッドの実行を開始します。
(5) 以前に一時停止されたプロセスまたはスレッドがまだ準備完了状態にある場合は、実行可能キューに再度追加され、次のスケジューリングを待ちます。
  プロセスまたはスレッド コンテキストの保存と復元のプロセスにはメモリの読み書きと CPU レジスタ操作が必要であり、時間と CPU リソースを消費するため、コンテキストの切り替えは非常にリソースを消費する操作です。したがって、頻繁な切り替えが必要なアプリケーションの場合、コンテキスト切り替えがボトルネックとなり、システムのパフォーマンスに影響を与える可能性があります。

** プロセス制御関数、getpid、getppid:** Unix/Linux では、getpid 関数を使用して現在のプロセスのプロセス ID を取得し、getppid 関数を使用して現在のプロセスの親プロセスのプロセス ID を取得できます。 。これら 2 つの機能はプロセス制御機能の基本コンポーネントの 1 つであり、プロセスの識別と制御に役立ちます。

論理制御フロー

論理制御フローとは、プログラムの実行時に従う順序とプロセスを指します。プログラムには複数のステートメントと操作が存在する場合があり、これらのステートメントと操作の間の関係と順序がプログラムの論理制御フローを構成します。プログラムの論理的な制御フローは、通常、以下の構造から構成されます。
(1) シーケンシャル構造: プログラムは、分岐やループを持たずに、ステートメントの順序でシーケンシャルに実行されます。これは最も単純なプログラム構造であり、すべてのプログラム構造の基礎です。
(2) 分岐構造: プログラムは条件に応じて異なるステートメントを実行します。分岐構造は通常、if ステートメントと switch ステートメントによって実装されます。
(3) ループ構造: プログラムはループ条件に基づいてコードの一部を繰り返し実行します。ループ構造は通常、while ステートメント、for ステートメント、および do-while ステートメントを使用して実装されます。
(4) ジャンプ構造: プログラムは任意の場所で別の場所にジャンプして実行を継続できます。ジャンプ構造は通常 goto ステートメントによって実装されますが、実際のプログラミングではその使用は推奨されません。
これらのプログラム構造を組み合わせて、より複雑なプログラム フローを形成できます。たとえば、ループ構造を分岐構造内にネストしたり、ループ構造内で分岐構造を使用したりできます。実際のプログラミングでは、プログラム構造の適切な組み合わせを選択することで、プログラムの効率と可読性を向上させることができます。

同時

同時実行性とは、複数のタスクまたはプロセスが同時に実行されることを意味します。オペレーティング システムでは、多くの場合、複数のタスクまたはプロセスを同時に処理する必要があり、これには同時メモリの使用が必要です。

同時実行テクノロジには、マルチプロセス、マルチスレッド、非同期プログラミングなどのステップが含まれます。マルチプロセスとは、オペレーティング システムで複数の独立したプロセスを同時に実行することを指し、各プロセスは独自のアドレス空間とレジスタ状態を持ちます。マルチスレッドとは、1 つのプロセス内で複数の独立したプロセスを同時に実行することを意味し、各スレッドは独自のスタックとレジスタ状態を持ちますが、プロセスのアドレス空間を共有します。非同期プログラミングとは、非同期コールバック、イベント ループ、およびその他のテクノロジを使用して、応答速度とリソース使用率を確保しながら単一スレッドで複数のタスクを処理することを指します。

同時実行テクノロジの利点: システムの応答速度とリソース使用率が向上し、複数のタスクを同時に実行できるため、システムの効率と信頼性が向上します。ただし、同時実行テクノロジは、スレッドの安全性の問題、デッドロックの問題、競合状態などのいくつかの問題も引き起こします。これらの問題を回避するには、ロック メカニズムの使用や共有データの回避など、いくつかのベスト プラクティスと同時プログラミングの設計パターンを使用する必要があります。

ユーザーモードとカーネルモード

ユーザー モードとカーネル モードは、オペレーティング システムの 2 つの異なる実行モデルです。

プログラムがユーザー モードで実行される場合、プログラムは、プロセス アドレス空間内のプログラム自体など、制限されたリソースにのみアクセスできますが、ハードウェア デバイスやカーネル コードなどのシステム リソースには直接アクセスできません。プログラムがシステム リソースにアクセスする必要がある場合、システム コールを通じてカーネル モードに入る必要があり、カーネルは関連する操作を完了します。ユーザー モードは、アプリケーション エラーや悪意のあるコードがシステムに損害を与えることを防ぐセキュリティ メカニズムです。

カーネル モードは、オペレーティング システムがシステム リソースを完全に制御できるため、特権モードまたはシステム モードとも呼ばれます。カーネル モードのコードは、すべてのハードウェア デバイスとメモリ空間に直接アクセスし、特権命令を実行できます。割り込みベクタテーブルの設定やCPUステータスの操作など。カーネル モードのコードはシステム リソースにアクセスできるため、カーネル コードの正確性とセキュリティを確保する必要があります。

プロセスがシステム コールを開始すると、ユーザー モードからカーネル モードに切り替わり、システムは対応する操作をカーネル モードで実行し、結果をユーザー モード プロセスに返します。このプロセスはコンテキスト切り替えと呼ばれ、オペレーティング システムの一般的なメカニズムでもあります。コンテキストの切り替えには、CPU 状態の切り替えとメモリ アクセスの切り替えが含まれるためです。したがって、そのオーバーヘッドは比較的大きく、システムのパフォーマンスを向上させるにはコンテキスト スイッチの数を可能な限り減らす必要があります。

状態遷移における割り込みの役割:
アプリケーションがユーザー空間で実行される場合、アプリケーションは独自のメモリ空間と CPU によって提供される限られたリソースのみにアクセスできます。アプリケーションがデバイス ドライバーやシステム サービスなどのカーネル空間リソースにアクセスする必要がある場合、システム コールを通じてカーネル空間に入る必要があります。これは、カーネル空間にはすべてのシステム リソースとハードウェア デバイスが含まれており、より高いアクセス許可と特権が付与されているためです。

最新のオペレーティング システムでは、ユーザー空間とカーネル空間は完全に分離されており、相互のメモリ空間に直接アクセスすることはできません。したがって、アプリケーションがカーネル空間にアクセスする必要がある場合は、割り込み操作を使用する必要があります。割り込みは、CPU がハードウェア イベントまたはソフトウェア リクエストに応答して命令を実行している間に、現在のタスクを一時停止するメカニズムです。アプリケーションがシステム コールを呼び出すと、割り込みがトリガーされ、CPU の制御がカーネル空間に移されます。カーネルは必要な操作を実行し、結果をアプリケーションに返し、制御をアプリケーションに戻します。

したがって、操作を中断することで、アプリケーションはシステムのセキュリティと安定性を損なうことなくカーネル空間に安全にアクセスできます。同時に、カーネルはアプリケーション要求のセキュリティ検証と制御を実行して、システムのセキュリティと安定性を確保できます。

システムコールのエラー処理

オペレーティング システムでは、アプリケーションがシステム コールを開始するときにエラーが発生することがあります。エラーは、アプリケーション自体のエラーまたはオペレーティング システムの問題によって発生する可能性があり、通常、オペレーティング システムはエラー コードを返します。システムコールでどのような問題が発生したかをアプリケーションに通知します。アプリケーションは、エラー状態に適切に応答するために、これらのエラーを正しく処理する必要があります。

システムコールのエラー処理は通常以下の手順で行われます。
(1) システムコールの戻り値を確認する。システムコールは通常、操作の結果を表す整数値またはエラーコードを返します。アプリケーションは、この戻り値をチェックして、操作が成功したかどうかを判断する必要があります。
(2) システムコールがエラーコードを返した場合、アプリケーションはエラーの種類に応じた適切な対応を行う必要があります。たとえば、ファイルを開くのに失敗した場合、アプリケーションはファイルを開くことを再試行するか、ユーザーにエラー メッセージを表示するかを選択できます。
(3) アプリケーションは、コードに適切なエラー処理コードを追加する必要があります。たとえば、ファイルを開くのに失敗した場合、アプリケーションは、エラーが発生したときにリソースが解放されるように、開いているファイル記述子を閉じるコードを追加する必要があります。
(4) アプリケーションは、後でエラーのトラブルシューティングに使用できるように、エラー メッセージをログ ファイルに記録することを選択できます。
システム コール エラーを処理する場合、アプリケーションは、アプリケーションの正確性と安定性を確保するために、エラーの種類と処理方法を慎重に検討する必要があります。

エラー報告機能(unix_error)

unix_error はエラー報告関数であり、通常は UNIX システム コールのエラーを処理するために使用されます。これは、CSAPP の場合は csapp.c ファイルにあり、その主な機能は、UNIX システム コール エラーを人間が判読できるエラー メッセージに変換し、そのエラー メッセージを標準エラー ストリーム (stderr) に出力することです。その実装は次のとおりです。

void unix_error(char *msg) /* UNIX-style error */
{
    fprintf(stderr, "%s: %s\n", msg, strerror(errno));
    exit(0);
}

この関数は、エラー メッセージのプレフィックスを表す文字列パラメータ msg を受け取ります。関数 strerror を使用してエラー コード errno をエラー メッセージに変換し、プレフィックスとエラー メッセージを標準エラー ストリームに出力します。最後に、exit 関数を呼び出してプログラムを終了します。

たとえば、ファイルを開くために open 関数を呼び出しているときにエラーが発生した場合、unix_error 関数を使用してエラー メッセージを報告できます。

int fd = open("nonexistent_file", O_RDONLY);
if (fd == -1) {
    unix_error("open error");
}

上記のコードでは、ファイルを開くのに失敗した場合、unix_error 関数は次のエラー メッセージを出力します。

open error: No such file or directory

unix_error 関数を使用すると、UNIX システム コールのエラーを簡単に処理し、より優れたエラー レポート機能を提供できます。

エラー処理ラッパー関数 (Fork)

エラー処理はオペレーティング システムにおいて非常に重要な問題です。エラー処理を容易にするために、エラー処理ラッパー関数を使用してシステム コールをラップし、それらが成功したかどうかを確認できます。これらのラッパー関数はシステム コール エラーを戻り値に変換するため、アプリケーションはエラーをチェックして適切なアクションを実行できます。
以下は、fork システム コールをカプセル化するエラー処理ラッパー関数 Fork の例です。

pid_t Fork(void) 
{
    pid_t pid;

    if ((pid = fork()) < 0)
        unix_error("Fork error");
    return pid;
}

この関数は、子プロセスのプロセス ID を返します。fork 関数の呼び出しが失敗した場合、関数はエラー メッセージを出力し、プログラムを終了します。
Fork 関数を使用すると、エラーが発生すると例外がスローされ、プログラムが終了します。例えば:

pid_t pid = Fork();
if (pid == 0) {  // Child process
    // ...
} else if (pid > 0) {  // Parent process
    // ...
} else {  // Error
    printf("Fork error\n");
    exit(1);
}

上記のコードでは、Fork 関数の呼び出しが失敗すると、プログラムはエラー メッセージを出力して終了します。それ以外の場合は、親プロセスと子プロセスでそれぞれ異なる操作が実行されます。

Fork は子プロセスを作成し、2 つのフォークの例を返します。Unix
/Linux では、fork() は子プロセスを作成するために使用されるシステム コールです。fork() 関数を呼び出した後、元のプロセスの子プロセスである新しいプロセスが作成されます。この子プロセスは親プロセスと同じコードとデータを持ちますが、異なるプロセス ID とシステム リソース (ファイル記述子、メモリ マップ、タイマーなど) を持ちます。fork() 関数のプロトタイプは次のとおりです。

#include <unistd.h>
pid_t fork(void);

このうち、pid_t はプロセスの ID を表す整数データ型です。fork() 関数は 2 回戻ります。親プロセスでは新しい子プロセスの PID を返し、子プロセスでは 0 を返します。fork() 関数の呼び出しが失敗すると、負の数が返されます。
以下は、fork() 関数を使用して子プロセスを作成する方法を示す簡単なサンプル プログラムです。

#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();  // 创建子进程

    if (pid < 0) {  // 创建进程失败
        printf("fork error\n");
    } else if (pid == 0) {  // 子进程
        printf("child process: pid=%d\n", getpid());
    } else {  // 父进程
        printf("parent process: child pid=%d\n", pid);
    }

    return 0;
}

このサンプルプログラムでは、まず fork() 関数を呼び出して子プロセスを作成します。fork() 関数が子プロセスの作成に成功すると、子プロセスに "child process: pid=XXX" が出力されます (XXX は子プロセスのプロセス ID)、"parent process: child pid=XXX" が子プロセスに出力されます。親プロセス。XXX は新しく作成された子プロセスのプロセス ID です。親プロセスと子プロセスは同じコードを実行しますが、実行順序と出力は異なる場合があることに注意してください。

fork() 関数は、開いているすべてのファイル記述子、信号処理関数、仮想メモリなどを含む親プロセスのメモリ イメージをコピーすることに注意してください。したがって、変数やデータ構造は親プロセスと子プロセス間で共有されません。親プロセスと子プロセス間でデータを共有する必要がある場合は、パイプ、共有メモリ、メッセージ キューなどのプロセス間通信 (IPC) メカニズムを使用できます。

fork が呼び出されたときに
何が起こるかをキャプチャする fork が呼び出されたときに何が起こるかをキャプチャするために使用されます。頂点はステートメントの実行を表し、エッジは変数に対応します。このプロセス グラフは、最初にメッセージを出力し、次に
ここに画像の説明を挿入します
fork を呼び出すプログラムを記述します。子プロセスを作成します。子プロセスでは、プログラムはメッセージを出力し、exit() を呼び出して子プロセスを終了します。親プロセスでは、プログラムはメッセージを出力し、その後実行を継続し、最後にメッセージを出力します。

このプロセス グラフでは、頂点はプログラム内のさまざまなステートメントを表し、エッジは制御フローを表します。特に、エッジ上のラベルは、そのエッジに関連付けられた変数または条件を表します。この例では、pid は fork によって返された値を格納する変数です。pid が 0 に等しい場合は、子プロセスが現在実行されていることを意味し、それ以外の場合は、親プロセスが現在実行されていることを意味します。

プロセス図の例

以下は、メッセージを出力してユーザー入力を待つ単純なプロセスを示すプロセス図の例です。ユーザーが文字列を入力すると、プロセスはその文字列を出力して終了します。
ここに画像の説明を挿入します
このプロセス グラフでは、頂点はプログラム内のさまざまなステートメントを表し、エッジは制御フローを表します。この例では、プログラムは最初にメッセージを出力し、次にユーザー入力を待ちます。入力が完了すると、プログラムは入力文字列をbufferという名前の変数に保存し、文字列を出力します。最後に、プログラムは exit() を呼び出してプロセスを終了します。

プロセス図内の矢印は、プロセスの制御フローを表します。この例は単純ですが、プロセス図を使用してプログラムの制御フローを記述できることを示しています。プログラムをプロセス グラフに変換することで、プログラムの実行をより深く理解し、その動作をより適切に分析できるようになります。

考える?

すべてのプロセスには子プロセスと親プロセスがありますか?
はい、すべてのプロセスには親プロセスがあり、1 つ以上の子プロセスがある場合があります。プロセスが fork() 関数を呼び出すと、新しいプロセスが作成されます。子スレッド、およびこれ子スレッドは現在のプロセスのコピーになります。この子プロセスは親プロセスと同じコードとデータを持ちますが、別々のアドレス空間を持ちます。

子プロセスは、ファイル記述子、ユーザー ID、グループ ID などの一部の属性を親プロセスから継承します。子プロセスと親プロセスの作成時の状態は多少異なりますが、最も重要なことは、子プロセスは親プロセスの実行コンテキストを継承することです。つまり、子プロセスが実行を開始すると、親プロセスの現在の命令から実行を開始し、その後独自の実行パスを継続します。

子プロセスが正常に作成されると、親プロセスと子プロセスが並行して実行を開始します。これらは独立した実行コンテキストを持っているため、それらの動作は互いに独立しています。子プロセスが完了すると、親プロセスにシグナルを送信して、親プロセスが終了したことを伝えます。親プロセスは、wait() や waitpid() などの関数を使用して、子プロセスが終了するのを待ってそのステータスを確認することも、子プロセスの終了信号を直接無視することもできます。

要約すると、すべてのプロセスには親プロセスがあり、場合によっては 1 つ以上の子プロセスがあります。これらのプロセスは、作成時に多少異なる状態になる可能性がありますが、互いに独立しており、システム上で並行して実行できます。

待機の仕組みの簡単な例

親プロセスと 2 つの子プロセスがあるとすると、子プロセス 1 は 2 秒間スリープし、子プロセス 2 は 3 秒間スリープしてから終了します。親プロセスは、両方の子プロセスが終了するまで待ってから終了する必要があります。この処理は wait() 関数を使用して実装できます。サンプルコードは次のとおりです。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    pid_t pid1, pid2;
    int status1, status2;

    pid1 = fork();
    if (pid1 == 0) {
        // 子进程1
        sleep(2);
        printf("Child process 1 is exiting.\n");
        exit(0);
    } else {
        pid2 = fork();
        if (pid2 == 0) {
            // 子进程2
            sleep(3);
            printf("Child process 2 is exiting.\n");
            exit(0);
        } else {
            // 父进程
            wait(&status1);
            printf("Child process 1 has exited with status %d.\n", status1);
            wait(&status2);
            printf("Child process 2 has exited with status %d.\n", status2);
            printf("Parent process is exiting.\n");
            exit(0);
        }
    }

    return 0;
}

この例では、親プロセスはまず 2 つの子プロセスを forks() して取り出し、次に wait() 関数を使用して子プロセスが終了してリソースを再利用するのを待ちます。子プロセスが終了すると親プロセスにシグナルを送り、親プロセスはwait()関数でシグナルを捕捉し、子プロセスから子プロセスの終了ステータスを取得します。

このプログラムを実行すると、次の出力が表示されます。
ここに画像の説明を挿入します

wait() 関数
wait() 関数は、子プロセスが終了するのを待ち、そのリソースをリサイクルするために使用されます。子プロセスが終了しても、すぐにシステムから消えるわけではなく、「ゾンビ プロセス」になります。プロセス (メモリ、ファイルの説明、ステータスなどを含む) は引き続き占有されますが、いかなる操作も実行できなくなります。

親プロセスは wait() 関数を使用して、子プロセスが終了するのを待ち、子プロセスのリソースをリサイクルすることで、子プロセスをシステムから完全に削除できます。 、子プロセスのリソースはリサイクルされず、システム リソースを占有し続けます。

親プロセスが wait() 関数を呼び出すと、親プロセスはブロックされ、子プロセスが終了するまで待機します。複数の子プロセスが同時に終了した場合、wait() 関数は子プロセスの 1 つのステータスを返し、子プロセスをリサイクルします。子プロセスのリソース 親プロセスが子プロセスの終了を待っていない場合、wait() 関数は子プロセスが終了するまでブロックされます。

要約すると、wait() 関数は、子プロセスが終了してリソースを再利用するのを待つために使用されます。親プロセスが子プロセスのリソースをリサイクルしない場合、子プロセスはゾンビ プロセスとなり、システム リソースを占有します。

waitpid 関数:
waitpid 関数は、指定された子プロセスが終了し、そのリソースをリサイクルするのを待機できます。これには 3 つのパラメータがあります:
pid: 待機する子プロセスのプロセス ID。-1 の場合は、子プロセスが終了するまで待機します。終わり。
status: 子プロセスの終了ステータスを保存するためのポインタ。
オプション: オプションパラメータ。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main()
{
    pid_t pid1, pid2;
    int status1, status2;

    pid1 = fork();
    if (pid1 < 0) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid1 == 0) {
        // 子进程1
        printf("I am child process 1. My pid is %d.\n", getpid());
        sleep(2);
        exit(1);
    } else {
        pid2 = fork();
        if (pid2 < 0) {
            perror("fork");
            exit(EXIT_FAILURE);
        } else if (pid2 == 0) {
            // 子进程2
            printf("I am child process 2. My pid is %d.\n", getpid());
            sleep(4);
            exit(2);
        } else {
            // 父进程
            printf("I am parent process. My pid is %d.\n", getpid());
            printf("Waiting for child process %d and %d...\n", pid1, pid2);

            // 等待子进程1结束
            waitpid(pid1, &status1, 0);
            if (WIFEXITED(status1)) {
                printf("Child process %d terminated with exit status %d.\n", pid1, WEXITSTATUS(status1));
            } else {
                printf("Child process %d terminated abnormally.\n", pid1);
            }

            // 等待子进程2结束
            waitpid(pid2, &status2, 0);
            if (WIFEXITED(status2)) {
                printf("Child process %d terminated with exit status %d.\n", pid2, WEXITSTATUS(status2));
            } else {
                printf("Child process %d terminated abnormally.\n", pid2);
            }
        }
    }

    return 0;
}

プログラムは 2 つの子プロセスを作成し、それぞれ 2 秒と 4 秒待ってから終了します。親プロセスは、waitpid 関数を使用して子プロセスが終了するのを待ち、子プロセスの終了ステータスを出力します。出力は次のとおりです。
ここに画像の説明を挿入します
親プロセスは、まず子プロセス 1 が終了するまで待機し、次に子プロセス 2 が終了するまで待機していることがわかります。子プロセスの終了ステータスはそれぞれ 1 と 2 です。

execve はさまざまなプログラムを実行します。execve
() は、新しいプログラムを開始して実行するために使用できる Linux/Unix システム コールで、現在のプロセスのコードとデータが新しいプログラムのコードとデータに置き換えられます。さまざまなプログラムの機能を実現します
execve は 3 つのパラメータを受け取ります:
(1) path: 実行するプログラムのパスと名前を指定します。
(2)argv: プログラムのコマンドラインパラメータを含む文字列配列です。argv の最初の要素は通常プログラムの名前で、後続の要素はプログラムのパラメータです。
(3)envp: プログラムの環境変数を含む文字列配列です。

execve はまず path で指定されたパスから実行するプログラム ファイルを検索します。該当するファイルが見つかった場合は、現在のプロセスをそのファイルに置き換えて新しいプログラムを開始します。対応するファイルが見つからない場合は、execve は実行します。関数呼び出しが失敗し、-1 が返されます。
例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    char *argv[] = {"/bin/ls", "-l", "/tmp", NULL};  // 要执行的程序和参数
    char *envp[] = {NULL};  // 环境变量
    if (execve("/bin/ls", argv, envp) == -1) {
        printf("execve failed\n");
        exit(1);
    }
    return 0;
}

execve 関数の呼び出しが成功すると、現在のプロセスが新しいプログラムに置き換えられるため、execve 以降のコードは実行されないことに注意してください。新しいプログラムの実行後に元のプログラムのコードを実行し続けたい場合は、新しいプログラムで exit 関数を呼び出してプログラムを終了し、元のプログラムで fork や waitpid などのシステム コールを使用して待機することができます。新しいプログラムの実行結果。

新しいプログラムはスタックフレーム構造を開始します

新しいプログラムが開始されると、そのスタック フレーム構造には、新しいプログラムのエントリ ポイント、コマンド ライン パラメーター、環境変数などの情報が含まれます。具体的には、スタック フレーム構造は通常、次の部分で構成されます: argc: 数値を示す
整数プログラムが受け取るコマンドラインパラメータの数。
argv: プログラムが受け取るコマンドライン引数の文字列を指すポインタの配列。

int main(int argc, char *argv[]);

envp: プログラムで使用される環境変数を指すポインターの配列。
戻りアドレス: プログラムのエントリポイントを指すアドレス。
ローカル変数: プログラムは実行中にいくつかのローカル変数を作成する場合があり、これらの変数はスタック フレーム構造に格納されます。
この情報は、新しいプログラムのスタック フレーム構造に順次格納されます。この情報は、プログラムの実行中にアクセスして変更できます。一般に、オペレーティング システムは、新しいプログラムに適切な実行環境を提供するために、新しいプログラムのスタック フレーム構造を作成および初期化する責任を負います。

ゾンビプロセス

ゾンビ プロセスとは、実行は完了した (終了した) プロセスを指しますが、その親プロセスが終了ステータスを取得するために wait() や waitpid() などのシステム コールをまだ呼び出していないため、プロセスのプロセス記述子がシステム内に存在しますが、操作を実行できず、スケジュールすることもできません。

Linux システムでは、ゾンビ プロセスのプロセス ステータスには Z または Z+ のマークが付けられ、ps コマンドを実行すると表示できます。ゾンビ プロセスはあまり多くのシステム リソースを占有しませんが、親プロセスが wait() やその他のシステム コールを呼び出してプロセスのリソースをリサイクルしない場合、それらは徐々に蓄積され、最終的にはシステム リソースの浪費につながります。

ゾンビプロセスの生成を避けるため、親プロセスは子プロセス作成後、速やかにwait()やwaitpid()などのシステムコールを呼び出し、子プロセスの終了ステータスを取得する必要があります。親プロセスがこれらのシステム コールを時間内に呼び出すことができない場合は、シグナル ハンドラーの使用またはデーモン プロセスの使用を検討して問題を解決できます。

システムは、初期プロセスが孤立したプロセスをリサイクルするように調整します。

プロセスが終了する前にその親プロセスが終了すると、そのプロセスは孤立プロセス (Orphan Process)、つまり親プロセスのないプロセスになります。Linux システムでは、これらの孤立プロセスはシステムによってプロセス番号 1 の init プロセスに新しい親プロセスとして自動的に割り当てられるため、孤立プロセスの存在が回避されます。

init プロセスは Linux システムの最初のプロセスであり、他のプロセスを開始し、その実行ステータスを監視する役割を果たします。プロセスが孤立プロセスになると、システムによってその親プロセスとして init プロセスに再割り当てされます。このようにして、init プロセスが wait() や waitpid() などのシステム コールを呼び出すと、これらの孤立したプロセスのリソースを取得してリサイクルできるため、システム リソースの無駄が回避されます。

init プロセスは孤立したプロセスのリサイクルのみを担当することに注意してください。ゾンビ プロセスの場合、その親プロセスはリサイクルのために wait() やその他のシステム コールを呼び出す必要があります。そうしないと、ゾンビ プロセスが常に存在することになり、無駄が発生します。システムリソース。
例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid;
    pid = fork();
    if (pid < 0) {
        printf("Fork Failed\n");
        exit(1);
    }
    else if (pid == 0) {
        printf("Child process\n");
        while (1) {
            sleep(1);  // 子进程不退出,一直在运行
        }
    }
    else {
        printf("Parent process\n");
        exit(0);  // 父进程退出
    }
    return 0;
}

このプログラムでは、親プロセスが子プロセスを作成し、作成後すぐに終了しますが、子プロセスはまだ実行中です。このプログラムを実行すると、以下に示すように ps コマンドを実行してプロセスのステータスを表示できます。
ここに画像の説明を挿入します

ゾンビ現象の例:
親プロセスと子プロセスがあるとします。親プロセスは子プロセスを作成し、子プロセスが終了するのを待ちます。そのプロセス記述子はまだシステム内に存在しますが、親プロセスはまだ呼び出していません。 wait() または waitpid() などのシステムコールで終了ステータスを取得します。この時点で、子プロセスはゾンビプロセスになります。

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    pid_t pid;
    pid = fork();
    if (pid < 0) {
        printf("Fork Failed\n");
        exit(1);
    }
    else if (pid == 0) {
        printf("Child process\n");
        exit(0);
    }
    else {
        printf("Parent process\n");
        sleep(2);  // 等待子进程结束
        // 父进程未调用wait()或waitpid()等系统调用获取子进程终止状态
        printf("Parent process exit without calling wait()\n");
    }
    return 0;
}

このプログラムでは、親プロセスが子プロセスを作成し、子プロセスが終了するのを待ちます。ただし、親プロセスでは子プロセスの終了ステータスを取得するための wait() や waitpid() などのシステムコールが呼び出されないため、子プロセスがゾンビプロセスになってしまいます。このプログラムを実行すると、以下のように ps コマンドを実行してプロセスのステータスを表示できます。
ここに画像の説明を挿入します
プロセスのステータスが Z または Z+ とマークされており、ゾンビ プロセスであることがわかります。この場合、親プロセスは、wait() や waitpid() などのシステム コールを呼び出して子プロセスの終了ステータスを取得し、そのリソースを再利用する必要があります。

おすすめ

転載: blog.csdn.net/m0_56898461/article/details/129941185