Mécanisme de pages chaudes et froides dans le gestionnaire de mémoire Linux

Introduction

Lors des opérations d'accès à la mémoire, le processus suit généralement cette séquence :

  1. Le CPU émet une instruction d'accès mémoire
  2. Conversion d'adresse, l'UMM convertit via la table de pages ou utilise le TLB pour obtenir l'adresse physique
  3. Accès au cache
  4. En cas de cache manquant, accès à la mémoire physique et chargement dans le cache

Ainsi, accéder à une mémoire physique dont le contenu se trouve dans le cache permet d'améliorer considérablement la vitesse d'accès. Basé sur ce principe, Linux désigne comme pages chaudes celles qui restent dans le cache, mettant en œuvre un mécanisme d'optimisation appelé pages chaudes/froides.

Structure percpu_pageset_t

Chaque processeur dispose de son propre cache exclusif (L1 et L2), et dans Linux, l'allocation mémoire aboutit finalement à une zone de mémoire. Par conséquent, chaque zone contient un struct per_cpu_pageset spécifique à chaque processeur, responsable de la gestion des pages chaudes et froides lors des allocations du processeur dans cette zone.

struct zone {
    struct per_cpu_pageset pageset[NR_CPUS];
}

La structure de données associée comprend un membre per_cpu_pages dans per_cpu_pageset, où se trouve l'implémentation des pages chaudes et froides. Les pages chaudes et froides sont représentées par une liste chaînée, les pages chaudes étant placées en tête de liste, les froides en queue. Les variables membres ont les significations suivantes :

  • count : nombre de pages dans la liste chaînée des pages chaudes/froides
  • high : quand count dépasse high, cela signifie que trop de pages chaudes/froides sont en cache, entraînant alors un retour groupé de pages au système partenaire
  • batch : quantité de pages à retirer ou ajouter par lot
  • list : liste chaînée des pages chaudes/froides
struct per_cpu_pages {
    int count;          /* nombre de pages dans la liste */
    int high;           /* seuil maximal, vidage nécessaire */
    int batch;          /* taille de bloc pour ajout/suppression partenaire */
    struct list_head list;  /* la liste des pages */
};

struct per_cpu_pageset {
    struct per_cpu_pages pcp;
#ifdef CONFIG_NUMA
    s8 expire;
#endif
#ifdef CONFIG_SMP
    s8 stat_threshold;
    s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS];
#endif
} ____cacheline_aligned_in_smp;

Allocation de page unique

Lorsqu'une zone répond aux conditions d'allocation, buffered_rmqueue tentera d'allouer de la mémoire à partir de cette zone.

D'abord, buffered_rmqueue vérifie si gfp_flags est marqué avec __GFP_COLD. Les deux !! dans !!(gfp_flags & __GFP_COLD) servent à limiter la valeur de cold à 0 ou 1. Selon gfp_flags, le type de migration migratetype est déterminé, lié à la gestion des fragments mémoire.

Les pages chaudes/froides optimisent principalement l'allocation et la libération de pages individuelles, c'est-à-dire lorsque l'ordre d'allocation est 0. D'abord, la liste chaînée des pages chaudes/froides pcp sur la zone actuelle du processeur courant est trouvée. Si pcp ne cnotient pas de pages en cache, rmqueue_bulk est appelé pour allouer pcp->batch pages depuis la zone actuelle vers la liste chaînée des pages chaudes/froides. Si des pages existent et que cold vaut 1, cela indique qu'il faut prioritairement allouer des pages froides, donc parcours de la fin vers le début ; sinon, parcours du début vers la fin. La première page correspondant au type de migration est retournée. On peut voir que le concept de chaud/froid n'indique que l'ordre de restitution, les pages entrant récemment dans la liste sont relativement plus chaudes, mais l'allocation ne garantit pas vraiment que la "page chaude" allouée soit dans le cache, seulement que la probabilité est plus élevée.

Cette vérification &page->lru == &pcp->list indique que dans la liste chaînée des pages chaudes/froides, aucune page correspondant au type de migration n'a été trouvée. Dans ce cas, rmqueue_bulk est appelé pour allouer un lot de pages du type de migration approprié vers la liste chaînée des pages chaudes/froides avant de tenter à nouveau l'allocation.

static struct page *buffered_rmqueue(struct zone *preferred_zone,
            struct zone *zone, int order, gfp_t gfp_flags)
{
    unsigned long flags;
    struct page *page;
    int cold = !!(gfp_flags & __GFP_COLD);
    int cpu;
    int migratetype = allocflags_to_migratetype(gfp_flags);

retry:
    cpu = get_cpu();
    if (likely(order == 0)) {
        struct per_cpu_pages *pcp_data;

        pcp_data = &zone_pcp(zone, cpu)->pcp;
        local_irq_save(flags);
        // Si la liste chaînée des pages chaudes/froides est vide, allouer depuis la zone les pages du type de migration approprié vers la liste chaînée
        if (!pcp_data->count) {
            pcp_data->count = rmqueue_bulk(zone, 0,
                    pcp_data->batch, &pcp_data->list, migratetype);
            if (unlikely(!pcp_data->count))
                goto failure;
        }

        // Si l'allocation de page froide est demandée, recherche de la fin vers le début, sinon du début vers la fin, trouver la première page correspondant au type de migration
        /* Trouver une page du type de migration approprié */
        if (cold) {
            list_for_each_entry_reverse(page, &pcp_data->list, lru)
                if (page_private(page) == migratetype)
                    break;
        } else {
            list_for_each_entry(page, &pcp_data->list, lru)
                if (page_private(page) == migratetype)
                    break;
        }

        // Si aucune page appropriée n'est trouvée, retour au début pour tenter à nouveau d'allouer des pages correspondant au type de migration vers la liste, puis allouer une page depuis la liste
        /* Allouer davantage à la liste pcp si nécessaire */
        if (unlikely(&page->lru == &pcp_data->list)) {
            pcp_data->count += rmqueue_bulk(zone, 0,
                    pcp_data->batch, &pcp_data->list, migratetype);
            page = list_entry(pcp_data->list.next, struct page, lru);
        }

        list_del(&page->lru);
        pcp_data->count--;
    }
    ...
}

Libération de page unique

Dans le noyau, l'API de base pour libérer une page est __free_pages. __free_pages vérifie si l'ordre d'allocation de la page à libérer est 0. Si c'est le cas, free_hot_page est appelé. free_hot_page appellera free_hot_cold_page avec des paramètres indiquant que la page libérée est une page chaude.

void __free_pages(struct page *page, unsigned int order)
{
    if (put_page_testzero(page)) {
        if (order == 0)
            free_hot_page(page);
        else
            __free_pages_ok(page, order);
    }
}

void free_hot_page(struct page *page)
{
    free_hot_cold_page(page, 0);
}

void free_cold_page(struct page *page)
{
    free_hot_cold_page(page, 1);
}

Voici le code relatif à la liste chaînée des pages chaudes/froides dans free_hot_cold_page. Le paramètre cold détermine si la page est insérée en tête ou en queue de liste, puis le champ private de la page est défini selon son type de migration.

Si le nombre total de pages dans la liste chaînée des pages chaudes/froides count dépasse high, free_pages_bulk est appelé pour retourner batch pages au système partenaire. Cette méthode de libération par lots est appelée fusion paresseuse, capable d'éviter de nombreuses opérations de fusion inutiles (fusion suivie de réallocation).

static void free_hot_cold_page(struct page *page, int cold)
{
    struct zone *zone = page_zone(page);
    struct per_cpu_pages *pcp;
    unsigned long flags;

    ...

    pcp = &zone_pcp(zone, get_cpu())->pcp;
    if (cold)
        list_add_tail(&page->lru, &pcp->list);
    else
        list_add(&page->lru, &pcp->list);
    set_page_private(page, get_pageblock_migratetype(page));
    pcp->count++;
    if (pcp->count >= pcp->high) {
        free_pages_bulk(zone, pcp->batch, &pcp->list, 0);
        pcp->count -= pcp->batch;
    }
    ...
}

Résumé

Le mécanisme des pages chaudes/froides présente les avantages suivants :

  • Les pages libérées devenues chaudes, si elles sont à nouveau accédées dans un court laps de temps, nécessiteront probablement seulement de rétablir le mappage sans avoir à charger le contenu dans le cache, offrant ainsi une vitesse d'accès plus rapide.
  • Grâce à l'allocation et la libération groupées de pages depuis/vers le système partenaire, le nombre d'utilisations du système partenaire est réduit, diminuant ainsi les opérations de fractionnement et de fusion du système partenaire tout en accélérant les allocatoins.
  • Adoptant une conception par processeur, il distingue les caches des différents processeurs, évitant simultanément la concurrence lors de l'allocation de mémoire unique par plusieurs processeurs.

Étiquettes: linux-kernel memory-management page-allocation Caching virtual-memory

Publié le 24 août à 12h24