Do czego służy wywołanie systemowe brk ()?


184

Zgodnie z instrukcją dla programistów Linuksa:

brk () i sbrk () zmieniają lokalizację przerwania programu, która określa koniec segmentu danych procesu.

Co oznacza tutaj segment danych? Czy to tylko segment danych lub dane, BSS i sterta łącznie?

Według wiki:

Czasami obszary danych, BSS i hałdy są łącznie nazywane „segmentem danych”.

Nie widzę powodu, aby zmieniać rozmiar tylko segmentu danych. Jeśli są to dane, BSS i sterty zbiorczo, wówczas ma to sens, ponieważ sterty zyskają więcej miejsca.

Co prowadzi mnie do drugiego pytania. We wszystkich artykułach, które do tej pory czytałem, autor mówi, że stos rośnie w górę, a stos rośnie w dół. Ale nie wyjaśniają, co się dzieje, gdy kupa zajmuje całą przestrzeń między stertą a stosem?

wprowadź opis zdjęcia tutaj


1
Co robisz, gdy brakuje Ci miejsca? zamieniasz na HDD. Gdy wykorzystasz przestrzeń, zwalniasz ją w celu uzyskania innych informacji.
Igoris Azanovas,

28
@Igoris: Myląca jest pamięć fizyczna (którą można w razie potrzeby zamienić na dysk, używając pamięci wirtualnej) i przestrzeń adresową . Gdy wypełnisz swoją przestrzeń adresową, żadna zamiana nie zwróci tych adresów w środku.
Daniel Pryden,

7
Przypominamy, że brk()wywołanie systemowe jest bardziej przydatne w języku asemblera niż w C. W C malloc()powinno się go używać zamiast brk()do celów alokacji danych - ale to w żaden sposób nie unieważnia proponowanego pytania.
alecov,

2
@Brian: Kupa jest złożoną strukturą danych do obsługi regionów o różnych rozmiarach i wyrównaniach, wolnej puli itp. Stosy wątków są zawsze ciągłymi (w wirtualnej przestrzeni adresowej) sekwencjami kompletnych stron. W większości systemów operacyjnych za stosami, stertami i plikami mapowanymi w pamięci znajduje się alokator stron.
Ben Voigt,

2
@Brian: Kto powiedział, że nie ma żadnego „Stos” manipulowany przez brk()a sbrk()? Stosami zarządza alokator stron na znacznie niższym poziomie.
Ben Voigt

Odpowiedzi:


233

Na opublikowanym diagramie „przerwa” - adres obsługiwany przez brki sbrk- jest linią przerywaną na górze stosu.

uproszczony obraz układu pamięci wirtualnej

Dokumentacja, którą przeczytałeś, opisuje to jako koniec „segmentu danych”, ponieważ w tradycyjnym (pre-shared-library, pre- mmap) Unixie segment danych był ciągły ze stertą; przed uruchomieniem programu jądro ładuje bloki „tekst” i „dane” do pamięci RAM, zaczynając od adresu zero (w rzeczywistości nieco powyżej adresu zero, aby wskaźnik NULL rzeczywiście nie wskazywał na nic) i ustawił adres przerwania na koniec segmentu danych. Pierwsze wywołanie do mallocużyłoby następnie sbrkdo przeniesienia podziału i utworzenia stosu pomiędzy górą segmentu danych a nowym, wyższym adresem przerwania, jak pokazano na schemacie, a kolejne użycie mallocspowodowałoby zwiększenie stosu jako niezbędne.

W międzyczasie stos zaczyna się na górze pamięci i rośnie. Stos nie potrzebuje jawnych wywołań systemowych, aby był większy; albo zaczyna się od przydzielonej mu tyle pamięci RAM, ile może mieć (było to tradycyjne podejście), albo pod stosem znajduje się region zarezerwowanych adresów, do którego jądro automatycznie przydziela pamięć RAM, gdy zauważy tam próbę zapisu (jest to nowoczesne podejście). Tak czy inaczej, na dole przestrzeni adresowej może być, ale nie musi, istnieć obszar „ochronny”, który może być użyty do stosu. Jeśli ten region istnieje (wszystkie nowoczesne systemy to robią), jest nieodmapowany; jeśli albostos lub sterta próbuje w nie wyrosnąć, pojawia się błąd segmentacji. Jednak tradycyjnie jądro nie próbowało egzekwować granicy; stos może urosnąć w stertę lub sterty mogą urosnąć w stertę, i tak czy inaczej skrobią się nawzajem po danych, a program się zawiesi. Gdybyś miał szczęście, natychmiast by się zawiesił.

Nie jestem pewien, skąd pochodzi liczba 512 GB na tym schemacie. Oznacza 64-bitową wirtualną przestrzeń adresową, co jest niezgodne z bardzo prostą mapą pamięci, którą tam masz. Rzeczywista 64-bitowa przestrzeń adresowa wygląda mniej więcej tak:

mniej uproszczona przestrzeń adresowa

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

Nie jest to zdalnie skalowane i nie powinno być interpretowane jako dokładnie tak, jak robi to dany system operacyjny (po jego narysowaniu odkryłem, że Linux faktycznie przybliża plik wykonywalny do adresu zero, niż myślałem, i że współużytkowane biblioteki pod zaskakująco wysokimi adresami). Czarne obszary tego diagramu nie są odwzorowane - każdy dostęp powoduje natychmiastową awarię - i są gigantyczne w stosunku do szarych obszarów. Jasnoszare regiony to program i jego wspólne biblioteki (mogą istnieć dziesiątki wspólnych bibliotek); każdy ma niezależnysegment tekstowy i danych (oraz segment „bss”, który również zawiera dane globalne, ale jest inicjowany do zera wszystkich bitów, zamiast zajmować miejsce w pliku wykonywalnym lub bibliotece na dysku). Sterta niekoniecznie jest już ciągła z segmentem danych pliku wykonywalnego - narysowałem to w ten sposób, ale wygląda na to, że Linux przynajmniej tego nie robi. Stos nie jest już związany z górną krawędzią wirtualnej przestrzeni adresowej, a odległość między stertą a stosem jest tak ogromna, że ​​nie musisz się martwić o jej przekroczenie.

Przerwa jest nadal górną granicą stosu. Nie pokazałem jednak, że mogą istnieć dziesiątki niezależnych przydziałów pamięci gdzieś w czerni, wykonane mmapzamiast brk. (System operacyjny będzie próbował trzymać je z dala od tego brkobszaru, aby się nie zderzyły).


7
+1 za szczegółowe wyjaśnienie. Czy wiesz, czy mallocnadal polega na tym, brkczy używa mmapmożliwości „oddania” oddzielnych bloków pamięci?
Anders Abel,

18
Zależy to od konkretnej implementacji, ale IIUC wielu obecnych mallocużywa brkobszaru dla małych alokacji i indywidualnych mmapdla dużych (powiedzmy,> 128K) alokacji. Zobacz na przykład dyskusję na temat MMAP_THRESHOLD na stronie Linux-a malloc(3).
zwolnić

1
Rzeczywiście dobre wyjaśnienie. Ale jak powiedziałeś, Stack nie znajduje się już na szczycie wirtualnej przestrzeni adresowej. Czy dotyczy to tylko 64-bitowej przestrzeni adresowej, czy dotyczy to nawet 32-bitowej przestrzeni adresowej. A jeśli stos znajduje się na górze przestrzeni adresowej, gdzie zdarzają się anonimowe mapy pamięci? Czy znajduje się w górnej części wirtualnej przestrzeni adresowej tuż przed stosem.
nik

3
@Nikhil: to skomplikowane. Większość systemów 32-bitowych umieszcza stos na samej górze przestrzeni adresowej trybu użytkownika , która często jest tylko dolną 2 lub 3G pełnej przestrzeni adresowej (pozostała przestrzeń jest zarezerwowana dla jądra). Nie mogę obecnie wymyślić takiego, który tego nie zrobił, ale nie znam ich wszystkich. Większość 64-bitowych procesorów nie pozwala na wykorzystanie całej przestrzeni 64-bitowej; wysokie 10 do 16 bitów adresu musi mieć wartość zero lub jeden. Stos jest zwykle umieszczony w górnej części użytecznych niskich adresów. Nie mogę dać ci reguły mmap; jest bardzo zależny od systemu operacyjnego.
zwolnić

3
@RiccardoBestetti Marnuje przestrzeń adresową , ale to nieszkodliwe - 64-bitowa wirtualna przestrzeń adresowa jest tak duża, że ​​gdybyś wypalił jej gigabajt co sekundę , zabrałoby ci to 500 lat. [1] Większość procesorów nie pozwala nawet na użycie więcej niż 2 ^ 48 do 2 ^ 53 bitów adresu wirtualnego (jedynym znanym mi wyjątkiem jest POWER4 w trybie haszowania stronicowania). Nie marnuje fizycznej pamięci RAM; nieużywane adresy nie są przypisywane do pamięci RAM.
zwolnienie

26

Minimalny możliwy do uruchomienia przykład

Do czego służy wywołanie systemowe brk ()?

Prosi jądro, abyś mógł czytać i pisać w ciągłym fragmencie pamięci zwanym stertą.

Jeśli nie zapytasz, może cię to zepsuć.

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

Z 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 w górę .

Powyższe może nie trafić na nową stronę i nie uszkodzić nawet bez brk, więc tutaj jest bardziej agresywna wersja, która alokuje 16 MB i jest bardzo prawdopodobne, że ulegnie awarii 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;
}

Testowane na Ubuntu 18.04.

Wizualizacja wirtualnej przestrzeni adresowej

Przed 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

Aby lepiej zrozumieć przestrzenie adresowe, powinieneś zapoznać się ze stronicowaniem: Jak działa stronicowanie x86?.

Dlaczego potrzebujemy zarówno brki sbrk?

brk można oczywiście zaimplementować za pomocą sbrk + obliczeń przesunięcia, oba istnieją tylko dla wygody.

W backendu jądro Linux v5.0 ma pojedyncze wywołanie systemowe, brkktóre służy do implementacji obu: https://github.com/torvalds/linux/blob/v5.0/arch/x86/entry/syscalls/syscall_64. tbl # L23

12  common  brk         __x64_sys_brk

Czy brkPOSIX?

brkkiedyś był POSIX, ale został usunięty w POSIX 2001, dlatego potrzeba _GNU_SOURCEdostępu do opakowania glibc.

Usunięcie jest prawdopodobnie spowodowane wprowadzeniem mmap, które jest nadzbiorem, który umożliwia przydzielenie wielu zakresów i więcej opcji alokacji.

Myślę, że nie ma uzasadnionego przypadku, w którym powinieneś używać brkzamiast malloclub mmapobecnie.

brk vs malloc

brkto jedna stara możliwość wdrożenia malloc.

mmapjest nowszym, znacznie silniejszym mechanizmem, który prawdopodobnie wszystkie systemy POSIX obecnie używają do implementacji malloc. Oto minimalne runnable mmapprzykład alokacji pamięci .

Czy mogę mieszać brki Malloc?

Jeśli twoja mallocimplementacja jest zaimplementowana brk, nie mam pojęcia, jak to może nie wysadzić rzeczy w powietrze, ponieważ brkzarządza tylko jednym zakresem pamięci.

Nie znalazłem jednak nic na ten temat w dokumentacji glibc, np .:

Pewnie coś tam po prostu zadziała, jak przypuszczam, ponieważ mmapprawdopodobnie jest używane malloc.

Zobacz też:

Więcej informacji

Jądro wewnętrznie decyduje, czy proces może mieć tyle pamięci, i wyznacza strony pamięci na takie użycie.

To wyjaśnia, w jaki sposób stos różni się od stosu: jaka jest funkcja instrukcji push / pop używanych w rejestrach w zestawie x86?


4
Skoro pjest wskaźnikiem do pisania int, czy nie powinno tak być brk(p + 2);?
Johan Boulé

Mała uwaga: Wyrażenie w pętli for agresywnej wersji powinno być prawdopodobnie*(p + i) = 1;
lima.sierra

Nawiasem mówiąc, dlaczego musimy użyć brk(p + 2)zamiast po prostu go zwiększyć sbrk(2)? Czy BRK jest naprawdę potrzebny?
Yi Lin Liu

1
@YiLinLiu Myślę, że to tylko dwie bardzo podobne nakładki C dla jednego zaplecza jądra ( brksyscall). brkjest nieco wygodniej przywrócić wcześniej przydzielony stos.
Ciro Santilli 法轮功 冠状 病 六四 事件 法轮功

1
@CiroSantilli 中心 改造 中心 996ICU 六四 事件 Biorąc pod uwagę rozmiar int jako 4 bajty i rozmiar int * jako 4 bajty (na maszynie 32-bitowej), zastanawiałem się, czy nie powinien zostać zwiększony o zaledwie 4 bajty (zamiast 8 - (2 * sizeof int)). Nie powinien wskazywać następnego dostępnego miejsca na sterty - które będzie w odległości 4 bajtów (nie 8). Popraw mnie, jeśli coś tu brakuje.
Saket Sharad

10

Możesz użyć siebie brki sbrksamego, aby uniknąć „kosztów ogólnych”, na które wszyscy zawsze narzekają. Ale nie możesz łatwo użyć tej metody w połączeniu z, mallocwięc jest ona odpowiednia tylko wtedy, gdy nie musisz freenic. Ponieważ nie możesz. Ponadto należy unikać wywołań biblioteki, które mogą być używane mallocwewnętrznie. To znaczy. strlenjest prawdopodobnie bezpieczny, ale fopenprawdopodobnie nie jest.

Zadzwoń sbrktak, jak byś zadzwonił malloc. Zwraca wskaźnik do bieżącego przerwania i zwiększa przerwę o tę kwotę.

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

Chociaż nie można darmowe indywidualne przydziały (bo nie ma malloc-napowietrznych , pamiętam), to może uwolnić całą przestrzeń wywołując brko wartości zwracanej przez pierwsze wywołanie sbrk, więc przewijanie BRK .

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

Możesz nawet ułożyć te regiony w stos, odrzucając najnowszy region, przewijając przerwę do początku regionu.


Jeszcze jedna rzecz ...

sbrkjest również przydatny w golfie kodowym, ponieważ jest o 2 znaki krótszy niż malloc.


7
-1, ponieważ: malloc/ z freepewnością może (i zrobić) przywrócić pamięć do systemu operacyjnego. Nie zawsze mogą to zrobić, kiedy chcesz, ale to jest kwestia niedostosowania heurystyki do twojego przypadku użycia. Co ważniejsze, niebezpieczne jest wywoływanie sbrkz niezerowym argumentem w dowolnym programie, który mógłby kiedykolwiek wywołać malloc- i prawie wszystkie funkcje biblioteki C mogą wywoływać mallocwewnętrznie. Jedynymi, które na pewno nie będą, są funkcje bezpieczne dla sygnału asynchronicznego .
zwolnić

A przez „to jest niebezpieczne” mam na myśli „twój program się zawiesi”.
zwolnić

Zredagowałem, aby usunąć przechwalającą się pamięć , i wspomniałem o niebezpieczeństwie korzystania z funkcji biblioteki wewnętrznie malloc.
luser droog

1
Jeśli chcesz dokonać fantazyjnej alokacji pamięci, oprzyj ją na górze malloc lub na mmap. Nie dotykaj brk i sbrk, to relikty z przeszłości, które wyrządzają więcej szkody niż pożytku (nawet strony informują cię, abyś trzymał się od nich z daleka!)
Eloff

3
To jest głupie. Jeśli chcesz uniknąć narzutów malloc dla wielu małych alokacji, zrób jedną dużą alokację (z malloc lub mmap, a nie sbrk) i zrezygnuj z tego. Jeśli trzymasz węzły binarnego drzewa w tablicy, możesz użyć wskaźników 8b lub 16b zamiast wskaźników 64b. Działa to świetnie, gdy nie musisz usuwać żadnego węzła, dopóki nie będziesz gotowy do usunięcia wszystkich węzłów. (np. zbuduj posortowany słownik w locie.) Używanie sbrkdo tego jest przydatne tylko dla golfa, ponieważ ręczne użycie mmap(MAP_ANONYMOUS)jest lepsze pod każdym względem, z wyjątkiem rozmiaru kodu źródłowego.
Peter Cordes

3

Istnieje specjalnie wyznaczone anonimowe mapowanie pamięci prywatnej (tradycyjnie zlokalizowane tuż poza danymi / bss, ale współczesny Linux faktycznie dostosuje lokalizację za pomocą ASLR). Zasadniczo nie jest to lepsze niż jakiekolwiek inne mapowanie, które można utworzyć mmap, ale Linux ma pewne optymalizacje, które pozwalają rozszerzyć koniec tego mapowania (przy użyciu brksyscall) w górę przy zmniejszonym koszcie blokowania w stosunku do tego, co mmaplub co mremapbyśmy ponieśli. Dzięki temu mallocimplementacje są atrakcyjne w przypadku implementacji głównej sterty.


Masz na myśli to możliwe , aby rozwinąć koniec tego odwzorowania w górę, tak?
zwolnić

Tak, naprawione. Przepraszam za to!
R .. GitHub ZATRZYMAJ LÓD

0

Mogę odpowiedzieć na twoje drugie pytanie. Malloc zawiedzie i zwróci wskaźnik zerowy. Dlatego zawsze sprawdzasz, czy wskaźnik jest zerowy podczas dynamicznego przydzielania pamięci.


więc jaki jest pożytek z brk i sbrk?
nik

3
@NikhilRathod: malloc()użyje brk()i / lub sbrk()pod maską - i Ty też możesz, jeśli chcesz wdrożyć własną wersję malloc().
Daniel Pryden,

@Daniel Pryden: w jaki sposób brk i sbrk mogą pracować na stercie, gdy jest on między stosem a segmentem danych, jak pokazano na powyższym schemacie. aby to zadziałało, stos powinien być na końcu. Czy mam rację?
nik

2
@Brian: Daniel powiedział, że system operacyjny zarządza segmentem stosu , a nie wskaźnikiem stosu ... bardzo różne rzeczy. Chodzi o to, że nie ma syskall sbrk / brk dla segmentu stosu - Linux automatycznie przydziela strony przy próbach zapisu na końcu segmentu stosu.
Jim Balter,

1
I Brian, odpowiedziałeś tylko na połowę pytania. Druga połowa dzieje się, jeśli spróbujesz pchnąć na stos, gdy nie ma dostępnego miejsca ... dostajesz błąd segmentacji.
Jim Balter,

0

Sterta jest umieszczana na końcu w segmencie danych programu. brk()służy do zmiany (rozszerzenia) wielkości stosu. Gdy sterty nie będzie już rosnąć, każde mallocpołączenie zakończy się niepowodzeniem.


Mówisz więc, że wszystkie diagramy w Internecie, takie jak te w moim pytaniu, są błędne. Jeśli to możliwe, proszę o wskazanie właściwego schematu.
nik

2
@Nikkhil Pamiętaj, że górną częścią tego diagramu jest koniec pamięci. Wierzchołek stosu przesuwa się w dół na schemacie wraz ze wzrostem stosu. Górna część stosu przesuwa się w górę na diagramie, gdy jest rozwinięta.
Brian Gordon,

0

Segment danych to część pamięci, w której przechowywane są wszystkie dane statyczne, odczytywane z pliku wykonywalnego przy uruchomieniu i zwykle wypełnione zerami.


Przechowuje również niezainicjowane dane statyczne (nieobecne w pliku wykonywalnym), które mogą być śmieciami.
luser droog

Niezainicjowane dane statyczne ( .bss) są inicjowane przez system operacyjny na zero-bit przed uruchomieniem programu; jest to faktycznie zagwarantowane przez standard C. Przypuszczam, że niektóre systemy wbudowane mogą nie zawracać sobie głowy (nigdy takiego nie widziałem, ale nie działają tak wszystkie wbudowane)
zwolnij

@zwol: Linux ma opcję czasu kompilacji, aby nie zerować stron, które zostały zwrócone mmap, ale zakładam, .bssże nadal będą wyzerowane. Przestrzeń BSS jest prawdopodobnie najbardziej kompaktowym sposobem wyrażenia faktu, że program chce mieć tablice zerowe.
Peter Cordes

1
@PeterCordes Standard C mówi, że zmienne globalne zadeklarowane bez inicjatora są traktowane tak, jakby były inicjowane do zera. Implementacja AC, która umieszcza takie zmienne .bssi nie zeruje, .bssbyłaby zatem niezgodna. Ale nic nie zmusza implementacji C do użycia, .bssa nawet posiadania czegoś takiego.
zwol

@PeterCordes Również linia między „implementacją C” a programem może być bardzo rozmyta, np. Zwykle jest niewielka część kodu z implementacji, statycznie powiązana z każdym plikiem wykonywalnym, który działa wcześniej main; kod ten mógłby wyzerować .bssobszar, a nie zmusić jądro do zrobienia tego, i to nadal byłoby zgodne.
zwol

0

malloc używa wywołania systemowego brk do alokacji pamięci.

zawierać

int main(void){

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

uruchom ten prosty program ze strace, wywoła on system brk.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.