GNU/Linux >> Znalost Linux >  >> Linux

Co dělá systémové volání brk()?

Existuje speciálně určené anonymní mapování soukromé paměti (tradičně umístěné hned za data/bss, ale moderní Linux ve skutečnosti polohu upraví pomocí ASLR). V zásadě to není o nic lepší než jakékoli jiné mapování, které byste mohli vytvořit pomocí mmap , ale Linux má některé optimalizace, které umožňují rozšířit konec tohoto mapování (pomocí brk syscall) nahoru se sníženými náklady na zamykání ve srovnání s mmap nebo mremap by vznikly. Díky tomu je atraktivní pro malloc implementace, které se mají použít při implementaci hlavní haldy.


Můžete použít brk a sbrk abyste se vyhnuli „mallocké režii“, na kterou si všichni neustále stěžují. Ale nemůžete snadno použít tuto metodu ve spojení s malloc takže je to vhodné pouze tehdy, když nemusíte free cokoliv. Protože nemůžeš. Také byste se měli vyvarovat volání knihoven, která mohou používat malloc vnitřně. Tj. strlen je pravděpodobně bezpečný, ale fopen pravděpodobně není.

Zavolejte na číslo sbrk stejně jako byste zavolali malloc . Vrátí ukazatel na aktuální přerušení a zvýší přerušení o tuto hodnotu.

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

I když nemůžete uvolnit jednotlivé příděly (protože neexistuje malloc-overhead , pamatujte), můžete uvolnit celý prostor voláním brk s hodnotou vrácenou prvním voláním sbrk , čímž se přetočí brk .

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

Tyto oblasti můžete dokonce naskládat na sebe a poslední oblast zahodit přetočením konce na začátek oblasti.

Ještě jedna věc...

sbrk je také užitečné v kódovém golfu, protože je o 2 znaky kratší než malloc .


Minimální spustitelný příklad

Co dělá systémové volání brk()?

Požádá jádro, aby vám umožnilo číst a zapisovat do souvislého bloku paměti zvaného halda.

Pokud se nezeptáte, může vás to napadnout.

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

S 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.

Výše uvedené nemusí narazit na novou stránku a nemusí dojít k chybě ani bez brk , takže zde je agresivnější verze, která alokuje 16 MiB a je velmi pravděpodobné, že dojde k chybě bez 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;
}

Testováno na Ubuntu 18.04.

Vizualizace virtuálního adresního prostoru

Před brk :

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

Po brk(p + 2) :

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

Po brk(b) :

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

Abyste lépe porozuměli adresním prostorům, měli byste se seznámit se stránkováním:Jak funguje stránkování x86?.

Proč potřebujeme obě brk a sbrk ?

brk lze samozřejmě implementovat pomocí sbrk + offsetové výpočty, oba existují jen pro pohodlí.

V backendu má linuxové jádro v5.0 jediné systémové volání brk který se používá k implementaci obou:https://github.com/torvalds/linux/blob/v5.0/arch/x86/entry/syscalls/syscall_64.tbl#L23

12  common  brk         __x64_sys_brk

Je brk POSIX?

brk býval POSIX, ale byl odstraněn v POSIX 2001, takže je potřeba _GNU_SOURCE pro přístup k obalu glibc.

Odstranění je pravděpodobně způsobeno zavedením mmap , což je nadmnožina, která umožňuje alokaci více rozsahů a více možností alokace.

Myslím, že neexistuje žádný platný případ, kdy byste měli použít brk místo malloc nebo mmap v dnešní době.

brk vs malloc

brk je jedna stará možnost implementace malloc .

mmap je novější mnohem výkonnější mechanismus, který pravděpodobně všechny systémy POSIX v současnosti používají k implementaci malloc . Zde je minimální spustitelný mmap příklad přidělení paměti.

Mohu kombinovat brk a malloc?

Pokud je vaše malloc je implementován pomocí brk , nemám ponětí, jak to nemůže vyhodit věci do povětří, protože brk spravuje pouze jeden rozsah paměti.

V glibc docs jsem však o tom nic nenašel, např.:

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

Věci tam budou pravděpodobně fungovat, předpokládám, od mmap se pravděpodobně používá pro malloc .

Viz také:

  • Co je na brk/sbrk nebezpečné/starší?
  • Proč volání sbrk(0) dvakrát dává jinou hodnotu?

Další informace

Interně jádro rozhodne, zda proces může mít tolik paměti, a vyčlení stránky paměti pro toto použití.

To vysvětluje, jak se zásobník porovnává s haldou:Jaká je funkce instrukcí push / pop používaných v registrech v sestavě x86?


V diagramu, který jste zveřejnili, je „přerušení“ – adresa upravená pomocí brk a sbrk —je tečkovaná čára v horní části haldy.

Dokumentace, kterou jste si přečetli, to popisuje jako konec „datového segmentu“, protože v tradičních (pre-shared-libraries, pre-mmap ) Unix datový segment byl spojitý s haldou; před spuštěním programu jádro načte bloky "text" a "data" do paměti RAM počínaje adresou nula (ve skutečnosti trochu nad adresou nula, takže ukazatel NULL skutečně neukazuje na nic) a nastaví adresu přerušení na konec datového segmentu. První volání na malloc by pak použil sbrk přesunout rozdělení a vytvořit haldu mezi horní část datového segmentu a nová, vyšší adresa přerušení, jak je znázorněno na diagramu, a následné použití malloc by ho použil k tomu, aby se halda podle potřeby zvětšila.

Mezitím zásobník začíná v horní části paměti a roste dolů. Zásobník nepotřebuje explicitní systémová volání, aby byl větší; buď začíná s přidělenou tolik RAM, kolik může mít (toto byl tradiční přístup), nebo je pod zásobníkem oblast vyhrazených adres, kterým jádro automaticky přidělí RAM, když zaznamená pokus o zápis. (to je moderní přístup). Ať tak či onak, ve spodní části adresního prostoru může nebo nemusí být „strážní“ oblast, kterou lze použít pro zásobník. Pokud tato oblast existuje (všechny moderní systémy to dělají), je trvale nezmapovaná; pokud buď zásobník nebo halda se do něj snaží vrůst, dostanete chybu segmentace. Tradičně se však jádro nepokusilo vynutit si hranici; zásobník by se mohl rozrůst do hromady, nebo by se hromada mohla rozrůst do hromady a v každém případě by si navzájem čmárali data a program by se zhroutil. Pokud byste měli velké štěstí, okamžitě by se zhroutil.

Nejsem si jistý, odkud pochází číslo 512 GB v tomto diagramu. Znamená to 64bitový virtuální adresní prostor, který je v rozporu s velmi jednoduchou mapou paměti, kterou tam máte. Skutečný 64bitový adresní prostor vypadá spíše takto:

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

Toto není ani vzdáleně škálovatelné a nemělo by to být interpretováno přesně tak, jak kterýkoli daný operační systém dělá věci (poté, co jsem to nakreslil, jsem zjistil, že Linux ve skutečnosti staví spustitelný soubor mnohem blíže k adrese nula, než jsem si myslel, že ano, a sdílené knihovny na překvapivě vysokých adresách). Černé oblasti tohoto diagramu nejsou zmapované – jakýkoli přístup způsobí okamžitou segfault – a jsou gigantické vzhledem k šedým oblastem. Světle šedé oblasti jsou program a jeho sdílené knihovny (sdílených knihoven mohou být desítky); každý má nezávislou textový a datový segment (a segment "bss", který také obsahuje globální data, ale je inicializován na všechny bity nula, místo aby zabíral místo ve spustitelném souboru nebo knihovně na disku). Hromada již není nutně spojitá s datovým segmentem spustitelného souboru – nakreslil jsem to tak, ale vypadá to, že alespoň Linux to nedělá. Zásobník již není navázán na vrchol virtuálního adresového prostoru a vzdálenost mezi haldou a zásobníkem je tak obrovská, že se nemusíte bát, že ji překročíte.

Přerušení je stále horní hranicí haldy. Co jsem však neukázal je, že by tam někde mohly být desítky nezávislých alokací paměti, vytvořené pomocí mmap místo brk . (OS se bude snažit udržet je daleko od brk oblast, aby se nesrazily.)


Linux
  1. Jaký je rozdíl mezi voláním knihovny a voláním systému v Linuxu?

  2. Co přesně init dělá?

  3. Co udělal systém volání tuxcall?

  1. Jaká je aktuální úroveň běhu systému Linux?

  2. Co dělá „lc_all=c“?

  3. Co znamená schopnost ep?

  1. Zjistit iniciační systém pomocí Shell?

  2. Co znamená tilda (~) na konci názvu souboru?

  3. Point of Uniq -u a co to dělá??