【Linux】プロセス制御の基礎知識

目次

1. ファックレビュー 

2. プロセスの終了

1. プロセスが終了すると、オペレーティング システムは何をしますか?

2. プロセスの終了、一般的な方法

1. メイン関数、リターン + リターンコード

2. exit() 関数

3. プロセス待ち 

1. リサイクル処理方法

(1.waitメソッド

追加の理解: ゾンビプロセスとメモリリークの違い

(2.waitpid関数

a、パラメータpid 

b、パラメータステータス

c、パラメータオプション

4. プロセスの置き換え

1. 概念と原則 

2. プロセス交換方法

3. fork + execl 関数を試してみる

その他追加された機能:

4. execl 関数を使用して他の実行可能プログラムを実行する方法

5. 演習 - 簡単なシェルコマンドインタープリタを作成する

1. フレームワークの作成:

2. 指示の受信と処理

3. 子プロセスの置換、親プロセスの待機

結論


1. ファックレビュー 

fork 機能は Linux において非常に重要な機能で、既存のプロセスから新しいプロセスを作成します。新しいプロセスは子プロセスであり、元のプロセスは親プロセスです。
#include <unistd.h>
pid_t フォーク(void);
戻り値: プロセスからは 0 が返され、親プロセスは子プロセス ID を返し、エラーの場合は -1 が返されます。
プロセスは fork を呼び出し、カーネル内の fork コードに制御が移された後、カーネルは次のことを行います。
  1. 新しいメモリ ブロックとカーネル データ構造を子プロセスに割り当てます。
  2. 親プロセスのデータ構造の内容の一部を子プロセスにコピーします。
  3. 子プロセスをシステム プロセス リストに追加します。
  4. フォークが戻り、スケジューラーのスケジューリングを開始します。

fork の理解についてはすでに説明したので、ここでは詳しく説明しません。 

前のアドレス空間の章 ( [Linux] アドレス空間の概念_華国山~~プログラマーのブログ-CSDN ブログ) では、子プロセスを作成するときにシステムがコピーオンライトの決定を採用することを最初に理解しました。システムがコピーを子プロセスに直接コピーしたらどうなるでしょうか? プロセス = カーネル データ構造 + コード & データであることがわかります。プロセスが作成されると、コードは読み取り専用状態になりますが、データは書き込み可能にできます。そこで、データのコピーを次の場所にコピーしてみませんか?子プロセス?システムではどのデータが使用されるかわからないため、めったに使用されないデータをコピーするとメモリ使用量が減少します。

では、なぜ OS は親プロセスと子プロセスを分離するためにコピーオンライト技術を選択するのでしょうか?

1. 使用する場合、子プロセスに割り当てると効率的なメモリ パフォーマンスが得られます。

2. システムがコードを実行するとき、どのメモリがアクセスされるかはわかりません。

データ変更の前後: 

結論: コンピュータ システムでは、親プロセスが子プロセスを作成すると、子プロセスは親プロセスのコードとデータを継承します。初期状態では、このコードとデータのアクセス許可は読み取り専用です。子プロセスがこれらのコードとデータを変更する必要がある場合、コピーオンライト操作が実行され、変更が必要なデータ部分のアクセス許可が読み取り専用から書き込み可能に変更されます。

2. プロセスの終了

1. プロセスが終了すると、オペレーティング システムは何をしますか?

プロセスが終了すると、オペレーティング システムはプロセスによって要求されたカーネル データ構造、コード、およびデータを解放します。これは本質的にメモリの解放です。

2. プロセスの終了、一般的な方法

1. メイン関数、リターン + リターンコード

C/C++ プログラムを作成して実行すると、次の状況が発生します。

 (a、コードは実行され、結果は正しいです。

   (b、コードの実行後の結果は正しくありません。

main 関数を作成すると、多くの場合 0 が返されます (常に 0 になるわけではありません)。これは、上位レベルの親プロセスに提供されるプロセス終了コードです。戻り値が 0 でない場合は、結果が次のとおりであることを意味します。正しくありません。そうでない場合は、通常どおり終了し、0 を返します。

補足: 最終プロセスの終了コード取得コマンドecho $?

それでは、main() 関数の戻り値にはどのような意味があるのでしょうか?

シェル スクリプトでプログラムを呼び出すとき、プログラムの main 関数の戻りコードをチェックすることで、プログラムが正常に実行されたかどうかを判断できます。戻りコードが 0 の場合は、プログラムの実行が成功したことを意味し、戻りコードが 0 以外の値の場合は、プログラムの実行が失敗したか、エラーが発生したことを意味します。このようにして、メイン関数の戻りコードに基づいて、対応するプロンプト情報の出力やエラー処理の実行など、後続の処理を実行できます (つまり、プログラムの実行が完了するまで後続の処理は行われません)。strerror() 関数でエラーの原因を調べます)(チャットポイントより)

 (c、コードが完了していないため、プログラムがクラッシュします。(信号セクションで説明します)

2. exit() 関数

コード内のどこかで exit を呼び出すと、プロセスが終了します。ここでシステムレベルのinterface_exit()を追加します。

次に、この 2 つの違いを実験してみましょう。

行バッファでは、改行文字を追加しない場合、印刷データは最初にバッファ領域に格納され、プロセスの終了後にディスプレイにフラッシュされます。

    int main()
  6 {
  7   cout << "lisan";
  8   sleep(3);
  9   exit(11); // _exit(11);                                     
 10   return 0;                    
 11 }                              
 12      

2 つの関数を試してください。_exit() 関数は、プロセスが終了するときに lisan を出力しません。その理由を次の図に示します。

_exit() はプログラムを直接終了するため、バッファ内のデータはフラッシュされません。では、バッファをどこに置くべきでしょうか? _exit() がオペレーティング システムのインターフェイスであり、exit() がライブラリ関数であることがわかっているため、バッファを管理するプログラムがオペレーティング システム上にあると大まかに推測できます。


3. プロセス待ち 

なぜ処理に待ち時間が必要なのでしょうか? 親プロセスが次のステップに進む前に、親プロセスはデータを取得し、子プロセスを作成し、子プロセスがデータを返すのを待つ必要があります。また、子プロセスが終了する場合、親プロセスが早期に終了すると、子プロセスがゾンビプロセスとなり、メモリリークが発生します。

要するに;

  1. 前に述べたように、子プロセスが終了し、親プロセスが無視されると、「ゾンビ プロセス」問題が発生し、メモリ リークが発生する可能性があります。
  2. さらに、プロセスがゾンビになると、プロセスは無敵になり、死んだプロセスを誰も強制終了できないため、「まばたきせずに強制終了」の kill -9 は役に立ちません。
  3. 最後に、親プロセスによって子プロセスに割り当てられたタスクがどの程度完了しているかを知る必要があります。たとえば、子プロセスが完了したかどうか、結果が正しいか正しくないか、または正常に終了したかどうかなどです。
  4. 親プロセスは子プロセスのリソースを再利用し、プロセス待機を通じて子プロセスの終了情報を取得します。

次の手順を実行します。

        int main()
  6 {
 12     pid_t pd = fork();
 14     if (pd < 0)
 15     {
 16       // 程序失败
 17       perror("fork ");
 18     }else if(pd == 0)
 19     {
 20       // 子进程
 21       int a = 5;
 22       while(a--)                                                   
 23       {
 24       printf("是子进程:getpid:%d,getppid:%d\n",getpid(), getppid()    );
 25       sleep(1);
 26       }      
 27     }else{
 28       // 父进程
 29       while(1)
 30       {
 31       printf("是父进程:getpid:%d,getppid:%d\n",getpid(), getppid()    );
 32       sleep(1);
 33       }   
 34     }
 35 }

 

では、プロセスをどのように受け取るのでしょうか? (親プロセスは早期に終了しますが、子プロセスはオペレーティング システムによって採用され、リサイクルされます。このアイデア: これはプログラミングのアイデアであり、将来学習します)

1. リサイクル処理方法

(1.waitメソッド

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* ステータス);
戻り値: 成功した場合は待機中のプロセスの PID を返し、 失敗した場合は -1を返します
パラメータ: 子プロセスの終了ステータスを取得するための出力パラメータ。気にしない場合は、 NULL に設定できます。

追加の理解: ゾンビプロセスとメモリリークの違い

子プロセスがゾンビ状態になると、そのコードとデータは解放されますが、その PCB (task_struct) のカーネル データ構造は保持されることがわかっており、オペレーティング システムがそれをリサイクルしないと、メモリ リークも発生します。 ; 私たちが作成したアプリケーションでは、new および malloc を通じてヒープ領域に適用したメモリは使用後に解放する必要があり、そうしないとメモリ リークが発生します。理解する:

前者はオペレーティング システム レベルであり、後者はインプロセスです。後者のプロセスは終了し、システムがメモリを再利用するため、メモリ リークは発生しません。前者では、オペレーティング システムはゾンビの PCB を処理しません。プロセスが完了し、メモリを再利用することはできなくなります。

(2.waitpid関数

pid_ t waitpid(pid_t pid, int* ステータス, int オプション);
戻り値:
正常に戻る場合、 waitpid は 収集された子プロセスのプロセス IDを返します
オプションWNOHANG が設定されており 呼び出し中に収集すべき終了した子プロセスが存在しないことが waitpid によって検出された場合は、0 が返されます
呼び出し中にエラーが発生した場合は 、 -1 が返され errno はエラーを示す対応する値が設定されます。 

a、パラメータpid 

Pid=-1、  子プロセスを待ちます。 wait と同等
Pid>0。プロセス IDがpidに等しい 子プロセスを待ちます

status についての知識 を追加すると、子プロセスの戻りコードを記録するために status が使用されることがわかっていますが、同時に、プログラムの終了時に 3 つの状況が発生することもわかっています。

これらのさまざまな状況をステータスの観点からどのように表現すればよいでしょうか?
 

b、パラメータステータス

wait と waitpid の両方に status パラメータがあります。これは出力パラメータであり、オペレーティング システムによって入力されます。
NULL が渡された場合は、子プロセスの終了ステータス情報は関係ないことを意味します。それ以外の場合、オペレーティング システムは、このパラメータに基づいて子プロセスの終了情報を親プロセスにフィードバックします。ステータスは単純に整数として扱うことはできませんが、ビットマップとして扱うことができます。具体的な詳細は次のとおりです (ステータスの下位 16 ビットのみが調査されます - リトルエンディアン マシン)
32、トップ15を研究します

 

 では、終了ステータスを取得するにはどうすればよいでしょうか? ?

(ステータス >> 8) & 0xff // 0xff -> 0000 0000....1111 1111 は最後の 8 ビットを保持します

これが正常終了ですが、異常終了の場合はどうなるのでしょうか?プロセスが異常終了したことはわかっていますが、実際、システムはプロセスを強制終了し、プロセスに強制終了シグナルを送信します。プロセスが異常終了すると、そのプロセスの戻りコードは意味を失います。

では、この信号をどうやって取得するのでしょうか?

(ステータス >> 7) & 0x7F // 0000... 111 1111 は最後の 7 ビットを保持します (注: ステータスが右にシフトされている場合、このビット操作は最後のビットに基づいて右にシフトされます)

プロセスの内部コードの問題だけでなく、kill -9 でプロセスが強制終了され、エラー メッセージが 9 になるなどの外部理由も原因で、プロセスが異常終了します。

ただし、ステータスの構成を理解した上でビット演算を行う必要があり、理解していても問題ありませんが、長い目で見ると不便になるため、利便性を考慮して以下のようにしています。

プロセスの終了ステータスを取得するためによく使用されます (推奨) 

WIFEXITED(status):      子プロセスの正常終了によって返されるステータスの場合は true。( プロセスが正常に終了したか確認してください )
WEXITSTATUS(status): WIFEXITEDがゼロ以外の 場合、子プロセスの終了コードを抽出します。 (プロセスの終了コードを表示)

補充:

c、パラメータオプション

このオプションのデフォルトは 0 で、子プロセスの実行中、親プロセスはブロックされて待機していることを意味します。WNOHANG パラメータはマクロ定義であり、親プロセスが非ブロック状態であることを意味します。(WNOHANG の理解: HANG は専門用語です。プロセスがスタックしている場合、プロセスはブロッキング キュー内にあるか、スケジュールされるのを待っているため、プロセス HANG と呼ばれます。したがって、NOHANG は非ブロッキング待機を意味します)

Linux は C 言語で書かれています。Wait は基本的にシステム内の関数です。疑似コードを通じてそれを理解します。

 ノンブロッキング待機とは、子プロセスを待機しないことを意味しますか? 本質的に、ノンブロッキング待機はノンブロッキングコールに基づいたポーリング スキームです。人間の言葉で言うと、私は Zhang San に助けを求めますが、Zhang San は忙しいと言います。私はまず自分のことをしてから、毎分彼に電話します。 , 彼が仕事を終えたかどうかを確認するために。

4. プロセスの置き換え

1. 概念と原則 

fork      を使用して 子プロセスを作成した後、親プロセスと同じプログラムが実行されます ( ただし、異なるコード分岐が実行される場合があります )。 子プロセスは、 別のプログラムを実行するために exec関数を呼び出すことがよくあります。 プロセスが exec 関数を呼び出すと プロセスのユーザー空間コードとデータは新しいプログラムに完全に置き換えられ 新しいプログラムのスタートアップ ルーチンから実行が開始されます。exec を呼び出しても 新しいプロセスは作成されない ため execを呼び出す前後でプロセスのIDは変わりません(つまり、exec を呼び出しても新しい子プロセスは作成されません)

2. プロセス交換方法

方法: execl 関数を使用

男に聞いてみよう

今日は最も単純なexeclを学びます。

int execl (const char* path, const char* arg, ...) // パス。コマンドラインに書き込むだけです。

path: ターゲットプログラムのアドレス + パス

arg: 関数パラメータ

... : 可変パラメータ リストを意味します。 注: パラメータ リストは、パラメータ抽出の終了を示す NULL で終わる必要があります。

以下に例を示します。

 上記の観察から:

1. プロセスが置き換えられた後、「プロセス終了」は出力されませんが、execl 関数の呼び出しが成功すると、元のプロセスのコードとデータがすべて新しいプロセスに置き換えられるという事実によって証明できます。

2. execl 呼び出しが失敗した場合は、元のプロセスを続行しますが、この時点でプロセスを直接終了することもできます。

3. fork + execl 関数を試してみる

以下のコードを見てください。

  1 #include <iostream>
  2 #include <unistd.h>
  3 #include <sys/types.h>
  4 #include <sys/wait.h>
  5 using namespace std;
  6 
  7 int main()
  8 {
  9    pid_t pd = fork();
 10    if (pd == 0)
 11    {
 12     // 子进程
 13      cout << "子进程开始, pid:" << getpid() <<  endl;
 14      execl("/usr/bin/ls", "ls","-l", "-a", "--color=auto", NULL);
 15      exit(-1);
 16 
 17    }else if (pd)
 18    {
 19      // 父进程
 20      int status = 100;
 21      cout << "父进程开始" << endl;
 22      pid_t ret = waitpid(-1, &status, 0);                               
 23      if (ret)
 24      {
 25        cout << "子进程退出,打印子进程退出码:" << WEXITSTATUS(status) <<    endl;
 26      }else 
 27      {
 28        cout << "子进程未退出" << endl;
 29      }
 30 
 32    }
 33    else 
 34    {
 35      cout << "创建子进程失败" <<endl;
 36    }
 37   return 0;
 38 }

結果:

Q: 子プロセスを作成して置き換える必要があるのはなぜですか?

回答: 親プロセスがデータを読み取り、データを分析し、特定のタスクを完了するために子プロセスを割り当てるという考えを実現するためです。

質問: 親プロセスと子プロセス間のコード共有、書き込み時のデータのコピー? プロセスを置き換えるexecl関数についてはどうですか? コードは書き込み時にコピーされますか?

回答: はい、親プロセスと子プロセスが共有されている場合、execl 関数が呼び出されたときにコードが書き込み時にコピーされ、そうでない場合は親プロセスが影響を受けます。

その他追加された機能:

プロセス置換機能には、実際には次のような多数のインターフェイスがあります。

1. execv関数は、次のようにグラフ フローを使用します。

2.execlp関数

3. execvp関数、これは非常に使いやすいです。このように理解できます。命令モードはVectorに格納され、「P」はファイル パスを省略し、環境変数を自動的に検索します。

4.execle関数の「e」は環境変数を意味しており、新しいプログラムに環境変数を渡すことで、その環境変数の値を新しいプログラムで利用することができます。たとえば、環境変数を設定して新しいプログラムの動作に影響を与えたり、新しいプログラムで使用する必要がある構成情報を渡したりできます。

以下は、execle 関数を使用して環境変数を渡す方法を示す例です。

#include <unistd.h>

int main() {
    char *envp[] = {"MYVAR=Hello", "OTHERVAR=World", NULL};
    execle("/path/to/program", "/path/to/program", NULL, envp);
    return 0;
}

上の例では、2 つの環境変数を定義しMYVAROTHERVARそれらを新しいプログラムに渡しました。新しいプログラムはgetenv関数を使用してこれらの環境変数の値を取得できます。

execle 関数を使用する場合、システムのデフォルト環境変数を含む、環境変数の完全な配列を渡す必要があることに注意してください。カスタム環境変数だけを渡したい場合は、execve 関数 (これは実際のシステム コールであり、他の exec** 関数は単なるカプセル化です) を使用して、environ変数をパラメータとして渡すことができます。(チャットポイントより)

ここで注意すべき点は、プロセスの置き換えであっても環境変数はシステムデータであるため、子プロセスは親プロセスの環境変数をコピーするだけで置き換えられません。 

命名概要:

これらの関数プロトタイプは混同しやすいように見えますが ルールをマスターしていれば簡単に覚えられます。
l(list): パラメータがリスト内にあることを示します
v(vector): パラメータの配列
p(path): p で環境変数 PATH を自動検索します。
e(env): 環境変数を自分で管理することを示します

4. execl 関数を使用して他の実行可能プログラムを実行する方法

いいえ、次はテスト プログラムで呼び出す mypro プログラムです。 

makefile: 複数のファイルを一度にコンパイルできます。

一番右の図ではコマンド ライン パラメーターを使用しています。このブログのコマンド ライン パラメーターのセクションを参照してください。[Linux] プロセスの基本概念 [パート 2] - CSDN ブログ 

この時点で、基礎となるローダーのインターフェイスである exec*** 関数の機能を理解できます。

5. 演習 - 簡単なシェルコマンドインタープリタを作成する

 目標:

命令を読み取って実行できるシェルを作成します。

シェル実行コマンド:

1. フレームワークの作成:

継続的に命令を受け取るには無限ループを作成する必要があります。

// 属于是死循环
   13   while (1)
   14   {
   15     // 首先是打印地址
   16     cout << "[afeng@_myshell]$ ";
   17     fflush(stdout); // 解决缓冲区的问题
        }

単純にシェル名を出力することはできますが、行を折り返すことはできません。行を折り返せない場合はバッファーの問題が発生するため、fflush 関数を使用して更新できます。

2. 指示の受信と処理

cin と scanf は命令にスペースが含まれるため使用できません。cin と scanf はスペースを検出すると入力が早期に終了します。ここでは、getline、入力ストリーム関数 fgets などのスペース文字を受け取ることができる関数を使用します。まず命令をポインタ配列に保存しますが、シェルを作成しているだけなので命令プログラムを呼び出すことにし、呼び出す場合はプロセスを置き換える必要があります。(ここで区別する必要があるのは、子プロセス置換は他のプログラムを起動するためだけに使用し、親プロセスは変更されないということです。) プロセス置換関数 exec*** を使用するには、命令を分割する必要があります。

    // 然后开始接收指令
   20     char instruct[NUM];
   21     memset(instruct, '\0', sizeof instruct);
   22     if (fgets(instruct, sizeof instruct, stdin) == NULL)
   23     {
   24       continue;
   25     }
   26     instruct[strlen(instruct) - 1] = '\0';
          // 在输入指令后,我们会通过回车键确认,但回车键被当做'\n'记录,所以需要纠正。
   27 
   28     // 开始拆分出指令
   29     char* argv[100] = {0};
   30     argv[0] = strtok(instruct," ");
   31     int i = 1;                                                        
W> 32     while (argv[i++] = strtok(NULL, " "));

3. 子プロセスの置換、親プロセスの待機

次のステップは、子プロセスと親プロセスを作成し、子プロセスを置き換えることです。Linux でパスなしで対応する命令を実行できる根拠は、環境変数にパスがすでに存在していることであることがわかっています。 , システムが自動的に検索します。

36     // 内置命令 1.我们通过子进程替换打印我们需要的结果,父进程不受影响
   37     // 当需要更改路径时,目标是父进程
   38     if (strcmp(argv[0],"cd") == 0)
   39     {
   40         if (argv[1] != NULL)
   41           chdir(argv[1]);
   42         continue;
   43     }
   44 
   45     pid_t pd = fork();
   46     if (pd == 0) // child
   47     {
   48       execvp(argv[0], argv);
   49       exit(-1);                                                       
   50     }
   51     else{
   52       // parent
   53       int status;
   54       pid_t ret = waitpid(pd, &status, 0);
   55       if (ret > 0 )
   56       {
   57         cout << "子进程运行成功,退出码:" << WEXITSTATUS(status)<< endl;
   58       }else{
   59         cout << "子进程运行失败,退出码:" << WEXITSTATUS(status)<< endl;
            }
   61     }
   62   }
   63   return 0;
   64 }

結論

   このセクションは以上です。閲覧していただきありがとうございます。ご提案がございましたら、コメント欄に残してください。お友達に何かを持っていく場合は、「いいね!」を残してください。あなたの「いいね!」と注目がブログ投稿になります。メイン創造の原動力

おすすめ

転載: blog.csdn.net/qq_72112924/article/details/133175671