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


183

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

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

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

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

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

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

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

введите описание изображения здесь


1
Так что вы делаете, когда вы находитесь вне космоса? Вы переключаетесь на HDD. Когда вы используете пространство, вы отпускаете его для другой информации.
Игорь Азановас

29
@Igoris: вы путаете физическую память (которую вы можете при необходимости заменить на диск, используя виртуальную память) и адресное пространство . Когда вы заполняете свое адресное пространство, никакая перестановка не вернет вам эти адреса в середине.
Даниэль Приден

7
Напомним, что brk()системный вызов более полезен на ассемблере, чем на языке C. В C его malloc()следует использовать вместо brk()каких-либо целей выделения данных, но это никоим образом не лишает законной силы предложенный вопрос.
alecov

2
@Brian: куча - это сложная структура данных для обработки областей разного размера и выравнивания, свободного пула и т. Д. Стеки потоков всегда представляют собой непрерывные (в виртуальном адресном пространстве) последовательности полных страниц. В большинстве операционных систем есть распределитель страниц, лежащий в основе стеков, кучи и отображаемых в памяти файлов.
Бен Фойгт

2
@Brian: Кто сказал, что есть какой-то "стек", которым манипулируют brk()и sbrk()? Стеки управляются распределителем страниц на гораздо более низком уровне.
Бен Фойгт

Ответы:


233

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

упрощенное изображение макета виртуальной памяти

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

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

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

менее упрощенное адресное пространство

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

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

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


7
+1 за подробное объяснение. Знаете ли вы, mallocвсе еще полагается brkили использует его, mmapчтобы иметь возможность «отдавать» отдельные блоки памяти?
Андерс Абель

18
Это зависит от конкретной реализации, но IIUC многие современные mallocиспользуют brkобласть для небольших распределений и отдельные mmapдля больших (скажем,> 128 КБ). Смотрите, например, обсуждение MMAP_THRESHOLD на странице руководства Linux malloc(3).
Звол

1
Действительно хорошее объяснение. Но, как вы сказали, стек больше не находится на вершине виртуального адресного пространства. Это верно только для 64-разрядного адресного пространства или верно даже для 32-разрядного адресного пространства. И если стек находится наверху адресного пространства, где происходят анонимные карты памяти? Это на вершине виртуального адресного пространства непосредственно перед стеком.
Ник

3
@Nikhil: это сложно. Большинство 32-битных систем помещают стек в самую верхнюю часть адресного пространства пользовательского режима , которое часто является только младшими 2 или 3G в полном адресном пространстве (оставшееся пространство зарезервировано для ядра). Я не могу сейчас думать о том, что не сделал, но я не знаю их всех. Большинство 64-битных процессоров фактически не позволяют вам использовать все 64-битное пространство; старшие 10-16 битов адреса должны быть все-ноль или все-один. Стек обычно располагается в верхней части используемых младших адресов. Я не могу дать вам правило для mmap; это очень зависит от ОС.
Звол

3
@RiccardoBestetti. Это тратит впустую адресное пространство , но это безвредно - 64-битное виртуальное адресное пространство настолько велико, что если вы прожигаете его гигабайт каждую секунду, вам все равно понадобится 500 лет, чтобы закончиться. [1] Большинство процессоров даже не позволяют использовать более 2 ^ 48 - 2 ^ 53 бит виртуального адреса (единственное исключение, которое я знаю, это POWER4 в режиме хэшированной таблицы страниц). Это не тратит физическую оперативную память; неиспользуемые адреса не присваиваются ОЗУ.
zwol

26

Минимальный исполняемый пример

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

GitHub вверх по течению .

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

Такое brkPOSIX?

brkРаньше это был POSIX, но он был удален в POSIX 2001, поэтому необходимо _GNU_SOURCEбыло получить доступ к оболочке glibc.

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

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

brk против malloc

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

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

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

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

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

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

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

Больше информации

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

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


4
Так pкак указатель на тип int, не должно ли это быть brk(p + 2);?
Йохан Буле

Небольшое примечание: выражение в цикле for агрессивной версии, вероятно, должно выглядеть следующим образом*(p + i) = 1;
lima.sierra

Кстати, почему мы должны использовать brk(p + 2)вместо того, чтобы просто увеличить его на sbrk(2)? BRK действительно необходим?
И Линь Лю

1
@YiLinLiu Я думаю, что это просто два очень похожих C-интерфейса для одного ядра ( brksyscall). brkНемного удобнее восстановить ранее выделенный стек.
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功

1
@CiroSantilli 96 改造 中心 996ICU 六四 事件 Учитывая размер int равным 4 байта и размер int * как 4 байта (на 32-битной машине), мне было интересно, не увеличится ли он всего на 4 байта (вместо 8 - (2 * размер int)). Не должно ли оно указывать на следующее доступное хранилище кучи - которое будет на расстоянии 4 байта (не 8). Поправь меня, если я что-то здесь упустил.
Сакет Шарад

10

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

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

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

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

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

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


Еще кое-что ...

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


7
-1 потому что: malloc/ freeнаверняка может (и делает) вернуть память операционной системе. Возможно, они не всегда делают это, когда вы этого хотите, но дело в том, что эвристика была неправильно настроена для вашего варианта использования. Что еще более важно, небезопасно вызывать sbrkс ненулевым аргументом в любой программе, которая может когда-либо вызываться malloc- и почти все функции библиотеки C могут вызываться mallocвнутри. Единственные, которые определенно не будут - это функции, защищенные от асинхронных сигналов .
Звол

И под «это небезопасно» я подразумеваю «ваша программа потерпит крах».
Звол

Я отредактировал, чтобы удалить возвращающуюся память , и упомянул об опасности использования библиотечных функций внутри malloc.
luser droog

1
Если вы хотите сделать необычное выделение памяти, либо разместите его на вершине malloc, либо на вершине mmap. Не трогайте brk и sbrk, это реликвии из прошлого, которые приносят больше вреда, чем пользы (даже страницы руководства говорят, что вам следует избегать их!)
Eloff

3
Это глупо. Если вы хотите избежать издержек malloc для большого количества небольших выделений, сделайте одно большое выделение (с помощью malloc или mmap, а не sbrk) и распределите его самостоятельно. Если вы храните узлы вашего двоичного дерева в массиве, вы можете использовать индексы 8b или 16b вместо указателей 64b. Это прекрасно работает, когда вам не нужно удалять какие-либо узлы, пока вы не будете готовы удалить все узлы. (например, создайте отсортированный словарь на лету.) Использование sbrkдля этого полезно только для code-golf, потому что ручное использование mmap(MAP_ANONYMOUS)лучше во всех отношениях, кроме размера исходного кода.
Питер Кордес

3

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


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

Да, исправлено. Извини за это!
R .. GitHub СТОП ПОМОГАТЬ ICE

0

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


тогда какая польза от brk и sbrk?
Ник

3
@NikhilRathod: malloc()будет использовать brk()и / или sbrk()под капотом - и вы тоже можете, если вы хотите реализовать свою собственную настроенную версию malloc().
Даниэль Приден

@Daniel Pryden: как brk и sbrk могут работать с кучей, когда она находится между стеком и сегментом данных, как показано на рисунке выше. для этого на работе куча должна быть в конце. Я прав?
Ник

2
@ Брайан: Дэниел сказал, что ОС управляет сегментом стека , а не указателем стека ... очень разные вещи. Дело в том, что для сегмента стека нет системного вызова sbrk / brk - Linux автоматически распределяет страницы при попытках записи в конец сегмента стека.
Джим Балтер

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

0

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


Итак, вы говорите, что все диаграммы в интернете, как и в моем вопросе, неверны. Если возможно, пожалуйста, укажите мне правильную схему.
Ник

2
@Nikkhil Имейте в виду, что верхняя часть этой диаграммы конец памяти. Вершина стека перемещается вниз на диаграмме по мере роста стека. Вершина кучи перемещается вверх на диаграмме по мере ее расширения.
Брайан Гордон

0

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


Он также содержит неинициализированные статические данные (не представленные в исполняемом файле), которые могут быть мусором.
Люзер Дрог

Неинициализированные статические данные ( .bss) инициализируются операционной системой до нуля со всеми битами до запуска программы; это фактически гарантировано стандартом C. Некоторые встроенные системы могут не беспокоить, я полагаю (я никогда не видел ни одной, но я не работаю со всеми
этими

@zwol: Linux имеет опцию времени компиляции, чтобы не обнулять возвращаемые страницы mmap, но я бы предположил, что .bssвсе равно будет обнулено. Пространство BSS, вероятно, является наиболее компактным способом выразить тот факт, что программе нужны массивы нулей.
Питер Кордес

1
@PeterCordes Стандарт C говорит, что глобальные переменные, объявленные без инициализатора, обрабатываются как если бы они были инициализированы нулем. Реализация AC, которая помещает такие переменные в .bssнулевое значение .bssи поэтому не будет соответствовать. Но ничто не заставляет реализацию C использовать .bssвообще или даже иметь такую ​​вещь.
zwol

@PeterCordes Кроме того, грань между «реализацией C» и программой может быть очень размытой, например, обычно есть небольшой фрагмент кода из реализации, статически связанный с каждым исполняемым файлом, который выполняется раньше main; этот код может обнулять .bssобласть, а не заставлять ядро ​​делать это, и это все равно будет соответствовать.
zwol

0

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

включают

int main(void){

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.