Что делает системный вызов brk ()? - PullRequest
163 голосов
/ 09 августа 2011

Согласно руководству для программистов Linux:

brk () и sbrk () изменяют место остановки программы, которое определяет конец сегмента данных процесса.

Что означает сегмент данных здесь?Это просто сегмент данных или данные, BSS и куча вместе взятые?

Согласно вики:

Иногда области данных, BSS и кучи вместе называют «сегментом данных».

Я вижунет причин для изменения размера только сегмента данных.Если это данные, BSS и куча вместе, то это имеет смысл, поскольку куча получит больше места.

Что подводит меня ко второму вопросу.Во всех статьях, которые я до сих пор читал, автор говорит, что куча растет вверх, а стек - вниз.Но они не объясняют, что происходит, когда куча занимает все пространство между кучей и стеком?

enter image description here

Ответы [ 8 ]

210 голосов
/ 09 августа 2011

На размещенной вами диаграмме "разрыв" - адрес, которым манипулируют brk и sbrk - это пунктирная линия в верхней части кучи.

simplified image of virtual memory layout

В прочитанной вами документации это описывается как конец "сегмента данных", потому что в традиционных (pre-shared-library, pre- mmap) Unix сегмент данных был непрерывным с кучей; перед запуском программы ядро ​​загружало бы блоки «текст» и «данные» в ОЗУ, начиная с нулевого адреса (фактически немного выше нулевого адреса, чтобы указатель NULL действительно ни на что не указывал), и устанавливал адрес прерывания равным конец сегмента данных. Первый вызов malloc будет затем использовать sbrk для перемещения разбиения и создания кучи между вершиной сегмента данных и новым, более высоким адресом разбиения, как показано на схеме, и последующее использование malloc будет использовать его для увеличения кучи по мере необходимости.

Между тем, стек начинается с верхней части памяти и уменьшается. Стек не нуждается в явных системных вызовах, чтобы увеличить его; либо он запускается с выделением для него столько оперативной памяти, сколько он может иметь (это был традиционный подход), либо область зарезервированных адресов ниже стека, для которой ядро ​​автоматически выделяет ОЗУ, когда замечает попытку записи туда (это современный подход). В любом случае, в нижней части адресного пространства может быть или не быть «защитная» область, которую можно использовать для стека. Если этот регион существует (все современные системы делают это), он постоянно не отображается; если или попытается врасти в стек или кучу, вы получите ошибку сегментации. Традиционно, однако, ядро ​​не делало попыток установить границы; стек может вырасти в кучу, или куча может вырасти в стек, и в любом случае они будут перебирать данные друг друга, и программа будет аварийно завершать работу. Если вам очень повезет, он сразу же рухнет.

Я не уверен, откуда взято число 512 ГБ на этой диаграмме. Это подразумевает 64-битное виртуальное адресное пространство, которое несовместимо с очень простой картой памяти, которая у вас есть. Настоящее 64-битное адресное пространство выглядит примерно так:

less simplified address space

Это не для удаленного масштабирования, и его не следует интерпретировать как то, что делает любая конкретная ОС (после того, как я нарисовал ее, я обнаружил, что Linux фактически помещает исполняемый файл гораздо ближе к нулевому адресу, чем я думал, и общие библиотеки по удивительно высоким адресам). Черные области на этой диаграмме не отображены - любой доступ вызывает немедленную ошибку сегмента - и они гигантские относительно серых областей. Светло-серые области - это программа и ее общие библиотеки (могут быть десятки общих библиотек); у каждого есть независимый сегмент текста и данных (и сегмент «bss», который также содержит глобальные данные, но инициализируется нулевыми битами, а не занимает место в исполняемом файле или библиотеке на диске). Куча больше не обязательно непрерывна с сегментом данных исполняемого файла - я нарисовал его таким образом, но, похоже, Linux, по крайней мере, этого не делает. Стек больше не привязан к вершине виртуального адресного пространства, а расстояние между кучей и стеком настолько велико, что вам не нужно беспокоиться о его пересечении.

Перерыв все еще является верхним пределом кучи. Однако то, что я не показал, это то, что там, где-то в черном, могут быть десятки независимых распределений памяти, сделанных с mmap вместо brk. (ОС будет стараться держать их подальше от области brk, чтобы они не сталкивались.)

19 голосов

Пример минимального запуска

Что делает системный вызов brk ()?

Просит ядро ​​разрешить вам читать и писать внепрерывный кусок памяти, называемый кучей.

Если вы не спросите, это может вызвать ошибку.

Без 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;
}

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

Вышеприведенное может не попадать на новую страницу и не вызывать segfault даже без brk, поэтому вот более агрессивная версия, которая выделяет 16MiB и весьма вероятно, что segfault без 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;
}

Протестировано в Ubuntu 18.04.

Визуализация виртуального адресного пространства

До brk:

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

После brk(p + 2):

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

После brk(b):

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

Чтобы лучше понять адресные пространства, вы должны ознакомиться с подкачкой: Как работает подкачка x86работа? .

Зачем нам нужны brk и sbrk?

brk, конечно, может быть реализовано с помощью sbrk +смещение сРасчеты, оба существуют просто для удобства.

В бэкэнде ядро ​​Linux v5.0 имеет один системный вызов brk, который используется для реализации обоих: 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 раньше POSIX, но он был удален в POSIX 2001, поэтому для доступа к оболочке glibc _GNU_SOURCE.

Удаление, вероятно, связано с введением mmap, который является надмножеством, позволяющим распределять несколько диапазонов и иметь больше возможностей выделения.

Я думаю, что нет действительного случая, когда вам следует использоватьbrk вместо malloc или mmap в настоящее время.

brk против malloc

brk - одна старая возможность реализации malloc.

mmap - это новый, более мощный механизм, который, вероятно, все системы POSIX в настоящее время используют для реализации malloc.

Могу ли я смешать brk и malloc?

Если ваш malloc реализован с brk, я понятия не имею, как это может не сработатьвсе дело в том, что brk управляет только одним диапазоном памяти.

Однако я не смог найти ничего об этом в документации glibc, например:

Скорее всего, там все будет работать, я полагаю, поскольку mmap, вероятно, используется для malloc.

См. Также:

Подробнее

Внутренне ядро ​​решает, может ли процесс иметь столько памяти, и выделяет страниц памяти для этого использования.

Это объясняет, как стек сравнивается скуча: Какова функция инструкций push / pop, используемых для регистров в сборке x86?

9 голосов
/ 09 августа 2011

Вы можете использовать brk и sbrk самостоятельно, чтобы избежать "накладных расходов на malloc", на которые все всегда жалуются. Но вы не можете легко использовать этот метод в сочетании с malloc, поэтому он подходит только тогда, когда вам ничего не нужно free. Потому что ты не можешь. Также вам следует избегать любых библиотечных вызовов, которые могут использовать malloc внутри. То есть. strlen, вероятно, безопасно, но fopen, вероятно, нет.

Звоните sbrk так же, как вы звоните malloc. Возвращает указатель на текущий разрыв и увеличивает его на эту величину.

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

Хотя вы не можете освободить отдельные выделения (поскольку нет malloc-overhead , помните), вы можете освободить все пространство , позвонив по номеру brk со значением, возвращаемым при первом вызове sbrk, таким образом перематывает brk .

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

Вы можете даже сложить эти регионы, отбросив самый последний регион, перемотав разрыв в начало региона.


Еще одна вещь ...

sbrk также полезен в коде гольфа , поскольку он на 2 символа короче malloc.

3 голосов
/ 09 августа 2011

Существует специальное обозначенное анонимное сопоставление частной памяти (традиционно расположенное сразу за data / bss, но современный Linux фактически отрегулирует местоположение с помощью ASLR).В принципе, это не лучше, чем любое другое отображение, которое вы могли бы создать с помощью mmap, но в Linux есть некоторые оптимизации, которые позволяют расширить конец этого отображения (используя системный вызов brk) вверх с уменьшенной стоимостью блокировки по сравнению с тем, что mmap или mremap понесут.Это делает привлекательным использование malloc реализаций при реализации основной кучи.

0 голосов
/ 12 марта 2013

malloc использует системный вызов brk для выделения памяти.

include

int main(void){

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

запустите эту простую программу с помощью strace, она вызовет систему brk.

0 голосов
/ 09 августа 2011

Сегмент данных - это часть памяти, которая содержит все ваши статические данные, считанные из исполняемого файла при запуске и обычно заполненные нулями.

0 голосов
/ 09 августа 2011

Куча помещается последней в сегменте данных программы.brk() используется для изменения (расширения) размера кучи.Когда куча больше не может расти, любой malloc вызов не удастся.

0 голосов
/ 09 августа 2011

Я могу ответить на ваш второй вопрос. Malloc потерпит неудачу и возвратит нулевой указатель. Вот почему вы всегда проверяете нулевой указатель при динамическом выделении памяти.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...