FreeRTOS 16 · メモリ管理 — heap_1〜heap_5 のどれを選ぶか

Chapter 16

メモリ管理 — heap_1〜heap_5 のどれを選ぶか

この章がなぜ必要なのか——標準の malloc が使えないから.

C 標準ライブラリの malloc には、組み込みで困る性質がある。

FreeRTOS は malloc を使わず、用途に応じて選べる 5 つの実装を用意している。 どれを選ぶかは、システムの設計そのものである。

この章で使う既出の用語(定義は各リンク先). TCB(02 章 2 節)、スタック(02 章 2 節)、タスク(02 章 2 節)、FreeRTOS(06 章 4 節)、RAM(06 章 4 節)、最も一般的(08 章 1 節)、スレッドセーフ(11 章 1 節)、ブロック(13 章 1 節)、キュー(14 章 7 節)

1. FreeRTOS のメモリ確保 API

void *pvPortMalloc( size_t xWantedSize );
void  vPortFree( void *pv );
size_t xPortGetFreeHeapSize( void );
size_t xPortGetMinimumEverFreeHeapSize( void );

タスク・キュー・セマフォなどのカーネルオブジェクトは、 すべてこの pvPortMalloc() からメモリを取る。

アプリケーションからも直接使える(そして malloc の代わりに使うべきである)。

2. ヒープの領域はどこにあるか

#define configTOTAL_HEAP_SIZE  ( 20 * 1024 )

heap_1〜heap_4 では、この大きさの静的配列が確保される。

static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

これは「.bss にある巨大な配列」である.

つまり、configTOTAL_HEAP_SIZE を増やすとリンク時に RAM が消費される。 実行時に足りなくなるのではなく、ビルド時にリンカが「RAM が足りない」と怒る。

これは良いことである。実行してみないと分からない、という状況を避けられる。

heap_5 だけは例外で、複数の領域を実行時に登録する(後述)。

3. 5 つの実装

heap_1 — 解放できない、最も単純

void *pvPortMalloc( size_t xWantedSize )
{
    /* 単に次の空き位置を返して、ポインタを進めるだけ */
    pvReturn = pucAlignedHeap + xNextFreeByte;
    xNextFreeByte += xWantedSize;
    return pvReturn;
}

void vPortFree( void *pv ) { /* 何もしない */ }
観点評価
実行時間完全に一定(数命令)
断片化起こりえない
解放できない
コードサイズ最小

これが最も安全な選択肢である場合が多い.

起動時に全タスク・全キューを作り、以後は解放しない—— これは組み込みシステムの最も推奨される設計である。 その場合、heap_1 で完璧に足りる。

「解放できない」は欠点ではなく、「解放しない設計を強制する」という利点である。 IEC 61508 などの機能安全規格では、動的確保そのものが制限されることが多い。

heap_2 — 解放できるが、結合しない(非推奨)

空きブロックのリンクリストを持ち、最適合 (best fit) で探す。 解放したブロックはリストに戻すが、隣接する空きブロックを結合しない。

確保: [64][128][64] を確保して、真ん中を解放
空き: [64][----128 空き----][64][残り]

次に 200 バイト要求すると→ 128 の空きは使えず、残りから取る
128 バイトは永久に使われないかもしれない

これが断片化である。

heap_2 は FreeRTOS 公式で「非推奨 (deprecated)」とされている. heap_4 が上位互換なので、新規開発で選ぶ理由はない。 古いプロジェクトで見かけたら、heap_4 に移行を検討すること。

heap_4 — 結合する。最も一般的

heap_2 と同じくリンクリストだが、隣接する空きブロックを自動的に結合する (coalescence)。

static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert )
{
    /* アドレス順に空きリストへ挿入する */
    for( pxIterator = &xStart;
         pxIterator->pxNextFreeBlock < pxBlockToInsert;
         pxIterator = pxIterator->pxNextFreeBlock ) {}

    /* ★ 直前のブロックと連続しているなら結合 */
    puc = ( uint8_t * ) pxIterator;
    if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert ) {
        pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
        pxBlockToInsert = pxIterator;
    }

    /* ★ 直後のブロックと連続しているなら結合 */
    puc = ( uint8_t * ) pxBlockToInsert;
    if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock ) {
        if( pxIterator->pxNextFreeBlock != pxEnd ) {
            pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;
            pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock;
        }
        ...
    }
    ...
}

確保は最初適合 (first fit) である(アドレス順のリストを先頭から見て、最初に入るところ)。

観点評価
実行時間一定ではない(リストの長さに依存)
断片化起きるが、結合により大幅に軽減される
解放できる
用途最も一般的な選択

heap_3 — 標準 malloc のラッパー

void *pvPortMalloc( size_t xWantedSize )
{
    vTaskSuspendAll();
    {
        pvReturn = malloc( xWantedSize );      /* 標準ライブラリを呼ぶ */
    }
    ( void ) xTaskResumeAll();
    return pvReturn;
}

vTaskSuspendAll() で囲むことでスレッドセーフにしているだけである。

観点評価
ヒープの場所リンカスクリプトで決まる(configTOTAL_HEAP_SIZE は使われない)
実行時間標準ライブラリ次第。予測不能
用途既存コードが malloc を多用していて、統一したい場合

heap_3 は積極的に選ぶものではない. 標準 malloc の欠点(予測不能な実行時間、断片化)をそのまま引き継ぐ。 しかも vTaskSuspendAll() の区間が長くなるので、全タスクの起床が遅れる(10 章)。

heap_5 — 複数の領域を扱える

heap_4 と同じアルゴリズムだが、連続していない複数のメモリ領域を 1 つのヒープとして扱える。

static const HeapRegion_t xHeapRegions[] =
{
    { ( uint8_t * ) 0x20000000UL, 0x00010000 },   /* 内蔵 SRAM 64 KB */
    { ( uint8_t * ) 0x60000000UL, 0x00100000 },   /* 外付け SDRAM 1 MB */
    { NULL, 0 }                                    /* 終端 */
};

int main( void )
{
    vPortDefineHeapRegions( xHeapRegions );    /* ★ 他の何よりも先に呼ぶ */
    ...
}

vPortDefineHeapRegions() は最優先で呼ぶこと.

これを呼ぶ前に pvPortMalloc() が呼ばれると失敗する。 xTaskCreate() を含む、あらゆる FreeRTOS API の前に呼ぶ必要がある。

また、領域はアドレスの昇順に並べる必要がある。順序が違うと正しく動かない。

4. 5 つの比較表

heap_1heap_2heap_3heap_4heap_5
解放不可可可可可
結合—しないライブラリ次第するする
実行時間一定可変不定可変可変
断片化なし大きいあり小さい小さい
複数領域不可不可不可不可可
コードサイズ最小小最小中中
推奨度★★★非推奨★★★★★★(必要なとき)

ヒープの動作を見る

確保と解放を繰り返して、断片化がどう進むか、結合がどう効くかを確かめられる。

5. 選び方の指針

状況選択
起動時に全部作り、解放しないheap_1(または静的確保)
動的な確保・解放があるheap_4
外付け RAM も使うheap_5
既存の malloc 依存が多いheap_3(暫定)
認証が必要(動的確保禁止)静的確保のみ(次節)

迷ったら heap_4 を選ぶ. 汎用性が高く、断片化にも強い。

6. 静的確保 — ヒープを使わない

#define configSUPPORT_STATIC_ALLOCATION   1
#define configSUPPORT_DYNAMIC_ALLOCATION  0     /* 動的を完全に禁止 */

すべてのカーネルオブジェクトに、静的確保版の API がある。

/* タスク */
StaticTask_t xTaskBuffer;
StackType_t  xStack[ 256 ];
TaskHandle_t h = xTaskCreateStatic( vTask, "T", 256, NULL, 2, xStack, &xTaskBuffer );

/* キュー */
StaticQueue_t xQueueBuffer;
uint8_t ucStorage[ 10 * sizeof( int ) ];
QueueHandle_t q = xQueueCreateStatic( 10, sizeof( int ), ucStorage, &xQueueBuffer );

/* セマフォ */
StaticSemaphore_t xSemBuffer;
SemaphoreHandle_t s = xSemaphoreCreateBinaryStatic( &xSemBuffer );

/* タイマ */
StaticTimer_t xTimerBuffer;
TimerHandle_t t = xTimerCreateStatic( "T", 100, pdTRUE, NULL, cb, &xTimerBuffer );

configSUPPORT_STATIC_ALLOCATION = 1 にすると、 アイドルタスクとタイマタスクのメモリも自分で提供する必要がある。

void vApplicationGetIdleTaskMemory( StaticTask_t **ppxIdleTaskTCBBuffer,
                                    StackType_t **ppxIdleTaskStackBuffer,
                                    uint32_t *pulIdleTaskStackSize )
{
    static StaticTask_t xIdleTaskTCB;
    static StackType_t  uxIdleTaskStack[ configMINIMAL_STACK_SIZE ];

    *ppxIdleTaskTCBBuffer   = &xIdleTaskTCB;
    *ppxIdleTaskStackBuffer = uxIdleTaskStack;
    *pulIdleTaskStackSize   = configMINIMAL_STACK_SIZE;
}

void vApplicationGetTimerTaskMemory( StaticTask_t **ppxTimerTaskTCBBuffer,
                                     StackType_t **ppxTimerTaskStackBuffer,
                                     uint32_t *pulTimerTaskStackSize )
{
    static StaticTask_t xTimerTaskTCB;
    static StackType_t  uxTimerTaskStack[ configTIMER_TASK_STACK_DEPTH ];

    *ppxTimerTaskTCBBuffer   = &xTimerTaskTCB;
    *ppxTimerTaskStackBuffer = uxTimerTaskStack;
    *pulTimerTaskStackSize   = configTIMER_TASK_STACK_DEPTH;
}

これを定義し忘れるとリンクエラーになる. 忘れようがないので安心してよい。

静的確保の利点

利点内容
失敗しないヒープ不足で NULL が返ることがない
ビルド時に分かるRAM が足りなければリンカが教えてくれる
断片化しないヒープを使わないので当然
静的解析できるメモリマップが完全に確定する
認証に通りやすい動的確保の禁止要件を満たせる

新規プロジェクトでは静的確保を既定にすることを強く推奨する.

「起動時に作って、以後解放しない」設計なら、動的確保を使う理由が 1 つもない。 記述が少し冗長になるだけで、得られる安心感は大きい。

7. ヒープの監視

size_t free_now = xPortGetFreeHeapSize();                 /* 現在の空き */
size_t free_min = xPortGetMinimumEverFreeHeapSize();      /* 過去最小の空き */

xPortGetMinimumEverFreeHeapSize() が最も重要である。 「これまでで最も苦しかったとき、どれだけ残っていたか」を教えてくれる。

/* 起動から十分に時間が経った後で確認する */
if( xPortGetMinimumEverFreeHeapSize() < 1024 ) {
    log_warn( "heap margin too small" );
}

注意: heap_1 と heap_3 では xPortGetMinimumEverFreeHeapSize() が使えない. heap_1 は解放がないので「最小」に意味がなく、heap_3 は標準ライブラリの内部を見られない。

確保失敗を捕まえる

#define configUSE_MALLOC_FAILED_HOOK  1

void vApplicationMallocFailedHook( void )
{
    /* ヒープが足りなかった。ここで止めるか、ログを残す */
    taskDISABLE_INTERRUPTS();
    for( ;; );
}

開発中は必ず有効にすること。 これがないと、xTaskCreate() が静かに失敗して、後で「なぜかタスクが動かない」になる。

/* 戻り値を必ず確認する習慣も重要 */
if( xTaskCreate( vTask, "T", 256, NULL, 2, NULL ) != pdPASS ) {
    log_error( "task create failed" );
}

8. アラインメントとオーバーヘッド

#define portBYTE_ALIGNMENT  8       /* Cortex-M では 8 */

pvPortMalloc() は、必ずこの境界に揃ったアドレスを返す。

heap_4 の場合、1 回の確保あたりのオーバーヘッドはこうなる。

項目サイズ
BlockLink_t 構造体8 バイト(32 bit CPU)
アラインメント調整0〜7 バイト

小さな確保を大量に行うと、オーバーヘッドが無視できない.

8 バイトを 100 回確保すると、実際に消費されるのは \((8 + 8) \times 100 = 1600\) バイト——要求の 2 倍である。

小さなオブジェクトを大量に扱うなら、 自前の固定サイズプールを作る方が効率的である。

/* 固定サイズプールの例: カウンティングセマフォで個数を管理 */
static Item_t pool[ 32 ];
static SemaphoreHandle_t xPoolSem;    /* Counting(32, 32) */
static StackType_t freeList[ 32 ];

9. この章のまとめ

ポイント内容
標準 malloc の問題非スレッドセーフ・予測不能・断片化・可視性なし
heap_1解放不可。一定時間・断片化なし。最も安全
heap_2結合しない。非推奨
heap_3標準 malloc のラッパー。積極的には選ばない
heap_4結合する。最も一般的な選択
heap_5複数領域。vPortDefineHeapRegions() を最優先で呼ぶ
静的確保新規プロジェクトの既定にすべき。失敗しない・解析できる
監視xPortGetMinimumEverFreeHeapSize() が最重要
フックconfigUSE_MALLOC_FAILED_HOOK は開発中必須
オーバーヘッド1 確保あたり 8 バイト+アラインメント

次章では、ヒープと並ぶもう 1 つのメモリ—— スタックと、その溢れによる事故を扱う。