Понимание распределение Heap Memory в C — sbrk и brk
В этом уроке мы изучим память процесса, чтобы лучше понять, как память распределяется в heap в C
. Для начала давайте взглянем на общую структуру памяти процесса.
Память стека
Когда вызывается функция, память стека увеличивается, чтобы вместить кадр стека функции. Когда функция завершает работу, размер стековой памяти уменьшается по мере того, как она выходит из стекового фрейма функции. Память стека начинается с верхнего края памяти и растет вниз, к heap.
Регистр специального назначения в вашем процессоре, называемый указателем стека, отслеживает текущую вершину стека. Каждый кадр стека содержит аргументы функции, локальные переменные и информацию о связи вызовов, например адреса возврата для функции. Поскольку функции можно вызывать внутри функций, мы часто можем видеть в стеке несколько кадров.
В C
нам обычно не нужно слишком подробно беспокоиться о стековой памяти. Знание того, как работают функциональные фреймы, дает важный контекст, но обычно мы не взаимодействуем со стеком напрямую. Когда мы выделяем память в C
, мы взаимодействуем с heap, которую мы рассмотрим далее.
Heap Memory
Heap — это динамическая область памяти, которая растет по направлению к стеку. Когда вы выделяете память в программе, эта память выделяется в heap. Для выделения памяти мы обычно используем malloc
, основанный на другом наборе функций — brk()
и sbrk()
. Эти функции работают путем изменения размера разрыва программы, который представляет собой ячейку памяти, обозначающую конец heap и начало нераспределенной памяти.
brk и sbrk
Изменение размера кучи процесса — простая задача. Нам просто нужно указать ядру изменить местоположение остановки программы (которое определяется как текущая вершина кучи). Поскольку память, находящаяся после прерывания программы, является нераспределенной памятью, мы можем переместить разрыв в нераспределенную память, чтобы выделить некоторую новую память для нашего процесса. С этим процессом связаны две функции:
brk(void *end_data_segment)
: устанавливает прерывание программы в место, указанное вend_data_segment
.sbrk(intptr_t increment)
: увеличивает размер разрыва программы с шагом.
Чтобы увидеть, как работают эти функции, давайте рассмотрим простой пример.
#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[]){
int *currentBreak = sbrk(0);
printf("%p\n",currentBreak);
}
В этом примере укажите приращение 0 к sbrk
. Когда для sbrk
предоставляется приращение 0, возвращается текущий адрес прерывания.
Если вы укажете в sbrk
значение, отличное от 0, вы получите адрес предыдущего разрыва, то есть адрес до того, как разрыв был изменен. Мы можем увидеть этот эффект, используя несколько приращений подряд.
#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[]){
void *currentBreak = sbrk(0x5);
printf("First increment of 0x5: %p\n",currentBreak);
currentBreak = sbrk(0x5);
printf("Second increment of 0x5: %p\n",currentBreak);
currentBreak = sbrk(0x5);
printf("Third increment of 0x5: %p\n",currentBreak);
currentBreak = sbrk(0x5);
printf("Fourth increment of 0x5: %p\n",currentBreak);
currentBreak = sbrk(0x5);
printf("Fifth increment of 0x5: %p\n",currentBreak);
}
Когда вы запустите этот набор приращений, вы получите результат, аналогичный показанному на следующем рисунке.
Обратите внимание, что разрыв между первым и вторым приращением больше указанного нами 0x5. Это связано с тем, что printf
выделяет память для использования в качестве буфера для стандартного вывода. Когда это происходит изначально, мы видим большие изменения в разрыве кучи. Третий, четвертый и пятый приращения последовательно перемещаются на приращение 0x5, поскольку между ними в куче не размещается никаких дополнительных данных.
В большинстве случаев мы не используем sbrk
и brk
напрямую для настройки кучи в C. Вместо этого мы используем более удобную для пользователя версию этого процесса, известную как malloc
и free
.