0Pricing
C Academy · Урок

Как работает malloc

Куча и списки свободной памяти.

«Как работает malloc» — бесплатный урок C Academy на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C Academy содержит 4 уроков всего.

Что на самом деле делает malloc

Когда вы вызываете malloc(n), библиотека C передаёт вам указатель как минимум на n доступных байт. Но куча — это всего лишь область памяти процесса, которой управляет распределитель памяти.

Задача распределителя — вести учёт: отслеживать используемые и свободные байты и эффективно повторно использовать освобождённую память.

Куча поступает из OS

Распределитель не создаёт память из ничего. Он запрашивает у операционной системы большие блоки с помощью системных вызовов вроде brk/sbrk или mmap.

Затем он разделяет эти блоки на меньшие части для ваших вызовов malloc. Обращение к OS обходится дорого, поэтому распределители запрашивают память большими объёмами и повторно используют её.

/* Conceptual: grow the heap by 4096 bytes */
void *base = sbrk(4096);
if (base == (void *)-1) {
    /* out of memory */
}

sbrk и граница программы

sbrk(n) перемещает «границу программы» вверх на n байт и возвращает прежнее значение границы. Открытая таким образом область становится доступной памятью кучи.

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

void *prev_break = sbrk(0);   /* current break */
sbrk(1024);                   /* grow by 1 KB */
/* prev_break now points to fresh memory */

Метаданные блока

Для каждого выделения распределитель хранит рядом с данными небольшой заголовок: размер блока и информацию о том, свободен ли он. Благодаря этому заголовку free может работать только с переданным ему указателем на данные.

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

typedef struct block {
    size_t size;
    int free;
    struct block *next;
} block_t;

Указатель сразу после заголовка

Распространённый приём — арифметика указателей: пользовательский указатель равен header + 1. Если дан пользовательский указатель, заголовок находится на один block_t перед ним.

Так free(p) восстанавливает размер выделенного блока, хотя вам не нужно передавать этот размер отдельно.

block_t *hdr = (block_t *)user_ptr - 1;
printf("block size = %zu\n", hdr->size);

Демонстрация небольшой структуры заголовка

Разместим заголовок в статическом буфере и прочитаем его обратно. Это показывает, как настоящий распределитель разделяет область на заголовок и полезную нагрузку.

Вызовы OS не используются, поэтому программа работает где угодно.

#include <stdio.h>
#include <stddef.h>

typedef struct { size_t size; int free; } block_t;
static char buffer[256];

int main(void) {
    block_t *h = (block_t *)buffer;
    h->size = 64;
    h->free = 0;
    void *payload = (char *)buffer + sizeof(block_t);
    printf("header bytes = %zu\n", sizeof(block_t));
    printf("payload offset = %ld\n", (long)((char *)payload - buffer));
    printf("size field = %zu\n", h->size);
    return 0;
}

Идея списка свободных блоков

Многие распределители объединяют свободные блоки в связный список. Когда вы вызываете malloc, распределитель просматривает этот список в поисках достаточно большого блока.

Когда вы вызываете free, блок помечается как свободный и возвращается в список для последующего повторного использования, что позволяет избежать нового запроса к OS.

block_t *find_free(block_t *head, size_t size) {
    block_t *b = head;
    while (b && !(b->free && b->size >= size))
        b = b->next;
    return b;
}

Что должен делать free

free(p) находит заголовок для p, помечает блок как свободный и в идеале объединяет его с соседними свободными блоками (слияние), чтобы уменьшить фрагментацию.

Двойной вызов free для одного указателя или освобождение указателя, не относящегося к куче, приводит к неопределённому поведению, поскольку метаданные повреждаются.

void my_free(void *p) {
    if (!p) return;
    block_t *hdr = (block_t *)p - 1;
    hdr->free = 1;
    /* real allocators coalesce neighbors here */
}

Фрагментация

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

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

Требования к выравниванию

malloc должен возвращать память, выровненную для любого типа. В большинстве 64-разрядных систем это означает выравнивание по границе 16 байт, соответствующее требованиям max_align_t.

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

#include <stdalign.h>
/* alignof(max_align_t) is the strictest required alignment */
size_t a = alignof(max_align_t);

Собираем всё вместе

Таким образом, минимальному распределителю нужны: источник памяти (статический буфер, sbrk или mmap), заголовки каждого блока, стратегия поиска свободного места и обработка выравнивания.

В следующих уроках мы создадим эти компоненты: сначала распределитель bump_alloc, затем списки свободных блоков, а после этого выравнивание и разделение блоков.

/* The four pillars of a custom allocator */
/* 1. memory source   2. block headers */
/* 3. free-block search   4. alignment */

Быстрая проверка

Проверьте своё понимание внутреннего устройства распределителей памяти.

Итоги

malloc управляет кучей, полученной от OS через sbrk или mmap, и разделяет её на блоки со скрытыми заголовками, в которых хранятся размер и состояние свободен/занят.

Списки свободных блоков позволяют повторно использовать память, выравнивание подходит для любого типа, а фрагментация остаётся главной проблемой. Эти идеи лежат в основе распределителя, который мы создадим дальше.

Часто задаваемые вопросы

Урок «Как работает malloc» бесплатный?

Да — полный текст урока «Как работает malloc» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C Academy, подпишись на CoddyKit PRO. Курс C Academy содержит 4 уроков всего.

Чему я научусь в уроке «Как работает malloc»?

Куча и списки свободной памяти. Ты практикуешь C Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать C Academy?

Предыдущий опыт не требуется. C Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.

Сколько времени занимает урок «Как работает malloc»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке C Academy?

Да. Каждый урок C Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Как работает malloc
  2. Простой линейный распределитель
  3. Списки свободной памяти и повторное использование
  4. Выравнивание и разделение
← Назад к C Academy