Linux-Speicherverwaltung: (1) Partnersystem

Artikelbeschreibung:

1. Speicherfragmentierungsproblem und Partnersystemalgorithmus

Speicherfragmentierung ist ein schwer zu lösendes Problem bei der Speicherverwaltung. Der Linux-Kernel berücksichtigt bei der Übernahme des Buddy-System-Algorithmus, wie die Speicherfragmentierung reduziert werden kann. Welche Art von Speicherblöcken können im Buddy-System-Algorithmus zwei Speicherblöcke zu Partnern werden? Tatsächlich muss der Partnersystemalgorithmus die folgenden drei Grundbedingungen erfüllen:

  1. Beide Speicherblöcke sind gleich groß
  2. Die Adressen der beiden Speicherblöcke sind fortlaufend
  3. Beide Speicherblöcke müssen vom gleichen großen Speicherblock getrennt sein

Die Aufteilung der Speicherblöcke ist in der folgenden Abbildung dargestellt:

Fügen Sie hier eine Bildbeschreibung ein

Beim Zusammenführen von Speicherblöcken können folgende Situationen auftreten:

Fügen Sie hier eine Bildbeschreibung ein

Zu diesem Zeitpunkt werden P0 und P3 zu Löchern, was zu einer externen Fragmentierung führt . Mit der Zeit wird die externe Fragmentierung immer schwerwiegender und die Speichernutzung nimmt ebenfalls ab.

Eine häufig verwendete akademische Technologie zur Lösung des Problems der externen Fragmentierung ist die sogenannte Speicherkomprimierung , bei der die Position mobiler Seiten verwendet wird, um freie Seiten zu einem Stück zu verbinden. Den Migrationsattributen der Seite nach zu urteilen, kann der vom Benutzerprozess zugewiesene und verwendete Speicher migriert werden, die vom Kernel selbst verwendeten Speicherseiten können jedoch nicht zufällig migriert werden.

Warum können vom Kernel selbst verwendete Seiten nicht migriert werden?

Denn um diese Art von Seite zu migrieren, müssen Sie zunächst die Zuordnung der physischen Seite trennen und dann die Zuordnungsbeziehung wiederherstellen. Wenn der Kernel während des Trennens der Zuordnungsbeziehung weiterhin auf diese Seite zugreift, greift er auf falsche Zeiger und Speicher zu, was zu Hoppla-Fehlern im Kernel und sogar zu einem Systemabsturz führen kann. Als sensibler Bereich muss der Kernel dies sicherstellen Der verwendete Speicher ist sicher.

Dies unterscheidet sich vom Benutzerprozess. Nachdem die vom Benutzerprozess verwendete Seite die Zuordnungsbeziehung unterbrochen hat und der Benutzerprozess weiterhin auf diese Seite zugreift, tritt eine Seitenfehlerausnahme auf. Bei der Behandlung von Seitenfehlerausnahmen können physische Seiten neu zugewiesen und dann dem virtuellen Speicher zugeordnet werden. Dieser Prozess ist für Benutzerprozesse sicher.

Während der Entwicklungsphase von Linux 2.6.24 führten Community-Experten eine Funktion zur Verhinderung der Fragmentierung ein, die sogenannte Antifragmentierung . Tatsächlich wird dies durch die Verwendung von Migrationstypen erreicht. Migrationstypen werden nach Seitenblöcken unterteilt. Die Größe eines Seitenblocks entspricht genau der maximalen Speicherblockgröße, die der Seitenzuordner zuweisen kann, also 2 hoch MAX_ORDER-1 Bytes, was normalerweise 4 MB beträgt.

#define MAX_ORDER 11

Die Seitentypen sind wie folgt:

  • Nicht migrierender Typ UNMOVABLE: Das Merkmal besteht darin, dass er einen festen Speicherort im Speicher hat und nicht an andere Orte verschoben werden kann. Beispielsweise fällt der Speicher, den der Kernel selbst verwenden muss, in diese Kategorie.
  • Beweglicher Typ MOVABLE: Zeigt Seiten an, die nach Belieben verschoben werden können. Dies ist normalerweise eine Seite, die zu einer Anwendung gehört.
  • Recycelbare Seiten: Diese Seiten können nicht direkt verschoben, aber recycelt werden. Der Inhalt der Seite kann erneut gelesen oder abgerufen werden. Das typischste Beispiel ist die Objektzuweisung mithilfe des Slab-Mechanismus.

Daher enthält die Datenstruktur free_area im Partnersystem verknüpfte MIGRATE_TYPES-Listen, was je nach Auftragsgröße 0 ~ MAX_ORDER-1 free_areas in der Speicherverwaltungszone (Zone) entspricht. Jeder freie_Bereich verfügt über mehrere entsprechende verknüpfte Listen entsprechend dem Typ MIGRATE_TYPES, wie in der folgenden Abbildung dargestellt:

Fügen Sie hier eine Bildbeschreibung ein

// 内存管理区,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
};

Im Linux-Kernel, der diese Technologie verwendet, sind die Seiten in allen Seitenblöcken vom gleichen Migrationstyp, und es werden keine anderen Seitentypen dazwischen gemischt.

Abschließend wird die Definition des Buddy-Systems angegeben : Das Buddy-System ist eine dynamische Speicherverwaltungsmethode, die üblicherweise in Betriebssystemen verwendet wird. Wenn der Benutzer eine Anfrage stellt, wird dem Benutzer ein Speicherblock mit geeigneter Größe zugewiesen, und wenn der Benutzer ihn freigibt, wird er verwendet der Speicherblock Recycle. Im Begleitsystem entspricht die Größe des Speicherblocks der Ordnungsstärke von 2 Seiten. Der maximale Bestellwert im Linux-Kernel wird durch MAX_ORDER dargestellt, normalerweise 11, d. , 1024 aufeinanderfolgende Seiten. 1024 Seiten entsprechen 4 MB zusammenhängendem physischen Speicher.

2. Initialisierung des physischen Speichers im Partnersystem

Wenn der Kernel startet, kennt der Kernel die Größe des physischen DDR-Speichers und berechnet die Startadresse des High-End-Speichers und das Speicherlayout des Kernelraums. Die physische Speicherseite wird dem Partnersystem hinzugefügt. So geht's Fügen Sie die physische Speicherseite zum Partnersystem hinzu? Wolltuch?

Wenn der Kernel initialisiert wird, werden alle Seiten zunächst als Typ MIGRATE_MOVABLE markiert, siehe Funktion 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);
        ...
    }
}

Frage:

Unter Linux werden bei der Kernel-Initialisierung alle Seiten zunächst als Typ MIGRATE_MOVABLE markiert. Wann werden die Seiten also auf den entsprechenden Typ eingestellt?

chatGPT-Antwort:

Unter Linux werden MIGRATE_MOVABLEbei der Initialisierung des Kernels alle Seiten als Typen markiert. Dies liegt daran, dass der Kernel während der Initialisierungsphase die spezifischen Verwendungszwecke und Attribute jeder Seite noch nicht kennt. Der spezifische Seitentyp wird zur Laufzeit dynamisch bestimmt und kann basierend auf dem Zustand des Systems angepasst werden.

Im Linux-Kernel kann sich der Seitentyp ändern, und der spezifische Zeitpunkt und die Bedingungen hängen von verschiedenen Situationen ab. Hier sind einige häufige Änderungen am Seitentyp:

  1. Beim Zuweisen von Speicher: Wenn der Kernel Speicher über Funktionen wie usw. dynamisch zuweist kmalloc, vmalloclegt der Kernel den Seitentyp basierend auf dem Kontext und den Anforderungen der Zuweisung fest.

  2. Seitenmigration: In Situationen mit knappem Speicher führt der Linux-Kernel möglicherweise eine Seitenmigration durch und verschiebt Seiten von einem Typ auf einen anderen, um bestimmte Speicheranforderungen zu erfüllen.

  3. Speicherrückgewinnung: Wenn das System Speicher freigeben muss, ändert der Kernel möglicherweise den Typ der Seite in den entsprechenden Typ, um die Wiederverwendung oder Neuzuweisung zu erleichtern.

  4. Speicher-Hot-Plug: Auf Systemen, die Speicher-Hot-Plug unterstützen, kann der Kernel den Seitentyp neu bewerten und aktualisieren, wenn Speicher hinzugefügt oder entfernt wird.

Insgesamt handelt es sich bei der Einstellung von Seitentypen um einen dynamischen Prozess, der vom Betriebszustand des Systems und der Verwaltungsstrategie des Kernels abhängt. Diese dynamische Natur ermöglicht es dem Kernel, den Speicher je nach Bedarf flexibel zu verwalten.

Betrachten wir als Nächstes, wie physische Seiten zum Buddy-System hinzugefügt werden. Sollten wir Seiten einzeln hinzufügen oder 2 n Seiten gleichzeitig hinzufügen?

In der Funktion free_low_memory_core_early() werden alle Speicherblöcke durch die Funktion for_each_free_mem_range() durchlaufen, um die Startadresse und Endadresse des Speicherblocks zu ermitteln.

<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);
    ...
}

Übergeben Sie den Speicherblock an die Funktion __free_pages_memory(), die wie folgt definiert ist:

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. Vorausgesetztes Wissen: die Beziehung zwischen Zonenlisten

Da der Kernel Zonen zum Verwalten eines Speicherknotens verwendet, kann ein Speicherknoten in mehrere verschiedene Zonen unterteilt werden. Der Kernel verwendet die Zonenlisten-Datenstruktur, um die Zone eines Speicherknotens zu verwalten.

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

Verwandte Datenstrukturen:

// 每一个 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];
    ...
}

Wir gehen davon aus, dass es im System nur einen Speicherknoten und zwei Zonen gibt, nämlich ZONE_DMA32 und ZONE_NORMAL. Dann ist die Beziehung zwischen Zonentyp, _zonerefs[]-Array und zone_idx in Zonelist wie in der folgenden Abbildung dargestellt:

Fügen Sie hier eine Bildbeschreibung ein

In diesem Fall wird bei der Zuweisung von physischem Speicher Zonen vom Typ ZONE_NORMAL Vorrang eingeräumt, d. h. die Zone mit dem größten zone_idx-Wert wird zuerst zugewiesen.

4. Schneller Pfad zur Seitenzuordnung

Das Buddy-System ist das grundlegende Speicherzuweisungssystem im Linux-Kernel. Das Konzept des Buddy-Systems ist nicht schwer zu verstehen, aber die Zuweisung physischer Seiten war schon immer der komplexeste Teil der Speicherverwaltung. Es umfasst recht komplizierte Mechanismen wie Seitenrecycling, Speicherregularisierung und direktes Speicherrecycling. Der Seitenzuordner ist der grundlegendste Zuordner in der Speicherverwaltung des Linux-Kernels und basiert auf dem Buddy-System-Algorithmus und dem zonenbasierten Designkonzept. Dieser Artikel konzentriert sich darauf, wie zusammenhängender physischer Speicher zugewiesen wird, wenn ausreichend Speicher vorhanden ist.

Der schnelle Prozess der Speicherzuweisung ist in der folgenden Abbildung dargestellt:

Fügen Sie hier eine Bildbeschreibung ein

Der Ablauf der Funktion get_page_from_freelist() ist in der folgenden Abbildung dargestellt:

Fügen Sie hier eine Bildbeschreibung ein

Um den Lesern ein realistischeres Verständnis zu ermöglichen, wird dieser Prozess im Folgenden anhand des Flussdiagramms rund um den Quellcode erläutert:

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;
}

Kommentare zur Datenstruktur 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;
};

Supongo que te gusta

Origin blog.csdn.net/qq_58538265/article/details/135170144
Recomendado
Clasificación