Linux カーネルのメモリ管理: メモリ割り当てメカニズム

Linux ベースのシステムに存在するさまざまなメモリ アロケータを示す以下の図を見て、後で説明します。

カーネル メモリ アロケータの概要

あらゆる種類のメモリ要求を満たすことができる割り当てメカニズムがあります。必要なメモリの種類に応じて、目標に最も近いメモリを選択できます。メインのアロケータはページ アロケータで、ページのみを処理します (ページは提供できるメモリの最小単位です)。次に、SLAB アロケータがあります。これはページ アロケータの上に構築され、ページ アロケータからページを取得し、(SLAB とキャッシュを介して) より小さいメモリ エンティティを返します。これは、kmalloc アロケータが依存するアロケータです。

ページアロケータ

ページ アロケータは Linux システムの最下位レベルのアロケータであり、他のアロケータによって依存されます。システムの物理メモリは、ページ フレームと呼ばれる固定サイズのブロックで構成されます。カーネルでは、ページ フレームは構造体ページのインスタンスとしてカーネル内で表されます。ページは、オペレーティング システムが低レベルのメモリ要求を許可できるメモリの最小単位です。

ページ割り当てAPI

カーネル ページ アロケーターはバディ アルゴリズムを使用してページ ブロックを割り当て、解放することがわかっています。ページは、2 のべき乗のサイズのブロックに割り当てられます (バディ アルゴリズムから最良の結果を得るために)。これは、1 ページ、2 ページ、4 ページ、8 ページ、16 ページなどを割り当てることができることを意味します。

1. alloc_pages(mask, order) は、2 ページに order を乗じて適用し、適用されたブロックの最初のページを指す構造体ページ構造のインスタンスを返します。メモリの 1 ページのみが要求される場合、order の値は 0 である必要があります。以下は alloc_page(mask) の実装です。

struct page *alloc_pages(gfp_t mask, unsigned int order)
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)

__free_pages() は、alloc_pages() 関数によって割り当てられたメモリを解放するために使用されます。引数として、割り当てられたページへのポインタを、割り当てられたときと同じ順序で受け取ります。

void __free_pages(struct page *page, unsigned int order);

2. 同じように動作する他の関数もありますが、構造体ページのインスタンスではなく、予約されたブロックのアドレス (仮想アドレス) を返します。たとえば、__get_free_pages(mask, order) と __get_free_page(mask) は次のようになります。

unsigned long __get_free_pages(gfp_t mask, unsigned int order);
unsigned long get_zeroed_page(gfp_t mask);

free_pages() は、__get_free_pages() で割り当てられたページを解放するために使用されます。address addr パラメータは、割り当てられたページの開始領域を示し、パラメータの順序は割り当てられたときと同じである必要があります。

free_pages(unsigned long addr, unsigned int order);

Linux メモリ管理プロジェクト開発チュートリアル→クリックして学習

上記のどちらの場合でも、マスクはリクエストに関する詳細、つまりメモリ領域とアロケーターの動作を指定します。マスクのオプションの値は次のとおりです。

  • GFP_USER: ユーザーのメモリ割り当てに使用されます。
  • GFP_KERNEL: カーネル メモリ割り当ての共通フラグ。
  • GFP_HIGHMEM: HIGH_MEM 領域からメモリを要求します。
  • GFP_ATOMIC: スリープ状態にせずにメモリをアトミックに割り当てます。割り込みコンテキストからメモリを割り当てる必要がある場合に使用されます。

GFP_HIGHMEM を使用する場合は、__get_free_pages() (または __get_free_page()) と一緒に使用しないでください。これは、HIGHMEM メモリが連続していることが保証されていないため、この領域から割り当てられたメモリ アドレスを返すことができないためです。グローバルでは、メモリ関連関数では GFP_* のサブセットのみが許可されます。

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
 struct page *page;
 /*
 * __get_free_pages() returns a 32-bit address, which cannot represent
 * a highmem page
 */
 VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0);
 page = alloc_pages(gfp_mask, order);
 if (!page)
 return 0;
 return (unsigned long) page_address(page);
}
alloc_pages() /__get_free_pages() 可以分配的最大页面数是1024。这意味着在一个4KB大小的系统上,您最多可以分配1024 * 4KB = 4MB。kmalloc也是一样。

変換関数

page_to_virt() 関数は、構造体ページ (alloc_pages() によって返されるページなど) をカーネル アドレスに変換するために使用されます。virt_to_page() はカーネル仮想アドレスを受け取り、それに関連付けられた構造体ページ インスタンスを返します (alloc_pages() 関数を使用して割り当てられたかのように)。virt_to_page() と page_to_virt() は両方とも <asm/page.h> で定義されています。

struct page *virt_to_page(void *kaddr);
void *page_to_virt(struct page *pg);

page_address() マクロによって返される仮想アドレスは、構造体ページ インスタンスの開始アドレス (論理アドレス) に対応します。

void *page_address(const struct page *page);

get_zeroed_pa​​ge() 関数でどのように使用されるかを確認できます。

unsigned long get_zeroed_page(unsigned int gfp_mask)
{
  struct page * page;
  page = alloc_pages(gfp_mask, 0);
  if (page) {
    void *address = page_address(page);
    clear_page(address);
    return (unsigned long) address;
  }
  return 0;
}

__free_pages() と free_pages() は混同されやすいです。これらの主な違いは、free_pages() は仮想アドレスをパラメータとして受け入れるのに対し、__free_pages() は構造体ページ構造をパラメータとして受け入れることです。

スラブアロケータ

スラブ アロケータは kmalloc() が依存するものです。その主な目的は、メモリ割り当てが少ない場合にバディ システムによって引き起こされるメモリ割り当て/割り当て解除によって引き起こされる断片化を解消し、頻繁に使用されるオブジェクトのメモリ割り当てを高速化することです。

バディアルゴリズム

メモリ割り当て要求のサイズは 2 の累乗に丸められ、バディ アロケータは対応するリストを検索します。要求されたエントリが存在しない場合、次の親リスト (ブロックのサイズが前のリストの 2 倍) のエントリは 2 つの部分 (バディと呼ばれます) に分割されます。アロケータは前半を使用し、残りの部分は次のリストに追加されます。これは再帰的なメソッドで、バディ アロケーターが分割可能なチャンクを見つけた場合、またはチャンクが最大サイズに達して空きチャンクがなくなった場合に停止します。

たとえば、最小割り当てサイズが 1 KB でメモリ サイズが 1 MB の場合、バディ アロケータは 1 KB ホールの空​​のリスト、2 KB ホールの空​​のリスト、2 KB ホールの空​​のリスト、4 KB ホールの空​​のリストを作成します。 KB ホール、8 KB、16 KB、32 KB、64 KB、128 KB、256 KB、512 KB、および 1 MB ホールのリスト。穴があるだけの 1MB リストを除いて、最初はすべて空です。70K サイズのブロックを割り当てたいとします。バディ アロケータはそれを 128K に切り上げ、最終的にその 1MB を 2 つの 512K チャンク、次に 256K、最後に 128K に分割し、128K チャンクの 1 つをユーザーに割り当てます。シナリオの概要は次のとおりです。

バディアルゴリズムを使用した割り当て

解放は割り当てと同じくらい高速です。次の図は、リサイクル アルゴリズムをまとめたものです。

リサイクルにバディアルゴリズムを使用する 

スラブアロケータ解析

  • スラブ: これは、複数のページ フレームで構成される連続した物理メモリです。各スラブは同じサイズの等しいブロックに分割され、inode、mutex などの特定のタイプのカーネル オブジェクトを格納するために使用されます。各スラブはオブジェクトの配列です。
  • キャッシュ: リンク リスト内の 1 つ以上のスラブで構成され、カーネル内では struct kmem_cache_t 構造体のインスタンスとして表されます。キャッシュには、同じタイプのオブジェクトのみが格納されます (たとえば、inode のみ、またはアドレス空間構造のみ)。

スラブは次のいずれかの状態になります。

  • 空: これは、スラブ上のすべてのオブジェクト (チャンク) が空きとしてマークされる場所です。
  • 部分的: 使用済みオブジェクトと空きオブジェクトがスラブ内に同時に存在します。
  • フル: スラブ上のすべてのオブジェクトが使用済みとしてマークされます。

キャッシュの構築はメモリ アロケータに依存し、最初は各スラブが空としてマークされます。コードがカーネル オブジェクトにメモリを割り当てると、システムはキャッシュの部分/空きスラブ内でそのタイプのオブジェクトの空き場所を探します。見つからない場合、システムは新しいスラブを割り当て、キャッシュに追加します。新しいオブジェクトはこのスラブから割り当てられ、スラブは部分的としてマークされます。メモリが使い果たされる (解放される) と、オブジェクトは単に初期化された状態でスラブ キャッシュに戻されます。

そのため、カーネルは、メモリをゼロに初期化し、以前の内容を消去するためのヘルパー関数も提供します。スラブは、使用されているオブジェクトの数の参照カウントを保持するため、キャッシュ内のすべてのスラブがいっぱいで別のオブジェクトが要求された場合、スラブ アロケーターは新しいスラブを追加する役割を果たします。

スラブ キャッシュの概要 

これは、オブジェクトごとのアロケーターの作成に少し似ています。システムはオブジェクトのタイプごとにキャッシュを割り当て、同じタイプのオブジェクトのみを同じキャッシュに格納できます (たとえば、task_struct 構造のみ)。

カーネルには、コンパクトさ、キャッシュフレンドリーさ、または生の速度が必要かどうかに応じて、さまざまなタイプのスラブ アロケータがあります。

  • SLOB、可能な限りコンパクト
  • 可能な限りキャッシュに優しい SLAB
  • SLUB は非常にシンプルで、必要な命令オーバーヘッド数が少なくなります

kmalloc

kmalloc は、ユーザー空間の malloc() などのカーネル メモリ割り当て関数です。kmalloc によって返されるメモリは、物理メモリと仮想メモリで連続しています。

kmalloc アロケータは、カーネル内の一般的な高レベル メモリ アロケータであり、SLAB アロケータに依存します。kmalloc によって返されるメモリは、HIGH_MEM が指定されていない限り、LOW_MEM 領域から割り当てられるため、カーネル論理アドレスを持ちます。これは <linux/slab.h> で宣言されており、ドライバーで kmalloc を使用するときに含める必要があります。プロトタイプは次のとおりです。

void *kmalloc(size_t size, int flags);

size は、割り当てるメモリのサイズ (バイト単位) を指定します。フラグは、メモリがどこにどのように割り当てられるかを決定します。使用可能なフラグは、ページ アロケーター フラグ (GFP_KERNEL、GFP_ATOMIC、GFP_DMA など) と同じです。

  • GFP_KERNEL: コードがスリープする可能性があるため、割り込みハンドラーではこのフラグを使用できません。常に LOM_MEM 領域からメモリを返します (したがって、論理アドレス)。
  • GFP_ATOMIC: これにより、割り当てのアトミック性が保証されます。割り込みコンテキストで使用される唯一のフラグ。緊急メモリプールを使用するため、悪用しないでください。
  • GFP_USER: : ユーザー空間プロセスにメモリを割り当てます。カーネルに割り当てられるメモリとはまったく異なります。
  • GFP_HIGHUSER: これにより、HIGH_MEMORY 領域からメモリが割り当てられます。
  • GFP_DMA: DMA_ZONE からメモリを割り当てます。

メモリの割り当てが成功すると、kmalloc は、物理的に連続していることが保証された、割り当てられたブロックの仮想アドレスを返します。エラーが発生した場合は NULL を返します。

kmalloc は、小さなメモリを割り当てるときに SLAB キャッシュに依存します。この場合、カーネルは、割り当てられた領域サイズを、それを収容できる最小の SLAB キャッシュのサイズに丸めます。常にこれをデフォルトのメモリ アロケータとして使用してください。ARM および x86 アーキテクチャでは、各割り当ての最大サイズは 4MB で、合計割り当ての最大サイズは 128MB です。

kfree 関数は、kmalloc によって割り当てられたメモリを解放するために使用されます。以下は kfree() のプロトタイプです。

void kfree(const void *ptr)

例:

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

void *ptr;
static int alloc_init(void)
{
  size_t size = 1024; /* allocate 1024 bytes */
  ptr = kmalloc(size, GFP_KERNEL);
  if(!ptr) {
    /* handle error */
    pr_err("memory allocation failed\n");
    return -ENOMEM;
  } else {
    pr_info("Memory allocated successfully\n");
  }

  return 0;
}
static void alloc_exit(void)
{
  kfree(ptr);
  pr_info("Memory freed\n");
}
module_init(alloc_init);
module_exit(alloc_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xxx");

他の同様の関数は次のとおりです。

1 void kzalloc(size_t size, gfp_t flags);
2 void kzfree(const void *p);
3 void *kcalloc(size_t n, size_t size, gfp_t flags);
4 void *krealloc(const void *p, size_t new_size, gfp_t flags);

krealloc() は、カーネル内のユーザー空間の realloc() 関数です。kmalloc() によって返されるメモリには以前の内容が保持されているため、ユーザー空間に公開されるとセキュリティ上のリスクが生じる可能性があります。すべてゼロ値のメモリを取得するには、kzalloc を使用する必要があります。kzfree() は kzalloc() の解放関数であり、kcalloc() は配列にメモリを割り当てます。そのパラメータ n と size はそれぞれ配列の要素数と要素のサイズを表します。

kmalloc() はカーネルによって永続的にマップされたメモリ領域 (物理的に連続していることを意味します) を返すため、virt_to_phys() を使用してメモリ アドレスを物理アドレスに変換するか、virt_to_bus() を使用してメモリ アドレスを IO に変換できます。バスアドレス。これらのマクロは、(必要に応じて) __pa() または __va() を内部的に呼び出します。PAGE_SHIFT によって下にシフトされた物理アドレス (virt_to_phys(kmalloc'ed address)) は、割り当てられたブロックの最初のページの PFN を生成します。

vmalloc

vmalloc() によって要求されるメモリは、仮想アドレスでのみ連続しており、物理アドレスでは連続しません。

返されるメモリは常に HIGH_MEM 領域からのものです。メモリが物理的に連続していると断言できないため、返されたアドレスを物理アドレスやバス アドレスに変換することはできません。これは、vmalloc() によって返されたメモリをマイクロプロセッサの外部では使用できないことを意味します (DMA 目的で簡単に使用することはできません)。ソフトウェア内にのみ存在する大きなシーケンス (ネットワーク バッファなど) にメモリを割り当てるために vmalloc() を使用するのは正しいことです (たとえば、ページを割り当てるために vmalloc() を使用することに意味はありません)。vmalloc() は、メモリを取得し、ページ テーブルを構築し、仮想アドレスの連続範囲に再マッピングする必要があるのに対し、kmalloc() はこれをまったく行わないため、kmalloc() またはページ アロケータ関数よりも遅いことに注意してください。

vmalloc API を使用する前に、コードに次のヘッダー ファイルを含める必要があります。

#include <linux/vmalloc.h>

以下は vmalloc ファミリのプロトタイプです。

1 void *vmalloc(unsigned long size);
2 void *vzalloc(unsigned long size);
3 void vfree( void *addr);

size は、割り当てる必要があるメモリの量です。メモリの割り当てが成功すると、割り当てられたメモリ ブロックの最初のバイトのアドレスが返されます。失敗した場合は NULL を返します。vfree 関数は、vmalloc() によって割り当てられたメモリを解放するために使用されます。

vmalloc の例は次のとおりです。

#include<linux/init.h>
#include<linux/module.h>
#include <linux/vmalloc.h>
void *ptr;
static int my_vmalloc_init(void)
{
    unsigned long size = 8192;
    ptr = vmalloc(size);
    if(!ptr) {
        /* handle error */
        printk("memory allocation failed\n");
        return -ENOMEM;
    } else {
        pr_info("Memory allocated successfully\n");
    }
    return 0;
}
static void my_vmalloc_exit(void) /* function called at the time of

*/
{
    vfree(ptr); //free the allocated memory
    printk("Memory freed\n");
}
module_init(my_vmalloc_init);
module_exit(my_vmalloc_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xxx");

/proc/vmallocinfo を使用すると、システム上の vmalloc によって使用されているすべてのメモリを表示できます。VMALLOC_START と VMALLOC_END は、vmalloc アドレス範囲を区切る 2 つの記号です。これらはアーキテクチャに依存しており、<asm/pgtable.h> で定義されています。

メモリ割り当てを内部で処理します

メモリ ページを割り当てる下位レベルのアロケータに注目してみましょう。カーネルは、フレーム ページ (物理ページ) が実際に必要になるまで (これらのページが読み取りまたは書き込みによって実際にアクセスされるとき)、フレーム ページ (物理ページ) の割り当てを報告します。このオンデマンド割り当ては遅延割り当てと呼ばれ、使用されないページが割り当てられるリスクを排除します。

ページが要求されるたびに、ページ テーブルのみが更新され、ほとんどの場合、新しいエントリが作成されます。これは、仮想メモリのみが割り当てられることを意味します。ページにアクセスしたときのみ、ページフォールトと呼ばれる割り込みがスローされます。この割り込みには、ページ フォールト ハンドラーと呼ばれる専用のハンドラーがあり、仮想メモリへのアクセスがすぐに成功しなかった場合に MMU によって呼び出されます。

実際、ページ フォールト割り込みは、ページ テーブル内のエントリにこのタイプのアクセスを許可する適切な許可ビットが設定されていないページに対しては、アクセス タイプ (読み取り、書き込み、実行) に関係なく発生します。この割り込みへの応答は、次の 3 つの方法のいずれかで行うことができます。

  • ハード フォールト: ページはどこにも (物理メモリにもメモリ マップされたファイルにも) 存在しません。これは、ハンドラーがフォールトをすぐに解決できないことを意味します。ハンドラーは、I/O 操作を実行して障害の解決に必要な物理ページを準備し、システムが問題の解決に取り組んでいる間、中断されたプロセスを一時停止して別のプロセスに切り替えることがあります。
  • ソフト フォールト: ページはメモリ内の別の場所 (別のプロセスのワーキング セット内) に存在します。これは、障害ハンドラーが物理メモリのページを適切なページ テーブル エントリに追加し、ページ テーブル エントリを調整し、中断された命令を再開することにより、障害を即座に解決できることを意味します。
  • 解決できない障害: これにより、バス エラーまたは segv が発生します。デフォルトの動作を変更するために SIGSEV シグナル ハンドラーがインストールされていない限り、SIGSEGV は問題のあるプロセスに送信され、プロセスを終了します (デフォルトの動作)。

メモリ マッピングは通常、物理ページが接続されていない状態で開始されますが、代わりに、物理メモリが関連付けられていない仮想アドレス範囲を定義します。カーネルは、試行されたアクセスが正当であるかどうかを判断し、ページ フォールト ハンドラーの動作を指定するためのいくつかのフラグを提供するため、メモリにアクセスすると、ページ フォールト例外に応答して実際の物理メモリが後で割り当てられます。したがって、ユーザースペース brk()、mmap() および同様のものが (仮想) スペースを割り当てますが、物理メモリは後で追加されます。

割り込みコンテキストでページ フォールトが発生すると、二重フォールト割り込みが発生し、通常はカーネルがパニックを起こします (panic() 関数を呼び出します)。そのため、割り込みコンテキストに割り当てられたメモリはメモリ プールから取得され、ページ フォールト割り込みはトリガーされません。二重障害の処理中に割り込みが発生すると、三重障害例外が生成され、CPU がシャットダウンし、オペレーティング システムがただちに再起動されます。この動作は実際にはアークに依存します。

コピーオンライト (CoW)

CoW (fork() で頻繁に使用される) は、1 つのプロセスが使用する (データに書き込む) まで、2 つ以上のプロセスによって共有されるデータに何倍ものメモリを割り当てないカーネル機能です。この場合、メモリはそのプロセスに割り当てられます。私的なコピー。以下に、ページ フォールト ハンドラーが CoW を管理する方法を示します (単一ページのケース スタディ)。

  1. PTE をプロセス ページ テーブルに追加し、書き込み不可としてマークします。
  2. マッピングにより、プロセス VMA リストに VMA が作成されます。ページが VMA に追加され、VMA は書き込み可能としてマークされます。
  3. ページ アクセス (最初の書き込み) で、エラー ハンドラーは違いに気づきます。これは、これが CoW であることを意味します。次に、(以前に追加した PTE に) 物理ページを割り当て、PTE フラグを更新し、TLB エントリをフラッシュし、共有アドレスから新しい場所にコンテンツをコピーする do_wp_page() 関数を実行します。

この記事の特典として、無料の C++ 学習情報パッケージ、技術ビデオ/コード、主要メーカーからの 1,000 件のインタビュー質問 (C++ の基礎、ネットワーク プログラミング、データベース、ミドルウェア、バックエンド開発、オーディオおよびビデオ開発など) を受け取ることができます。 、Qt開発)↓↓↓ ↓↓↓下記からご覧ください↓↓記事下部をクリックして無料で入手してください↓↓

おすすめ

転載: blog.csdn.net/m0_60259116/article/details/133383178