GNU/Linux >> Linux Esercitazione >  >> Linux

Cosa fa la chiamata di sistema brk()?

Esiste una speciale mappatura della memoria privata anonima designata (tradizionalmente posizionata appena oltre data/bss, ma il moderno Linux regolerà effettivamente la posizione con ASLR). In linea di principio non è migliore di qualsiasi altra mappatura che potresti creare con mmap , ma Linux ha alcune ottimizzazioni che rendono possibile espandere la fine di questa mappatura (utilizzando il brk syscall) verso l'alto con costi di blocco ridotti rispetto a quanto mmap o mremap incorrerebbe. Questo lo rende interessante per malloc implementazioni da utilizzare durante l'implementazione dell'heap principale.


Puoi usare brk e sbrk te stesso per evitare il "maloc overhead" di cui tutti si lamentano sempre. Ma non puoi usare facilmente questo metodo insieme a malloc quindi è appropriato solo quando non devi free qualsiasi cosa. Perché non puoi. Inoltre, dovresti evitare qualsiasi chiamata alla libreria che potrebbe utilizzare malloc internamente. Cioè. strlen è probabilmente sicuro, ma fopen probabilmente non lo è.

Chiama sbrk proprio come chiameresti malloc . Restituisce un puntatore all'interruzione corrente e incrementa l'interruzione di quella quantità.

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

Sebbene non sia possibile liberare allocazioni individuali (perché non esiste un malloc-overhead , ricorda), puoi libera l'intero spazio chiamando brk con 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);
}

Puoi persino impilare queste regioni, scartando la regione più recente riavvolgendo l'interruzione all'inizio della regione.

Un'altra cosa...

sbrk è utile anche nel code golf perché è più corto di 2 caratteri rispetto a malloc .


Esempio eseguibile minimo

Cosa fa la chiamata di sistema brk()?

Chiede al kernel di permetterti di leggere e scrivere su un pezzo contiguo di memoria chiamato heap.

Se non lo chiedi, potrebbe essere un 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 segfault anche senza il brk , quindi qui c'è una versione più aggressiva che alloca 16 MiB ed è molto probabile che segfault senza il 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 di 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, dovresti familiarizzare con il paging:come funziona il paging x86?.

Perché abbiamo bisogno di entrambi brk e sbrk ?

brk potrebbe ovviamente essere implementato con sbrk + calcoli di offset, entrambi esistono solo per comodità.

Nel backend, il kernel Linux v5.0 ha un'unica chiamata di sistema brk utilizzato 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

È brk POSIX?

brk era POSIX, ma è stato rimosso in POSIX 2001, quindi la necessità di _GNU_SOURCE per accedere al wrapper glibc.

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

Penso che non ci siano casi validi in cui dovresti usare brk invece di malloc o mmap al giorno d'oggi.

brk rispetto a malloc

brk è una vecchia possibilità di implementare malloc .

mmap è il nuovo meccanismo rigorosamente più potente che probabilmente tutti i sistemi POSIX utilizzano attualmente per implementare malloc . Ecco un mmap minimo eseguibile esempio di allocazione della memoria.

Posso mescolare brk e malloc?

Se il tuo malloc è implementato con brk , non ho idea di come ciò possa non far saltare in aria le cose, dal momento che brk gestisce solo un singolo intervallo di memoria.

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

  • https://www.gnu.org/software/libc/manual/html_mono/libc.html#Resizing-the-Data-Segment

Probabilmente le cose funzioneranno lì, suppongo, dal mmap è probabilmente usato per malloc .

Vedi anche:

  • Cosa c'è di non sicuro/precedente in brk/sbrk?
  • Perché chiamare sbrk(0) due volte dà un valore diverso?

Ulteriori informazioni

Internamente, il kernel decide se il processo può disporre di tanta memoria e assegna pagine di memoria per tale utilizzo.

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


Nel diagramma che hai postato, la "interruzione"—l'indirizzo manipolato da brk e sbrk —è la linea tratteggiata in cima all'heap.

La documentazione che hai letto lo descrive come la fine del "segmento di dati" perché nelle librerie tradizionali (pre-shared-libraries, pre-mmap ) Unix il segmento di dati era continuo con l'heap; prima dell'avvio del programma, il kernel caricava i blocchi "testo" e "dati" nella RAM a partire dall'indirizzo zero (in realtà poco sopra l'indirizzo zero, in modo che il puntatore NULL non puntasse veramente a nulla) e impostava l'indirizzo di interruzione a la fine del segmento di dati. La prima chiamata a malloc userebbe quindi sbrk per spostare la separazione e creare l'heap in mezzo la parte superiore del segmento dati e il nuovo indirizzo di interruzione più alto, come mostrato nel diagramma, e il successivo utilizzo di malloc lo userebbe per rendere l'heap più grande se necessario.

Nel frattempo, lo stack inizia in cima alla memoria e cresce verso il basso. Lo stack non ha bisogno di chiamate di sistema esplicite per renderlo più grande; o inizia con tutta la 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). In ogni caso, potrebbe esserci o meno una regione di "guardia" 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 uno dei due lo stack o l'heap tenta di crescere al suo interno, si ottiene un errore di segmentazione. Tradizionalmente, tuttavia, il kernel non tentava di imporre un confine; lo stack potrebbe crescere nell'heap, o l'heap potrebbe crescere nello stack, e in entrambi i casi scarabocchierebbero sui dati l'uno dell'altro e il programma andrebbe in crash. Se eri molto fortunato, si bloccava immediatamente.

Non sono sicuro da dove provenga il numero 512 GB in questo diagramma. Implica uno spazio di indirizzi virtuali a 64 bit, che è incoerente con la mappa di memoria molto semplice che hai lì. Un vero spazio di indirizzi a 64 bit è più simile a questo:

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

Questo non è in scala remota, e non dovrebbe essere interpretato esattamente come fa un dato sistema operativo (dopo averlo disegnato ho scoperto che Linux in realtà mette l'eseguibile molto più vicino all'indirizzo 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 segfault immediato) e sono gigantesche rispetto alle aree grigie. Le regioni grigio chiaro sono il programma e le sue librerie condivise (possono esserci dozzine di librerie condivise); ognuno ha un indipendente segmento di testo e dati (e segmento "bss", che contiene anche dati globali ma è inizializzato a tutti i bit zero anziché occupare spazio nell'eseguibile o nella libreria su disco). L'heap non è più necessariamente continuo con il segmento di dati dell'eseguibile:l'ho disegnato in questo modo, ma sembra che Linux, almeno, non lo faccia. Lo stack non è più ancorato alla parte superiore dello spazio degli indirizzi virtuali e la distanza tra l'heap e lo stack è così enorme che non devi preoccuparti di attraversarla.

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


Linux
  1. Qual è l'attuale runlevel del sistema Linux?

  2. Controllo se errno !=EINTR:cosa significa?

  3. Cosa fa il permesso di esecuzione?

  4. Cosa significa 'rc' in `.bashrc`, ecc.?

  5. Cosa significa il suffisso .d in Linux?

Cosa significa nell'output di Ps?

Qual è la differenza tra chiamata di sistema e chiamata di libreria?

Cosa fa -e in un bash shebang?

Cosa fa esattamente init?

Cosa ha fatto la chiamata di sistema tuxcall?

Cosa significa la capacità ep?