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


в соответствии с руководством по Linux-программиста:

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

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

согласно wiki:

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

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

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

9 145

9 ответов:

Я вижу много частичных ответов, но нет полного ответа. Вот эта фотография, которую вы снова опубликовали:

simplified image of virtual memory layout

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

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

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

less simplified address space

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

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

минимальный работоспособный пример

что делает системный вызов 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;
}

протестировано на Ubuntu 14.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 раньше был POSIX, но он был удален в POSIX 2001, поэтому необходимость _GNU_SOURCE для доступа к glibc обертка.

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

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

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

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

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

вызов sbrk так же, как вы бы назвали malloc. Он возвращает указатель к текущему разрыву и увеличивает разрыв на эту сумму.

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

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

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

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


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

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

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

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

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

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

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

включить

int main(void){

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

запустить простую программу с трассированием, он будет вызывать системы БРК.

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

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

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