Linux プロセスに関する一連の記事の第 1 部では、main() 関数と環境関連の C 関数について説明することで、Linux プロセスに関する理解を深めます。
この記事では、プロセスのメモリ レイアウトと C 関数を終了するプロセスについて説明します。
Linux プロセス シリーズ:パート 1、パート 2 (この記事)、パート 3
プロセスのメモリ レイアウト
Linux のプロセスのメモリ レイアウトは、すべてを詳細に提示して説明しようとすると、非常に複雑になる可能性があります。そのため、ここでは重要な内容のみを紹介します。
プロセスのメモリ レイアウトを視覚化しようとすると、次のようになります。
上記のレイアウトの各コンポーネントを 1 つずつ説明しましょう:
- コマンドライン引数と環境変数は、プロセス メモリ レイアウトの上位アドレスに保存されます。
- スタック セグメントが続きます。これは、関数のローカル変数や、関数が呼び出されるたびに保存されるその他の情報を格納するためにプロセスによって使用されるメモリ領域です。この他の情報には、戻りアドレス、つまり関数が呼び出された場所のアドレス、マシンレジスタなどの呼び出し元の環境に関する情報がスタックに格納されます。また、ここで言及する価値があるのは、再帰関数が呼び出されるたびに新しいスタック フレームが生成されるため、ローカル変数の各セットが他のセットに干渉しないことです。
- ヒープ セグメントは、動的メモリ割り当てに使用されるセグメントです。このセグメントは単一のプロセスに限定されず、システムで実行されているすべてのプロセス間で共有されます。どのプロセスでも、このセグメントからメモリを動的に割り当てることができます。このセグメントはプロセス間で共有されるため、このセグメントのメモリは慎重に使用する必要があり、そのメモリを使用してプロセスが完了したらすぐに割り当てを解除する必要があります。
- 上の図からわかるように、スタックは下向きに成長し、ヒープは上向きに成長します。
- プログラムで初期化されていないすべてのグローバル変数は、BSS セグメントに格納されます。実行時に、初期化されていないすべてのグローバル変数は値ゼロで初期化されます。 BSS は「Block Started by Symbol」の略です。
- 初期化されたグローバル変数はすべてデータ セグメントに格納されます。
- 最後に、テキスト セグメントは、CPU が実行するマシン命令を含むメモリ領域です。通常、このセグメントは、実行中の同じプログラムの異なるインスタンス間で共有されます。 CPU 命令を変更しても意味がないため、このセグメントには読み取り専用権限があります。
上の図はメモリ レイアウトの単なる論理的な表現であることに注意してください。特定のシステムで、プロセスのメモリ レイアウトがこのようになるという保証はありません。また、これら以外にも、シンボル テーブル、デバッグ情報などのセグメントがいくつか存在します。
プロセス終了関数 exit() および _exit()
次の関数は、プロセスを終了させる可能性があります:
<オール>exit() 関数と _exit() 関数の違いは、前者が制御をカーネルに戻す前に何らかのクリーンアップをサポートしているのに対し、他の 2 つの関数はすぐにカーネルに戻ることです。
関数 _exit は POSIX で指定されていますが、_Exit は ISO C で指定されています。これを除けば、この 2 つの間に大きな違いはありません。
すでに上で説明したように、クリーンアップは exit() と _exit() の主な違いです。これを実際に証明する前に、別の関数「atexit()」を理解しましょう。
以下はプロトタイプです:
int atexit(void (*function)(void));
名前が示すように、これは関数ポインターを受け取り、その特定の関数をこのプログラムのクリーンアップ関数として登録するシステム コールです。これは、プロセスが正常に終了し、プロセスの終了がクリーンアップをサポートするたびに、登録された関数が呼び出されることを意味します。
上記の段落の最後の行をもう一度確認すると、関数「atexit」が、exit() 関数と _exit() 関数を区別するクリーンアップ プロセスの一部であることがわかります。したがって、ここに atexit() および exit() 関数を使用するコードがあります..
#include<stdio.h> #include<stdlib.h> #include<unistd.h> extern char **environ; void exitfunc(void) { printf("\n Clean-up function called\n"); } int main(int argc, char *argv[]) { int count = 0; atexit(exitfunc); printf("\n"); while(environ[count++] != NULL) { // Dos some stuff } exit(0); }
上記のコードでは、関数「exitfunc()」は、関数 atexit() を使用して、クリーンアップ関数としてカーネルに登録されます。
上記のコードを実行すると:
$ ./environ Clean-up function called
クリーンアップ関数が呼び出されたことがわかります。
上記のコードの呼び出しを exit() から _exit() に変更した場合:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> extern char **environ; void exitfunc(void) { printf("\n Clean-up function called\n"); } int main(int argc, char *argv[]) { int count = 0; atexit(exitfunc); printf("\n"); while(environ[count++] != NULL) { // Dos some stuff } _exit(0); }
このプログラムを実行すると、以下が表示されます:
$ ./environ $
したがって、今回はクリーンアップ関数「exitfunc()」が呼び出されていないことがわかります。これは、exit() 関数と _exit() 関数の違いを示しています。