OS実験 - フィボナッチ数列のC++プログラム実装(親子プログラム fork() ピュア版と共有メモリ版)

        今学期のオペレーティングシステムの授業では、フィボナッチ数列の生成に関連する 2 つのコンピューター実験を行いましたが、2 つの重要な知識ポイント (親プログラムと子プログラムの確立と接続、および共有メモリの使用) が含まれていました。このブログでは、2 つの方法について詳しく説明し、困っている同志に参考にしてもらいます。

目次

fork () の概念の理解

親子プログラム fork() 純粋版

アイデア1

アイデア2

共有メモリ バージョン

1. 構造を定義する

2.共有メモリを作成する

  2.1 共有メモリの整数 ID 値を作成する

  2.2 アドレス空間に共有メモリを追加する

3. 対話的にフィボナッチ数列を入力し、親プログラムとサブルーチンを作成

4.共有メモリを再利用する

  4.1 共有メモリ セグメントをメモリ空間から分離する

  4.2 共有メモリ セグメントを完全に削除する

フローチャート

ソースコード

操作の結果は次のとおりです。

要約する


fork () の概念の理解

        プログラムの実装を説明する前に、fork() の概念をすべての人に説明したいと思います。なぜなら、ピアと通信するとき、誰もがこの概念についてまだ少し曖昧であり、多くの人が fork() ) は fork( ) です。ここでは、プログラムを使用して説明します。

int main(){
	int a=5, b=7;
	pid_t  pid;
	pid = fork();		/* fork another process */
	if (pid < 0) {		 /* error occurred */
		fprintf(stderr, "Fork Failed");
		exit(-1);
	}
	else if (pid == 0) { 	/* child process */
		a=a*2;
		printf (“this is Child process, a=%d, b=%d\n”,a,b);
	}
	else {			 /* parent process */
		printf (“this is parent process, a=%d, b=%d\n”,a,b);
		wait (NULL);/* parent will wait for the child to complete */
		a=a+1; b=b+3;
		printf (“Child Complete, a=%d, b=%d\n”,a,b);
		exit(0);
}   }

 まず, 親プロセスが子プロセスを作成するとき, 最初に pid_t 型の変数を定義する必要があります. 上記のプログラムでは, 変数に pid という名前を付けました. この変数の機能は, 誰が親プロセスで誰が親プロセスであるかを区別することです.子プロセス。

fork() 関数の機能は、子プロセスを作成し、同時に pid_t 型の値を返すことであるため、上記のプログラムには

pid = fork();

この操作により、親プロセスとまったく同じプロセスが作成されます。これを子プロセスと呼びます。この時点で、pid 値が作用します。

親プロセスと子プロセスに pid があるので、では――

pid が 0 の場合、プロセスが子プロセスであることを意味します。pid が 0 ではなく、特定の正の整数である場合、プロセスが親プロセスであることを意味し、整数は実際には子プロセスの識別子です。プロセス. 親プロセスは子プロセスを取得する必要があります. プロセスの識別子だけがあなたの息子であることを知ることができます.

pid の値を決定したら、プログラムの分岐ステートメントに従って実行できます。子プロセスは pid = 0 でコード セグメントを実行し、親プロセスは pid > 0 でコード セグメントを段階的に実行します。

現時点では!心に留めておかなければならない一言!——

子プロセスと親プロセスは、2 つの別個のプロセス空間です。

子プロセスと親プロセスは、2 つの別個のプロセス空間です。

子プロセスと親プロセスは、2 つの別個のプロセス空間です。

つまり、子プロセスは子プロセスのタスクを実行し、親プロセスは親プロセスのタスクを実行し、2 つが互いに影響を与えることはありません。子プロセスの操作は親プロセスに影響を与えず、生成された値は親プロセスに返されません; 同様に、親プロセスの操作は子プロセスに影響しません。

親プロセスの場合 (赤いセクションは親プロセスの実行中のコードです):

intメイン(){

  pid_t pid; int a=5、b=7;

  pid = フォーク (); /* 別のプロセスを fork */

  if (pid < 0) { /* エラーが発生しました */

  fprintf(stderr, "フォーク失敗");

  終了 (-1);

  }

  else if (pid == 0) { /* 子プロセス */

  a=a*2;

  printf (“これは子プロセスです。a=%d, b=%d\n”,a,b);

  }

  else { /* 親プロセス */

  printf (“これは親プロセスです。a=%d, b=%d\n”,a,b);

  wait (NULL);/* 親は子が完了するのを待ちます */

  a=a+1; b=b+3;

  printf (“子の完了、a=%d、b=%d\n”,a,b);

  終了 (0);

}

親プログラムの実行中のコードには、wait(NULL) というコード行があり、この操作行の機能は、サブルーチンの完了を待機することです。つまり、サブルーチンの実行が終了した後でのみ、親プログラムは wait(NULL) ステートメントの後に操作を実行します。

 子プロセスの場合 (赤いセクションは子プロセスの実行中のコードです):

intメイン(){

  pid_t pid; int a=5、b=7;

  pid = フォーク (); /* 別のプロセスを fork */

  if (pid < 0) { /* エラーが発生しました */

  fprintf(stderr, "フォーク失敗");

  終了 (-1);

  }

  else if (pid == 0) { /* 子プロセス */

  a=a*2;

  printf (“これは子プロセスです。a=%d, b=%d\n”,a,b);

  }

  else { /* 親プロセス */

  printf (“これは親プロセスです。a=%d, b=%d\n”,a,b);

  wait (NULL);/* 親は子が完了するのを待ちます */

  a=a+1; b=b+3;

  printf (“子の完了、a=%d、b=%d\n”,a,b);

  終了 (0);

}

次に、wait(NULL) の前に、サブルーチンは「this is Child process, a=10, b=7」と出力し、親プロセスは「this is parent process, a=5 , b=7」と出力します。

ここで注意が必要です。子プロセスと親プロセスによって出力される内容は時間スケールで決定されません。つまり、親プロセスが最初に印刷し、子プロセスが後で印刷する場合や、子プロセスが印刷し、親プロセスが後で印刷する場合があります。

したがって、ここで実行されているプログラムは異なる結果を生成しますが、違いは印刷の順序にあります!

したがって、このプログラムでは、3 つの最終結果があります。

  • フォークに失敗しました
  • これは子プロセス、a=10、b=7

        これは親プロセス、a=5、b=7

        子コンプリート、a=6、b=10

  • これは親プロセス、a=5、b=7

        これは子プロセス、a=10、b=7

        子コンプリート、a=6、b=10

一般的に fork は出力結果の違いに着目します. もちろん実際の運用ではシステム自体の性質で生成順序が決まる場合もありますので, 最終的な出力結果は一つしかない場合もありますが, これが実際のシステムです.上記の状況で、上記で分析したことは、誰もが明確である限り、すべての可能な状況です。

次に、コードリンクを入力してください〜

(コードは Linux 環境でコンパイルおよび生成する必要があることに注意してください。VS でコンパイルする場合、VS には一部のヘッダー ファイルが含まれておらず、コンパイルできません。一部のヘッダー ファイルは手動で追加する必要があります。ここでは展開しません。自分でブログを参照できます)

親子プログラム fork() 純粋版

        この方法では、fork() コマンドによって親子プログラムが生成され、サブプログラムでフィボナッチ数列が生成されて出力され、親プログラムはサブプログラムの完了を待つだけで済みます。

        実際の実装では、実際には多くのオプションから選択できますが、ここでは 2 つの簡単なアイデアを紹介します—

        アイデア 1: main() 関数の外側に関数を定義して親子プログラムを作成し、繰り返しを追加して、サブプログラムが独自のサブプログラムを作成し、サブプログラムのサブプログラムが独自のサブプログラムを作成するようにするサブプログラム、レイヤーごとにネスト、各サブプログラム プログラムは、独自の管轄に属するフィボナッチ数のみを出力します。

        アイデア 2: グローバル変数 vector<int> 配列を事前に定義し、親プログラムを 1 つだけ定義し、サブルーチンを 1 つだけ生成する サブルーチンは、再帰式に従ってフィボナッチ数を連続的に繰り返し生成し、グローバル変数の配列に格納します。親プログラムは、グローバル変数の配列を出力できます。

        2番目の考え方の方が考えやすいようですが、やはり親子プログラムはペアしかなく、構造もさほど複雑ではないので、方法1の場合は親子の階層ごとのネスティング、子プログラムは少し複雑かもしれませんが、考えてみると非常に興味深いものです (最初は最初のものを考えましたが、後で 2 番目の考え方を発見しました。u1s1 と 2 番目は本当に簡単です)。

アイデア1

-------------------------------------------------- ---簡単な説明---------------------------------------------- -------------------

サブプログラムが独自のサブプログラムを作成し、サブプログラムのサブプログラムが独自のサブプログラムを作成するように、 main() 関数の外側に関数を定義して親子プログラムを作成し、反復を追加します。 、レイヤーごとにネストされ、各サブプログラムはそれ自体で管理されるフィボナッチ数に属しているだけです。

-------------------------------------------------- -------------------------------------------------- ------------------------------

決定する必要があるタスクは次のとおりです。

1.親プログラムが何をすべきかを決定する

2. サブルーチンが何をすべきかを決定する

したがって、親プログラムの場合、実際には、プログラムで何もする必要はなく、サブルーチンが印刷を完了するのを待つだけです。

したがって、サブルーチンでは、最初に出力したいフィボナッチ数を取得して出力し、次のレベルのネストに入る必要があります。

この種の考え方は言語で説明するのは簡単ではないかもしれないので、コードを使って見てみましょう。

このプログラムでは、作成者は main() 関数の外側に関数 ForkProcess を定義しています。その内容は次のとおりです。

void ForkProcess(int n, int fib0, int fib1) {
            //如果Fibonacci的级数小于三,就不需要再生成了,因为初始的两个Fibonacci数我们都已知.
            if(n < 3) return;
            else {
                    pid_t pid;
                    pid = fork();
                    if(pid < 0) {
                            cout << "Error! Fork Failed!" << endl;
                            exit(-1);
                    }
                    else if(pid == 0) {
                            int temp = fib1;
                            fib1 = fib1 + fib0;
                            fib0 = temp;//获取到该级子程序应该打印的Fibonacci数.
                            cout << fib1 << endl;
                            //再生成下一级子程序并打印Fibonacci数.
                            ForkProcess(--n, fib0, fib1);
                    }
                    else wait(NULL);//父程序的操作,等待子程序完成.
                }
            }

この関数は、フィボナッチ数列を生成するためのすべての操作を既に実行しています. 必要なのは、メイン関数でこの関数を参照し、系列 n と初期フィボナッチ数列 fib0 および fib1 を渡すことだけです.

上記の関数を解析すると、ネストと親子プログラムの生成が理解できます。

全体的なコードの実装は次のとおりです。

#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>

using namespace std;

void ForkProcess(int n, int fib0, int fib1){};
//这里笔者偷个懒简写了一下,为了方便阅读main函数代码
//运行的时候把上面的代码copy过来就好

int main() {
    cout << "请输入想要生成的Fibonacci数列的数字个数" << endl;
    int n;
    int fib0 = 0;
    int fib1 = 1;
    cout << "Fibonacci数列为:" << endl;
    if(n == 1) {//如果级数为1,直接打印一个数然后退出程序
        cout << fib0 << endl;
        return 0;
    }
    else if(n == 2) {//如果级数为2,直接打印两个数然后退出程序
    cout << fib0 << endl;
    cout << fib1 << endl;
    return 0;
    }
    else {//否则,运行ForkProgress程序
    cout << fib0 << endl;
    cout << fib1 << endl;
    ForkProcess(n, fin0, fib1);
    return 0;
    }
}

ここまでで案1の第一案が実現しましたが、まだ理解できる感じですか?もちろん、著者はこれが少し難しいと考えており、考えるのも理解するのも簡単ではありません。したがって、より単純な方法を採用することもできます - アイデア 2.

アイデア2

-------------------------------------------------- ---簡単な説明---------------------------------------------- -------------------

事前にグローバル変数 vector<int> 配列を定義し, 親プログラムを 1 つだけ定義し, サブルーチンを 1 つだけ生成する. サブルーチンは再帰式に従ってフィボナッチ数を連続的に繰り返し生成し, グローバル変数配列に格納する. 親プログラムはグローバル変数 配列は出力できます。

-------------------------------------------------- -------------------------------------------------- ------------------------------

この方法は、親子プログラムのペアのみを使用するため、理解しやすいです〜

まず、親プロセスが何をし、子プロセスが何をするかを明確にする

親プロセス - グローバル変数配列のすべての値を出力します

サブプロセス - 生成されたフィボナッチ数をグローバル変数配列に格納します

コードは以下のように表示されます:

#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<vector>

using namespace std;

vector<int> result;

int main() {
    int fib0 = 0;
    int fib1 = 1;
    int n;
    cout << "请输入想要生成的Fibonacci数列的级数:" << endl;
    cin >> n;
    //对两种特殊情况进行处理,即n=1和n=2的情况.
    if(n == 1) {
        cout << fib0 << endl;
        return 0;
    }
    if(n == 2) {
        cout << fib0 << endl;
        cout << fib1 << endl;
        return 0;
    }
    //如果n>3,则调用父子程序生成Fibonacci数列.
    pid_t pid;
    pid = fork();
    if(pid < 0) {
        cout << "Fork Failed" << endl;
        return 0;
    }
    else if(pid == 0) {//子程序操作
        result.push_back(fib0);
        result.push_back(fib1);
        while(n >= 3) {
            //获取下一个Fibonacci数并将其放入数组            
            int temp = fib1;
            fib1 = fib0 + fib1;
            fib0 = temp;
            result.push_back(fib1);
            --n;
        }
    }
    else {//父程序操作
        wait(NULL);//等待子程序完成
        int num = result.size();
        for(int i = 0; i < num; i++) {
                cout << result[i] << "  ";//将所有Fibonacci数打印出来
        }
        cout << endl;
    }
    return 0;
}
        
        

このコードの焦点は、サブルーチンの操作です。つまり、フィボナッチ数列を生成して配列を生成するための転送ステーションとして fib0 と fib1 を継続的に使用します。これにより、生成プロセスのスペースの複雑さが軽減されるだけでなく、シンプルで便利、そんな考え方が学べます。

ここまでで、親子プログラム fork() のピュア版のコードが完成しました!

わからない場合は、メッセージと交換を残してから、共有メモリの実装方法を共有してください〜

-------------------------------------------------- -------------------------------------------------- ------------------------------

 実は途中でずっと更新を求めていて、この部分の内容を更新していない、そうでなければこの記事はずっと前に送信されたものです。

来て!

-------------------------------------------------- -------------------------------------------------- ------------------------------

共有メモリ バージョン

トピックの説明は、おおよそ次のようになります。

トピックの説明を読んだ後、親子プログラムを一度だけ作成する必要があることは明らかです。

サブルーチンは、生成されたフィボナッチ数列の対応する数を共有メモリに書き込む責任があり、親プログラムは共有メモリからフィボナッチ数列を出力する責任があります。トピックは終わりです!

したがって、共有メモリを作成する方法に焦点が当てられます。

共有メモリの作成は、大まかに以下のステップに分けられます。

1. 構造体を定義する(親子プログラムの共有メモリセグメントの格納形式を明確にするためのものです。格納形式が単純なint型やchar型の変数であれば当然構造体を定義する必要はありません) 〜)

2.共有メモリを作成する

  • 共有メモリの整数識別値を作成(シリアル番号をマーク、マーク)
  • 共有メモリをアドレス空間に追加します (入力する前にマークしてください)

3.共有メモリを再利用する

  • メモリ空間から共有メモリ セグメントを切り離す
  • 共有メモリ セグメントを完全に削除する

 大まかなフレームワークを明確にした後、具体的なフレームワークの実装の詳細に焦点を当て、次に、プログラム コードの実装シーケンスに従って、コードのアイデアを段階的に分析します—

1. 構造を定義する

        構造体を実現するには、サブルーチンと親プログラムの間で共有メモリセグメントの格納形式を定義する必要があります.ここでは、教科書にある構造体形式を使用して実験を行います.

typedef struct{
    long fib_sequence[MAX_SEQUENCE];
    int sequence_size;
}shared_data;

        このうち、MAX_SEQUENCE は 20 とあらかじめ定義されており、使用するコマンドは

#define MAX_SEQUENCE 20

2.共有メモリを作成する

    2.1 共有メモリの整数 ID 値を作成する

        関連するコマンドは次のとおりです。

segment_id = shmget(IPC_PRIVATE, size, S_IRUSR | S_IWUSR)

        最初のパラメータは共有メモリ セグメントのキーワード (識別子) を参照し、IPC_PRIVATE として指定すると、新しい共有メモリ セグメントが生成されます。2 番目のパラメーターは、共有メモリ セグメントのサイズを参照します (サイズはバイト数に応じて計算されることに注意してください。ここでは、パラメーターを渡すために size_t 型の変数を定義する必要があります)。最後の 3 番目のパラメータは、共有メモリ セグメントの使用方法 (読み取り、書き込み、またはその両方) を指定するモードを識別するために使用されます。

        このディレクティブに必要なヘッダー ファイルは次のとおりです。

#include<sys/ipc.h>
#include<sys/shm.h>

        共有メモリの整数識別子の値が正常に作成された場合、shmget() 関数によって返される共有メモリ セグメントの整数識別子の値が segmeng_id から取得され、失敗した場合は -1 が返されます。この機能に基づいて、エラー チェック、エラー通知、および処理を実行します。

  2.2 アドレス空間に共有メモリを追加する

        関連するコマンドは次のとおりです。

shared_memory = (变量类型强转)shmat(segment_id, NULL, 0);

        コマンドの下線部分は、特定の状況に応じて調整されます。ここで、かっこ内に shared_data* を記入する必要があります. 具体的な説明は、shmat() 関数が正常に実行された場合、共有メモリ領域を定義したため、接続された共有メモリ領域のメモリ内の最初の場所へのポインタを返すことです.事前にshared_data.型としてメモリを指定しているため、ここでのポインタは強制的にshared_data*型に変換されます.同様に、shared_memoryを定義する場合もデータ型をshared_data*と指定します.
        shmat() 関数では、呼び出しに 3 つのパラメーターが必要です。

  • 1 つ目は、結合する共有メモリ セグメントの整数の識別値、つまり、segment_id です。
  • The second is a pointer location in the memory, which indicates the location of the shared memory to be added. 値 NULL が渡された場合、オペレーティング システムはユーザーの場所を選択します (したがって、通常は NULL を選択します)。
  • 3番目のパラメータは、追加する共有メモリ領域が読み取り専用モードか書き込み専用モードかを指定するフラグを表し、パラメータ0を渡すことで、共有メモリ領域が読み取りまたは書き込み可能であることを意味します。

        このコマンドは、必要なヘッダー ファイルを作成します。

#include <sys/types.h>
#include <sys/shm.h>

        同様に、失敗すると -1 が返されます。このプロパティに基づいて、エラー チェック、エラー通知、および処理を実行します。

        この時点で、共有メモリが作成されます。

3. 対話的にフィボナッチ数列を入力し、親プログラムとサブルーチンを作成

        まず、対話して、生成するシーケンスの数をプログラム ランナーに手動で入力させます。

        1~MAX_SEQUENCEの範囲を超えると再入力を求められます。pid_t 型の pid 変数を定義します。

        pid = fork() としてサブルーチンを作成します。

        pid 値に基づいて親プログラムと子プログラムを判別します — 子プログラムの場合、pid 値は 0、親プログラムの場合、pid 値は 0 より大きい、プログラムの作成に失敗した場合、 pid 値が負です。

        サブルーチンは、フィボナッチ数列を生成し、コンテンツを共有メモリに書き込む必要があります。親プログラムは、サブルーチンが終了した後、共有メモリにフィボナッチ数列を出力します。

4.共有メモリを再利用する

  4.1 共有メモリ セグメントをメモリ空間から分離する

        関連するコマンドは次のとおりです。

shmdt(shared_memory)

このコマンドで必要なヘッダー ファイルは次のとおりです。

#include <sys/types.h>
#include <sys/shm.h>

同様に、失敗すると -1 が返されます。このプロパティに基づいて、エラー チェック、エラー通知、および処理を実行します。

  4.2 共有メモリ セグメントを完全に削除する

        関連するコマンドは次のとおりです。

shmctl(segment_id, IPC_RMID, NULL);

        このコマンドに含まれるヘッダー ファイルは次のとおりです。

#include <sys/types.h>
#include <sys/shm.h>

        同様に、失敗すると -1 が返されます。このプロパティに基づいて、エラー チェック、エラー通知、および処理を実行します。

        この時点で、共有メモリが回復されます。

共有メモリのいくつかの操作の詳細については、百度百科事典を直接参照できます。これは本当に包括的です。

ポータルをください— https://baike.baidu.com/item/shmget/6875075

フローチャート

 ソースコード

        内容を理解してから書いていただければと思いますので、ここに画像を貼り付けておきます。もう一度入力しても、直接 Ctrl+C, V~

        

 操作の結果は次のとおりです。

(コードは vs では実行できないことに注意してください。Linux 環境では g++ でのみコンパイルおよび実行できます)

 -------------------------------------------------- -------------------------------------------------- ----------------------------

要約する

 ついに終わりました!

この部分は実際に理解するのは難しいことではありません。鍵は練習することです。forkの使い方、親子プログラムの関係、共有メモリの作成などを理解したら、あとはコードの整理です!

ご不明な点がございましたら、お気軽にお問い合わせください。今後、OS関連のコンテンツを随時公開していきますので、どうぞよろしくお願いします!

おすすめ

転載: blog.csdn.net/wyr1849089774/article/details/130312176