目次
プロセスの作成
fork() 関数
この関数の使い方については前回の記事で詳しく説明したので、この記事では主にfork() について詳しく説明します。
fork 関数は、既存のプロセスから新しいプロセスを作成する Linux の非常に重要な関数です。新しいプロセスは子プロセスであり、元のプロセスは親プロセスです。
#include <unistd.h>
pid_t fork(void);
戻り値: 子プロセスでは 0 を返し、親プロセスでは子プロセス ID を返し、エラーの場合は -1 を返します。
次に、より体系的な観点から説明します。
fork() が子プロセスを作成するときにオペレーティング システムが何を行うかを説明してください。
1. まず第一に、この質問の出発点は次のとおりです。fork() は子プロセスを作成します。システムにはもう 1 つのプロセスが存在します。
2. 次に、そのプロセスが何であるかを答えてください。
プロセス = カーネル データ構造 (OS) + プロセス コードとデータ (通常はディスクから、つまり C/C++ プログラムのロード結果)
3. その後、子プロセスに対して次の操作が実行されます。
a. 新しいメモリ ブロックとデータ構造を子プロセスに割り当てます。
b.親プロセスのデータ構造の内容の一部を子プロセスにコピーします。
c. 子プロセスをシステムプロセスリストに追加します。
d.fork() は戻り、スケジューラ呼び出しを開始します。
子プロセスは親プロセスからデータをどのように継承しますか
プロセスは独立しているため、子プロセスの作成と、対応するカーネル構造の子プロセスへの割り当ては、子プロセスに対して一意である必要があります。理論的には、サブプロセスにも独自のコードとデータが必要です。
一般に、子プロセスにはロードされたプロセスがない、つまり、子プロセスには独自のコードとデータがないため、子プロセスは「親プロセスのコードとデータ」のみを使用できる可能性があります。
これは間違っています。プロセスが独立しているという意味ではありません。親プロセスのコードとデータはどのように使用できるのでしょうか?
ここでもケースバイケースで説明します。
コードは書き換え不可、読み取りのみなので、親子で共有しても問題ありません!
データ: 変更される可能性があるため、切り離す必要があります。
では、データの場合、どのように分離すればよいのでしょうか?
1. 作成時のコピー分割
まず最初の方法を見てみましょう。子プロセスを作成するときは、それを直接コピーして分離するだけですが、何が問題になるでしょうか?
1. 子プロセスを作成したら、すぐに実行できますか? これもその 1 つです。
2. すぐに機能するとしても、すべてのデータにアクセスできますか? これが 2 番目です。
3. すべてのデータにアクセスできたとしても、すべてのデータへのアクセスは書き込まれていますか? 書かれていない場合はコピーする必要は全くありません。
したがって、問題は、子プロセスがまったく使用しないデータ領域をコピーできる可能性があることです。使用されている場合でも、それは読み取りのみである可能性があります。
この状況は次のようにもよく示されています。
2 つの異なる文字列定数のアドレスを出力すると、それらのアドレスが同じであることがわかります。
コンパイラは、この const で変更された変数の内容は変更できないことを認識しているため、同じ内容を持つ後続の変数はその変数を直接指します。
これは皆さんに伝えたいことです。コンパイラーはプログラムをコンパイルするときに、スペースを節約する方法を認識します。さらに、メモリを直接使用するこの種のシステム インターフェイスでは、.
したがって、子プロセスを作成する場合、アクセスされないデータや読み取り専用のデータをコピーする必要はありません。
しかし、ここで疑問が生じます。どのような種類のデータがコピーする価値があるのか、またどのような種類のデータをコピーする必要があるのかということです。
将来、親プロセスまたは子プロセスによって書き込まれる可能性のあるデータである必要があります。
ただし、一般的に、どのようなスペースが書き込まれてもよいかは、OS であっても事前に知ることはできません。
でも、知っていても事前にコピーして、すぐに使いますか?
答えは、書き込むことができるデータが非常に多く、決してそうではありませんが、スペースが与えられているのに使用しないため、スペースの無駄が発生するということです。
そこで OS は、親プロセスと子プロセスのデータを分離するコピーオンライトというテクノロジーを選択しました。
2. コピーオンライト★
上記と組み合わせると、コピーオンライトは、書き込み可能なデータが必要なときに、OS が対応するスペースを提供することを意味します。
OS が親プロセス データと子プロセス データを分離するためにコピーオンライト テクノロジを採用する理由:
1. 使用されると、再度割り当てられます。これは、メモリの効率的な使用の現れです。
2. OS は、コードが実行される前にどのスペースがアクセスされるかを予測できません。
ここに別の問題があります。
では、親プロセスのfork()の前のコードは、子プロセスと共有されているのでしょうか?
答えは共有され、子プロセスは親プロセス fork() の前にすべてのコードを共有します。
以下にイメージを説明しますが、このイメージもプロセス概念と状態の同時実行で詳しく説明しました。
つまり、EIP にはコードの次の行のアドレスがあり、これらの格納されたデータはコンテキスト データと呼ばれます。
子プロセスが親プロセスを継承すると、コピーオンライトが発生し、親プロセスのコンテキストデータが子プロセスにコピーされます。
親プロセスと子プロセスは後で別々にスケジュールされますが、コードは異なり、それぞれが EIP を変更しますが、子プロセスはすでに自身の EIP コードの初期値がフォーク後のコードであると考えているため、これは重要ではありません。 ()。
したがって、子プロセスは fork() の後に実行されますが、fork() の前のコードの子プロセスがそれを認識できないという意味ではありません。
プロセスが終了しました
プロセスが終了すると、OS は何をしますか?
プログラムは実行するとプロセスになり、プロセスはプロセスコードとデータ + カーネル データ構造で構成されることがわかっています。
したがって、プロセスの終了とは、カーネル データ構造と、プロセスによって適用された対応するデータおよびコードを解放することであり、本質的にはシステム リソースを解放することです。
プロセスを終了する一般的な方法
コードが実行され、結果は正しいです
この状況は非常に一般的で、Lituo などのアルゴリズムの質問を作成する場合、質問の結果が正しければ、最終提出後に合格と表示されます。
または、コンパイラーの下で自分でそれを作成すると、最終出力も予想される結果と一致します。
main 関数には return 0 という戻り値があることに気づいたでしょうか。その戻り値にはどのような意味があり、なぜ常に 0 を返すのでしょうか。
実はmain関数の戻り値は必ずしも0になるわけではありませんが、普段書くときは0を書くことが多いです。
main関数の最後に返される値をプロセスの終了コードと呼びます。
一般に、0 は成功を表し、実行中のプロセスの結果が正しいことを示します。
非ゼロフラグの演算結果は不正であるが、詳細は後述する。
たとえば、10 を返します。
次に、$? を使用して、最新のプロセスの終了コードを出力します。
最初の実行で取得された終了コードは、最後に返された 10 であることがわかります。
2回目で0になっているのは、最後のecho $?もプロセスであり、正常に実行されたので0を返しているからです。
では、main 関数の戻り値は何を意味するのでしょうか?
上位プロセスに戻るために使用され、プロセスの実行結果を判断するために使用されますが、無視できます。
たとえば、足し算と合計のプログラムを作成すると、答えが正しければ 0 が返され、答えが間違っていれば 1 が返されます。
この時点では、合計は正しいプロセスであるため、結果は 0 を返すはずです。
また、sum の計算ロジックを間違えて正しい結果が得られなかった場合には、プログラムの最終結果が正しくないことを示す 1 が返されますが、これが main 関数の戻り値の意味でもあります。
終了コード★
先ほど述べたゼロ以外の終了コードに戻ると、ゼロ以外の値が無数に存在し、さまざまなエラー原因を識別するために使用できます。
プログラムの終了後、エラーの原因の詳細を特定するのに便利です。
これらの理由は Linux でも定義されており、印刷して確認できます。ここでは関数 strerror() を使用する必要があります。
これは、エラー コードの説明文字列を返します。
コードを入力します。
次に、結果を出力します。
100まではエラーの種類が多いことが分かりました。
もちろん、これらの終了コードと意味を自分で使用することもできますが、自分で定義したい場合は、一連の終了スキームを自分で設計することもできます。
コードは最後まで実行されますが、結果は正しくありません
同上。
コードの異常終了
プログラムが異常終了した場合、終了コードは無意味であり、通常、終了コードに対応する return 文は実行されません。
では、なぜプログラムがクラッシュするのでしょうか?
コードを使用してプロセスを終了する方法
戻る
まず、return 終了コードによってプロセスが終了することがわかりますが、もちろん、プロセスを終了するのは main 関数内の return ステートメントだけです。
終了して _exit★
まずは紹介文を見てみましょう
これにより、プロセスが正常に終了することがわかります。
次に、関数パラメータは status であり、終了コードを識別するために使用されます。
これは main 関数とは異なります。exit関数はどこでも呼び出され、プロセスは直接終了します。
次の例を参照してください。
return 200の場合はaに200を返すだけでプログラムは終了しません。
func() の後の出力 world ステートメントは実行されず、初めて終了コードが 200 であることがわかります。
それでは、_exit とは何ですか?またその違いは何ですか?
説明が多く、非常に抽象的なので、例と図を使用して exit との違いを説明します。
まず、先ほどの例ですが、exit を _exit に変更します。
結果は exit と変わらないことがわかります。
ただし、プログラムを次のように変更します。
現時点では、printf にはバッファを更新するための改行文字「\n」がないため、この時点でコンテンツはバッファに格納されていると予想されます。そのため、プログラムは 1 秒間の実行後にコンテンツを画面に出力します。 、その後、終了コードは 111 を返します。
静止画なので効果は実証できませんが、1秒後に結果が表示されます。
ただし、exit を _exit に変更すると、
しかし、何も出力されていないことがわかりました。
直接結論を言えば、exit はプログラムの最後にバッファの内容を画面にリフレッシュし、_exit はバッファの内容をリフレッシュせずに直接終了します。
次の図に示すように
ただし、通常は exit を使用することをお勧めします。
ライブラリ関数はカプセル化されたシステム インターフェイスであり、内部のライブラリ関数は実際には exit であり、システム インターフェイスは _exit であることがわかります。
そして、私たちが通常話しているバッファとはどこにあるのか、それについては後で説明しますが、オペレーティング システム内にあってはなりません。
_exit がオペレーティング システム内にあり、オペレーティング システムによって維持されている場合は、_exit を更新することはできますが、更新することはできません。これは、_exit がオペレーティング システム内になく、C 言語ライブラリによって提供される必要があることを示します。
プロセスの作成と終了についてはこれで終わりです。