応用編 第3章: ヒープ自作 — malloc / free を書く#

malloc / free を自分で書き、断片化と戦います。

サンプルコード#

  • ディレクトリ: docs/os-on-arduino/code/adv3_heap

  • ビルドと書き込み:

uv run pio run -d adv3_heap -t upload
uv run pio device monitor -b 115200

S を 1 文字送ると出力が始まります。

コードを見る: docs/os-on-arduino/code/adv3_heap

ソースコード#

この章のスケッチです。ファイル名をクリックすると開きます。コードは原稿リポジトリから自動で同期されているので、ここに出ているものが常に最新です。

src/main.cpp — 75 行

GitHub で開く

 1/**
 2 * 応用編 第3章: ヒープ自作 — malloc / free を書く
 3 *
 4 * 本章の課題 3.9.1 をそのまま実行する。
 5 *
 6 *   heap_init() → a=malloc(100) → b=malloc(100) → c=malloc(100)
 7 *               → free(b) → free(a)
 8 *
 9 * 各ステップで heap_dump() を呼び、free(a) のあとに a と b が
10 * ひとつの空きブロックへ結合されることを確かめる。
11 * 最後に外部断片化(合計は足りるのに連続では取れない)を再現する。
12 */
13
14#include <Arduino.h>
15#include "os_heap.h"
16
17static void step(const char *label)
18{
19    Serial.println();
20    Serial.print(F("### ")); Serial.println(label);
21    heap_dump();
22}
23
24void setup()
25{
26    Serial.begin(115200);
27    while (!Serial) { }
28    while (Serial.read() != 'S') { delay(1); }
29
30    Serial.println(F("=== Advanced 3: my_malloc / my_free ==="));
31
32    heap_init();
33    step("heap_init()");
34
35    void *a = my_malloc(100);  step("a = my_malloc(100)");
36    void *b = my_malloc(100);  step("b = my_malloc(100)");
37    void *c = my_malloc(100);  step("c = my_malloc(100)");
38
39    my_free(b);                step("my_free(b)");
40    my_free(a);                step("my_free(a)  <- a と b が結合されるはず");
41    my_free(c);                step("my_free(c)  <- ヒープが 1 つの空きブロックに戻る");
42
43    // 課題: ヒープを満杯にしてから 1 つおきに解放すると、
44    // 空き合計は足りるのに連続では取れない(外部断片化)
45    Serial.println();
46    Serial.println(F("### 外部断片化の確認"));
47
48    void *p[8];
49    for (int i = 0; i < 8; i++) {
50        p[i] = my_malloc(496);   // (ヘッダ 16 + 496) × 8 = 4096 で満杯
51    }
52    my_free(p[1]);
53    my_free(p[3]);
54    my_free(p[5]);               // 496 バイトの穴が 3 つ
55    Serial.print(F("空き合計 = ")); Serial.println((unsigned)heap_free_total());
56
57    void *d = my_malloc(600);
58    Serial.print(F("my_malloc(600) -> ")); Serial.println(d ? "OK" : "NULL");
59    Serial.println(F("空きの合計は足りていても、連続した領域が無ければ取れない"));
60
61    my_free(p[2]);               // p[1]〜p[3] が 1520 バイトの穴に結合される
62    void *e = my_malloc(600);
63    Serial.print(F("my_free(p[2]) 後の my_malloc(600) -> ")); Serial.println(e ? "OK" : "NULL");
64
65    // 3.7 デバッグ機能: 二重 free を検出する
66    Serial.println();
67    Serial.println(F("### 二重 free の検出"));
68    my_free(p[0]);
69    my_free(p[0]);   // ここで [heap] double free detected が出る
70
71    Serial.println();
72    Serial.println(F("done."));
73}
74
75void loop() { }
include/os_heap.h — 22 行

GitHub で開く

 1// os_heap.h
 2//
 3// 本書 応用編 第3章「ヒープ自作」で作る freelist 型アロケータ。
 4
 5#pragma once
 6
 7#include <stddef.h>
 8#include <stdint.h>
 9
10#ifdef __cplusplus
11extern "C" {
12#endif
13
14void  heap_init(void);
15void *my_malloc(size_t n);
16void  my_free(void *p);
17void  heap_dump(void);          // 3.7 デバッグ機能
18size_t heap_free_total(void);   // 空き合計(断片化の観察用)
19
20#ifdef __cplusplus
21}
22#endif
src/os_heap.cpp — 128 行

GitHub で開く

  1// os_heap.cpp
  2//
  3// 本書 応用編 第3章の実装。ブロックヘッダ + 単方向フリーリスト + first-fit +
  4// 隣接結合。ヒープは明示的な配列として持つ(3.2.2 の「スタックとヒープの衝突」
  5// を避けるため)。
  6
  7#include <Arduino.h>
  8#include <string.h>
  9#include "os_heap.h"
 10
 11typedef struct block_header {
 12    size_t                size;   // データ部のバイト数 (ヘッダは含まない)
 13    int                   free;   // 1 なら空き、0 なら使用中
 14    struct block_header  *next;   // 次のブロックヘッダ
 15    uint32_t              magic;  // 二重 free / 破壊検出用
 16} block_header_t;
 17
 18#define HEAP_MAGIC 0xA110CA7E    // "ALLOCATE" の語呂合わせ
 19#define ALIGN8(x)  (((x) + 7u) & ~7u)
 20#define HEAP_SIZE  4096
 21
 22static uint8_t heap_area[HEAP_SIZE] __attribute__((aligned(8)));
 23static block_header_t *heap_head = NULL;
 24
 25void heap_init(void)
 26{
 27    heap_head        = (block_header_t *)heap_area;
 28    heap_head->size  = HEAP_SIZE - sizeof(block_header_t);
 29    heap_head->free  = 1;
 30    heap_head->next  = NULL;
 31    heap_head->magic = HEAP_MAGIC;
 32}
 33
 34void *my_malloc(size_t n)
 35{
 36    if (n == 0) return NULL;
 37    n = ALIGN8(n);                 // 8 の倍数に切り上げ
 38
 39    block_header_t *b = heap_head;
 40    while (b != NULL) {
 41        if (b->magic != HEAP_MAGIC) {
 42            // ヘッダが破壊されている (バッファオーバーラン等)
 43            return NULL;
 44        }
 45        if (b->free && b->size >= n) {
 46            // 余りが十分なら分割 (split)
 47            if (b->size >= n + sizeof(block_header_t) + 8) {
 48                block_header_t *rest =
 49                    (block_header_t *)((uint8_t *)b + sizeof(block_header_t) + n);
 50                rest->size  = b->size - n - sizeof(block_header_t);
 51                rest->free  = 1;
 52                rest->next  = b->next;
 53                rest->magic = HEAP_MAGIC;
 54                b->size = n;
 55                b->next = rest;
 56            }
 57            b->free = 0;
 58            return (uint8_t *)b + sizeof(block_header_t);
 59        }
 60        b = b->next;
 61    }
 62    return NULL;                   // 空きなし
 63}
 64
 65void my_free(void *p)
 66{
 67    if (p == NULL) return;
 68
 69    block_header_t *b =
 70        (block_header_t *)((uint8_t *)p - sizeof(block_header_t));
 71
 72    if (b->magic != HEAP_MAGIC) {
 73        // 不正なポインタ、もしくはヘッダ破壊
 74        Serial.println(F("[heap] bad pointer or corrupted header"));
 75        return;
 76    }
 77    if (b->free) {
 78        // すでに free 済み → 二重 free 検出
 79        Serial.println(F("[heap] double free detected"));
 80        return;
 81    }
 82    b->free = 1;
 83
 84    // 後続ブロックと結合
 85    if (b->next != NULL && b->next->free) {
 86        b->size += sizeof(block_header_t) + b->next->size;
 87        b->next  = b->next->next;
 88    }
 89
 90    // 前のブロックと結合 (単方向リストなので先頭から探す)
 91    block_header_t *prev = heap_head;
 92    while (prev != NULL && prev->next != b) prev = prev->next;
 93    if (prev != NULL && prev->free) {
 94        prev->size += sizeof(block_header_t) + b->size;
 95        prev->next  = b->next;
 96    }
 97}
 98
 99void heap_dump(void)
100{
101    block_header_t *b = heap_head;
102    int i = 0;
103    size_t total_free = 0, total_used = 0;
104
105    Serial.println(F("---- heap dump ----"));
106    while (b != NULL) {
107        char line[72];
108        snprintf(line, sizeof(line), "[%02d] +%04u size=%5u %s",
109                 i, (unsigned)((uint8_t *)b - heap_area),
110                 (unsigned)b->size, b->free ? "FREE" : "USED");
111        Serial.println(line);
112        if (b->free) total_free += b->size;
113        else         total_used += b->size;
114        b = b->next;
115        i++;
116    }
117    Serial.print(F("total free=")); Serial.print((unsigned)total_free);
118    Serial.print(F(" used="));      Serial.println((unsigned)total_used);
119}
120
121size_t heap_free_total(void)
122{
123    size_t total = 0;
124    for (block_header_t *b = heap_head; b != NULL; b = b->next) {
125        if (b->free) total += b->size;
126    }
127    return total;
128}

実機での出力#

Arduino UNO R4 WiFi の実機で実際に取得した出力です(macOS / Windows の 両方で同じ結果を確認しています)。手元の出力と見比べてください。

ヒープの状態を段階ごとにダンプしています。

../_images/adv3_heap.gif

S を送ると、すべてのダンプがまとめて出ます。最後は「空きの合計は 足りていても連続領域が無ければ取れない」ところと、二重 free の検出です#

=== Advanced 3: my_malloc / my_free ===

### heap_init()
---- heap dump ----
[00] +0000 size= 4080 FREE
total free=4080 used=0

### a = my_malloc(100)
---- heap dump ----
[00] +0000 size=  104 USED
[01] +0120 size= 3960 FREE
total free=3960 used=104

### b = my_malloc(100)
---- heap dump ----
[00] +0000 size=  104 USED
[01] +0120 size=  104 USED
[02] +0240 size= 3840 FREE
total free=3840 used=208
...

つまずきやすいポイント#

(読者からの質問に応じて随時追記します。質問は GitHub Issues へどうぞ)