Slab と kmalloc:オブジェクトの再利用、領域の確保、ライフサイクル

このページの目次

オブジェクトは kfree されているのに、システムのエイドメモリがすぐに増えない。この現象は2つの勘定で説明できる。⁠呼び出し元がまだいくつのオブジェクトを保持しているか、アロケータがいくつのページを保持しているかである。

200バイトのオブジェクトから始め、スロット内のオーバーヘッドと予備容量を計算し、割り当て、CPU跨ぎの解放、失敗時のロールバック、アドレスの再利用を追跡する。

説明すべき現象読むべき箇所
200バイトのオブジェクトがなぜより多くのスペースを占有するのか?第1節:オブジェクト、スロット、ページ全体
kmalloc、cache、kvmalloc の選択基準は?第2節:割り当てと解放の契約
kfree の後、ページはいつ返還されるのか?第3~4節:キャッシュと所有権
リークとUAFの証拠はどのように異なるのか?第5~6節:検出と読み取り専用統計

1. 200バイトのオブジェクトで3種類のスペースを明確にする

ビジネスで200バイトが必要で、使用されているキャッシュの実際のオブジェクトストライドが256バイト、保持ページが4096バイトであり、ページ外の管理メタデータを無視すると仮定する。1つのslabページには16個のスロットを配置でき、16個のスロットがすべて占有されている場合、ビジネスの有効コンテンツは3200バイト、スロット内のアライメントなどのオーバーヘッドは合計896バイトとなる。

10個のオブジェクトのみが生存している場合、同じページの4096バイトは次のように勘定する必要がある。

去向計算バイト数
ビジネスコンテンツ10 × 2002000
使用中のスロット内部オーバーヘッド10 × (256 − 200)560
再利用可能な空スロット6 × 2561536
合計1つの保持ページ4096

ビジネスコンテンツ以外の2096バイトには明確な用途があり、すべてをリークとして扱うことはできない。

少数の長寿命オブジェクトがなぜ多くのページを保持し続けるのか

2つのslabにそれぞれ1つのオブジェクトしか生存していない場合、合計で400バイトのビジネスコンテンツしかないが、2つのページを保持し続ける必要があるかもしれない。アロケータは任意のオブジェクトを自由に移動できない。呼び出し元がポインタを保持しているため、任意の移動はこれらのポインタを無効にするからである。一部のサブシステムは独自の回収プロトコルを通じてオブジェクトを解放できるが、これはページ整理によるユーザーページの移動能力とは異なる。

オブジェクトは解放されているが、それらを保持するslabは保持し続ける必要がある場合がある 2つのslabにそれぞれ1つのオブジェクトが残っている slab A slab B オブジェクトが1つのページに集中していると仮定 slab A slab B 各マスは1つのスロットを表す;通常のslabオブジェクトは任意に移動できない。下段は、占有分布が解放可能なページ数に与える影響のみを示している。

これは、slab order を増やすことが両刃の剣である理由も説明している。一度に多くのオブジェクトスロットを取得することで共有リソースへの補充頻度を減らせるが、より高い順序の連続ページに依存することになる。1つの長寿命オブジェクトが、より大きな保持ブロックの返還を妨げる可能性もある。デバッグ用の redzone、割り当て追跡、アライメントはオブジェクトのストライドを変更するため、チェックを有効にすると、スペースの占有状況と割り当て成功率が変化する可能性がある。slab管理とデバッグ を参照。

オブジェクトの数を同じまま、集中型と分散型のレイアウトを切り替える。以下では、同じスロットサイズを使って、コンテンツ、スロット内のオーバーヘッド、空スロット、保持ページをそれぞれ勘定する。スロットをクリックして、独自の寿命分布を構築することもできる。

このモデルでは、1ページあたりのスロット数 、 B、 B、 B。 は生存オブジェクトを持つページの数、 は生存オブジェクトの総数である。

図を準備しています
同じ4つのオブジェクトで、なぜ4倍のページを占有できるのか?

有効コンテンツ、使用中のスロットの内部オーバーヘッド、ページ全体の保持は異なる数量である。分散した少量の生存オブジェクトは、複数のslabが返還されないようにでき、1つのスロットを解放しても1つのページが解放されたわけではない。

2. kmalloc と kmem_cache は呼び出し元にそれぞれ何を決定させるか

kmalloc は要求サイズとフラグに基づいて一般的な割り当てパスを選択し、小規模なオブジェクトは通常適切なサイズクラスに入る。より大きな要求は、すべてのサイズに同名のキャッシュが存在する必然性がないため、直接高い順序のページに依存できる。サイズクラスと各用途のグループ分けは、アーキテクチャ、設定、バージョンによって異なり、キャッシュの名前を1つ並べただけで、すべてのマシンで同じであるとは限らない。

専用の kmem_cache は、固定のオブジェクトレイアウト、アライメント、フラグ、およびオプションのコンストラクタ要件を表す。類似したキャッシュは条件を満たす場合にマージできるが、その条件はサイズが近いだけでなく、フラグ、アライメント、コンストラクタ、およびデバッグ要件を含む。sysfs で異なる名前が同じ基盤となるキャッシュを指す場合もある。専用キャッシュを定義する理由は、オブジェクトの用途と管理ニーズから来るべきであり、単に見栄えの良い統計名を得るためではない。

オブジェクトを再利用する場合、初期化の責任は依然として呼び出し元にある

コンストラクタは通常、slab内の新しいオブジェクトの初期作成に使用され、呼び出し元に割り当てられるたびに再実行されるものではない。前のユーザーが書き込んだフィールドが、次の alloc でコンストラクタによって自動的に復元されることを期待してはならない。kmalloc/kmem_cache_alloc もゼロ初期化を保証しない。ゼロにする必要がある場合は、適切なインターフェースを使用するかフィールドを明示的に初期化し、オブジェクトを他のスレッドに公開する。

インターフェースを選択する場合、連続性と解放インターフェースは1組の契約である

割り当て方法適した要件解放と境界
kmalloc/kzalloc通常のカーネルバッファ、物理的に連続した割り当て結果kfree;要求された有効範囲を超えてアクセスしてはならない
kmalloc_array/kcalloc要素数 × 要素サイズの配列乗算オーバーフローをチェックする割り当てインターフェース、NULL の処理も必要
kmem_cache_alloc作成済みのキャッシュの固定オブジェクト対応する kmem_cache_free を使用し、他のキャッシュと混用してはならない
kvmalloc/kvzalloc仮想連続、物理的に不連続の大きなバッファを受け入れ可能kvfree;使用シナリオと GFP はフォールバックパスの要件に適合する必要がある
vmalloc複数のバックアップページへの仮想連続マッピングvfree;単一のDMA物理バッファとして使用してはならない

実際の割り当てサイズは要求サイズより大きくなる可能性があるが、末尾のスペースはビジネスが自由に使用できる暗黙の許可ではない。サイズクラスで丸める必要がある場合は、適切なインターフェースを使用し、要求/割り当て契約を基準とする。DMAデバイスがバッファにアクセスできるかどうかは、DMA API を経由して確認する必要があり、「kmalloc が連続している」からといって、任意のデバイスがそのCPUアドレスを直接使用できるわけではない。メモリ割り当てガイド を参照。

3. 割り当て、CPU跨ぎの解放、ページ返還の一連の流れ

CPU 0 がオブジェクトを割り当てるとき、ローカルに適切な空スロットがあれば、それを迅速に取得できる。ローカルにオブジェクトがない場合、共有リザーブまたはパーシャルスラブから補充する必要がある。これらも不足している場合に限り、ページアロケータに保持ページを要求して、さらに多くのスロットを構築する。小規模なオブジェクトの割り当てでも、ページアロケータのスローパスに入る可能性があるため、「200バイトしか割り当てない」からといって、スリープや失敗の可能性がないわけではない。

オブジェクトが CPU 1 に渡されて処理され、解放されると、それは CPU 1 が現在使用しているローカルスラブ/キャッシュに属していない可能性がある。アロケータは、所属するスラブ、空の数、キャッシュの帰属などの情報を維持する必要があり、必要に応じてアトミック更新やロックを使用する。すべての free が単に現在のCPUのリストヘッダーに2回ポインタを書き込むだけと仮定してはならない。

オブジェクトの要求から物理ページへ:高速パスはキャッシュに依存、低速パスはリソースを補充 オブジェクトの割り当て / 解放 kmalloc または専用キャッシュ CPU側オブジェクトキャッシュ ローカルでの再利用とバッチ処理を優先 共有リソースとパーシャルスラブ 必要に応じて他のオブジェクトリザーブと調整 ページアロケータ 保持ページの追加 / 返還 キャッシュ階層を示すものであり、特定のバージョンの freelist/sheaf 構造に固定されるものではない;デバッグと NUMA 制約により高速パスが変更される可能性がある。

オブジェクトを解放することは、その使用権をアロケータに返還するにすぎない。一定のオブジェクトバッチが空になり、slab が返還条件を満たすと、保持ページがページアロケータに戻される可能性がある。一定数の空スラブまたはローカルオブジェクト容量を保持することで、次の割り当てラウンドが高速化されるが、低負荷後のメモリ減少が瞬時に完了するわけではない。異常の判断は、安定した負荷下で継続的に増加しているかどうか、キャッシュの設計された保持ポリシー、および参照チェーンと組み合わせて行うべきであり、1回の kfree の前後の MemFree を比較するだけでは不十分である。

ソースコードの読み取り:キャッシュの組織はバージョンによって変化する

旧版の SLUB では、高速パスを説明するために per-CPU active slab/freelist が一般的に使用されていた。今回確認した Linux 7.2 の mm/slub.c では、CPU側 sheaves オブジェクト配列、NUMA ノードの barn を使用して、バッチキャッシュと交換が行われている。

これらはすべて共有リソース操作の頻度を減らすことを目的としているが、同期、RT、およびデバッグパスは対象バージョンに応じて読み取る必要がある。kmalloc などのインターフェース契約を学ぶ際には、これらの内部組織の変化を別個に扱うべきである。

4. 割り当て失敗時、未完成品には明確な所有者が必要である

初期化プロセスを考慮する。キャッシュを作成し、ポインタ配列を割り当て、32個のオブジェクトを割り当てる必要があるとする。19番目のオブジェクトの割り当てに失敗した場合、成功した前の18個のオブジェクトは依然として初期化者に属している。配列のみを解放すると、それらのアドレスが失われる。キャッシュのみを破棄しても、生存しているオブジェクトを解放する代わりにはならない。

19回目のオブジェクト割り当てに失敗した場合、クリーンアップの責任は成功した記録に従って逆展開される。

取り消し順序解放するものなぜこのステップで処理しなければならないか
1成功した前の18個のオブジェクトそれらは依然として初期化者に属しており、アドレスを失ってはならない
2ポインタ配列すべてのオブジェクトが解放されて初めて、インデックスは不要になる
3キャッシュ生存オブジェクトがないことを確認してから、保持メカニズムを破棄する

複雑なドライバーでは、エントリ、バックグラウンドタスク、非同期参照を先に停止する必要がある。アロケータはスロットが空かどうかを知っていても、外部コールバックがまだそれをアクセスするかどうかは知らない。

実験コード:ロールバック順序を先に確認し、各エラーラベルを確認する

以下は、完全な教学モジュールの初期化パスである。少量のオブジェクトを割り当て、フィールドを検証し、返却前にクリーンアップする。このコードは段階的なロールバックを示すために使用され、モジュールのロードを要求せずにコンパイルチェックを行う。

#include <linux/errno.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>

struct kb_item {
    unsigned int id;
    unsigned char payload[196];
};

static int __init kb_alloc_init(void)
{
    struct kmem_cache *cache;
    struct kb_item **items;
    unsigned int made = 0;
    int result = -ENOMEM;

    cache = kmem_cache_create("kb_alloc_example", sizeof(struct kb_item),
                              0, 0, NULL);
    if (!cache)
        return -ENOMEM;
    items = kcalloc(32, sizeof(*items), GFP_KERNEL);
    if (!items)
        goto destroy_cache;
    while (made < 32) {
        struct kb_item *item = kmem_cache_zalloc(cache, GFP_KERNEL);
        if (!item)
            goto free_items;
        item->id = made;
        items[made++] = item;
    }
    result = 0;
    for (unsigned int i = 0; i < made; ++i) {
        if (items[i]->id != i || items[i]->payload[0] != 0) {
            result = -EINVAL;
            break;
        }
    }
free_items:
    while (made)
        kmem_cache_free(cache, items[--made]);
    kfree(items);
destroy_cache:
    kmem_cache_destroy(cache);
    return result;
}

static void __exit kb_alloc_exit(void) { }
module_init(kb_alloc_init);
module_exit(kb_alloc_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Slab allocation and rollback teaching example");

Kbuild では obj-m += kb_alloc_example.o を使用できる。実際の障害分岐が期待通りに実行されるかどうかは、別の制御された失敗注入が必要である。コンパイルが成功しただけでは、19回目の失敗パスが実行されたとは見なせない。生存オブジェクトがまだ参照されている場合に、キャッシュを破棄して「リークを修正」してはならない。

5. 解放後の再利用は UAF を説明し、検出ツールの異なる証拠も説明する

オブジェクト A が解放され、元のアドレスがその後オブジェクト B に割り当てられると仮定する。A の古いポインタは同じアドレスを指し続けているため、誤った書き込みは、すぐにクラッシュするのではなく、B を破壊する可能性がある。解放されたメモリをポイズンで埋めることで、後の異常の識別に役立つが、誤ったアクセスが常に発生した瞬間に検出されるとは限らない。割り当て/解放追跡も、後のチェックポイントで破壊を発見する可能性がある。

検出ツール:不正アクセスと到達不能な割り当ては2種類の証拠である

KASAN の異なるモードは、シャドウメモリやタグなどのメカニズムを利用して不正アクセスをチェックし、コスト、カバー範囲、および粒度が異なる。Generic KASAN の 1:8 シャドウ比率はシステム全体の唯一のコストではなく、quarantine、アライメント、および他のメタデータが含まれる可能性がある。タグベースの検出も、同じタグを持つエラーやチェック粒度内にあるエラーをすべて識別できるとは限らない。KASAN を参照。

kmemleak は、主に疑わしいオブジェクトが既知のルートから到達可能かどうかをスキャンすることで、潜在的なリークを報告する。それはすべての C ポインタセマンティクスを追跡する証明機ではない。隠蔽されたポインタやエンコードされたポインタは誤報をもたらし、偶然ポインタのような値は、リークが到達可能であるかのように見せる可能性がある。したがって、それは「どの割り当てをチェックする価値があるか」に答えるが、KASAN はより直接的に特定の不正アクセスを対象とする。kmemleak を参照。

通常の slab オブジェクトの回収責任は、それを使用するサブシステムに属する。回収可能なキャッシュセマンティクスを持つサブシステムは、shrinker を登録して、圧力下で不要なオブジェクトを解放できる。キャッシュを某種の回収可能な用途としてマークしただけでは、カーネルが自動的に理解して、まだ使用されているビジネスオブジェクトを破棄するわけではない。

6. オブジェクトサイズ、スロットストライド、ページオーダの読み取り専用比較

from pathlib import Path
import os

root = Path("/sys/kernel/slab/dentry")
if not root.exists():
    print("現在 dentry slab sysfs は公開されていない")
else:
    print("実際のキャッシュパス:", root.resolve())
    values = {}
    for name in ("object_size", "slab_size", "order", "objects", "slabs"):
        try:
            values[name] = (root / name).read_text().strip()
        except OSError as error:
            values[name] = type(error).__name__
        print(name, values[name])
    if values["order"].isdigit():
        print("現在の order で計算した slab バイト数:",
              os.sysconf("SC_PAGE_SIZE") * (1 << int(values["order"])))

読み取り結果を確認する際は、単位を先に確認する。

フィールドどのように理解すべきか
object_sizeオブジェクトコンテンツのサイズに関連するフィールド
slab_sizeオブジェクトスロットのサイズに関連するフィールド、⁠スラブ全体のバイト数ではない
order基本ページサイズと組み合わせて、スラブ全体の保持スペースを計算する必要がある
objects、slabsノード分布情報が付随する可能性があるため、行全体を直接整数に変換してはならない

既存の slab の実際のオーダ、デバッグモード、およびフォールバックには、より詳細な統計情報が必要である。現在の設定値からすべての履歴割り当てを逆算することはできない。

/proc/slabinfo と組み合わせて使用する際は、active objects、容量、および保持ページを別々に確認する。統計情報はローカルキャッシュと実装の勘定方法の影響を受けるため、active 数を無条件にビジネスがまだ保持している正確なオブジェクト数と見なしてはならない。次に ページ回収とスワップ で、slab shrinker を通常のファイルページや匿名ページと同じ圧力モデルに組み込む。