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_1 | heap_2 | heap_3 | heap_4 | heap_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 つのメモリ—— スタックと、その溢れによる事故を扱う。