web-dev-qa-db-de.com

Was macht der Systemaufruf brk ()?

Laut Linux-Programmierhandbuch:

brk () und sbrk () ändern die Position der Programmunterbrechung, die das Ende des Datensegments des Prozesses definiert.

Was bedeutet das Datensegment hier? Ist es nur das Datensegment oder Daten, BSS und Heap kombiniert?

Laut Wiki:

Manchmal werden die Daten-, BSS- und Heap-Bereiche zusammen als "Datensegment" bezeichnet.

Ich sehe keinen Grund, die Größe nur des Datensegments zu ändern. Wenn es sich um Daten handelt, BSS und Heap insgesamt, ist dies sinnvoll, da Heap mehr Speicherplatz erhält.

Das bringt mich zu meiner zweiten Frage. In allen Artikeln, die ich bisher gelesen habe, sagt der Autor, dass der Haufen nach oben und der Stapel nach unten wächst. Was sie aber nicht erklären, ist, was passiert, wenn der Heap den gesamten Raum zwischen Heap und Stack einnimmt?

enter image description here

165
nik

In dem Diagramm, das Sie gepostet haben, ist der "Umbruch" - die von brk und sbrk manipulierte Adresse - die gepunktete Linie oben auf dem Haufen.

simplified image of virtual memory layout

Die Dokumentation, die Sie gelesen haben, beschreibt dies als das Ende des "Datensegments", da in herkömmlichen (vorinstallierten Bibliotheken, premmap) Unix das Datensegment mit dem Heap fortlaufend war. Vor dem Programmstart lud der Kernel die Blöcke "text" und "data" in RAM ab Adresse Null (eigentlich etwas über Adresse Null), so dass der NULL-Zeiger wirklich nicht darauf hinwies Der erste Aufruf von malloc würde dann sbrk verwenden, um den Breakup zu verschieben und den Heap zu erstellen dazwischen) Das obere Ende des Datensegments und die neue, höhere Unterbrechungsadresse, wie im Diagramm gezeigt, und die nachfolgende Verwendung von malloc würden dazu verwendet, den Heap nach Bedarf zu vergrößern.

In der Zwischenzeit beginnt der Stapel am oberen Rand des Speichers und wächst nach. Der Stack benötigt keine expliziten Systemaufrufe, um größer zu werden. Entweder wird so viel RAM zugewiesen, wie es jemals sein kann (dies war der traditionelle Ansatz) oder es gibt einen Bereich reservierter Adressen unterhalb des Stacks, dem der Kernel automatisch zuweist RAM wenn es einen Schreibversuch bemerkt (dies ist der moderne Ansatz). In beiden Fällen kann es eine "Schutz" -Region am unteren Rand des Adressraums geben oder auch nicht Wenn diese Region existiert (alle modernen Systeme tun dies), ist sie permanent nicht zugeordnet. Wenn entweder der Stapel oder der Heap versucht, hineinzuwachsen, erhalten Sie einen Segmentierungsfehler. Der Kernel machte keinen Versuch, eine Grenze zu erzwingen, der Stapel könnte in den Haufen hineinwachsen, oder der Haufen könnte in den Stapel hineinwachsen, und so oder so würden sie über die Daten des jeweils anderen kritzeln, und das Programm würde abstürzen sofort abstürzen.

Ich bin mir nicht sicher, woher die Nummer 512GB in diesem Diagramm kommt. Dies impliziert einen virtuellen 64-Bit-Adressraum, der nicht mit der sehr einfachen Speicherzuordnung übereinstimmt, die Sie dort haben. Ein echter 64-Bit-Adressraum sieht ungefähr so ​​aus:

less simplified address space

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

Dies ist nicht remote skalierbar, und es sollte nicht so interpretiert werden, wie ein bestimmtes Betriebssystem genau funktioniert (nachdem ich es gezeichnet habe, habe ich festgestellt, dass Linux die ausführbare Datei tatsächlich viel näher an die Adresse Null heranführt, als ich dachte, und die gemeinsam genutzten Bibliotheken bei überraschend hohen Adressen). Die schwarzen Bereiche dieses Diagramms sind nicht zugeordnet - jeder Zugriff führt zu einem sofortigen Segfault - und sie sind gigantisch relativ zu den grauen Bereichen. Die hellgrauen Bereiche sind das Programm und seine gemeinsam genutzten Bibliotheken (es können Dutzende von gemeinsam genutzten Bibliotheken vorhanden sein). Jedes hat ein nabhängiges Text- und Datensegment (und ein "bss" -Segment, das ebenfalls globale Daten enthält, jedoch auf alle Bits Null initialisiert ist, anstatt Speicherplatz in der ausführbaren Datei oder Bibliothek auf der Festplatte zu belegen). Der Heap ist nicht mehr unbedingt kontinuierlich mit dem Datensegment der ausführbaren Datei - ich habe es so gezeichnet, aber es sieht so aus, als würde Linux das zumindest nicht tun. Der Stapel ist nicht mehr an den oberen Rand des virtuellen Adressraums gebunden, und der Abstand zwischen dem Heap und dem Stapel ist so groß, dass Sie sich nicht darum kümmern müssen, ihn zu überqueren.

Die Pause ist immer noch die Obergrenze des Haufens. Was ich jedoch nicht gezeigt habe, ist, dass es irgendwo Dutzende von unabhängigen Speicherzuordnungen geben könnte, die mit mmap anstelle von brk erstellt wurden. (Das Betriebssystem wird versuchen, diese vom Bereich brk fernzuhalten, damit sie nicht kollidieren.)

214
zwol

Minimales lauffähiges Beispiel

Was macht der Systemaufruf brk ()?

Bittet den Kernel, Ihnen das Lesen und Schreiben eines zusammenhängenden Speicherbereichs zu ermöglichen, der als Heap bezeichnet wird.

Wenn Sie nicht danach fragen, kann dies zu Fehlern führen.

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

Mit 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 upstream .

Das oben Genannte trifft möglicherweise nicht auf eine neue Seite und schlägt auch ohne brk nicht fehl. Daher ist hier eine aggressivere Version, die 16 MB zuweist und sehr wahrscheinlich ohne brk fehlschlägt:

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

Getestet unter Ubuntu 18.04.

Visualisierung des virtuellen Adressraums

Vor brk:

+------+ <-- Heap Start == Heap End

Nach brk(p + 2):

+------+ <-- Heap Start + 2 * sizof(int) == Heap End 
|      |
| You can now write your ints
| in this memory area.
|      |
+------+ <-- Heap Start

Nach brk(b):

+------+ <-- Heap Start == Heap End

Um Adressräume besser zu verstehen, sollten Sie sich mit Paging vertraut machen: Wie funktioniert x86-Paging? .

Warum brauchen wir sowohl brk als auch sbrk?

brk könnte natürlich mit sbrk + Offsetberechnungen implementiert werden, beide existieren nur zur Vereinfachung.

Im Backend verfügt der Linux-Kernel v5.0 über einen einzigen Systemaufruf brk, mit dem beide implementiert werden: https://github.com/torvalds/linux/blob/v5.0/ Arch/x86/entry/syscalls/syscall_64.tbl # L2

12  common  brk         __x64_sys_brk

Ist brk POSIX?

brk war früher POSIX, wurde aber in POSIX 2001 entfernt, weshalb _GNU_SOURCE für den Zugriff auf den glibc-Wrapper erforderlich ist.

Das Entfernen ist wahrscheinlich auf die Einführung mmap zurückzuführen, bei der es sich um eine Obermenge handelt, die die Zuweisung mehrerer Bereiche und weiterer Zuweisungsoptionen ermöglicht.

Ich denke, es gibt keinen gültigen Fall, in dem Sie heutzutage brk anstelle von malloc oder mmap verwenden sollten.

brk vs malloc

brk ist eine alte Möglichkeit, malloc zu implementieren.

mmap ist der neuere, streng leistungsfähigere Mechanismus, mit dem wahrscheinlich alle POSIX-Systeme derzeit malloc implementieren.

Kann ich brk und malloc mischen?

Wenn Ihr malloc mit brk implementiert ist, habe ich keine Ahnung, wie das die Dinge möglicherweise nicht in die Luft jagen kann, da brk nur einen einzigen Speicherbereich verwaltet.

Ich konnte jedoch nichts darüber in den glibc-Dokumenten finden, z.

Es wird wahrscheinlich nur dort funktionieren, da mmap wahrscheinlich für malloc verwendet wird.

Siehe auch:

Weitere Infos

Intern entscheidet der Kernel, ob der Prozess so viel Speicher haben kann, und kennzeichnet Speicherseiten für diese Verwendung.

Dies erklärt, wie der Stack mit dem Heap verglichen wird: Welche Funktion haben die Push/Pop-Anweisungen, die für Register in der x86-Assembly verwendet werden?

Sie können brk und sbrk selbst verwenden, um den "malloc overhead" zu vermeiden, über den sich immer alle beschweren. Sie können diese Methode jedoch nicht einfach in Verbindung mit malloc verwenden, sodass sie nur dann sinnvoll ist, wenn Sie free nichts müssen. Weil du nicht kannst. Außerdem sollten Sie Bibliotheksaufrufe vermeiden, die malloc intern verwenden. Dh strlen ist wahrscheinlich sicher, fopen aber wahrscheinlich nicht.

Rufen Sie sbrk auf, genau wie Sie malloc aufrufen würden. Es gibt einen Zeiger auf die aktuelle Unterbrechung zurück und erhöht die Unterbrechung um diesen Betrag.

void *myallocate(int n){
    return sbrk(n);
}

Während Sie einzelne Zuordnungen nicht freigeben können (weil es keinen Malloc-Overhead gibt , denken Sie daran), können Sie Geben Sie den gesamten Speicherplatz frei , indem Sie brk mit dem Wert aufrufen, der beim ersten Aufruf von sbrk, also das brk zurückspulen .

void *memorypool;
void initmemorypool(void){
    memorypool = sbrk(0);
}
void resetmemorypool(void){
    brk(memorypool);
}

Sie können diese Regionen sogar stapeln und die letzte Region verwerfen, indem Sie die Unterbrechung an den Anfang der Region zurückspulen.


Noch eine Sache ...

sbrk ist auch nützlich in Code Golf , da es 2 Zeichen kürzer als malloc ist.

9
luser droog

Es gibt eine spezielle zugewiesene Zuordnung des anonymen privaten Speichers (traditionell direkt hinter den Daten/bss, aber modernes Linux passt den Speicherort tatsächlich mit ASLR an). Im Prinzip ist es nicht besser als jedes andere Mapping, das Sie mit mmap erstellen könnten, aber Linux verfügt über einige Optimierungen, die es ermöglichen, das Ende dieses Mappings (mit dem Syscall brk) nach oben zu erweitern Sperrkosten im Verhältnis zu dem, was mmap oder mremap verursachen würde. Dies macht es attraktiv für malloc -Implementierungen, die beim Implementieren des Hauptheaps verwendet werden.

3
R..

malloc verwendet den Systemaufruf brk, um Speicher zuzuweisen.

umfassen

int main(void){

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

führen Sie dieses einfache Programm mit strace aus, es ruft brk system auf.

0
skanzariya

Ich kann Ihre zweite Frage beantworten. Malloc schlägt fehl und gibt einen Nullzeiger zurück. Aus diesem Grund suchen Sie beim dynamischen Zuweisen von Speicher immer nach einem Nullzeiger.

0
Brian Gordon

Der Heap wird zuletzt im Datensegment des Programms platziert. brk() dient zum Ändern (Erweitern) der Größe des Heapspeichers. Wenn der Heap nicht mehr wachsen kann, schlägt ein malloc -Aufruf fehl.

0
Anders Abel

Das Datensegment ist der Teil des Speichers, der alle statischen Daten enthält, die beim Start aus der ausführbaren Datei eingelesen und normalerweise mit Nullen gefüllt werden.

0
monchalve