Linuxプログラマーマニュアルによると:
brk()およびsbrk()は、プロセスのデータセグメントの終了を定義するプログラムブレークの場所を変更します。
ここでのデータセグメントとはどういう意味ですか?データセグメントまたはデータ、BSS、およびヒープを組み合わせただけですか?
ウィキによると:
データ、BSS、およびヒープ領域は、まとめて「データセグメント」と呼ばれることがあります。
データセグメントだけのサイズを変更する理由はありません。それがデータ、 BSS であり、ヒープが集合的である場合、ヒープがより多くのスペースを取得するので意味があります。
それは私の2番目の質問に私をもたらします。私がこれまでに読んだすべての記事で、著者はヒープが上向きに成長し、スタックが下向きに成長すると言っています。しかし、彼らが説明していないのは、ヒープがヒープとスタックの間のすべてのスペースを占有するとどうなるかということです。
投稿した図では、「ブレーク」、つまりbrk
およびsbrk
によって操作されるアドレスは、ヒープの上部にある点線です。
読んだドキュメントでは、これを「データセグメント」の終わりとして説明しています。これは、従来の(事前共有ライブラリ、pre _mmap
)Unixではデータセグメントがヒープと連続していたためです。プログラムの開始前に、カーネルは「テキスト」および「データ」ブロックをアドレスゼロから始まるRAMにロードします(実際にはアドレスゼロより少し上にあるため、NULLポインターは実際には何も指していません)ブレークアドレスをデータセグメントの末尾に設定します。 malloc
への最初の呼び出しは、sbrk
を使用してブレークを移動し、ヒープを作成しますの間にデータセグメントの上部と、新しい、より高いブレークアドレスダイアグラムに示されているmalloc
を後で使用すると、必要に応じてヒープが大きくなります。
その間、スタックはメモリの先頭から始まり、下に向かって成長します。スタックは、それを大きくするために明示的なシステムコールを必要としません。可能な限り多くのRAMが割り当てられている(これが従来のアプローチである)か、スタックの下に予約アドレスの領域があり、カーネルがRAMそこに書き込もうとすることに気づいたとき(これが最新のアプローチです)。いずれにしても、スタックに使用できるアドレス空間の下部に「ガード」領域がある場合とない場合があります。この領域が存在する場合(すべての最新システムがこれを実行します)、永久にマッピングされません。 eitherスタックまたはヒープがスタックに成長しようとすると、セグメンテーションエラーが発生します。ただし、伝統的に、カーネルは境界を強制しようとしませんでした。スタックがヒープに成長したり、ヒープがスタックに成長したりする可能性があり、どちらの方法でも互いのデータを落書きしてプログラムがクラッシュします。非常に幸運だった場合、すぐにクラッシュします。
この図の512GBがどこから来たのかはわかりません。 64ビットの仮想アドレス空間を意味しますが、これはそこにある非常に単純なメモリマップとは矛盾しています。実際の64ビットアドレス空間は、次のようになります。
Legend: t: text, d: data, b: BSS
これはリモートでスケーリングするものではなく、特定のOSがどのように動作するかを正確に解釈すべきではありません(私が描いた後、Linuxは実際に実行可能ファイルを思っていたよりもアドレス0にはるかに近づけることを発見し、共有ライブラリ驚くほど高いアドレスで)。この図の黒い領域はマップされていません-アクセスすると即座にセグメンテーション違反が発生します-そしてそれらは灰色の領域に対して巨大です。薄い灰色の領域はプログラムとその共有ライブラリです(数十の共有ライブラリが存在する可能性があります)。それぞれに独立テキストおよびデータセグメント(およびグローバルデータを含むが、ディスク上の実行可能ファイルまたはライブラリのスペースを占有するのではなく、全ビットゼロに初期化される「bss」セグメント)があります。ヒープは、実行可能ファイルのデータセグメントと必ずしも連続しているわけではありません-私はそのように描画しましたが、少なくともLinuxはそうしていません。スタックは仮想アドレス空間の最上部に固定されなくなり、ヒープとスタックの間の距離は非常に大きいため、それを越えることを心配する必要はありません。
ブレークは依然としてヒープの上限です。ただし、ここで示していないのは、mmap
の代わりにbrk
を使用して、黒のどこかに数十個の独立したメモリの割り当てがあることです。 (OSは、これらが衝突しないようにbrk
エリアから遠ざけようとします。)
最小限の実行可能な例
Brk()システムコールは何をしますか?
ヒープと呼ばれる連続したメモリチャンクを読み書きできるようにカーネルに要求します。
聞かないと、セグメンテーション違反になる可能性があります。
brk
なし:
#define _GNU_SOURCE
#include <unistd.h>
int main(void) {
/* Get the first address beyond the end of the heap. */
void *b = sbrk(0);
int *p = (int *)b;
/* May segfault because it is outside of the heap. */
*p = 1;
return 0;
}
brk
の場合:
#define _GNU_SOURCE
#include <assert.h>
#include <unistd.h>
int main(void) {
void *b = sbrk(0);
int *p = (int *)b;
/* Move it 2 ints forward */
brk(p + 2);
/* Use the ints. */
*p = 1;
*(p + 1) = 2;
assert(*p == 1);
assert(*(p + 1) == 2);
/* Deallocate back. */
brk(b);
return 0;
}
上記は、brk
がなくても新しいページにヒットせず、セグメンテーション違反ではない可能性があるため、16MiBを割り当てるより積極的なバージョンで、brk
なしでセグメンテーション違反が発生する可能性が非常に高くなります。
#define _GNU_SOURCE
#include <assert.h>
#include <unistd.h>
int main(void) {
void *b;
char *p, *end;
b = sbrk(0);
p = (char *)b;
end = p + 0x1000000;
brk(end);
while (p < end) {
*(p++) = 1;
}
brk(b);
return 0;
}
Ubuntu 18.04でテスト済み。
仮想アドレス空間の視覚化
brk
の前:
+------+ <-- Heap Start == Heap End
brk(p + 2)
の後:
+------+ <-- Heap Start + 2 * sizof(int) == Heap End
| |
| You can now write your ints
| in this memory area.
| |
+------+ <-- Heap Start
brk(b)
の後:
+------+ <-- Heap Start == Heap End
アドレス空間をよりよく理解するには、ページングに精通する必要があります。 x86ページングはどのように機能しますか? 。
なぜbrk
とsbrk
の両方が必要なのですか?
brk
は、もちろんsbrk
+ offsetの計算で実装できますが、両方とも便宜上存在しています。
バックエンドでは、Linuxカーネルv5.0にはbrk
という単一のシステムコールがあり、両方を実装するために使用されます。 https://github.com/torvalds/linux/blob/v5.0/Arch/x86/entry /syscalls/syscall_64.tbl#L2
12 common brk __x64_sys_brk
brk
POSIXですか?
brk
はPOSIXでしたが、POSIX 2001で削除されたため、_GNU_SOURCE
がglibcラッパーにアクセスする必要がありました。
削除は、mmap
の導入による可能性があります。これは、複数の範囲を割り当てることができるスーパーセットであり、より多くの割り当てオプションがあります。
最近、brk
またはmalloc
の代わりにmmap
を使用する必要がある有効なケースはないと思います。
brk
VS malloc
brk
は、malloc
を実装する古い可能性の1つです。
mmap
は、malloc
を実装するために現在すべてのPOSIXシステムが使用している可能性が高い、より厳密で強力なメカニズムです。
brk
とmallocを混在させることはできますか?
malloc
がbrk
で実装されている場合、brk
が単一の範囲のメモリのみを管理するため、それがどのように爆破できないのかわかりません。
しかし、glibc docsでそれについて何も見つけることができませんでした。例えば:
mmap
がmalloc
に使用される可能性が高いため、物事はおそらくそこで機能するでしょう。
こちらもご覧ください:
詳細情報
内部的には、カーネルはプロセスがその量のメモリを持つことができるかどうかを決定し、その使用法について memory pages を指定します。
これにより、スタックとヒープの比較方法が説明されます。 x86アセンブリのレジスタで使用されるプッシュ/ポップ命令の機能は何ですか?
brk
とsbrk
を自分で使用して、誰もが常に不満を抱いている「mallocオーバーヘッド」を回避できます。ただし、このメソッドをmalloc
と組み合わせて簡単に使用することはできないため、free
を使用する必要がない場合にのみ適切です。できないから。また、malloc
を内部で使用する可能性のあるライブラリー呼び出しを避ける必要があります。すなわち。 strlen
はおそらく安全ですが、fopen
はおそらく安全ではありません。
sbrk
を呼び出すのと同じようにmalloc
を呼び出します。現在のブレークへのポインターを返し、その分だけブレークを増やします。
void *myallocate(int n){
return sbrk(n);
}
個々の割り当てを解放することはできませんが(malloc-overhead、覚えていないため)、あなたはcanを解放しますスペース全体brk
の最初の呼び出しで返された値を使用してsbrk
を呼び出すことにより、brkを巻き戻します。
void *memorypool;
void initmemorypool(void){
memorypool = sbrk(0);
}
void resetmemorypool(void){
brk(memorypool);
}
これらのリージョンを積み重ねて、ブレークをリージョンの先頭に巻き戻すことで最新のリージョンを破棄することもできます。
もう1つ...
sbrk
は、 code golf でも便利です。これは、malloc
よりも2文字短いためです。
特別に指定された匿名プライベートメモリマッピングがあります(従来はdata/bssのすぐ上にありますが、最近のLinuxは実際にASLRで位置を調整します)。原則として、mmap
で作成できる他のどのマッピングよりも優れているわけではありませんが、Linuxには、ロックコストを削減しながら、このマッピングの終わりを(brk
syscallを使用して)拡張できるいくつかの最適化がありますmmap
またはmremap
が発生することに関連して。これにより、メインヒープを実装するときにmalloc
実装が魅力的になります。
mallocはbrkシステムコールを使用してメモリを割り当てます。
含める
int main(void){
char *a = malloc(10);
return 0;
}
この単純なプログラムをstraceで実行すると、brkシステムが呼び出されます。
2番目の質問に答えることができます。 Mallocは失敗し、nullポインターを返します。これが、メモリを動的に割り当てるときに常にNULLポインターをチェックする理由です。
ヒープは、プログラムのデータセグメントの最後に配置されます。 brk()
は、ヒープのサイズを変更(拡張)するために使用されます。ヒープがこれ以上成長できない場合、malloc
呼び出しは失敗します。
データセグメントは、すべての静的データを保持するメモリの部分であり、起動時に実行可能ファイルから読み込まれ、通常はゼロで埋められます。