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

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

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

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

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

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

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

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

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

enter image description here

4b9b3361

Ответ 1

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

simplified image of virtual memory layout

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

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

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

less simplified address space

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

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

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

Ответ 2

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

What does brk( ) system call do?

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

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

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

GitHub upstream.

Вышеприведенное может не попасть на новую страницу и не вызвать 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. Вот пример минимального запуска памяти mmap.

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

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

Однако я не смог найти что-либо об этом на glibc docs, например:

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

Смотрите также:

Подробнее

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

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

Ответ 3

Вы можете использовать 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 также полезен в code golf, потому что он на 2 символа короче malloc.

Ответ 4

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

Ответ 5

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

Ответ 6

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

Ответ 7

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

Ответ 8

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

включить

int main(void){

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

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

Ответ 9

  • Системный вызов, который обрабатывает выделение памяти, sbrk(2). Это увеличивает или уменьшает адресное пространство процесса на определенное количество байтов.

  • Функция выделения памяти malloc(3) реализует один конкретный тип распределения. malloc(), который, вероятно, будет использовать системный вызов sbrk().

Системный вызов sbrk(2) в ядре выделяет дополнительный кусок пространства от имени процесса. Библиотечная функция malloc() управляет этим пространством с уровня пользователя.