Linux メモリ管理: (1) パートナー システム

記事の説明:

  • Linuxカーネルバージョン:5.0

  • アーキテクチャ: ARM64

  • 参考資料と画像ソース:「Running Linux Kernel」

  • Linux 5.0 カーネル ソース コード アノテーション ウェアハウス アドレス:

    zhangzihengya/LinuxSourceCode_v5.0_study (github.com)

1. メモリの断片化問題とパートナーシステムのアルゴリズム

メモリの断片化は、メモリ管理において解決するのが難しい問題です。Linux カーネルは、バディ システム アルゴリズムを採用する際に、メモリの断片化を軽減する方法を考慮します。バディシステムアルゴリズムでは、2 つのメモリブロックがパートナーになれるのはどのようなメモリブロックですか? 実際、パートナー システム アルゴリズムには、満たす必要のある次の 3 つの基本条件があります。

  1. 両方のメモリブロックは同じサイズです
  2. 2 つのメモリ ブロックのアドレスは連続しています。
  3. 両方のメモリ ブロックを同じ大きなメモリ ブロックから分離する必要があります

メモリ ブロックの分割を次の図に示します。

ここに画像の説明を挿入します

メモリ ブロックをマージするプロセス中に、次の状況が発生する可能性があります。

ここに画像の説明を挿入します

このとき、P0 と P3 が穴になり、外部フラグメンテーションが発生しますが、時間の経過とともに外部フラグメンテーションはさらに深刻になり、メモリ使用率も低下します。

外部断片化の問題を解決するために一般的に使用されている学術テクノロジーは、モバイル ページの位置を使用して空きページを 1 つの部分に接続するメモリ コンパクションと呼ばれます。ページの移行属性から判断すると、ユーザープロセスが割り当てて使用するメモリは移行できますが、カーネル自体が使用するメモリページは気軽に移行できません。

カーネル自体が使用するページを移行できないのはなぜですか?

この種のページを移行するには、最初に物理ページのマッピングを切断し、次にマッピング関係を再確立する必要があるためです。マッピング関係を切断するプロセス中に、カーネルがこのページにアクセスし続けると、不正なポインタとメモリにアクセスし、カーネル内で Oops エラーが発生し、さらにはシステム クラッシュを引き起こす可能性があります。カーネルは機密領域として、次のことを保証する必要があります。使用されているメモリは安全です。

これはユーザープロセスとは異なり、ユーザープロセスが使用するページのマッピング関係が切れた後、ユーザープロセスがそのページにアクセスし続けるとページフォルト例外が発生します。ページ フォールト例外処理では、物理ページを再割り当てし、仮想メモリにマップできます。このプロセスはユーザー プロセスにとって安全です。

Linux 2.6.24 の開発段階で、コミュニティの専門家は、アンチフラグメンテーション と呼ばれる断片化を防ぐ機能を導入しました。実際、これは移行タイプを使用することで実現されます。移行タイプはページ ブロックに応じて分割されます。ページ ブロックのサイズは、ページ アロケーターが割り当てることができる最大メモリ ブロック サイズ、つまり 2 の MAX_ORDER-1 乗バイト (通常は 4MB) です。

#define MAX_ORDER 11

ページの種類は次のとおりです。

  • 非移行型 UNMOVABLE: メモリ内での位置が固定され、他の場所に移動できないのが特徴で、例えばカーネル自身が使用する必要のあるメモリなどがこれに該当します。
  • 可動タイプ MOVABLE: 自由に移動できるページを示し、通常はアプリケーションに属するページです。
  • リサイクル可能なページ: これらのページは直接移動できませんが、リサイクルできます。ページのコンテンツは再読み取りまたは取得できます。最も典型的な例は、スラブ メカニズムを使用して割り当てられたオブジェクトです。

したがって、パートナー システムの free_area データ構造には MIGRATE_TYPES リンク リストが含まれます。これは、オーダーのサイズに応じて、メモリ管理ゾーン (ゾーン) 内の 0~MAX_ORDER-1 個の free_area に相当します。次の図に示すように、各 free_area には、MIGRATE_TYPES タイプに応じて、対応するリンク リストがいくつかあります。

ここに画像の説明を挿入します

// 内存管理区,zone 经常会被访问到,因此这个数据结构要求以 L1 高速缓存对齐
struct zone {
    
    
    ...
    // 管理空闲区域的数组,包含管理链表等
	struct free_area	free_area[MAX_ORDER];
    ...
}

struct free_area {
    
    
	struct list_head	free_list[MIGRATE_TYPES];
	unsigned long		nr_free;
};

enum migratetype {
    
    
	MIGRATE_UNMOVABLE,
	MIGRATE_MOVABLE,
	MIGRATE_RECLAIMABLE,
	MIGRATE_PCPTYPES,	/* the number of types on the pcp lists */
	MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
	MIGRATE_TYPES
};

このテクノロジを使用する Linux カーネルでは、すべてのページ ブロック内のページは同じ移行タイプであり、間に他のタイプのページが混在することはありません。

最後に、バディ システムの定義を示します: バディ システムは、オペレーティング システムで一般的に使用される動的ストレージ管理方法であり、ユーザーが要求を行うと、適切なサイズのメモリ ブロックがユーザーに割り当てられ、ユーザーが解放すると、適切なサイズのメモリ ブロックが割り当てられます。メモリブロックをリサイクルします。コンパニオン システムでは、メモリ ブロックのサイズは 2 ページの累乗になります。Linux カーネルの最大順序値は MAX_ORDER で表され、通常は 11 です。つまり、すべての空きページは 11 個のメモリ ブロック リンク リストにグループ化され、各メモリ ブロック リンク リストには 1、2、4、8、16、32 などが含まれます。 、連続1024ページ。1024 ページは、4MB の連続した物理メモリに対応します。

2. パートナーシステムでの物理メモリの初期化

カーネルが起動すると、カーネルは DDR 物理メモリのサイズを認識し、ハイエンド メモリの開始アドレスとカーネル空間のメモリ レイアウトを計算し、物理メモリ ページがパートナー システムに追加されます。物理メモリ ページをパートナー システムに追加しますか? 毛織物ですか?

カーネルが初期化されると、すべてのページが最初に MIGRATE_MOVABLE タイプとしてマークされます。fee_area_init_core()→memmap_init_zone() 関数を参照してください。

void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone,
		unsigned long start_pfn, enum memmap_context context,
		struct vmem_altmap *altmap)
{
    
    
    ...
    for (pfn = start_pfn; pfn < end_pfn; pfn++) {
    
    
        ...
        // 设置指定页块的MIGRATE_TYPES类型,最后调用set_pfnblock_flags_mask()来设置页块的迁移类型
        set_pageblock_migratetype(page, MIGRATE_MOVABLE);
        ...
    }
}

質問:

Linux では、カーネルが初期化されるときに、すべてのページが最初に MIGRATE_MOVABLE タイプとしてマークされますが、ページはいつ対応するタイプに設定されるのでしょうか?

チャットGPTの答え:

MIGRATE_MOVABLELinux では、カーネルの初期化時にすべてのページがタイプとしてマークされます。これは、初期化段階ではカーネルが各ページの具体的な用途と属性をまだ認識していないためです。特定の種類のページは実行時に動的に決定され、システムの状態に基づいて調整できます。

Linux カーネルでは、ページのタイプが変更される可能性があり、具体的なタイミングや条件はさまざまな状況に応じて異なります。一般的なページ タイプの変更をいくつか示します。

  1. メモリを割り当てるとき: カーネルが などの関数を通じてメモリを動的に割り当てるときkmallocvmallocカーネルは割り当てのコンテキストと要件に基づいてページ タイプを設定します。

  2. ページ移行: メモリが不足している状況では、Linux カーネルはページ移行を実行し、特定のメモリのニーズを満たすために、あるタイプから別のタイプにページを移動することがあります。

  3. メモリ再利用: システムがメモリを解放する必要がある場合、カーネルは、リサイクルまたは再割り当てを容易にするために、ページのタイプを適切なタイプに変更することがあります。

  4. メモリのホットプラグ: メモリのホットプラグをサポートするシステムでは、メモリの追加または削除時にカーネルがページのタイプを再評価して更新することがあります。

全体として、ページ タイプの設定は、システムの動作ステータスとカーネルの管理戦略に依存する動的なプロセスです。この動的な性質により、カーネルは必要に応じてメモリを柔軟に管理できます。

次に、物理ページがバディ システムにどのように追加されるかを考えてみましょう。ページを 1 つずつ追加する必要がありますか、それとも一度に 2 n ページずつ追加する必要がありますか?

free_low_memory_core_early() 関数では、for_each_free_mem_range() 関数を介してすべてのメモリ ブロックが走査され、メモリ ブロックの開始アドレスと終了アドレスが検索されます。

<start_kernel()->mm_init()->mem_init()->free_all_bootmem()->free_low_memory_core_early()>

static unsigned long __init free_low_memory_core_early(void)
{
    
    
    ...
    for_each_free_mem_range(i, NUMA_NO_NODE, MEMBLOCK_NONE, &start, &end,
				NULL)
		count += __free_memory_core(start, end);
    ...
}

メモリ ブロックを __free_pages_memory() 関数に渡します。この関数は次のように定義されます。

static void __init __free_pages_memory(unsigned long start, unsigned long end)
{
    
    
	int order;

	while (start < end) {
    
    
		// 取MAX_ORDER-1和__ffS(start)中的较小值
        // __ffS(start)函数计算start中第一个位为1的位置。注意,__ffS()=__ffS()-1
        // 假设start起始地址为0x63300,说明该地址以0xl00对齐。通过__ffS()函数来计算出的合适的order值为8,因为2的8次方等于0x100。
        order = min(MAX_ORDER - 1UL, __ffs(start));

		while (start + (1UL << order) > end)
			order--;

		// ->__free_pages_boot_core()
        memblock_free_pages(pfn_to_page(start), start, order);

		start += (1UL << order);
	}
}

// 得到 order 值后,就可以把内存块通过 __free_pages_boot_core 函数添加到伙伴系统中
static void __init __free_pages_boot_core(struct page *page, unsigned int order)
{
    
    
	...
    // 伙伴系统的核心函数,这里按照order的方式把内存块添加内存到伙伴系统中
	__free_pages(page, order);
}

3. 前提知識: ゾーンリスト間の関係

カーネルはゾーンを使用してメモリ ノードを管理するため、メモリ ノードは複数の異なるゾーンに分割される場合があります。カーネルは、ゾーンリスト データ構造を使用してメモリ ノードのゾーンを管理します。

// zonelist 是所有可用 zone 的链表,其中排在第一个的 zone 是页面分配器“最喜欢的”,也是首选,其他的 zone 是备选
struct zonelist {
    
    
	// 每一个 zoneref 数据结构描述一个 zone
	struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
};

関連するデータ構造:

// 每一个 zoneref 数据结构描述一个 zone
struct zoneref {
    
    
	// zone 成员指向实际的 zone
	struct zone *zone;	/* Pointer to actual zone */
	// zone_idx 是一个编号,使用 zone_idx() 函数获取的编号
	// 通常:
    // 0表示最低的 zone,如 ZONE_DMA32,用于最低4GB的内存访问的设备,如只支持32位的DMA设备。
	// 1表示 ZONE_NORMAL,4GB以后的物理内存,用于线性映射物理内存。若系统内存小于4GB,则没有这个内存管理区
    // 2表示 ZONE_HGHMEM,用于管理高端内存,这些高端内存是不能线性映射到内核地址空间的。注意,在64位Linux操作系统中没有这个内存管理区
	int zone_idx;		/* zone_idx(zoneref->zone) */
};

// 内存节点的数据结构
typedef struct pglist_data {
    
    
	...
	// 有两个 zonelist:
	// 其中一个是 ZONELIST_FALLBACK,指向本地的zone,即包含备选的 zone
	// 另一个是 ZONELIST_NOFALLBACK,用于 NUMA 系统,指向远端的内存结点的 zone
	struct zonelist node_zonelists[MAX_ZONELISTS];
    ...
}

システム内にメモリ ノードが 1 つだけあり、ZONE_DMA32 と ZONE_NORMAL の 2 つのゾーンがあると仮定すると、ゾーン タイプ、_zonerefs[] 配列、zonelist のzone_idx の関係は次の図のようになります。

ここに画像の説明を挿入します

この場合、物理メモリを割り当てるときに、ZONE_NORMAL タイプのゾーンに優先順位が与えられます。つまり、zone_idx 値が最大のゾーンが最初に割り当てられます。

4. ページ割り当てへのクイックパス

バディ システムは、Linux カーネルの基本的なメモリ割り当てシステムです。バディ システムの概念を理解するのは難しくありませんが、物理ページの割り当ては常にメモリ管理の最も複雑な部分であり、ページのリサイクル、メモリの正規化、ダイレクト メモリのリサイクルなどの非常に複雑なメカニズムが含まれます。ページ アロケータは、バディ システム アルゴリズムとゾーンベースの設計概念に基づいた、Linux カーネル メモリ管理の最も基本的なアロケータです。この記事では、十分なメモリがある場合に連続した物理メモリを割り当てる方法に焦点を当てます。

メモリを割り当てる簡単なプロセスを次の図に示します。

ここに画像の説明を挿入します

get_page_from_freelist() 関数のフローを次の図に示します。

ここに画像の説明を挿入します

読者がより現実的に理解できるように、このプロセスをソース コードのフローチャートに基づいて以下に説明します。

alloc_pages->...->__alloc_pages_nodemask

struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, int preferred_nid,
							nodemask_t *nodemask)
{
    
    
	struct page *page;
	// ALLOC_WMARK_LOW 允许分配内存的判断条件为低水位
	unsigned int alloc_flags = ALLOC_WMARK_LOW;
	gfp_t alloc_mask; /* The gfp_t that was actually used for allocation */
	// alloc_context 数据结构是伙伴系统分配函数中用于保存相关参数的数据结构
	struct alloc_context ac = {
    
     };

	// 伙伴系统能分配的最大内存块大小是 2 的 MAX_ORDER-1 次方
	if (unlikely(order >= MAX_ORDER)) {
    
    
		WARN_ON_ONCE(!(gfp_mask & __GFP_NOWARN));
		return NULL;
	}

	gfp_mask &= gfp_allowed_mask;
	alloc_mask = gfp_mask;
	// prepare_alloc_pages 函数会计算相关的信息并保存到 alloc_context 数据结构中
	if (!prepare_alloc_pages(gfp_mask, order, preferred_nid, nodemask, &ac, &alloc_mask, &alloc_flags))
		return NULL;

	// 用于确定首选的 zone
	finalise_ac(gfp_mask, &ac);

	// 内存碎片化方面的一个优化
	alloc_flags |= alloc_flags_nofragment(ac.preferred_zoneref->zone, gfp_mask);

	// 尝试从伙伴系统的空闲链表中分配内存
	// 若分配成功,则返回内存块的第一个页面的 page 数据结构
	// 若分配不成功,则会进入分配的慢速路径,即 __alloc_pages_slowpath 函数
	page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac);
	if (likely(page))
		goto out;
	...
    page = __alloc_pages_slowpath(alloc_mask, order, &ac);
    
out:
    ...
    return page;
}

alloc_pages->...->__alloc_pages_nodemask->prepare_alloc_pages

// 主要用于初始化页面分配器中用到的参数,这些参数会存放在 alloc_context 数据结构中
static inline bool prepare_alloc_pages(gfp_t gfp_mask, unsigned int order,
		int preferred_nid, nodemask_t *nodemask,
		struct alloc_context *ac, gfp_t *alloc_mask,
		unsigned int *alloc_flags)
{
    
    
	// gfp_zone() 函数根据分配掩码计算出 zone 的 zoneidx
	ac->high_zoneidx = gfp_zone(gfp_mask);
	// node_zonelist() 函数返回首选内存节点 preferred_nid 对应的 zonelist
	// 通常一个内存节点包含两个 zonelist :一个是 ZONELIST_FALLBACK,表示本地;另一个是 ZONELIST_NOFALLBACK,表示远端
	ac->zonelist = node_zonelist(preferred_nid, gfp_mask);
	ac->nodemask = nodemask;
	// 根据分配掩码来获取内存的迁移类型
	ac->migratetype = gfpflags_to_migratetype(gfp_mask);

	...

	// 使用新引入的故障注入技术
	if (should_fail_alloc_page(gfp_mask, order))
		return false;

	if (IS_ENABLED(CONFIG_CMA) && ac->migratetype == MIGRATE_MOVABLE)
		*alloc_flags |= ALLOC_CMA;

	return true;
}

alloc_pages->...->__alloc_pages_nodemask->get_page_from_freelist

// 从伙伴系统的空闲页面链表中尝试分配物理页面
static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
						const struct alloc_context *ac)
{
    
    
	struct zoneref *z;
	struct zone *zone;
	struct pglist_data *last_pgdat_dirty_limit = NULL;
	bool no_fallback;

retry:
	// 新增标志 ALLOC_NOFRAGMENT,表示需要避免内存碎片化
	no_fallback = alloc_flags & ALLOC_NOFRAGMENT;
	// preferred_zoneref 表示 zonelist 中首选和推荐的 zone
	z = ac->preferred_zoneref;
	// 从推荐的 zone 开始遍历所有的 zone
	for_next_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx,
								ac->nodemask) {
    
    
		...

		// 这是 NUMA 系统的一个特殊情况。当需要分配内存的 zone 不在本地内存结点(即在远端节点)时,
		// 要考虑的不是内存碎片化,而是内存的本地性,因为访问本地内存节点要比访问远端内存节点快很多
		if (no_fallback && nr_online_nodes > 1 &&
		    zone != ac->preferred_zoneref->zone) {
    
    
			...
		}

		// wmark_pages() 宏用来计算 zone 中某个水位的页面大小
		mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK);
		// zone_watermark_fast() 函数用于判断当前 zone 的空闲页面是否满足 WMARK_LOW
        // 用于测试当前 zone 的水位情况,以及检查是否满足多个页面(order大于0)的分配请求
		if (!zone_watermark_fast(zone, order, mark,
				       ac_classzone_idx(ac), alloc_flags)) {
    
    
			// 处理当前的 zone 不满足内存分配需求的情况
			int ret;

			...

			// 若 node_reclaim_mode 为 0,则表示可以从下一个 zone 或者内存节点中分配内存;
			// 否则,表示可以在这个 zone 中进行一些内存回收的动作
			if (node_reclaim_mode == 0 ||
			    !zone_allows_reclaim(ac->preferred_zoneref->zone, zone))
				continue;

			// node_reclaim() 函数尝试回收一部分内存
			ret = node_reclaim(zone->zone_pgdat, gfp_mask, order);
			...
		}

// 表示马上要从这个 zone 中分配内存了
try_this_zone:
		// rmqueue() 函数会从伙伴系统中分配内存,rmqueue() 函数是伙伴系统的核心分配函数
        // 若需要的内存块不能满足,那么可以从大内存块中“切”内存,就像切蛋糕一样
        // 如应用程序想分配order为5的内存块,但是order为5的空闲链表中没有空闲内存,而order为6的空闲链表中有,
		// 那么会把order为6的内存块取出来,然后把其中一块分配出去,把剩余的一块天骄到order为5的空闲链表中
		page = rmqueue(ac->preferred_zoneref->zone, zone, order,
				gfp_mask, alloc_flags, ac->migratetype);
		if (page) {
    
    
			// 当从伙伴系统分配页面成功之后需要设置页面的一些属性以及做必要的检查
			// 最后返回成功分配页面的 page 数据结构
			prep_new_page(page, order, gfp_mask, alloc_flags);

			...

			return page;
		} else {
    
    
#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT
			/* Try again if zone has deferred pages */
			if (static_branch_unlikely(&deferred_pages)) {
    
    
				if (_deferred_grow_zone(zone, order))
					goto try_this_zone;
			}
#endif
		}
	}

	// 当遍历完所有的 zone 后,还没有成功分配出所需要的内存,最后可能的情况是系统中产生了外碎片化。这时可以重新尝试一次
	if (no_fallback) {
    
    
		alloc_flags &= ~ALLOC_NOFRAGMENT;
		goto retry;
	}

	return NULL;
}

alloc_context データ構造のコメント:

struct alloc_context {
    
    
	// 指向每一个内存节点对应的 zonelist
	struct zonelist *zonelist;
	// 表示内存节点的掩码
	nodemask_t *nodemask;
	// 表示首选 zone 的 zoneref
	struct zoneref *preferred_zoneref;
	// 表示迁移类型
	int migratetype;
	// 分配掩码计算 zone 的 zoneidx,表示这个分配掩码允许内存分配的最高 zone
	enum zone_type high_zoneidx;
	// 用于指定是否传播脏页
	bool spread_dirty_pages;
};

おすすめ

転載: blog.csdn.net/qq_58538265/article/details/135170144