README.md 14 KB
Newer Older
Igor Zhirkov's avatar
Igor Zhirkov committed
1
# Assignment: Memory allocator
Igor Zhirkov's avatar
Igor Zhirkov committed
2
---
Igor Zhirkov's avatar
Igor Zhirkov committed
3
Лабораторная работа: аллокатор памяти
Igor Zhirkov's avatar
Igor Zhirkov committed
4

Igor Zhirkov's avatar
Igor Zhirkov committed
5 6 7 8

# Подготовка 

- Прочитайте про [автоматические переменные](https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html) в `Makefile`
Igor Zhirkov's avatar
Igor Zhirkov committed
9
- Прочитайте главу 12 (стр. 235-239), главу 13 (целиком) "Low-level programming: C, assembly and program execution". 
Igor Zhirkov's avatar
Igor Zhirkov committed
10 11 12 13 14 15 16
- Виртуальная память
  - Освежите ваши знания о виртуальной памяти и её организации (глава 4 "Low-level programming: C, assembly and program execution").
  - Вспомните, что такое файловые дескрипторы. С ними работают системные вызовы `open`, `close`, `write` и другие.
  - Прочитайте внимательно `map mmap`, в особенности значение флага `MAP_FIXED` и `MAP_FIXED_NOREPLACE`.
- Возможности языка. Удостоверьтесь, что вы знаете о том, как работают:
   - ключевые слова `inline` (стр. 280 в учебнике) и `restrict` (стр. 281 в учебнике).
   - Flexible array members (стр. 209 в учебнике).
Igor Zhirkov's avatar
Igor Zhirkov committed
17
   - Макрос `offsetof` и особенно [эта страница](https://p99.gforge.inria.fr/p99-html/group__flexible.html).
Igor Zhirkov's avatar
Igor Zhirkov committed
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
   - использование структур для создания новых псевдонимов типов (как просто `typedef`) но без неявного преобразования (см. [урок на Stepik](https://stepik.org/lesson/408350/step/15)).
 
На защите мы можем обсуждать любые вопросы по этим материалам.

# Аллокатор памяти

Мы уже неоднократно пользовались аллокатором памяти, который является частью стандартной библиотеки C. 
Работа с ним осуществляется через функции `malloc` и `free` (а также `calloc` и `realloc`).
Чтобы лучше прочувствовать то, как он устроен, мы напишем свою упрощённую версию аллокатора.

# Нулевое приближение

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

Аллокатор резервирует большую область памяти с помощью `mmap`. Он размечает её на блоки с заголовками, заголовки образуют связный список. 
В заголовке указано, свободен ли блок, его размер и кто его следующий блок.

- `malloc(size_t n)` : ищем свободный блок размера не меньше `n` и делим его на два блока:
   - блок размера $`n`$
   - оставшаяся часть
   
   При этом по ходу поиска мы объединяем соседние свободные блоки в свободные блоки большего размера.
- При освобождении памяти мы помечаем блок как незанятый и объединяем со
  следующим блоком, пока возможно (пока и текущий и следующий блок свободны и
  пока следующий блок идёт в памяти сразу после данного).
- Если большая область памяти кончается, мы резервируем ещё память. Сначала
  пытаемся сделать это вплотную, сразу после последнего блока, но если не
  получается — позволяем `mmap` выбрать подходящий адрес для начала нового
  региона.
 


# Первое приближение

Представим себе достаточно большую область памяти, которую мы выделяем под кучу. Назовём её *регионом*.
Разметим регион на блоки; каждый блок начинается с заголовка, а сразу после него идут данные.

```
|___заголовок1____|____данные1___||___заголовок2____|____данные2___||___заголовок3____|____...
```

Блоки заполняют собой всё пространство региона. 

## Заголовок 

В заголовке блока содержится ссылка на следующий блок и пометка о статусе блока (занят или свободен).

```c
/* mem_internals.h */

//См. https://stepik.org/lesson/408350/step/15
typedef struct { size_t bytes; } block_capacity;

struct block_header {
  struct block_header*    next;
  block_capacity capacity;
  bool           is_free;
  uint8_t        contents[];  // flexible array member
};
```
Куча задаётся ссылкой на заголовок первого блока.

## Размер и вместимость

У каждого блока есть две характеристики: *размер* и *вместимость*. Чтобы их не путать, мы создадим для них два типа.

```c
/* mem_internals.h */
typedef struct { size_t bytes; } block_capacity;
typedef struct { size_t bytes; } block_size;
```

Igor Zhirkov's avatar
Igor Zhirkov committed
90
Размер блока всегда на `offsetof(struct block_header, contents)` больше, чем его вместимость.
Igor Zhirkov's avatar
Igor Zhirkov committed
91 92 93

```c
/* mem_internals.h */
Igor Zhirkov's avatar
Igor Zhirkov committed
94 95
inline block_size size_from_capacity( block_capacity cap ) { 
   return (block_size) {cap.bytes + offsetof( struct block_header, contents ) }; 
Igor Zhirkov's avatar
Igor Zhirkov committed
96
}
Igor Zhirkov's avatar
Igor Zhirkov committed
97 98
inline block_capacity capacity_from_size( block_size sz ) { 
   return (block_capacity) {sz.bytes - offsetof( struct block_header, contents ) }; 
Igor Zhirkov's avatar
Igor Zhirkov committed
99 100 101
}
```

Igor Zhirkov's avatar
Igor Zhirkov committed
102 103
- В заголовке хранится вместимость блока, а не его суммарный размер вместе с заголовком.
- Нельзя использовать `sizeof( struct block_header )`, т.к. из-за выравнивания размер структуры будет больше. На машине автора, например, размер структуры был равен 24, а `offsetof( struct block_header, contents ) == 17`, что правильно.
Igor Zhirkov's avatar
Igor Zhirkov committed
104 105 106 107 108 109

## Алгоритм `malloc(n)`

- Перебираем блоки пока не находим "хороший".
Хороший блок — такой, в который можно уместить `n` байт.
- Если хорошего блока не нашлось, то см. второе приближение.
Igor Zhirkov's avatar
Fixes  
Igor Zhirkov committed
110
- Хороший блок может быть слишком большим, скажем, нам нужно выделить 20 байт, а его размер 30 мегабайт. Тогда мы разделяем блок на две части: в первом блоке будет `20 + offsetof( struct block_header, contents ) ` байт.
Igor Zhirkov's avatar
Igor Zhirkov committed
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
  Адрес содержимого этого блока и вернёт `malloc`.

## Алгоритм `free(void* addr)`

- Если `addr == NULL`, то не делаем ничего. 
- Нам нужно получить из `addr` (который указывает на начало поля `contents`) адрес начала заголовка (для этого вычтем из него `sizeof(struct mem)`).
  В заголовке блока установим `is_free = true`, всё.



# Второе приближение

Теперь мы опишем ещё несколько аспектов аллокации.

- что делать с большим количеством последовательно идущих свободных блоков?
- как избежать появления слишком маленьких блоков?
- что делать, если память в куче кончилась?


## Алгоритм `malloc(n)`

- Нет смысла выделять блок размером, скажем, 1 байт; даже его заголовок будет занимать больше места.
  Пусть минимальная вместимость блока будет обозначаться так:
  ```c
    #define BLOCK_MIN_CAPACITY 24
  ```

  Слишком маленькие блоки могут образовываться в двух случаях:
  - `n  < BLOCK_MIN_CAPACITY`. Тогда мы будем запрашивать блок не размера `n`, а размера `BLOCK_MIN_CAPACITY`.
  - Мы нашли хороший блок, и его размер немногим больше `n`. Если разделить блок на две части, вместимость второй части окажется меньше `BLOCK_MIN_CAPACITY`. Не будем делить такие блоки, а отдадим блок целиком.
  
- При поиске хорошего блока мы проходимся по блокам кучи. Прежде чем решать, хороший блок или нет, объединим его со всеми идущими за ним свободными блоками.
Igor Zhirkov's avatar
Fixes  
Igor Zhirkov committed
143
- Если в куче память кончилась, надо расширить кучу. Для этого будем использовать системный вызов `mmap`. Обязательно прочитайте `man` чтобы понять, с какими аргументами (`prot` и `flags`) его вызывать!
Igor Zhirkov's avatar
Igor Zhirkov committed
144 145
  
  - Сначала надо попытаться выделить память вплотную к концу кучи и разметить её в один большой свободный блок. Если в первом регионе кучи последний блок был свободен, надо объединить его со следующим.
Igor Zhirkov's avatar
Fixes  
Igor Zhirkov committed
146
  - Если выделить регион вплотную не получилось, то нужно выделить регион "где получится". Последний блок предыдущего региона будет связан с первым блоком нового региона. 
Igor Zhirkov's avatar
Igor Zhirkov committed
147 148
 
 
Igor Zhirkov's avatar
Igor Zhirkov committed
149 150 151
## Алгоритм `free(void* addr)`

- Помимо уже написанного про `free`, при освобождении блока можно объединить его со всеми следующими за ним свободными блоками.
Igor Zhirkov's avatar
Igor Zhirkov committed
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166

# Задание 

- Реализуйте аллокатор, используя заготовку в репозитории.
- Придумайте тесты, показывающие работу аллокатора в важных случаях:
   - Обычное успешное выделение памяти.
   - Освобождение одного блока из нескольких выделенных.
   - Освобождение двух блоков из нескольких выделенных.
   - Память закончилась, новый регион памяти расширяет старый.
   - Память закончилась, старый регион памяти не расширить из-за другого выделенного диапазона адресов, новый регион выделяется в другом месте.
  Тесты должны запускаться из `main.c`, но могут быть описаны в отдельном (отдельных) файлах.
  Алгоритм не самый простой, легко ошибиться. Чтобы не тратить времени на отладку, обязательно делайте разбиение на маленькие функции!
- Разберитесь с тем, как написан `Makefile` и исправьте его так, чтобы в итоговый код включался скомпилированный `main.c`. При желании вы можете написать свой `Makefile`, но только если он будет более красив и выразителен.
  

Igor Zhirkov's avatar
Igor Zhirkov committed
167 168 169 170 171
# Для самопроверки


- Прочитайте [правила хорошего стиля](https://gitlab.se.ifmo.ru/c-language/c-code-guidelines/-/blob/master/README.md). Ваше решение 
должно им соответствовать.
Igor Zhirkov's avatar
Igor Zhirkov committed
172
- Проверьте архитектуру.
Igor Zhirkov's avatar
Igor Zhirkov committed
173
- Пожалуйста, присылайте решение в виде pull-request. [Инструкция](https://gitlab.se.ifmo.ru/cse/main/-/wikis/%D0%9A%D0%B0%D0%BA-%D0%BF%D0%BE%D1%81%D0%BB%D0%B0%D1%82%D1%8C-%D0%B7%D0%B0%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5-%D0%BD%D0%B0-%D0%BF%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%BA%D1%83).  В крайнем случае допускается ссылка на репозиторий на https://gitlab.se.ifmo.ru или https://github.com .
Igor Zhirkov's avatar
Igor Zhirkov committed
174 175 176 177 178


# Дополнительные материалы

- Ulrich Drepper. ["What every programmer should know about memory"](https://people.freebsd.org/~lstewart/articles/cpumemory.pdf)
Igor Zhirkov's avatar
Fixes  
Igor Zhirkov committed
179 180
- [`man mmap` online](https://man7.org/linux/man-pages/man2/mmap.2.html)

Igor Zhirkov's avatar
Igor Zhirkov committed
181 182
- [Статья Doug Lea о том, как работает аллокатор в `glibc`](http://gee.cs.oswego.edu/dl/html/malloc.html). Текущая версия аллокатора работает по более сложному алгоритму. 
- [Исходный код одной из последних версий аллокатора в `glibc`. Очень много хорошо написанных комментариев](docs/malloc-impl.c)