Analyse der Initialisierung des Quellcodes des Linux-Kernel-Schedulers

Analyse der Initialisierung des Quellcodes des Linux-Kernel-Schedulers

1. Einleitung

Das Scheduler-Subsystem ist eines der Kernel-Subsysteme. Es ist für die rationale Zuweisung von CPU-Ressourcen innerhalb des Systems verantwortlich. Es muss in der Lage sein, die komplexen Planungsanforderungen verschiedener Aufgabentypen sowie verschiedener komplexer, gleichzeitiger Wettbewerbsumgebungen zu bewältigen. Gleichzeitig muss es die Gesamtdurchsatzleistung und die Echtzeitanforderungen (die an sich schon einen Widerspruch darstellen) berücksichtigen. Sein Design und seine Implementierung sind äußerst anspruchsvoll.

Um das Design und die Implementierung des Linux-Schedulers zu verstehen, nehmen wir die Linux-Kernel-Version 5.4 (die Standard-Kernel-Version von TencentOS Server3) als Objekt und analysieren ausgehend vom Initialisierungscode des Scheduler-Subsystems das Design und die Implementierung des Linux-Kernel-Schedulers.

2. Grundkonzepte des Schedulers

Bevor Sie den relevanten Code des Schedulers analysieren, müssen Sie die im Scheduler enthaltenen Kerndaten (Struktur) und ihre Funktionen verstehen.

2.1. Warteschlange ausführen (rq)

Der Kernel erstellt für jede CPU eine Ausführungswarteschlange. Alle bereiten (ausgeführten) Prozesse (Aufgaben) im System werden in der Kernel-Ausführungswarteschlange organisiert, und dann werden die Prozesse in der Ausführungswarteschlange gemäß der entsprechenden Strategie zur Ausführung an die CPU gesendet.

2.2 Planungsklasse (sched_class)

Der Kernel abstrahiert die Planungsrichtlinie (sched_class) stark, um eine Planungsklasse (sched_class) zu bilden. Die Planungsklasse kann den gemeinsamen Code (Mechanismus) des Planers vollständig von den Planungsstrategien entkoppeln, die durch verschiedene spezifische Planungsklassen bereitgestellt werden, was eine typische OO-Idee (objektorientiert) ist. Dieses Design macht den Kernel-Scheduler sehr erweiterbar. Entwickler können mit sehr wenig Code (im Grunde ohne Änderung des allgemeinen Codes) eine neue Scheduling-Klasse hinzufügen und so einen neuen Scheduler (eine neue Klasse) implementieren. Beispielsweise wurde die Deadline-Scheduling-Klasse in 3.x neu hinzugefügt. Auf Codeebene fügt sie nur die relevanten Implementierungsfunktionen der dl_sched_class-Struktur hinzu, was bequemerweise einen neuen Echtzeit-Scheduling-Typ hinzufügt.

Der aktuelle 5.4-Kernel verfügt über fünf Planungsklassen und die Priorität ist wie folgt von hoch nach niedrig verteilt:

stop_sched_class:

Die Planungsklasse mit der höchsten Priorität ist, wie beispielsweise idle_sched_class, ein dedizierter Planungstyp (mit Ausnahme von Migrationsthreads können oder sollten andere Aufgaben nicht auf die Planungsklasse „Stopp“ gesetzt werden). Diese Planungsklasse ist speziell für die Implementierung „dringender“ Aufgaben wie Active Balance oder Stop Machine konzipiert, die auf die Ausführung des Migrationsthreads angewiesen sind.

dl_sched_class:

Die Priorität der Deadline-Scheduling-Klasse steht nur der Stop-Scheduling-Klasse nach. Es handelt sich um einen Echtzeit-Scheduler (oder eine Scheduling-Strategie), der auf dem EDL-Algorithmus basiert.

rt_sched_class:

Die Priorität der RT-Planungsklasse ist niedriger als die der DL-Planungsklasse. Es handelt sich um einen auf Prioritäten basierenden Echtzeitplaner.

fair_sched_class:

Die Priorität des CFS-Schedulers ist niedriger als die der drei oben genannten Planungsklassen. Es handelt sich um einen Planungstyp, der auf der Idee einer fairen Planung basiert und die Standardplanungsklasse des Linux-Kernels ist.

idle_sched_class:

Der Leerlaufplanungstyp ist ein Swapper-Thread, der es dem Swapper-Thread hauptsächlich ermöglicht, die CPU zu übernehmen und die CPU über Frameworks wie cpuidle/nohz in einen Energiesparzustand zu versetzen.

2.3 Planungsdomäne (sched_domain)

Scheduling-Domänen wurden in 2.6 in den Kernel eingeführt. Durch die Einführung von mehrstufigen Scheduling-Domänen kann sich der Scheduler besser an die physikalischen Eigenschaften der Hardware anpassen (Scheduling-Domänen können sich besser an die Herausforderungen anpassen, die der mehrstufige CPU-Cache und die physikalischen Eigenschaften von NUMA für den Lastenausgleich mit sich bringen) und eine bessere Scheduling-Leistung erzielen (sched_domain ist ein Mechanismus, der für den CFS-Scheduling-Lastenausgleich entwickelt wurde).

2.4 Planungsgruppe (sched_group)

Die Planungsgruppe wird zusammen mit der Planungsdomäne in den Kernel eingeführt. Sie arbeitet mit der Planungsdomäne zusammen, um den CFS-Scheduler bei der Lastverteilung zwischen mehreren Kernen zu unterstützen.

2.5. Stammdomäne (root_domain)

Die Stammdomäne ist hauptsächlich für die Datenstruktur verantwortlich, die für den Lastausgleich von Echtzeit-Planungsklassen (einschließlich DL- und RT-Planungsklassen) entwickelt wurde, und unterstützt DL- und RT-Planungsklassen dabei, die angemessene Planung von Echtzeitaufgaben durchzuführen. Wenn die Kontrollgruppe „Isolate“ oder „Cpuset“ nicht zum Ändern der Planungsdomäne verwendet wird, befinden sich alle CPUs standardmäßig in derselben Stammdomäne.

2.6 Gruppenplanung (group_sched)

Um eine genauere Kontrolle über die Ressourcen im System zu haben, hat der Kernel den Cgroup-Mechanismus zur Ressourcenkontrolle eingeführt. Und group_sched ist der zugrunde liegende Implementierungsmechanismus der CPU-Kontrollgruppe. Über die CPU-Kontrollgruppe können wir einige Prozesse als Kontrollgruppe festlegen und die entsprechende Bandbreite, den Anteil und andere Parameter über die Steuerschnittstelle der CPU-Kontrollgruppe konfigurieren. Auf diese Weise können wir die CPU-Ressourcen je nach Gruppe genau steuern.

3. Scheduler-Initialisierung (sched_init)

Kommen wir zum Punkt und beginnen mit der Analyse des Initialisierungsprozesses des Kernel-Schedulers. Ich hoffe, dass durch die Analyse hier jeder Folgendes verstehen kann:

1. Wie wird die Ausführungswarteschlange initialisiert?

2. Wie ist die Gruppenplanung mit rq verknüpft (Gruppenplanung kann erst nach der Verknüpfung über group_sched durchgeführt werden)

3. CFS-Soft-Interrupt SCHED_SOFTIRQ-Registrierung

Initialisierung planen (sched_init)

start_kernel

|----setup_arch

|----alle Zonenlisten erstellen

|----mm_init

|----sched_init Planungsinitialisierung

Die Initialisierung der Planung erfolgt relativ spät in start_kernel. Zu diesem Zeitpunkt ist die Speicherinitialisierung abgeschlossen, sodass Sie sehen können, dass Speicherzuweisungsfunktionen wie kzmalloc in sched_init aufgerufen werden können.

sched_init muss die Ausführungswarteschlange (rq) für jede CPU, die globale Standardbandbreite von dl/rt, die Ausführungswarteschlange jeder Planungsklasse und die CFS-Soft-Interrupt-Registrierung initialisieren.

Als nächstes schauen wir uns die spezifische Implementierung von sched_init an (einige Codeteile wurden weggelassen):

void __init sched_init(void)
{
    vorzeichenloser langer ptr = 0;
    int ich;
 
    /*
     * Initialisieren Sie die globale Standarddatenstruktur zur Steuerung der CPU-Bandbreite für RT und DL *
     * rt_bandwidth und dl_bandwidth werden hier verwendet, um die globale DL- und RT-Bandbreitennutzung zu steuern und zu verhindern, dass der Echtzeitprozess zu viel CPU verwendet, was zu einem Verhungern des normalen CFS-Prozesses führen würde */
    init_rt_bandwidth(&def_rt_bandwidth, global_rt_period(), global_rt_runtime());
    init_dl_bandwidth(&def_dl_bandwidth, global_rt_period(), global_rt_runtime());
 
#ifdef CONFIG_SMP
    /*
     * Initialisieren Sie die Standard-Stammdomäne *
     * Die Root-Domäne ist eine wichtige Datenstruktur für den globalen Ausgleich von Echtzeitprozessen wie dl/rt. Am Beispiel von rt ist * root_domain->cpupri die höchste Priorität der RT-Aufgabe, die auf jeder CPU innerhalb der Root-Domäne ausgeführt wird, sowie * die Verteilung von Aufgaben mit unterschiedlicher Priorität auf der CPU. Durch die Daten von cpupri wird in rt Enqueue/Dequeue
     * Wenn der RT-Scheduler sicherstellen kann, dass Aufgaben mit hoher Priorität basierend auf der RT-Aufgabenverteilung zuerst ausgeführt werden*/
    init_defrootdomain();
#endif
 
#ifdef CONFIG_RT_GROUP_SCHED
    /*
     * Wenn der Kernel die RT-Gruppenplanung (RT_GROUP_SCHED) unterstützt, kann die Bandbreitensteuerung für RT-Aufgaben mithilfe von cgroup erfolgen.
     * Die Granularität wird verwendet, um die CPU-Bandbreitennutzung von RT-Aufgaben in jeder Gruppe zu steuern*
     * RT_GROUP_SCHED ermöglicht RT-Aufgaben, die Bandbreite als Ganzes in Form von CPU-Kontrollgruppen zu steuern. * Dies kann die RT-Bandbreitensteuerung flexibler machen (ohne RT_GROUP_SCHED kann nur die globale * Bandbreitennutzung von RT gesteuert werden, und die Bandbreite einiger RT-Prozesse kann nicht durch Angabe einer Gruppe gesteuert werden).
     */
    init_rt_bandwidth(&root_task_group.rt_bandwidth,
            global_rt_period(), global_rt_runtime());
#endif /* CONFIG_RT_GROUP_SCHED */
 
    /* Initialisiere die Ausführungswarteschlange für jede CPU */
    für_jede_mögliche_CPU(i) {
        Struktur rq *rq;
 
        rq = cpu_rq(i);
        raw_spin_lock_init(&rq->Sperre);
        /*
         * Initialisieren Sie die Ausführungswarteschlange von cfs/rt/dl auf rq. * Jeder Planungstyp hat seine eigene Ausführungswarteschlange auf rq und jede Planungsklasse verwaltet ihren eigenen Prozess. * Bei pick_next_task() wählt der Kernel Aufgaben von hoch nach niedrig entsprechend der Reihenfolge der Priorität der Planungsklasse aus. * Dadurch wird sichergestellt, dass Aufgaben der Planungsklasse mit hoher Priorität zuerst ausgeführt werden. *
         * Stop und Idle sind spezielle Planungstypen, die für spezielle Zwecke entwickelt wurden und es Benutzern nicht erlauben, * Prozesse des entsprechenden Typs zu erstellen, sodass der Kernel keine entsprechenden Ausführungswarteschlangen in rq entwirft */
        cfs_rq(&rq->cfs);
        init_rt_rq(&rq->rt);
        init_dl_rq(&rq->dl);
#ifdef CONFIG_FAIR_GROUP_SCHED
        /*
         * CFS-Gruppenplanung (group_sched), kann CFS über die CPU-Steuerungsgruppe steuern * kann eine CPU-Verhältnissteuerung zwischen Gruppen über cpu.shares bereitstellen (verschiedene Steuerungsgruppen können die CPU entsprechend den entsprechenden * Verhältnissen gemeinsam nutzen) und kann auch Quoten über cpu.cfs_quota_us festlegen (ähnlich der * Bandbreitensteuerung von RT). Die Bandbreitenkontrolle von CFS group_sched ist eine der grundlegenden Technologien für die Containerimplementierung*
         * root_task_group ist die standardmäßige Root-Task-Group, andere CPU-Kontrollgruppen verwenden sie als * übergeordnetes Element oder Vorgängerelement. Die Initialisierung hier verknüpft root_task_group mit der CFS-Ausführungswarteschlange von rq*. Was hier gemacht wird, ist sehr interessant. Direktes Setzen von root_task_group->cfs_rq[cpu] = &rq->cfs
         * Auf diese Weise wird der Prozess unter der CPU-Cgroup-Root oder der sched_entity der Cgroup tg direkt zu rq->cfs hinzugefügt
         * In der Warteschlange kann der Suchaufwand um eine Ebene reduziert werden.
         */
        root_task_group.shares = ROOT_TASK_GROUP_LOAD;
        INIT_LIST_HEAD(&rq->leaf_cfs_rq_list);
        rq->tmp_alone_branch = &rq->leaf_cfs_rq_list;
        init_cfs_bandwidth(&root_task_group.cfs_bandwidth);
        init_tg_cfs_entry(&root_task_group, &rq->cfs, NULL, i, NULL);
#endif /* CONFIG_FAIR_GROUP_SCHED */
 
        rq->rt.rt_runtime = def_rt_bandwidth.rt_runtime;
#ifdef CONFIG_RT_GROUP_SCHED
        /* Initialisiere die RT-Ausführungswarteschlange auf rq, ähnlich der Gruppenplanungsinitialisierung von CFS oben*/
        init_tg_rt_entry(&root_task_group, &rq->rt, NULL, i, NULL);
#endif
 
#ifdef CONFIG_SMP
        /*
         * Hier wird rq mit der Standard-Def_Root_Domain verknüpft. Wenn es sich um ein SMP-System handelt, erstellt der Kernel später in sched_init_smp eine neue Root_Domain und ersetzt diese Def_Root_Domain
         */
        rq_attach_root(rq, &def_root_domain);
#endif /* CONFIG_SMP */
    }
 
    /*
     * Registrieren Sie die Soft-Interrupt-Servicefunktion SCHED_SOFTIRQ von CFS. * Dieser Soft-Interrupt ist für den periodischen Lastausgleich und den Leerlauflastausgleich vorbereitet. */
    init_sched_fair_class();
 
    scheduler_running = 1;
}

4. Initialisierung der Multi-Core-Planung (sched_init_smp)

start_kernel

|----rest_init

|----kernel_init

|----kernel_init_freeable

|----smp_init

|----sched_init_smp

|---- sched_init_numa

|---- sched_init_domains

|---- build_sched_domains

Mit der Initialisierung der Multi-Core-Planung wird hauptsächlich die Initialisierung der Planungsdomäne/Planungsgruppe abgeschlossen (natürlich wird dies auch für die Stammdomäne durchgeführt, aber relativ gesehen ist die Initialisierung der Stammdomäne relativ einfach).

Linux ist ein Betriebssystem, das auf mehreren Chiparchitekturen und mehreren Speicherarchitekturen (UMA/NUMA) ausgeführt werden kann. Daher muss Linux in der Lage sein, sich an mehrere physische Strukturen anzupassen, weshalb die Gestaltung und Implementierung seines Planungsbereichs relativ komplex sind.

4.1 Implementierungsprinzip der Planungsdomäne

Bevor wir über den spezifischen Initialisierungscode der Planungsdomäne sprechen, müssen wir die Beziehung zwischen der Planungsdomäne und der physischen Topologiestruktur verstehen (da das Design der Planungsdomäne eng mit der physischen Topologiestruktur zusammenhängt. Wenn Sie die physische Topologiestruktur nicht verstehen, können Sie die Implementierung der Planungsdomäne nicht wirklich verstehen.)

Physikalische Topologie der CPU

Wir gehen von einem Computersystem aus (ähnlich einem Intel-Chip, jedoch mit reduzierter Anzahl von CPU-Kernen zur einfacheren Darstellung):

Ein Dual-Socket-Computersystem, bei dem jeder Socket aus 2 Kernen und 4 Threads besteht, sollte ein 4-Core-8-Thread-NUMA-System sein (das obige ist nur die physikalische Topologie von Intel, während die AMD ZEN-Architektur ein Chiplet-Design verwendet, das eine zusätzliche DIE-Domäne zwischen den MC- und NUMA-Domänen hat).

Erste Schicht (SMT-Domäne):

Wie im CORE0 in der obigen Abbildung gezeigt, bilden zwei Hyperthreads die SMT-Domäne. Bei Intel-CPUs werden L1 und L2 durch Hyperthreading gemeinsam genutzt (sogar Store-Buffs werden bis zu einem gewissen Grad gemeinsam genutzt), sodass bei der Migration zwischen SMT-Domänen kein Cache-Wärmeverlust auftritt.

Schicht 2 (MC-Domäne):

Wie in der obigen Abbildung gezeigt, befinden sich CORE0 und CORE1 im selben SOCKET und gehören zur MC-Domäne. Bei Intel-CPUs wird LLC im Allgemeinen gemeinsam genutzt (normalerweise L3). In diesem Bereich geht zwar durch die Prozessmigration die Wärme von L1 und L2 verloren, die Wärme des L3-Cache kann jedoch weiterhin aufrechterhalten werden.

Die dritte Schicht (NUMA-Domäne):

Wie in der obigen Abbildung gezeigt, führt die Prozessmigration zwischen SOCKET0 und SOCKET1 zum Verlust der gesamten Cache-Wärme und verursacht einen großen Overhead. Daher muss die Migration von NUMA-Domänen relativ vorsichtig erfolgen.

Genau aufgrund solcher physikalischen Eigenschaften der Hardware (Hardwarefaktoren wie Cache-Erhitzung auf verschiedenen Ebenen, NUMA-Zugriffslatenz usw.) abstrahiert der Kernel sched_domain und sched_group, um solche physikalischen Eigenschaften darzustellen. Beim Lastausgleich werden verschiedene Planungsstrategien (wie Lastausgleichshäufigkeit, Ungleichgewichtsfaktor und Aktivierungskernauswahllogik) entsprechend den entsprechenden Planungsdomänenmerkmalen implementiert, um ein besseres Gleichgewicht zwischen CPU-Last und Cache-Affinität zu erreichen.

Domänenimplementierung planen

Als Nächstes können wir sehen, wie der Kernel Planungsdomänen und Planungsgruppen in der obigen physischen Topologie einrichtet.

Der Kernel richtet entsprechend der physischen Topologie Planungsdomänen auf entsprechenden Ebenen ein und richtet dann auf jeder Ebene der Planungsdomänen entsprechende Planungsgruppen ein. Wenn die Planungsdomäne einen Lastausgleich durchführt, findet sie die am stärksten ausgelastete SG (sched_group) in der Planungsdomäne der entsprechenden Ebene und bestimmt dann, ob die Lasten der am stärksten ausgelasteten SG und der lokalen SG (aber der Planungsgruppe, in der sich die vordere CPU befindet) ungleichmäßig sind. Bei einer ungleichmäßigen Auslastung wird die am stärksten ausgelastete CPU aus der am stärksten ausgelasteten SG ausgewählt und die Auslastung dann zwischen den beiden CPUs ausgeglichen.

Die SMT-Domäne ist die Planungsdomäne der untersten Ebene. Sie können sehen, dass jedes Hyperthreading-Paar eine SMT-Domäne ist. Es gibt 2 Sched_Groups in der SMT-Domäne und jede Sched_Group hat nur eine CPU. Daher besteht der Lastausgleich der SMT-Domäne darin, eine Prozessmigration zwischen Hyperthreads durchzuführen. Dieser Lastausgleich hat die kürzeste Zeit und die entspanntesten Bedingungen.

Bei Architekturen ohne Hyperthreading (oder bei denen Hyperthreading auf dem Chip nicht aktiviert ist) ist die MC-Domäne die Domäne der niedrigsten Ebene (derzeit gibt es nur zwei Domänenebenen: MC und NUMA). Auf diese Weise ist jeder CORE in der MC-Domäne eine Sched_Group, und der Kernel kann sich während der Planung gut an solche Szenarien anpassen.

Die MC-Domäne besteht aus allen CPUs auf dem Socket, und jede SG besteht aus allen CPUs in der übergeordneten SMT-Domäne. Für die obige Abbildung besteht MCs SG also aus 2 CPUs. Der Kernel ist in der MC-Domäne auf diese Weise konzipiert, sodass die CFS-Planungsklasse beim Wake-up-Load-Balancing und beim Idle-Load-Balancing einen Ausgleich zwischen SGs in der MC-Domäne erfordern kann.

Dieses Design ist für Hyperthreading sehr wichtig und wir können diese Situation auch in einigen tatsächlichen Unternehmen beobachten. Wir betreiben beispielsweise ein Codec-Geschäft und haben festgestellt, dass die Testdaten in manchen virtuellen Maschinen besser und in manchen anderen schlechter sind. Nach der Analyse wurde festgestellt, dass dies daran lag, ob die Hyperthreading-Informationen transparent an die virtuelle Maschine übertragen wurden. Nachdem wir die Hyperthreading-Informationen an die virtuelle Maschine übergeben haben, bildet die virtuelle Maschine eine zweischichtige Planungsdomäne (SMT- und MC-Domäne). Beim Aufwecken des Lastausgleichs neigt CFS dazu, das Geschäft für den inaktiven SG zu planen (d. h. den inaktiven physischen CORE, nicht die inaktive CPU). Zu diesem Zeitpunkt, wenn die CPU-Auslastung des Geschäfts nicht hoch ist (nicht mehr als 40 %), kann es die Leistung des physischen CORE besser nutzen (es ist immer noch das alte Problem. Wenn ein Paar Hyperthreads auf einem physischen CORE gleichzeitig CPU-intensive Geschäfte ausführt, beträgt der erzielte Leistungsgewinn nur etwa das 1,2-fache eines einzelnen Threads.), wodurch ein besserer Leistungsgewinn erzielt wird. Wenn die Hyperthreading-Informationen nicht transparent übertragen werden, verfügt die virtuelle Maschine nur über eine Schicht physischer Topologie (MC-Domäne). Da der Dienst in diesem Fall wahrscheinlich über das Hyperthreading-Paar eines physischen CORE geplant wird, kann das System die Leistung des physischen CORE nicht vollständig nutzen, was zu einer geringen Dienstleistung führt.

Die NUMA-Domäne besteht aus allen CPUs im System. Alle CPUs auf einem Sockel bilden eine sg. Die NUMA-Domäne in der obigen Abbildung besteht aus 2 sgs. Eine Cross-NUMA-Prozessmigration kann nur durchgeführt werden, wenn ein großes Ungleichgewicht zwischen den NUMA-SGS besteht (und das Ungleichgewicht liegt hier auf der SG-Ebene, d. h. die Summe aller CPU-Lasten auf der SG muss mit einer anderen SG unausgeglichen sein) (da eine Cross-NUMA-Migration zu einem Wärmeverlust des gesamten Cache von L1, L2 und L3 führt und mehr Cross-NUMA-Speicherzugriffe verursachen kann, sodass hier Vorsicht geboten ist).

Aus der obigen Einführung können wir ersehen, dass sich der Kernel durch die Zusammenarbeit von sched_domain und sched_group an verschiedene physische Topologien anpassen kann (ob Hyperthreading aktiviert ist, ob NUMA aktiviert ist) und CPU-Ressourcen effizient nutzen kann.

smp_init

/*
 * Wird vom Bootprozessor aufgerufen, um den Rest zu aktivieren.
 *
 * In der SMP-Architektur muss BSP alle anderen Nicht-Boot-CPs hochfahren
 */
void __init smp_init(void)
{
    int Anzahl Knoten, Anzahl CPUs;
    vorzeichenlose Ganzzahl CPU;
 
    /* Für jede CPU einen inaktiven Thread erstellen */
    idle_threads_init();
    /* CPUHP-Thread beim Kernel registrieren */
    cpuhp_threads_init();
 
    pr_info("Sekundäre CPUs werden hochgefahren ...\n");
 
    /*
     * FIXME: Dies sollte im Userspace erfolgen --RR
     *
     * Wenn die CPU nicht online ist, rufen Sie sie mit cpu_up auf
     */
    für_jede_vorhandene_CPU(CPU) {
        wenn (Anzahl_Online-CPUs() >= Setup_Max_CPUs)
            brechen;
        wenn (!cpu_online(cpu))
            cpu_up(Zentrale CPU);
    }
     
    .............
}

Bevor Sie mit der Initialisierung der Planungsdomäne sched_init_smp tatsächlich beginnen, müssen Sie alle Nicht-Boot-CPUs hochfahren, um sicherzustellen, dass diese CPUs bereit sind. Anschließend können Sie mit der Initialisierung der Multi-Core-Planungsdomäne beginnen.

sched_init_smp

Schauen wir uns dann die spezifische Codeimplementierung der Multi-Core-Planungsinitialisierung an (wenn CONFIG_SMP nicht konfiguriert ist, wird die entsprechende Implementierung hier nicht ausgeführt).

sched_init_numa

Mit sched_init_numa() wird festgestellt, ob das System NUMA ist. Wenn ja, müssen NUMA-Domänen dynamisch hinzugefügt werden.

/*
 * Topologieliste, Bottom-Up.
 *
 * Standardmäßige physikalische Topologie unter Linux *
 * Hier gibt es nur drei Ebenen der physischen Topologie, und die NUMA-Domäne wird in sched_init_numa() automatisch erkannt. * Wenn eine NUMA-Domäne vorhanden ist, wird die entsprechende NUMA-Planungsdomäne hinzugefügt. *
 * Hinweis: Die standardmäßige Planungsdomäne default_topology kann einige Probleme aufweisen. Beispielsweise haben einige Plattformen keine DIE-Domänen (Intel-Plattformen), sodass sich LLC- und DIE-Domänen überschneiden können. * Nachdem die Planungsdomäne eingerichtet wurde, durchsucht der Kernel daher alle Planungen in cpu_attach_domain(). * Wenn es eine Planungsüberschneidung gibt, wird die überlappende Planungsdomäne entsprechend destroy_sched_domain*/ zerstört.
statische Struktur sched_domain_topology_level default_topology[] = {
#ifdef CONFIG_SCHED_SMT
    { cpu_smt_mask, cpu_smt_flags, SD_INIT_NAME(SMT) },
#endif
#ifdef CONFIG_SCHED_MC
    { CPU-Kerngruppenmaske, CPU-Kernflags, SD_INIT_NAME(MC) },
#endif
    { cpu_cpu_mask, SD_INIT_NAME(DIE) },
    { NULL, },
};

Standardmäßige physische Topologie unter Linux

/*
 * Initialisierung der NUMA-Planungsdomäne (Erstellen einer neuen physischen Topologiestruktur „sched_domain_topology“ basierend auf Hardwareinformationen)
 *
 * Der Kernel fügt die NUMA-Topologie standardmäßig nicht aktiv hinzu, Sie müssen sie konfigurieren (wenn NUMA aktiviert ist)
 * Wenn NUMA aktiviert ist, muss anhand der Hardwaretopologieinformationen bestimmt werden, ob die Domäne * sched_domain_topology_level hinzugefügt werden soll (erst nach dem Hinzufügen dieser Domäne erstellt der Kernel die NUMA-DOMÄNE, wenn * sched_domain später initialisiert wird).
 */
void sched_init_numa(void)
{
    ................
    /*
     * Hier prüfen wir, ob basierend auf der Entfernung eine NUMA-Domäne (auch mehrere NUMA-Domänen) vorhanden ist, und aktualisieren sie dann basierend auf der * Situation auf die physische Topologiestruktur. Wenn Sie später eine Planungsdomäne erstellen, verwenden Sie diese neue *physische Topologie, um eine neue Planungsdomäne zu erstellen*/
    für (j = 1; j < Ebene; i++, j++) {
        tl[i] = (Struktur sched_domain_topology_level){
            .mask = sd_numa_mask,
            .sd_flags = cpu_numa_flags,
            .flags = SDTL_OVERLAP,
            .numa_level = j,
            SD_INIT_NAME(NUMA)
        };
    }
 
    sched_domain_topology = tl;
 
    sched_domains_numa_levels = Ebene;
    sched_max_numa_distance = sched_domains_numa_distance[Ebene - 1];
 
    init_numa_topology_type();
}

Ermitteln Sie die physische Topologie des Systems. Wenn eine NUMA-Domäne vorhanden ist, fügen Sie sie zu sched_domain_topology hinzu. Anschließend wird die entsprechende Planungsdomäne basierend auf der physischen Topologie von sched_domain_topology eingerichtet.

sched_init_domains

Als nächstes analysieren wir die Funktion zur Erstellung von Planungsdomänen sched_init_domains

/*
 * Planerdomänen und -gruppen einrichten. Dies schließt vorerst nur isolierte
 * CPUs, könnte aber in Zukunft verwendet werden, um andere Sonderfälle auszuschließen.
 */
int sched_init_domains(const struct cpumask *cpu_map)
{
    int err;
 
    zalloc_cpumask_var(&sched_domains_tmpmask, GFP_KERNEL);
    zalloc_cpumask_var(&sched_domains_tmpmask2, GFP_KERNEL);
    zalloc_cpumask_var(&fallback_doms, GFP_KERNEL);
 
    arch_update_cpu_topology();
    ndoms_cur = 1;
    doms_cur = alloc_sched_domains(ndoms_cur);
    wenn (!doms_cur)
        doms_cur = &fallback_doms;
    /*
     * doms_cur[0] gibt die CPU-Maske an, die die Scheduling-Domäne überschreiben muss
     *
     * Wenn isolcpus= verwendet wird, um einige CPUs im System zu isolieren, werden diese CPUs nicht zur Planungsdomäne * hinzugefügt, d. h. diese CPUs nehmen nicht am Lastausgleich teil (der Lastausgleich umfasst hier DL/RT und CFS).
     * Hier wird „isolate“ von cpu_map und housekeeping_cpumask(HK_FLAG_DOMAIN) verwendet.
     * CPU wird entfernt, um sicherzustellen, dass die isolierte CPU nicht in der etablierten Planungsdomäne enthalten ist
     */
    cpumask_and(doms_cur[0], cpu_map, housekeeping_cpumask(HK_FLAG_DOMAIN));
    /* Implementierungsfunktion zum Einrichten der Planungsdomäne*/
    Fehler = build_sched_domains(doms_cur[0], NULL);
    register_sched_domain_sysctl();
 
    Rückgabefehler;
}
/*
 * Erstellen Sie Sched-Domänen für eine bestimmte Anzahl von CPUs und hängen Sie die Sched-Domänen an
 * zu den einzelnen CPUs
 */
statische int
build_sched_domains(const struct cpumask *cpu_map, struct sched_domain_attr *attr)
{
    Aufzählung s_alloc alloc_state = sa_none;
    Struktur sched_domain *sd;
    Struktur s_data d;
    Struktur rq *rq = NULL;
    : Int i, ret = -ENOMEM;
    Struktur sched_domain_topology_level *tl_asym;
    bool has_asym = falsch;
 
    wenn (WARN_ON(cpumask_empty(cpu_map)))
        gehe zu Fehler;
 
    /*
     * Die meisten Prozesse in Linux werden von CFS geplant, sodass sched_domain in CFS häufig aufgerufen und geändert wird (z. B. nohz_idle und verschiedene Statistiken in sched_domain). Daher wird sched_domain
     * Beim Design muss der Effizienz Priorität eingeräumt werden, daher verwendet der Kernel die Percpu-Methode, um sched_domain zu implementieren
     * Jede SD-Ebene zwischen CPUs ist eine unabhängig angewendete Percpu-Variable, sodass die Eigenschaften von Percpu verwendet werden können, um das Problem der Parallelitätskonkurrenz zwischen ihnen zu lösen * (1. Kein Sperrschutz erforderlich. 2. Keine Cacheline-Pseudofreigabe).
     */
    alloc_state = __visit_domain_allocation_hell(&d, cpu_map);
    wenn (alloc_state != sa_rootdomain)
        gehe zu Fehler;
 
    tl_asym = asym_cpu_capacity_level(cpu_map);
 
    /*
     * Richten Sie Domänen für die durch die CPU-Map angegebenen CPUs ein:
     *
     * Hier werden wir alle CPUs in cpu_map durchlaufen und entsprechende physische Topologiestrukturen für diese CPUs erstellen (
     * mehrstufige Planungsdomäne von for_each_sd_topology).
     *
     * Wenn die Planungsdomäne eingerichtet ist, wird der der CPU in der Planungsdomäne auf dieser Ebene entsprechende Bereich über tl->mask(cpu) abgerufen (dh die CPU und andere entsprechende CPUs bilden diese Planungsdomäne). Die der CPU in derselben Planungsdomäne entsprechende SD wird zu Beginn auf dieselbe initialisiert (einschließlich sd->pan,
     * sd->imbalance_pct und sd->flags).
     */
    für_jede_CPU(i, CPU-Map) {
        Struktur sched_domain_topology_level *tl;
 
        sd = NULL;
        für_jede_SD-Topologie(tl) {
            int dflags = 0;
 
            wenn (tl == tl_asym) {
                dflags |= SD_ASYM_CPUCAPACITY;
                hat_asym = wahr;
            }
 
            sd = build_sched_domain(tl, cpu_map, attr, sd, dflags, i);
 
            wenn (tl == sched_domain_topology)
                *pro_cpu_ptr(d.sd, i) = sd;
            wenn (tl->flags & SDTL_OVERLAP)
                sd->flags |= SD_OVERLAP;
            wenn (cpumask_equal(cpu_map, sched_domain_span(sd)))
                brechen;
        }
    }
 
    /*
     * Erstellen Sie die Gruppen für die Domänen
     *
     * Erstellen Sie eine Dispatch-Gruppe *
     * Die Rolle von sched_group können wir anhand der Implementierung von zwei Planungsdomänen erkennen: * 1. NUMA-Domäne 2. LLC-Domäne *
     * numa sched_domain->span umfasst alle CPUs in der NUMA-Domäne. Wenn ein Ausgleich erforderlich ist, * sollten NUMA-Domänen nicht auf CPUs basieren, sondern auf Sockets, d. h. nur Socket1 und Socket2
     * Die CPU wird nur dann zwischen den beiden Sockeln migriert, wenn ein extremes Ungleichgewicht besteht. Wenn sched_domain zur Implementierung dieser * Abstraktion verwendet wird, führt dies zu unzureichender Flexibilität (wie in der MC-Domäne unten zu sehen ist). Daher verwendet der Kernel sched_group zur * Darstellung eines CPU-Satzes und jeder Socket gehört zu einer sched_group. Eine Migration ist nur zulässig, wenn die beiden Sched_Groups nicht im Gleichgewicht sind*
     * Die MC-Domäne ist ähnlich. Die CPU kann zwar Hyperthreading haben, aber die Leistung des Hyperthreadings entspricht nicht der des physischen Kerns. Ein Paar* Hyperthreads entspricht ungefähr der 1,2-fachen Leistung eines physischen Kerns. Daher müssen wir beim Planen das Gleichgewicht zwischen Hyperthreading*-Paaren berücksichtigen, das heißt, wir müssen zuerst das Gleichgewicht zwischen den CPUs und dann das Gleichgewicht des Hyperthreadings innerhalb der CPU herstellen. Zu diesem Zeitpunkt wird sched_group zur Abstraktion verwendet. Eine sched_group repräsentiert eine physische CPU (zwei Hyperthreads). Zu diesem Zeitpunkt stellt LLC das Gleichgewicht zwischen den CPUs sicher und vermeidet so eine extreme Situation: das Gleichgewicht zwischen den Hyperthreads, aber das Ungleichgewicht auf den physischen Kernen. Gleichzeitig kann sichergestellt werden, dass der Kernel beim Planen und Auswählen von Kernen den physischen Threads Vorrang einräumt. Erst wenn die physischen Threads aufgebraucht sind, wird die Verwendung eines anderen Hyperthreads in Betracht gezogen, sodass das System die Rechenleistung der CPU besser nutzen kann.
    für_jede_CPU(i, CPU-Map) {
        für (sd = *per_cpu_ptr(d.sd, i); sd; sd = sd->parent) {
            sd->span_weight = cpumask_weight(sched_domain_span(sd));
            wenn (sd->flags & SD_OVERLAP) {
                wenn (build_overlap_sched_groups(sd, i))
                    gehe zu Fehler;
            } anders {
                wenn (build_sched_groups(sd, i))
                    gehe zu Fehler;
            }
        }
    }
 
    /*
     * Berechnen Sie die CPU-Kapazität für physische Pakete und Knoten
     *
     * sched_group_capacity wird verwendet, um die für sg verfügbare CPU-Leistung anzugeben*
     * sched_group_capacity berücksichtigt die unterschiedliche Rechenleistung der einzelnen CPUs (unterschiedliche Maximalfrequenzeinstellungen,
     * ARM große und kleine Kerne usw.), entfernen Sie die vom RT-Prozess verwendete CPU (sg ist für CFS vorbereitet, daher ist es erforderlich, * die vom DL/RT-Prozess auf der CPU verwendete CPU-Rechenleistung zu entfernen) usw., sodass die verfügbare Rechenleistung für CFS sg übrig bleibt (weil * beim Lastausgleich nicht nur die Belastung der CPU berücksichtigt werden sollte, sondern auch das CFS auf diesem sg
     * Verfügbare Rechenleistung. Wenn es auf dieser SG weniger Prozesse gibt, aber die sched_group_capacity auch klein ist, sollten Prozesse nicht auf diese SG migriert werden)
     */
    für (i = nr_cpumask_bits-1; i >= 0; i--) {
        wenn (!cpumask_test_cpu(i, cpu_map))
            weitermachen;
 
        für (sd = *per_cpu_ptr(d.sd, i); sd; sd = sd->parent) {
            Anspruchszuweisungen(i, sd);
            init_sched_groups_capacity(i, sd);
        }
    }
 
    /* Domänen anhängen */
    rcu_read_lock();
    /*
     * Binden Sie das rq jeder CPU an rd (root_domain) und prüfen Sie, ob sd überlappt. * Wenn ja, wird destroy_sched_domain() verwendet, um es zu entfernen (so können wir sehen, dass * Intel-Server nur 3 Planungsdomänen haben und die DIE-Domäne sich tatsächlich mit der LLC-Domäne überschneidet, also wird sie hier entfernt)
     */
    für_jede_CPU(i, CPU-Map) {
        rq = cpu_rq(i);
        sd = *per_cpu_ptr(d.sd, i);
 
        /* Verwenden Sie READ_ONCE()/WRITE_ONCE(), um Lade-/Speicherfehler zu vermeiden: */
        wenn (rq->cpu_capacity_orig > EINMALIGES LESEN(d.rd->max_cpu_capacity))
            EINMALIGE SCHREIBUNG(d.rd->max_cpu_capacity, rq->cpu_capacity_orig);
 
        cpu_attach_domain(sd, d.rd, i);
    }
    rcu_read_unlock();
 
    wenn (hat_asym)
        static_branch_inc_cpuslocked(&sched_asym_cpucapacity);
 
    wenn (rq && sched_debug_enabled) {
        pr_info("Stammdomänenspanne: %*pbl (max. CPU-Kapazität = %lu)\n",
            cpumask_pr_args(cpu_map), rq->rd->max_cpu_capacity);
    }
 
    ret = 0;
Fehler:
    __free_domain_allocs(&d, alloc_state, cpu_map);
 
    Rückkehr ret;
}

Bisher haben wir die Kernel-Planungsdomäne erstellt, und CFS kann sched_domain verwenden, um einen Lastenausgleich zwischen mehreren Kernen zu erreichen.

V. Fazit

Dieser Artikel stellt hauptsächlich die grundlegenden Konzepte des Kernel-Schedulers vor und führt durch die Analyse des Initialisierungscodes des Schedulers im 5.4-Kernel in die spezifische Implementierung grundlegender Konzepte wie Planungsdomänen und Planungsgruppen ein. Insgesamt weist der 5.4-Kernel im Vergleich zum 3.x-Kernel keine wesentlichen Änderungen in der Initialisierungslogik des Schedulers und im grundlegenden Design (Konzepte/Schlüsselstrukturen) im Zusammenhang mit dem Scheduler auf, was indirekt auch die „Stabilität“ und „Eleganz“ des Kernel-Scheduler-Designs bestätigt.

Oben finden Sie den detaillierten Inhalt der Analyse der Initialisierung des Quellcodes des Linux-Kernel-Schedulers. Weitere Informationen zur Initialisierung des Quellcodes des Linux-Kernel-Schedulers finden Sie in den anderen verwandten Artikeln auf 123WORDPRESS.COM!

Das könnte Sie auch interessieren:
  • Einführung in die Containerfunktion of() in der Linux-Kernel-Programmierung
  • Analysieren Sie die Kompilierung und das Brennen des Linux-Kernels und des Gerätebaums
  • Analyse der Verwendung des statischen Speichers des Hongmeng Light-Kernels
  • Detaillierte Erklärung der Kernel-Thread-Theorie und Beispiele in Java
  • Ein Artikel zeigt Ihnen, wie Sie Kernel in der Sprache C schreiben

<<:  Detaillierte Erläuterung des verschachtelten Routings im Vue-Router

>>:  Dreißig HTML-Codierungsrichtlinien für Anfänger

Artikel empfehlen

Website-Homepage-Design im Illustrationsstil Neuer Trend im Website-Design

Sie können sehen, dass ihre visuellen Effekte sehr...

Beispiel für einen Persistenzbetrieb mit Gearman + MySQL

Dieser Artikel verwendet die Gearman+MySQL-Method...

Detaillierte Erklärung des Nginx Reverse-Proxy-Beispiels

1. Reverse-Proxy-Beispiel 1 1. Erzielen Sie den E...

Implementierung eines Docker-Cross-Host-Netzwerks (manuell)

1. Einführung in Macvlan Vor dem Aufkommen von Ma...

Lösen Sie das Problem des IDEA-Konfigurations-Tomcat-Startfehlers

Beim Konfigurieren unterschiedlicher Servlet-Pfad...

Implementierung eines statischen Website-Layouts im Docker-Container

Serverplatzierung Es wird empfohlen, Cloud-Server...

Wann sollte eine Website Anzeigen schalten?

Als ich vor kurzem mit einem Internet-Veteranen ü...