Cosa fa la chiamata di sistema brk ()?


184

Secondo il manuale dei programmatori Linux:

brk () e sbrk () cambiano la posizione dell'interruzione del programma, che definisce la fine del segmento di dati del processo.

Cosa significa il segmento di dati qui? Sono solo il segmento di dati o i dati, BSS e heap combinati?

Secondo wiki:

A volte le aree di dati, BSS e heap vengono definite collettivamente "segmento di dati".

Non vedo alcun motivo per modificare la dimensione del solo segmento di dati. Se si tratta di dati, BSS e heap collettivamente, ha senso poiché l'heap avrà più spazio.

Il che mi porta alla mia seconda domanda. In tutti gli articoli che ho letto finora, l'autore afferma che l'heap cresce verso l'alto e lo stack cresce verso il basso. Ma quello che non spiegano è cosa succede quando l'heap occupa tutto lo spazio tra heap e stack?

inserisci qui la descrizione dell'immagine


1
Quindi cosa fai quando sei fuori dallo spazio? si passa a HDD. Dopo aver utilizzato lo spazio, lo si rilascia per altri tipi di informazioni.
Igoris Azanovas,

29
@Igoris: stai confondendo la memoria fisica (che puoi scambiare su disco secondo necessità, usando la memoria virtuale) e lo spazio degli indirizzi . Quando riempi lo spazio degli indirizzi, nessuna quantità di scambio ti restituirà quegli indirizzi nel mezzo.
Daniel Pryden,

7
Proprio come promemoria, la brk()chiamata di sistema è più utile nel linguaggio assembly che in C. In C, malloc()dovrebbe essere utilizzata invece che brk()per scopi di allocazione dei dati, ma ciò non invalida la domanda proposta in alcun modo.
alecov,

2
@Brian: l'heap è una struttura di dati complessa per la gestione di aree di dimensioni e allineamenti diversi, pool libero, ecc. Le pile di thread sono sempre sequenze contigue (nello spazio di indirizzi virtuali) di pagine complete. Nella maggior parte dei sistemi operativi, c'è un allocatore di pagina alla base di stack, heap e file mappati in memoria.
Ben Voigt,

2
@Brian: Chi ha detto che c'è qualche "Stack" manipolato da brk()e sbrk()? Gli stack sono gestiti dall'allocatore di pagine, a un livello molto più basso.
Ben Voigt,

Risposte:


233

Nel diagramma che hai pubblicato, l '"interruzione" - l'indirizzo manipolato da brke sbrk- è la linea tratteggiata nella parte superiore dell'heap.

immagine semplificata del layout della memoria virtuale

La documentazione che hai letto descrive questa come la fine del "segmento di dati" perché nelle mmapUnix tradizionali (pre-condivise, pre ) il segmento di dati era continuo con l'heap; prima dell'avvio del programma, il kernel caricava i blocchi di "testo" e "dati" nella RAM a partire dall'indirizzo zero (in realtà un po 'sopra l'indirizzo zero, in modo che il puntatore NULL non indicasse realmente nulla) e impostava l'indirizzo di interruzione su la fine del segmento di dati. La prima chiamata a mallocverrebbe quindi utilizzata sbrkper spostare la suddivisione e creare l'heap tra la parte superiore del segmento di dati e il nuovo indirizzo di interruzione più alto, come mostrato nel diagramma, e il successivo utilizzo di malloclo userebbe per ingrandire l'heap come necessario.

Nel frattempo, lo stack inizia nella parte superiore della memoria e cresce. Lo stack non ha bisogno di chiamate di sistema esplicite per ingrandirlo; o inizia con la quantità di RAM allocata che può mai avere (questo era l'approccio tradizionale) o c'è una regione di indirizzi riservati sotto lo stack, a cui il kernel alloca automaticamente la RAM quando nota un tentativo di scrivere lì (questo è l'approccio moderno). Ad ogni modo, potrebbe esserci o meno una regione di "protezione" nella parte inferiore dello spazio degli indirizzi che può essere utilizzata per lo stack. Se questa regione esiste (tutti i sistemi moderni lo fanno) è permanentemente non mappata; se neanchelo stack o l'heap tenta di crescere in esso, si ottiene un errore di segmentazione. Tradizionalmente, tuttavia, il kernel non faceva alcun tentativo di imporre un limite; lo stack potrebbe crescere nell'heap o l'heap potrebbe crescere nello stack e in entrambi i casi si scarabocchierebbero sui dati reciproci e il programma si arresterebbe in modo anomalo. Se tu fossi molto fortunato, si schianterebbe immediatamente.

Non sono sicuro da dove provenga il numero 512 GB in questo diagramma. Implica uno spazio di indirizzi virtuali a 64 bit, incompatibile con la semplicissima mappa di memoria disponibile. Un vero spazio degli indirizzi a 64 bit è più simile a questo:

spazio di indirizzi meno semplificato

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

Questo non è scalabile in remoto, e non dovrebbe essere interpretato esattamente come ogni dato sistema operativo fa cose (dopo averlo disegnato ho scoperto che Linux in realtà mette l'eseguibile molto più vicino a zero di quanto pensassi, e le librerie condivise a indirizzi sorprendentemente alti). Le regioni nere di questo diagramma non sono mappate - qualsiasi accesso provoca un immediato segfault - e sono gigantesche rispetto alle aree grigie. Le regioni grigio chiaro sono il programma e le sue librerie condivise (ci possono essere dozzine di librerie condivise); ognuno ha un indipendentesegmento di testo e dati (e segmento "bss", che contiene anche dati globali ma è inizializzato su zero-bit anziché occupare spazio nell'eseguibile o nella libreria su disco). L'heap non è più necessariamente in linea con il segmento di dati dell'eseguibile - l'ho disegnato in quel modo, ma almeno sembra che Linux non lo faccia. Lo stack non è più ancorato nella parte superiore dello spazio degli indirizzi virtuali e la distanza tra l'heap e lo stack è così enorme che non devi preoccuparti di attraversarlo.

L'interruzione è ancora il limite superiore dell'heap. Tuttavia, ciò che non ho mostrato è che potrebbero esserci dozzine di allocazioni indipendenti di memoria là fuori nel nero da qualche parte, fatte con mmapinvece di brk. (Il sistema operativo cercherà di tenerli lontani brkdall'area in modo che non si scontrino.)


7
+1 per una spiegazione dettagliata. Sai se mallocsi basa ancora brko se sta usando mmapper essere in grado di "restituire" blocchi di memoria separati?
Anders Abel,

18
Dipende dall'implementazione specifica, ma IIUC molti utenti attuali mallocutilizzano l' brkarea per allocazioni di piccole dimensioni e singoli utenti mmapper allocazioni di grandi dimensioni (diciamo,> 128 KB). Vedi, ad esempio, la discussione su MMAP_THRESHOLD nella malloc(3)manpage di Linux .
zwol,

1
Anzi una buona spiegazione. Ma come hai detto, Stack non si trova più nella parte superiore dello spazio degli indirizzi virtuali. È vero solo per lo spazio degli indirizzi a 64 bit o è vero anche per lo spazio degli indirizzi a 32 bit. E se lo stack si trova nella parte superiore dello spazio degli indirizzi, dove si verificano le mappe di memoria anonime? Si trova nella parte superiore dello spazio di indirizzi virtuale appena prima dello stack.
nik,

3
@Nikhil: è complicato. La maggior parte dei sistemi a 32 bit colloca lo stack in cima allo spazio degli indirizzi in modalità utente , che spesso è solo il 2 o 3G inferiore dello spazio degli indirizzi completo (lo spazio rimanente è riservato al kernel). Al momento non riesco a pensare a uno che non lo sapesse ma non li conosco tutti. La maggior parte delle CPU a 64 bit non ti consente effettivamente di utilizzare l'intero spazio a 64 bit; gli alti da 10 a 16 bit dell'indirizzo devono essere tutti zero o tutto uno. Lo stack viene generalmente posizionato vicino alla parte superiore degli indirizzi bassi utilizzabili. Non posso darti una regola per mmap; è estremamente dipendente dal sistema operativo.
zwol,

3
@RiccardoBestetti Spreca spazio di indirizzamento , ma è innocuo: uno spazio di indirizzamento virtuale a 64 bit è così grande che se si esaurisse un gigabyte di esso ogni secondo ci vorranno ancora 500 anni per finire. [1] La maggior parte dei processori non consente nemmeno l'uso di più di 2 ^ 48 a 2 ^ 53 bit di indirizzo virtuale (l'unica eccezione che conosco è POWER4 in modalità tabella di pagine con hash). Non spreca RAM fisica; gli indirizzi non utilizzati non sono assegnati alla RAM.
zwol

26

Esempio eseguibile minimo

Cosa fa la chiamata di sistema brk ()?

Chiede al kernel di farti leggere e scrivere in un pezzo contiguo di memoria chiamato heap.

Se non lo chiedi, potrebbe segfault.

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

Con 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 a monte .

Quanto sopra potrebbe non colpire una nuova pagina e non essere segfault anche senza il brk , quindi ecco una versione più aggressiva che alloca 16 MiB ed è molto probabile che segfault senza 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;
}

Testato su Ubuntu 18.04.

Visualizzazione dello spazio degli indirizzi virtuali

Prima brk :

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

Dopo brk(p + 2) :

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

Dopo brk(b) :

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

Per comprendere meglio gli spazi degli indirizzi, è necessario acquisire familiarità con il paging: come funziona il paging x86? .

Perché abbiamo bisogno di entrambi brk e sbrk?

brkpotrebbe ovviamente essere implementato con i sbrkcalcoli + offset, entrambi esistono solo per comodità.

Nel backend, il kernel Linux v5.0 ha una singola chiamata di sistema brkche viene utilizzata per implementare entrambi: https://github.com/torvalds/linux/blob/v5.0/arch/x86/entry/syscalls/syscall_64. TBL # L23

12  common  brk         __x64_sys_brk

brkPOSIX è ?

brkera POSIX, ma è stato rimosso in POSIX 2001, quindi la necessità _GNU_SOURCEdi accedere al wrapper glibc.

La rimozione è probabilmente dovuta all'introduzione mmap, che è un superset che consente di assegnare più intervalli e più opzioni di allocazione.

Penso che non ci siano casi validi in cui dovresti usare al brkposto malloco al mmapgiorno d'oggi.

brk vs malloc

brkè una vecchia possibilità di implementazione malloc.

mmapè il nuovissimo meccanismo rigorosamente più potente che probabilmente tutti i sistemi POSIX attualmente utilizzano per implementare malloc. Ecco un runnable minimommap allocazione della memoria .

Posso mescolare brk e malloc?

Se il tuo mallocè implementato con brk, non ho idea di come ciò non possa far esplodere le cose, da allorabrk che gestisce solo un singolo intervallo di memoria.

Tuttavia non sono riuscito a trovare nulla al riguardo sui documenti glibc, ad esempio:

Le cose probabilmente funzioneranno lì, suppongo da allora mmap è probabilmente usato per malloc.

Guarda anche:

Ulteriori informazioni

Internamente, il kernel decide se il processo può avere tanta memoria e stacca le pagine di memoria per quell'uso.

Questo spiega come lo stack si confronta con l'heap: qual è la funzione delle istruzioni push / pop usate sui registri nell'assembly x86?


4
Dato che pè un puntatore da digitare int, non dovrebbe essere stato brk(p + 2);?
Johan Boulé,

Piccola nota: l'espressione nel for-loop della versione aggressiva dovrebbe probabilmente essere*(p + i) = 1;
lima.sierra

A proposito, perché dobbiamo usare un brk(p + 2)invece di semplicemente aumentarlo di sbrk(2)? Brk è davvero necessario?
Yi Lin Liu,

1
@YiLinLiu Penso che siano solo due frontend C molto simili per un singolo backend del kernel ( brksyscall). brkè leggermente più conveniente ripristinare lo stack precedentemente allocato.
Ciro Santilli 21 冠状 病 六四 事件 法轮功

1
@CiroSantilli 新疆 改造 中心 996ICU 六四 事件 Considerando la dimensione di int come 4 byte e la dimensione di un int * come 4 byte (su una macchina a 32 bit), mi chiedevo che non dovesse essere incrementato di soli 4 byte (invece di 8 - (2 * sizeof int)). Non dovrebbe puntare alla successiva memoria heap disponibile, che sarà di 4 byte di distanza (non 8). Correggimi se mi manca qualcosa qui.
Saket Sharad,

10

È possibile utilizzare brke sbrkda soli per evitare il tutti "malloc in testa" è sempre lamentarsi. Ma non puoi facilmente usare questo metodo in combinazione con mallocquindi è appropriato solo quando non devi fare freenulla. Perché non puoi. Inoltre, è necessario evitare qualsiasi chiamata in biblioteca che può essere utilizzata mallocinternamente. Vale a dire. strlenè probabilmente sicuro, ma fopenprobabilmente non lo è.

Chiama sbrkcome faresti tu malloc. Restituisce un puntatore all'interruzione corrente e incrementa l'interruzione di tale importo.

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

Sebbene non sia possibile liberare singole allocazioni (poiché non esiste un overhead di malloc , ricordate), è possibile liberare l'intero spazio chiamando brkcon il valore restituito dalla prima chiamata a sbrk, riavvolgendo così il brk .

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

È anche possibile impilare queste regioni, scartando la regione più recente riavvolgendo la pausa all'inizio della regione.


Un'altra cosa ...

sbrkè utile anche nel codice golf perché è più breve di 2 caratteri malloc.


7
-1 perché: malloc/ freesicuramente può (e fare) restituire memoria al sistema operativo. Potrebbero non farlo sempre quando lo desideri, ma è una questione di euristica messa a punto in modo imperfetto per il tuo caso d'uso. Ancora più importante, non è sicuro chiamare sbrkcon un argomento diverso da zero in qualsiasi programma che possa mai chiamare malloc- e quasi tutte le funzioni della libreria C possono chiamare mallocinternamente. Le uniche che sicuramente non lo saranno sono le funzioni di sicurezza del segnale asincrono .
zwol,

E per "non è sicuro", intendo "il tuo programma andrà in crash".
zwol,

Ho modificato per rimuovere il vanto della memoria di ritorno e ho menzionato il pericolo che le funzioni di libreria utilizzino internamente malloc.
Luser droog

1
Se si desidera eseguire un'allocazione di memoria elaborata, basarla sulla parte superiore di Malloc o sulla parte superiore di mmap. Non toccare brk e sbrk, sono reliquie del passato che fanno più male che bene (anche le manpage ti dicono di starne alla larga!)
Eloff

3
Questo è sciocco. Se vuoi evitare il sovraccarico di malloc per molte piccole allocazioni, fai una grande allocazione (con malloc o mmap, non sbrk) e distribuiscila tu stesso. Se si mantengono i nodi dell'albero binario in un array, è possibile utilizzare indici 8b o 16b anziché puntatori 64b. Funziona alla grande quando non è necessario eliminare alcun nodo fino a quando non si è pronti per eliminare tutti i nodi. (ad esempio, costruire un dizionario ordinato al volo.) L'uso sbrkper questo è utile solo per il code-golf, perché l'uso manuale mmap(MAP_ANONYMOUS)è migliore in tutti i modi tranne la dimensione del codice sorgente.
Peter Cordes,

3

Esiste una speciale mappatura della memoria privata anonima designata (tradizionalmente situata appena oltre i dati / bss, ma Linux moderno regolerà effettivamente la posizione con ASLR). In linea di principio non è migliore di qualsiasi altra mappatura con cui potresti creare mmap, ma Linux ha alcune ottimizzazioni che consentono di espandere la fine di questa mappatura (usando la brksyscall) verso l'alto con un costo di blocco ridotto rispetto a ciò che mmapo mremappotrebbe incorrere. Ciò rende interessante l' mallocutilizzo da parte delle implementazioni durante l'implementazione dell'heap principale.


Volevi dire possibile espandere la fine di questa mappatura verso l'alto, sì?
zwol,

Sì, risolto. Mi dispiace per quello!
R .. GitHub smette di aiutare ICE il

0

Posso rispondere alla tua seconda domanda. Malloc fallirà e restituirà un puntatore nullo. Ecco perché controlli sempre un puntatore nullo durante l'allocazione dinamica della memoria.


allora a che serve brk e sbrk?
nik,

3
@NikhilRathod: malloc()utilizzerà brk()e / o sbrk()sotto il cofano - e puoi farlo anche se vuoi implementare la tua versione personalizzata di malloc().
Daniel Pryden,

@Daniel Pryden: come possono funzionare brk e sbrk sull'heap quando si trova tra lo stack e il segmento di dati, come mostrato nel diagramma sopra. perché questo funzioni, l'heap dovrebbe essere alla fine. Ho ragione?
nik,

2
@Brian: Daniel ha detto che il sistema operativo gestisce il segmento dello stack , non il puntatore dello stack ... cose molto diverse. Il punto è che non esiste syscall sbrk / brk per il segmento di stack: Linux alloca automaticamente le pagine quando tenta di scrivere alla fine del segmento di stack.
Jim Balter,

1
E Brian, hai risposto solo a metà della metà della domanda. L'altra metà è ciò che accade se si tenta di spingere nello stack quando non è disponibile spazio ... si ottiene un errore di segmentazione.
Jim Balter,

0

L'heap viene inserito per ultimo nel segmento di dati del programma. brk()viene utilizzato per modificare (espandere) la dimensione dell'heap. Quando l'heap non può più crescere, qualsiasi mallocchiamata fallirà.


Quindi stai dicendo che tutti i diagrammi su Internet, come quello nella mia domanda, sono sbagliati. Se possibile, puoi indicarmi un diagramma corretto.
nik,

2
@Nikkhil Tieni presente che la parte superiore di quel diagramma è la fine della memoria. La parte superiore della pila si sposta verso il basso sul diagramma man mano che la pila cresce. La parte superiore dell'heap si sposta verso l' alto sul diagramma man mano che viene espansa.
Brian Gordon,

0

Il segmento di dati è la porzione di memoria che contiene tutti i tuoi dati statici, letti dall'eseguibile all'avvio e di solito a zero.


Contiene anche dati statici non inizializzati (non presenti nell'eseguibile) che possono essere spazzatura.
Luser droog

I dati statici non .bssinizializzati ( ) sono inizializzati a zero-bit dal sistema operativo prima dell'avvio del programma; questo è in realtà garantito dallo standard C. Alcuni sistemi embedded potrebbero non infastidire, suppongo (non ne ho mai visto uno, ma non lavoro così tanto)
zwol

@zwol: Linux ha un'opzione di compilazione per non azzerare le pagine restituite mmap, ma suppongo che .bsssarebbe ancora azzerato. Lo spazio BSS è probabilmente il modo più compatto per esprimere il fatto che un programma vuole alcuni array di zerod.
Peter Cordes,

1
@PeterCordes Quello che dice lo standard C è che le variabili globali dichiarate senza inizializzatore vengono trattate come se inizializzate a zero. L'implementazione CA che inserisce tali variabili .bsse non azzererebbe .bsspertanto non conforme. Ma nulla obbliga un'implementazione C a usare .bsso addirittura avere una cosa del genere.
zwol,

@PeterCordes Inoltre, la linea tra "implementazione C" e il programma può essere molto sfocata, ad es. Di solito c'è una piccola porzione di codice dall'implementazione, staticamente collegata a ciascun eseguibile, che viene eseguita prima main; quel codice potrebbe azzerare l' .bssarea piuttosto che farlo fare al kernel, e sarebbe comunque conforme.
zwol,

0

malloc usa la chiamata di sistema brk per allocare memoria.

includere

int main(void){

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

esegui questo semplice programma con strace, chiamerà brk system.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.