web-dev-qa-db-ja.com

brk()システムコールは何をしますか?

Linuxプログラマーマニュアルによると:

brk()およびsbrk()は、プロセスのデータセグメントの終了を定義するプログラムブレークの場所を変更します。

ここでのデータセグメントとはどういう意味ですか?データセグメントまたはデータ、BSS、およびヒープを組み合わせただけですか?

ウィキによると:

データ、BSS、およびヒープ領域は、まとめて「データセグメント」と呼ばれることがあります。

データセグメントだけのサイズを変更する理由はありません。それがデータ、 BSS であり、ヒープが集合的である場合、ヒープがより多くのスペースを取得するので意味があります。

それは私の2番目の質問に私をもたらします。私がこれまでに読んだすべての記事で、著者はヒープが上向きに成長し、スタックが下向きに成長すると言っています。しかし、彼らが説明していないのは、ヒープがヒープとスタックの間のすべてのスペースを占有するとどうなるかということです。

enter image description here

165
nik

投稿した図では、「ブレーク」、つまりbrkおよびsbrkによって操作されるアドレスは、ヒープの上部にある点線です。

simplified image of virtual memory layout

読んだドキュメントでは、これを「データセグメント」の終わりとして説明しています。これは、従来の(事前共有ライブラリ、pre _mmap)Unixではデータセグメントがヒープと連続していたためです。プログラムの開始前に、カーネルは「テキスト」および「データ」ブロックをアドレスゼロから始まるRAMにロードします(実際にはアドレスゼロより少し上にあるため、NULLポインターは実際には何も指していません)ブレークアドレスをデータセグメントの末尾に設定します。 mallocへの最初の呼び出しは、sbrkを使用してブレークを移動し、ヒープを作成しますの間にデータセグメントの上部と、新しい、より高いブレークアドレスダイアグラムに示されているmallocを後で使用すると、必要に応じてヒープが大きくなります。

その間、スタックはメモリの先頭から始まり、下に向かって成長します。スタックは、それを大きくするために明示的なシステムコールを必要としません。可能な限り多くのRAMが割り当てられている(これが従来のアプローチである)か、スタックの下に予約アドレスの領域があり、カーネルがRAMそこに書き込もうとすることに気づいたとき(これが最新のアプローチです)。いずれにしても、スタックに使用できるアドレス空間の下部に「ガード」領域がある場合とない場合があります。この領域が存在する場合(すべての最新システムがこれを実行します)、永久にマッピングされません。 eitherスタックまたはヒープがスタックに成長しようとすると、セグメンテーションエラーが発生します。ただし、伝統的に、カーネルは境界を強制しようとしませんでした。スタックがヒープに成長したり、ヒープがスタックに成長したりする可能性があり、どちらの方法でも互いのデータを落書きしてプログラムがクラッシュします。非常に幸運だった場合、すぐにクラッシュします。

この図の512GBがどこから来たのかはわかりません。 64ビットの仮想アドレス空間を意味しますが、これはそこにある非常に単純なメモリマップとは矛盾しています。実際の64ビットアドレス空間は、次のようになります。

less simplified address space

              Legend:  t: text, d: data, b: BSS

これはリモートでスケーリングするものではなく、特定のOSがどのように動作するかを正確に解釈すべきではありません(私が描いた後、Linuxは実際に実行可能ファイルを思っていたよりもアドレス0にはるかに近づけることを発見し、共有ライブラリ驚くほど高いアドレスで)。この図の黒い領域はマップされていません-アクセスすると即座にセグメンテーション違反が発生します-そしてそれらは灰色の領域に対して巨大です。薄い灰色の領域はプログラムとその共有ライブラリです(数十の共有ライブラリが存在する可能性があります)。それぞれに独立テキストおよびデータセグメント(およびグローバルデータを含むが、ディスク上の実行可能ファイルまたはライブラリのスペースを占有するのではなく、全ビットゼロに初期化される「bss」セグメント)があります。ヒープは、実行可能ファイルのデータセグメントと必ずしも連続しているわけではありません-私はそのように描画しましたが、少なくともLinuxはそうしていません。スタックは仮想アドレス空間の最上部に固定されなくなり、ヒープとスタックの間の距離は非常に大きいため、それを越えることを心配する必要はありません。

ブレークは依然としてヒープの上限です。ただし、ここで示していないのは、mmapの代わりにbrkを使用して、黒のどこかに数十個の独立したメモリの割り当てがあることです。 (OSは、これらが衝突しないようにbrkエリアから遠ざけようとします。)

214
zwol

最小限の実行可能な例

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;
}

GitHubアップストリーム

上記は、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ページングは​​どのように機能しますか?

なぜbrksbrkの両方が必要なのですか?

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を混在させることはできますか?

mallocbrkで実装されている場合、brkが単一の範囲のメモリのみを管理するため、それがどのように爆破できないのかわかりません。

しかし、glibc docsでそれについて何も見つけることができませんでした。例えば:

mmapmallocに使用される可能性が高いため、物事はおそらくそこで機能するでしょう。

こちらもご覧ください:

詳細情報

内部的には、カーネルはプロセスがその量のメモリを持つことができるかどうかを決定し、その使用法について memory pages を指定します。

これにより、スタックとヒープの比較方法が説明されます。 x86アセンブリのレジスタで使用されるプッシュ/ポップ命令の機能は何ですか?

brksbrkを自分で使用して、誰もが常に不満を抱いている「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文字短いためです。

9
luser droog

特別に指定された匿名プライベートメモリマッピングがあります(従来はdata/bssのすぐ上にありますが、最近のLinuxは実際にASLRで位置を調整します)。原則として、mmapで作成できる他のどのマッピングよりも優れているわけではありませんが、Linuxには、ロックコストを削減しながら、このマッピングの終わりを(brk syscallを使用して)拡張できるいくつかの最適化がありますmmapまたはmremapが発生することに関連して。これにより、メインヒープを実装するときにmalloc実装が魅力的になります。

3
R..

mallocはbrkシステムコールを使用してメモリを割り当てます。

含める

int main(void){

char *a = malloc(10); 
return 0;
}

この単純なプログラムをstraceで実行すると、brkシステムが呼び出されます。

0
skanzariya

2番目の質問に答えることができます。 Mallocは失敗し、nullポインターを返します。これが、メモリを動的に割り当てるときに常にNULLポインターをチェックする理由です。

0
Brian Gordon

ヒープは、プログラムのデータセグメントの最後に配置されます。 brk()は、ヒープのサイズを変更(拡張)するために使用されます。ヒープがこれ以上成長できない場合、malloc呼び出しは失敗します。

0
Anders Abel

データセグメントは、すべての静的データを保持するメモリの部分であり、起動時に実行可能ファイルから読み込まれ、通常はゼロで埋められます。

0
monchalve