メモリがヒープとスタックに分かれているのはなぜですか? 使用できるモデルは 1 つだけですか? 各スレッドに個別のスタックがあるのはなぜですか?

「https://mp.weixin.qq.com/s/Yh2prf3U2qbNFyH_1viitQ」より転載
実は、コンピューターのメモリはもともと一枚のメモリであり、スタックが存在しないことは誰もが知っています。

プログラミングを学んでいるとき、誰もが「プログラム終了後もそのデータにアクセスしたい場合は、ヒープを使用する必要があります(ヒープが解放されなければ、プログラムによって変更されたデータはまだ存在します)」という文を聞いたことがあるはずです。 )"。実際、これがこのトピックのテーマだと思います。重要なのは、ヒープとスタックには独自の特徴があるということです。おそらくこの 2 つのことはご存知かもしれませんが、他の友達が答えを見たときに分からないことを避けるために説明します。 。

Linux プロセス内のすべてのスレッドはプロセスのアドレス空間を共有しますが、独自の独立した (プライベート) スタックを持ちます。ヒープの割り当てはスタックの割り当てとは異なり、通常、プロセスには C ランタイム ヒープがあり、このヒープはプロセス内のすべてのスレッドで共有されます。(プロセス下のすべてのスレッドには独自の独立したスタックがありますが、ヒープは共有プロセスのヒープです)

スタック: 最初の文で述べたように、スタックなどというものはありませんが、プログラミング言語の出現により、「関数」という概念があり、これらの関数は相互に呼び出すことができます (物を渡すのと同じように、例: Hu Xiaoran は Hu Xiaoran 2 に物を渡し、Hu Xiaoran 3 に物を渡します。その後、転送結果は後ろから前にフィードバックされる必要があります。この転送プロセスは呼び出しとして理解できます)。前後の区別です。これが呼び出しキューです。では、このキューの特徴は何ですか。つまり、最初にキューに呼び出された人が最後に出ます。これをよく「先入れ、後出し」と呼びます。 (FILO) するとスタックが表示され、スレッドが独立しているという特徴もあります。スレッドが終了すると、対応するスタックも終了します (スタックはスレッドの開始時に初期化され、各スレッドのスタックは互いに独立しているため、スタックはスレッド セーフです。スレッドを切り替えると、オペレーティング システムが自動的にスタックを切り替えます)。つまり、SS/ESP レジスタを切り替えます。高級言語ではスタック領域を明示的に割り当てたり解放したりする必要はありません。) もちろん、私が言っているのはメモリ スタックです。実際、「スタック」とはデータ構造であり、挿入と削除操作をテーブルの最後までにのみ制限する線形テーブルです。この機能はまさにさっき言ったFILO。したがって、C++ または Java (jvm) のメモリ スタックの概念を理解することができます。つまり、プログラミング言語の作成者は、データ構造「スタック」を使用してメモリを管理します (より詳しく言うと、最新の CPU アーキテクチャでは、スタックとは、関数呼び出しとローカル変数に最適なデータ構造を管理するものです。CPU が既成の命令をすでに提供しているためです)。

写真

ヒープ: 私たちがよく使うバイナリ ツリーのような、特別なデータ構造とみなすことができます。メモリヒープをもっと簡単に説明すると、自由に割り当てられるメモリのことで、全員が共有する空間であり、グローバルヒープとローカルヒープに分かれます。グローバル ヒープはすべて未割り当ての領域であり、ローカル ヒープはユーザーによって割り当てられた領域です。ヒープは、オペレーティング システムがプロセスを初期化するときに割り当てられます。動作中に追加のヒープをシステムに要求することもできますが、使い切った場合は必ずオペレーティング システムに返却してください。そうしないと、メモリ リークが発生します。これにより、プログラムは実行時に特定のサイズのメモリ空間を動的に申請できます。たとえば、プログラマはオペレーティング システムからメモリの一部を申請します。システムはプログラムからアプリケーションを受け取ると、リンク リストを走査します。空きメモリアドレスを記録して最初の空きメモリアドレスを見つけます。要求された領域よりも大きい領域を持つヒープノードは空きノードリストから削除され、そのノードの領域がプログラムに割り当てられます。その特徴は、割り当て速度が遅い、アドレスが不連続で断片化しやすい、プログラマが申請するものであると同時に、プログラマが責任を持って破壊しなければメモリリークが発生するという点です。Java のような高級言語では、jvm がすでにメモリのリサイクルを処理しているため、メモリのリサイクルについて心配する必要はありません。

写真

スレッドが異なれば、スタック領域と共有ヒープ領域も異なります。実際、各スレッドはヒープ内に新しいメモリ領域を作成できます。この方法はスレッド ローカル ストレージ (TLS) と呼ばれますが、スレッドによって作成されたヒープ領域には他のスレッドからもアクセスできます。たとえば、スレッドは Object* o = new Object() を使用して新しいオブジェクトを作成します。このオブジェクトはヒープ内にあり、ポインタ o はスタック内にあります。このスレッドがポインタ o の値を他のスレッドに送信する限り、スレッド、次に他のスレッド 別のポインタ p を使用して受信しても、他のスレッドは引き続きこのスレッドの新しいオブジェクトにアクセスできます これは共有ヒープ領域の説明です。

ヒープが共有されるため、1 つのスレッドが new を使用すると、他のすべてのスレッドが停止して待機する必要があり、膨大な同期コストが発生するため、スレッドで new を使用して新しいメモリ空間を開くことは一般的に推奨されません。各スレッドは n サイズのメモリをヒープに割り当てます。これには m 個のメモリ割り当て操作が必要で、すべてのスレッドは m 回待機する必要があります。新しい操作はすべて時間がかかることに注意してください。m*n サイズのメモリをヒープに割り当てることができるのは 1 回のみで、その後は各スレッドがアクセスする必要がある部分にアクセスします。これに必要なメモリ割り当ては 1 回だけであり、後続の操作には同期コストがかかりません。

ここまで述べてきましたが、メモリ スタックとメモリ ヒープの意味と役割を説明したいだけです。そのため、答えは「ヒープだけを使用したり、スタックだけをすべて使用したりする」ことはできないということです。プログラムとデータの保存に問題が発生します。

最後に、両者の特徴についてお話しましょう。

(1) スタックは、先入れ後出し、後入れ先出しの特性、継続的なストレージ、シンプルな操作、簡単な使用、および管理の必要性を備えており、ほとんどのチップはスタックに対してチップ レベルのハードウェア サポートを提供します。ポインタを移動するだけで、メモリの割り当てやリサイクルがすぐに行えます。たとえば、ローカル変数はスタック メモリを使用して、不必要なメモリ割り当て管理を削減します。スタックの作成と削除の時間計算量は O(1) であり、高速です。しかし、大容量のメモリを管理するには適さず、スタック内のデータのサイズとライフサイクルが決定されるため、柔軟性に欠けます。

(2) ヒープ メモリの管理メカニズムは比較的複雑であり、多数の小さなフラグメントの発生を防ぎ、検索を高速化するための対応する割り当て戦略が存在します。ヒープはメモリの動的作成と割り当てに使用され、ノードの作成と削除の時間計算量は O(logn) です。ヒープのリサイクル メカニズムも非常に複雑で、メモリのサイズとデータのライフ サイクルに応じて、オペレーティング システムのヒープ管理を伴う、対応するリサイクル メカニズムを採用する必要があります。ヒープ メモリの管理と適用は比較的複雑で、より多くのシステム リソースを消費するため、通常、ヒープ メモリはより長いライフ サイクルとより広範囲のグローバル変数で使用されます。

写真

各スレッドに個別のスタックがあるのはなぜですか?
4 つの関数 A、B、C、D があり、それぞれアドレス 100、200、300、400 があり、2 つのスレッドがそれらを同時に実行します。

1) スタックが 1 つだけの場合

写真

関数 A がスレッド 1 で実行されると、関数 B が呼び出され、関数 A の次の命令のアドレスがスタック (104) にプッシュされてから、関数 B が実行されます。

関数 B の実行中に、Yield() 関数 (青、Yield() の役割はスレッドの切り替えとして理解できます) が呼び出されていることがわかり、B で実行される次の命令のアドレスが最初にプッシュされます。スタック (204) に書き込み、その後 Yield() を実行すると、アドレス 300 のスレッド (スレッド 2) に切り替わります。

次に、関数 C が実行され、メソッド D が同様に呼び出され、304 がスタックにプッシュされます。

最後に、関数 D が実行され、404 がスタックにプッシュされ、D の Yield() がアドレス 204 にジャンプして実行を継続します (スレッド 1 で実行される次のステートメントに対応するアドレス 204 に切り替わります)。

関数 B は実行直後に戻ります。戻りアドレスはスタックの先頭の値 (404) です。ここでの戻りアドレスは 104 である必要があります。したがって、複数のスレッドがスタックを共有すると問題が発生します

2) スレッドごとに 1 つのスタック

写真

スレッドを切り替えるときは、スタックも切り替える必要があります。ここでは、スタック ポインタを格納するデータ構造体 TCB (スレッド コントロール ブロック) が必要で、各スレッドには TCB があります。

スレッド 2 の Yield() 関数は、次の形式に書き直す必要があります。

void Yield(){

TCB2.esp=esp;
esp=TCB1.esp;
jmp 204;
}
実行プロセス:

関数 A で、B を呼び出し、アドレス 104 をスタックにプッシュします (esp=1000)。

関数 B で Yield() を実行し、現在のスタック ポインタ TCB1.esp =esp を保存し、同時にスタック ポインタ esp=TCB2.esp を切り替え、アドレス 204 をスタックにプッシュし、関数 C (esp=1000) にジャンプします。 ;

関数 C で関数 D を呼び出し、アドレス 304 をスタックにプッシュします (esp=2000)。

関数 D は Yield() を実行し、スタック ポインタを保存し、スタック ポインタを切り替え、アドレス 404 をスタックにプッシュし、関数 B にジャンプして、アドレス 204 のコードを実行し続けます。


実行が完了したら、「}」を実行し、スレッド 1 スタックの先頭アドレス 204 をポップアップし、ここでアドレス 204の命令が繰り返し実行されていることを確認します。

スレッド 2 の Yield()

void Yield(){ TCB2.esp=esp; esp=TCB1.esp; }このように、2)の4番目のステップが実行されると、jmp 204 Jumpは使用されなくなりますが、'}'が実行され、スタックがスレッド 1 では、先頭アドレスがスタックからポップされます。



C/C++ でコンパイルされたプログラムが占有するメモリは、次の部分に分割されます。

1. スタック領域 (スタック) - コンパイラによって自動的に割り当ておよび解放され、関数のパラメータ値、ローカル変数値などが格納されます。データ構造内のスタックのように動作します。

2. ヒープ領域(ヒープ) - 通常、プログラマによって確保、解放されますが、プログラマが解放しない場合、プログラム終了時にOSによって再利用される場合があります。データ構造がヒープとは異なり、割り当て方法はリンクリストに似ていることに注意してください。

3. グローバル領域(静的領域)(静的) - グローバル変数と静的変数をまとめて格納 初期化済みのグローバル変数と静的変数は同一領域、初期化されていないグローバル変数と初期化されていない静的変数は同一領域 隣接する別の領域。・番組終了後にシステムにより解放されます。

4. リテラル定数領域 - 定数文字列がここに配置されます。プログラム終了後にシステムにより解放されます。

5. プログラム コード領域 - 関数本体のバイナリ コードを格納します。

//main.cpp
int a = 0; グローバル初期化領域
char *p1; グローバル未初期化領域
main()
{ int b; stack char s[] = “abc”; stack char *p2; stack char *p3 = “123456” ; 123456\0 は定数領域にあり、p3 はスタック上にあります。static int c =0; global (static) 初期化領域p1 = (char *)malloc(10); p2 = (char *)malloc(20); //10 バイトと 20 バイトの割り当て領域はヒープ領域にあります。strcpy(p1, "123456"); 123456\0 は定数領域に配置され、コンパイラはそれを p3 が指す "123456" と同じ場所に最適化する可能性があります。スタックはシステムによって自動的に割り当てられます。たとえば、関数内でローカル変数 int b を宣言すると、システムはスタック上に b 用のスペースを自動的に作成します。









プログラマー自身がヒープを申請してサイズを指定する必要があります。C では、p1 = (char *)malloc(10) などの malloc 関数が使用されます。C++ では、p2 = (char * など) の new 演算子が使用されます。 )malloc(10); ただし、p1 と p2 自体がスタック上にあることに注意してください。

おすすめ

転載: blog.csdn.net/leeshineCSDN/article/details/133310724