Segmentierungsfehler (Zuweisung des Computerspeichers)

Ursprünglicher Autor: Julien Pauli
  • Übersetzung


Wenn ich im Code einen Fehler mache, führt dies normalerweise zu einer "Segmentierungsfehlermeldung", die oft zu "Segfault" verkürzt wird. Und dann kamen meine Kollegen und das Management zu mir: „Ha! Wir müssen einen Segfault beheben! “-„ Ja, ich bin schuld “, antworte ich normalerweise. Aber wie viele von Ihnen wissen, was der "Segmentierungsfehler" tatsächlich bedeutet?

Um diese Frage zu beantworten, müssen wir in die fernen 1960er Jahre zurückkehren. Ich möchte erklären, wie ein Computer funktioniert oder wie moderne Computer auf Speicher zugreifen. Dies hilft zu verstehen, woher diese seltsame Fehlermeldung kommt.

Alle folgenden Informationen sind die Grundlagen der Computerarchitektur. Und ohne Notwendigkeit werde ich nicht tief in dieses Gebiet vordringen. Außerdem werde ich die bekannte Terminologie auf alle anwenden, sodass mein Beitrag für alle verständlich ist, die sich mit Computertechnologie nicht so gut auskennen. Wenn Sie sich eingehender mit dem Thema des Arbeitens mit dem Gedächtnis befassen möchten, können Sie auf die zahlreichen verfügbaren Literaturstellen verweisen. Vergessen Sie jedoch nicht, sich mit dem Quellcode des Kernels einiger Betriebssysteme, beispielsweise Linux, zu befassen. Ich werde hier nicht auf die Geschichte der Computertechnologie eingehen, einige Dinge werden nicht behandelt, andere sind stark vereinfacht.

Ein bisschen Geschichte


Es war einmal, dass Computer sehr groß waren, Tonnen wogen und gleichzeitig einen einzelnen Prozessor und einen Speicher von ungefähr 16 KB besaßen. Ein solches Monster kostete ungefähr 150.000 US-Dollar und konnte jeweils nur eine Aufgabe ausführen: Es wurde jeweils nur ein Vorgang ausgeführt. Die Speicherarchitektur in jenen Tagen kann schematisch wie folgt dargestellt werden:



Das heißt, das Betriebssystem machte beispielsweise ein Viertel des gesamten verfügbaren Speichers aus, und der Rest war für Benutzeraufgaben bestimmt. Zu dieser Zeit bestand die Aufgabe des Betriebssystems darin, die Hardware einfach über CPU-Interrupts zu steuern. Das Betriebssystem benötigte also einen eigenen Speicher, um Daten von Geräten zu kopieren und damit zu arbeiten ( PIO - Modus)) Um Daten auf dem Bildschirm anzuzeigen, musste ein Teil des Hauptspeichers verwendet werden, da das Video-Subsystem entweder keinen eigenen RAM hatte oder einige Kilobyte groß war. Und schon wurde das Programm selbst im Speicherbereich unmittelbar nach dem OS ausgeführt und seine Aufgaben gelöst.

Ressourcenfreigabe


Das Hauptproblem bestand darin, dass das Gerät, das 150.000 US-Dollar kostete, nur einen Task ausführte und tagelang mehrere Kilobyte Daten verarbeitete.

Aufgrund der enormen Kosten konnten es sich nur wenige Menschen leisten, mehrere Computer gleichzeitig zu kaufen, um mehrere Aufgaben gleichzeitig zu erledigen. Daher begannen die Menschen nach Wegen zu suchen, um den Zugriff auf die Rechenressourcen eines Computers zu teilen. So kam die Ära des Multitasking. Bitte beachten Sie, dass in jenen Tagen niemand über Multiprozessor-Computer nachdachte. Wie kann man also einen Computer mit einer CPU veranlassen, mehrere verschiedene Aufgaben auszuführen?

Die Lösung bestand in der Verwendung des Task-Schedulers (Scheduling): Während ein Prozess unterbrochen wurde und auf den Abschluss der E / A-Vorgänge wartete, konnte die CPU einen anderen Prozess ausführen. Ich werde mich hier nicht mehr mit dem Taskplaner befassen, da dies ein zu umfangreiches Thema ist, das nicht mit dem Speicher zusammenhängt.

Wenn der Computer mehrere Aufgaben gleichzeitig ausführen kann, sieht die Speicherzuordnung folgendermaßen aus: Die



Aufgaben A und B werden im Speicher abgelegt, da das Kopieren auf die Festplatte und umgekehrt zu teuer ist. Wenn der Prozessor eine bestimmte Aufgabe ausführt, greift er auf den Speicher für relevante Daten zu. Es gibt aber ein Problem.

Wenn ein Programmierer Code schreibt, um Aufgabe B abzuschließen, muss er die Grenzen der zugewiesenen Speichersegmente kennen. Angenommen, Task B belegt 10 bis 12 KB Speicher, dann muss jede Speicheradresse innerhalb dieser Grenzen fest codiert werden. Wenn der Computer jedoch drei Aufgaben gleichzeitig ausführt, wird der Speicher in eine größere Anzahl von Segmenten aufgeteilt, was bedeutet, dass das Segment für Aufgabe B verschoben werden kann. Dann muss der Programmcode neu geschrieben werden, damit er mit weniger Speicher arbeiten und alle Zeiger ändern kann.

Hier taucht ein weiteres Problem auf: Was passiert, wenn Task B auf das für Task A zugewiesene Speichersegment zugreift? Dies kann leicht passieren, da es bei der Arbeit mit Speicherzeigern ausreicht, einen kleinen Fehler zu machen, und das Programm auf eine völlig andere Adresse zugreift, wodurch die Datenintegrität eines anderen Prozesses verletzt wird. In diesem Fall kann Task A mit Daten arbeiten, die aus Sicherheitsgründen sehr wichtig sind. Es gibt keine Möglichkeit zu verhindern, dass B in den Speicherbereich A eindringt. Schließlich kann Task B aufgrund eines Programmierfehlers den Speicherbereich des Betriebssystems überschreiben (in diesem Fall von 0 bis 4 KB).

Adressraum


Damit Sie mehrere im Arbeitsspeicher abgelegte Aufgaben sicher ausführen können, benötigen wir Hilfe von Betriebssystem und Hardware. Insbesondere der Adressraum. Dies ist eine Art Speicherabstraktion, die vom Betriebssystem für einen bestimmten Prozess zugewiesen wird. Heute ist es ein grundlegendes Konzept, das überall angewendet wird. Zumindest ALLE Computer für zivile Zwecke haben genau diesen Ansatz gewählt, und das Militär hat möglicherweise seine eigenen Geheimnisse. Personal, Smartphones, Fernseher, Spielekonsolen, Smartwatches, Geldautomaten - stecken Sie in jedes Gerät, und es stellt sich heraus, dass der Speicher darin nach dem „Code-Stack-Heap“ -Prinzip zugewiesen ist.

Der Adressraum enthält alles, was Sie zum Abschließen des Vorgangs benötigen:

  • Maschinenanweisungen, denen die CPU folgen muss.
  • Die Daten, mit denen diese Maschinenanweisungen arbeiten.

Schematisch ist der Adressraum wie folgt aufgeteilt:



  • Der Stack ist ein Speicherbereich, in dem das Programm Informationen über die aufgerufenen Funktionen, ihre Argumente und jede lokale Variable in den Funktionen speichert. Die Größe des Bereichs kann sich während der Ausführung des Programms ändern. Wenn Funktionen aufgerufen werden, wächst der Stapel und nimmt ab, wenn er abgeschlossen ist.
  • Ein Heap ist ein Speicherbereich, in dem ein Programm tun kann, was es will. Die Größe der Fläche kann variieren. Der Programmierer hat die Möglichkeit, einen Teil des Heapspeichers mit einer Funktion zu nutzen malloc(), und dieser Speicherbereich vergrößert sich dann. Die Rückgabe der Ressourcen erfolgt mit free(), wonach der Heap reduziert wird.
  • Ein Codesegment ist ein Speicherbereich, in dem die Maschinenbefehle eines kompilierten Programms gespeichert sind. Sie werden vom Compiler generiert, können aber auch manuell geschrieben werden. Bitte beachten Sie, dass dieser Speicherbereich auch in drei Teile (Text, Daten und BSS) unterteilt werden kann. Dieser Speicherbereich hat eine feste Größe, die vom Compiler festgelegt wird. In unserem Beispiel sei es 1 Kb.

Da Stapel und Heap unterschiedlich groß sein können, befinden sie sich in entgegengesetzten Teilen des gemeinsamen Adressraums. Anweisungen zum Ändern der Größe werden durch Pfeile angezeigt. Es liegt in der Verantwortung des Betriebssystems, sicherzustellen, dass sich diese Bereiche nicht überschneiden.

Speichervirtualisierung


Angenommen, Task A verfügt über den gesamten verfügbaren Benutzerspeicher. Und hier tritt das Problem B auf. Wie soll es sein? Die Lösung wurde in der Virtualisierung gefunden .

Lassen Sie mich an eine der vorhergehenden Abbildungen erinnern, in denen sich A und B gleichzeitig im Speicher befinden:



Angenommen, A versucht, auf den Speicher in seinem eigenen Adressraum zuzugreifen, beispielsweise mit dem 11-KB-Index. Es ist sogar möglich, dass dies ihr eigener Stapel sein wird. In diesem Fall muss das Betriebssystem herausfinden, wie der Index 1500 nicht geladen werden soll, da er tatsächlich den Bereich der Aufgabe B anzeigen kann.

Tatsächlich ist der Adressraum, den jedes Programm als seinen Speicher betrachtet, ein virtueller Speicher . Fake. Und im Speicherbereich von Aufgabe A ist ein Index von 11 KB eine gefälschte Adresse. Das ist - die Adresse des virtuellen Speichers.

Jedes auf einem Computer ausgeführte Programm arbeitet mit gefälschtem (virtuellem) Speicher . Bei einigen Chips führt das Betriebssystem einen Trick aus, wenn es auf einen Speicherbereich zugreift. Dank der Virtualisierung kann kein Prozess auf Speicher zugreifen, der nicht dazu gehört: Task A passt nicht in den Speicher von Task B oder in das Betriebssystem. Gleichzeitig ist auf Benutzerebene dank des umfangreichen und komplexen Codes des Betriebssystemkerns alles absolut transparent.

Somit wird jeder Speicherzugriff vom Betriebssystem geregelt. Und dies sollte sehr effektiv erfolgen, um die Arbeit der verschiedenen laufenden Programme nicht zu verlangsamen. Die Effizienz wird von der Hardware, hauptsächlich der CPU und einigen Komponenten wie der MMU, bereitgestellt . Letzteres erschien in den frühen 1970er Jahren als separater Chip, und heutzutage sind MMUs direkt in den Prozessor integriert und werden von Betriebssystemen ausnahmslos verwendet.

Hier ist ein kleines C-Programm, das zeigt, wie man mit Speicheradressen arbeitet:

#include 
#include 
int main(int argc, char **argv)
{
    int v = 3;
    printf("Code is at %p \n", (void *)main);
    printf("Stack is at %p \n", (void *)&v);
    printf("Heap is at %p \n", malloc(8));
    return 0;
}

Auf meinem LP64 X86_64-Computer wird das folgende Ergebnis angezeigt:

Code is at 0x40054c
Stack is at 0x7ffe60a1465c
Heap is at 0x1ecf010

Wie bereits beschrieben, wird zuerst das Codesegment, dann der Heap und dann der Stack erstellt. Aber alle drei Adressen sind falsch. Eine Ganzzahlvariable mit dem Wert 3 wird überhaupt nicht unter der physischen Adresse 0x7ffe60a1465c gespeichert.Vergessen Sie niemals, dass alle Benutzerprogramme virtuelle Adressen manipulieren und auf der Kernel- oder Hardwaretreiberebene nur physische Kerneladressen zulässig sind.

Rufumleitung


Weiterleitung (Übersetzung, Übersetzung, Adressübersetzung) ist ein Begriff, der sich auf das Zuordnen einer virtuellen Adresse zu einer physischen Adresse bezieht. Die MMU macht das. Für jeden ausgeführten Prozess muss das Betriebssystem die Entsprechung aller virtuellen Adressen zu physischen Adressen speichern. Und das ist eine ziemlich schwierige Aufgabe. Tatsächlich muss das Betriebssystem bei jedem Aufruf den Speicher jedes Benutzerprozesses verwalten. Auf diese Weise wird die Alptraumrealität des physischen Gedächtnisses zu einer nützlichen, leistungsstarken und benutzerfreundlichen Abstraktion.

Schauen wir uns das genauer an.

Wenn der Prozess gestartet wird, reserviert das Betriebssystem eine feste Menge an physischem Speicher, auch wenn dieser 16 KB groß ist. Die Startadresse dieses Adressraums wird in einer speziellen Variablen gespeichert base. Und in der Variablenbounds Die Größe des zugewiesenen Speicherbereichs wird aufgezeichnet, in unserem Beispiel - 16 KB. Diese beiden Werte werden in jeder Prozesstabelle - PCB ( Process Control Block ) - aufgezeichnet .

Das ist also der virtuelle Adressraum:



Und das ist sein physisches Image: Das



Betriebssystem beschließt, einen Bereich von physischen Adressen von 4 bis 20 KB zuzuweisen, dh, der Wert base beträgt 4 KB und der Wert bounds 4 + 16 = 20 KB. Wenn ein Prozess zur Ausführung in die Warteschlange gestellt wird (ihm wird CPU-Zeit zugewiesen), liest das Betriebssystem die Werte beider Variablen von der Leiterplatte und kopiert sie in spezielle CPU-Register. Als nächstes startet der Prozess und versucht, beispielsweise auf eine virtuelle 2-KB-Adresse (in ihrem Heap) zuzugreifen. Zu dieser Adresse addiert die CPU den basevom OS empfangenen Wert . Daher ist die physikalische Adresse 2+ 4 = 6 Kb.

Physische Adresse = virtuelle Adresse + Basis

Wenn die resultierende physikalische Adresse (6 KB) aus den Grenzen des ausgewählten Bereichs (4-20 KB) herausgeschlagen wird, bedeutet dies, dass der Prozess versucht, auf Speicher zuzugreifen, der nicht dazu gehört. Dann löst die CPU eine Ausnahme aus und meldet das Betriebssystem, das diese Ausnahme verarbeitet. In diesem Fall signalisiert das System normalerweise eine Verletzung des Prozesses: SIGSEGV , Segmentierungsfehler. Dieses Signal unterbricht standardmäßig den Prozess (dies kann konfiguriert werden).

Speicherumverteilung


Wenn Task A aus der Ausführungswarteschlange ausgeschlossen ist, ist dies sogar noch besser. Dies bedeutet, dass der Scheduler aufgefordert wurde, eine andere Aufgabe auszuführen (z. B. B). Während B ausgeführt wird, kann das Betriebssystem den gesamten physischen Bereich von Task A neu verteilen. Während der Ausführung des Benutzerprozesses verliert das Betriebssystem häufig die Kontrolle über den Prozessor. Wenn der Prozess jedoch einen Systemaufruf ausführt, kehrt der Prozessor wieder zur Betriebssystemsteuerung zurück. Vor diesem Systemaufruf kann das Betriebssystem alles mit dem Speicher tun, einschließlich der vollständigen Neuverteilung des Adressraums des Prozesses auf eine andere physische Partition.

In unserem Beispiel ist dies ganz einfach: Das Betriebssystem verschiebt den 16-Kilobyte-Bereich auf einen anderen freien Speicherplatz der entsprechenden Größe und aktualisiert einfach die Werte der Variablen base und bounds für Task A. Wenn der Prozessor zu seiner Ausführung zurückkehrt, funktioniert der Weiterleitungsprozess weiterhin, der physische Adressraum ist jedoch bereits vorhanden hat sich geändert.

Aus Sicht von Aufgabe A ändert sich nichts, der eigene Adressraum befindet sich noch im Bereich von 0-16 Kb. Gleichzeitig steuern das Betriebssystem und die MMU jeden Speicherzugriff der Task vollständig. Das heißt, der Programmierer manipuliert den virtuellen Bereich von 0 bis 16 KB, und die MMU übernimmt die Zuordnung mit den physikalischen Adressen.

Nach der Neuverteilung sieht das Speicherbild folgendermaßen aus:



Der Programmierer muss sich keine Gedanken mehr darüber machen, mit welchen Speicheradressen sein Programm arbeiten wird, und er muss sich keine Gedanken mehr über Konflikte machen. Das Betriebssystem in Verbindung mit MMU beseitigt all diese Sorgen.

Speichersegmentierung


In den vorherigen Kapiteln haben wir uns mit Speicherweiterleitung und Neuzuweisung befasst. Unser Modell der Arbeit mit dem Gedächtnis hat jedoch einige Nachteile:

  • Wir gehen davon aus, dass jeder virtuelle Adressraum 16 KB groß ist. Das hat nichts mit der Realität zu tun.
  • Das Betriebssystem muss eine Liste mit 16 KB freien physischen Speicherbereichen führen, um diese für neue Startvorgänge oder die Neuverteilung der aktuell zugewiesenen Bereiche zuzuweisen. Wie kann dies alles effektiv durchgeführt werden, ohne die Leistung des gesamten Systems zu beeinträchtigen?
  • Wir weisen jedem Prozess 16 KB zu, aber es ist keine Tatsache, dass jeder von ihnen den gesamten ausgewählten Bereich verwendet. Wir verlieren einfach viel Gedächtnis von Grund auf. Dies wird als interne Fragmentierung bezeichnet. Der Speicher wird reserviert, aber nicht verwendet.


Betrachten wir zur Lösung einiger dieser Probleme ein komplexeres System zur Organisation des Gedächtnisses - die Segmentierung. Ihre Bedeutung ist einfach: Das Prinzip „Basis und Grenzen“ erstreckt sich auf alle drei Speichersegmente - Heap, Codesegment und Stack - und für jeden Prozess, anstatt das Speicherabbild als eine einzige Einheit zu betrachten.

Infolgedessen verlieren wir keinen Speicher mehr zwischen dem Stapel und dem Heap:



Wie Sie vielleicht bemerkt haben, wird der freie Speicherplatz im virtuellen Speicher von Task A nicht mehr im physischen Speicher zugewiesen. Und der Speicher wird jetzt viel effizienter genutzt. O muss nun für jede Aufgabe drei Paare speichern base und bounds, eine für jedes Segment. Die MMU ist nach wie vor an der Umleitung beteiligt, arbeitet jedoch mit drei und drei .base
bounds

Angenommen, für die Aufgabe A heap base beträgt der Parameter 126 KB und die Grenze 2 KB. Lassen Sie Aufgabe A auf die virtuelle Adresse von 3 KB (auf dem Heap) zugreifen. Dann ist die physikalische Adresse definiert als 3 - 2 Kb (der Beginn des Heap) = 1 Kb + 126 Kb (Verschiebung) = 127 Kb. Dies ist weniger als 128, was bedeutet, dass kein Konvertierungsfehler auftritt.

Segmentfreigabe


Die Segmentierung des physischen Speichers ermöglicht nicht nur, dass der virtuelle Speicher den physischen Speicher beansprucht, sondern ermöglicht es auch, physische Segmente unter Verwendung virtueller Adressräume verschiedener Prozesse gemeinsam zu nutzen.

Wenn Sie Task A zweimal ausführen, ist das Codesegment dasselbe: In beiden Tasks werden dieselben Maschinenanweisungen ausgeführt. Gleichzeitig verfügt jede Task über einen eigenen Stack und Heap, da sie mit unterschiedlichen Datensätzen arbeiten.



Gleichzeitig vermuten beide Prozesse nicht, dass sie ihr Gedächtnis mit jemandem teilen. Dieser Ansatz wurde durch die Einführung von Segmentschutzbits ermöglicht.

Für jedes erstellte physische Segment registriert das Betriebssystem einen Wertboundsdie von der MMU für die spätere Weiterleitung verwendet wird. Gleichzeitig wird aber auch das sogenannte Permission Flag registriert.

Da der Code selbst nicht geändert werden kann, werden alle Codesegmente mit RX-Flags erstellt. Dies bedeutet, dass der Prozess diesen Speicherbereich für die nachfolgende Ausführung laden kann, aber niemand darauf schreiben kann. Die anderen beiden Segmente - der Heap und der Stack - haben RW-Flags, dh der Prozess kann in diese beiden Segmente lesen und schreiben, Sie können jedoch keinen Code von ihnen ausführen. Dies diente der Gewährleistung der Sicherheit, damit ein Angreifer den Heap oder den Stack nicht beschädigen konnte, indem er Code einfügte, um Root-Rechte zu erhalten. Dies war nicht immer der Fall, und für die hohe Effizienz dieser Lösung ist Hardware-Support erforderlich. Auf Intel-Prozessoren wird dies als " NX-Bit " bezeichnet.

Flags können während der Programmausführung geändert werden, hierfür wird mprotect () verwendet .

Unter Linux können alle diese Speichersegmente mit den Dienstprogrammen / proc / {pid} / maps oder / usr / bin / pmap angezeigt werden .

Hier ist ein Beispiel in PHP:

$ pmap -x 31329
0000000000400000   10300    2004       0 r-x--  php
000000000100e000     832     460      76 rw---  php
00000000010de000     148      72      72 rw---    [ anon ]
000000000197a000    2784    2696    2696 rw---    [ anon ]
00007ff772bc4000      12      12       0 r-x--  libuuid.so.0.0.0
00007ff772bc7000    1020       0       0 -----  libuuid.so.0.0.0
00007ff772cc6000       4       4       4 rw---  libuuid.so.0.0.0
... ...

Es gibt alle notwendigen Details bezüglich der Speicherzuordnung. Virtuelle Adressen, Berechtigungen für jeden Speicherbereich werden angezeigt. Jedes gemeinsam genutzte Objekt (.so) befindet sich in Form mehrerer Teile (normalerweise Code und Daten) im Adressraum. Codesegmente sind ausführbar und werden im physischen Speicher von allen Prozessen gemeinsam genutzt, die ein solches gemeinsam genutztes Objekt in ihrem Adressraum platziert haben.

Shared Objects ist einer der größten Vorteile von Unix- und Linux-Systemen, der Speicherplatz spart.

Mit dem Systemaufruf mmap () können Sie auch einen gemeinsam genutzten Bereich erstellen, der in ein gemeinsam genutztes physisches Segment konvertiert wird. Dann hat jeder Bereich einen Index s, was "geteilt" bedeutet.

Segmentierungsbeschränkungen


Durch die Segmentierung konnten wir das Problem des nicht verwendeten virtuellen Speichers lösen. Wenn es nicht verwendet wird, befindet es sich aufgrund der Verwendung von Segmenten, die genau der verwendeten Speichermenge entsprechen, nicht im physischen Speicher.

Dies ist jedoch nicht ganz richtig.

Angenommen, ein Prozess hat 16 KB von einem Heap angefordert. Höchstwahrscheinlich erstellt das Betriebssystem ein Segment der entsprechenden Größe im physischen Speicher. Wenn der Benutzer dann 2 KB davon freigibt, muss das Betriebssystem die Segmentgröße auf 14 KB reduzieren. Aber was ist, wenn der Programmierer dann den Haufen nach weiteren 30 Kb fragt? Dann muss das vorherige Segment mehr als verdoppelt werden, und ist es möglich, dies zu tun? Vielleicht ist er bereits von anderen Segmenten umgeben, die ihm nicht erlauben zu wachsen. Dann muss das Betriebssystem nach freiem Speicherplatz bei 30 KB suchen und das Segment neu verteilen.



Der Hauptnachteil von Segmenten besteht darin, dass der physische Speicher aufgrund dieser Segmente stark fragmentiert ist, da sich die Segmente vergrößern und verkleinern, wenn der Benutzer Anforderungen verarbeitet, und Speicher freigibt. Und das Betriebssystem muss eine Liste der freien Sites führen und diese verwalten.

Fragmentierung kann dazu führen, dass ein Prozess so viel Speicher benötigt, dass er größer ist als alle freien Abschnitte. In diesem Fall wird das Betriebssystem hat den Prozess der Speicherzuweisung zu verweigern, selbst wenn die Gesamtmenge an freien Flächen deutlich mehr sein.

Das Betriebssystem versucht möglicherweise, die Daten kompakter zu platzieren, indem alle freien Bereiche zu einem großen Block zusammengefasst werden, der später für die Anforderungen neuer Prozesse und für die Neuverteilung verwendet werden kann.



Derartige Optimierungsalgorithmen belasten den Prozessor jedoch stark, und dennoch wird seine Leistung benötigt, um Benutzerprozesse auszuführen. Wenn das Betriebssystem beginnt, den physischen Speicher neu zu organisieren, kann nicht mehr auf das System zugegriffen werden.

Die Speichersegmentierung bringt daher viele Probleme mit sich, die mit der Speicherverwaltung und dem Multitasking zusammenhängen. Wir müssen die Segmentierungsfunktionen irgendwie verbessern und die Fehler beheben. Dies wird mit einem anderen Ansatz erreicht - virtuellen Speicherseiten.

Paginierung


Wie oben erwähnt, besteht der Hauptnachteil der Segmentierung darin, dass Segmente sehr häufig ihre Größe ändern, und dies führt zu einer Speicherfragmentierung, was zu einer Situation führen kann, in der das Betriebssystem nicht die erforderlichen Speicherbereiche für Prozesse zuweist. Dieses Problem wird mit Hilfe von Seiten gelöst: Jede Zuordnung, die der Kernel im physischen Speicher vornimmt, hat eine feste Größe. Das heißt, Seiten sind Bereiche des physischen Speichers mit einer festen Größe, nichts weiter. Dies vereinfacht die Verwaltung des freien Volumens erheblich und beseitigt die Fragmentierung.

Schauen wir uns ein Beispiel an: Ein virtueller Adressraum von 16 KB ist paginiert.



Wir sprechen hier nicht von Heap, Stack oder Codesegmenten. Teilen Sie den Speicher einfach in Stücke von 4 KB auf. Dann machen wir dasselbe mit dem physischen Gedächtnis:



Das Betriebssystem speichert eine Prozessseitentabelle, die die Beziehung zwischen der Seite des virtuellen Prozessspeichers und der Seite des physischen Speichers (Seitenrahmen, Seitenrahmen) darstellt.



Jetzt haben wir das Problem der Suche nach freiem Speicherplatz behoben: Der Seitenrahmen wird entweder verwendet oder nicht (nicht verwendet). Und der Kernel ist kein Beispiel dafür, dass es einfacher ist, eine ausreichende Anzahl von Seiten zu finden, um eine Prozessanforderung für die Speicherzuweisung zu erfüllen.

Eine Seite ist die kleinste und unteilbare Speichereinheit, mit der das Betriebssystem arbeiten kann.

Jeder Prozess verfügt über eine eigene Seitentabelle, in der Weiterleitungen angezeigt werden. Hierbei werden nicht die Werte der Grenzen der Region bereits verwendet, sondern die Nummer der virtuellen Seite (VPN, virtuelle Seitennummer) und Verschiebung (Offset).

Beispiel: Die Größe des virtuellen Raums beträgt 16 KB, daher benötigen wir 14 Bits, um die Adressen zu beschreiben (2 14 = 16 KB). Die Seitengröße beträgt 4 KB, daher benötigen wir 4 KB (16/4), um die gewünschte Seite auszuwählen:



Wenn der Prozess beispielsweise die Adresse 9438 (außerhalb der Grenzen von 16 384) verwenden möchte, fragt er im Binärcode 10.0100.1101.1110:



Dies ist 1246- Byte in der virtuellen Seite Nummer 2 (Byte "0100.1101.1110" auf der "10" -ten Seite). Jetzt muss das Betriebssystem nur noch die Tabelle der Prozessseiten durchsehen, um diese Seitenzahl 2 zu ermitteln. In unserem Beispiel entspricht dies dem achttausendsten Byte des physischen Speichers. Daher entspricht die virtuelle Adresse 9438 der physischen Adresse 9442 (8000 + Offset 1246).

Wie bereits erwähnt, hat jeder Prozess nur eine Seitentabelle, da jeder Prozess seine eigene Umleitung sowie Segmente hat. Aber wo genau sind all diese Tabellen gespeichert? Wahrscheinlich im physischen Gedächtnis, wo sonst können sie sein?

Wenn die Seitentabellen selbst im Speicher gespeichert sind, müssen Sie auf den Speicher zugreifen, um ein VPN zu erhalten. Dann verdoppelt sich die Anzahl der Aufrufe: Zuerst extrahieren wir die Nummer der gewünschten Seite aus dem Speicher und beziehen uns dann auf die auf dieser Seite gespeicherten Daten. Und wenn die Speicherzugriffsgeschwindigkeit niedrig ist, sieht die Situation ziemlich traurig aus.

Schnellvorlaufpuffer (TLB, Translation Lookaside Buffer)


Die Verwendung von Seiten als primäres Tool zur Unterstützung des virtuellen Speichers kann zu schwerwiegenden Leistungseinbußen führen. Das Aufteilen des Adressraums in kleine Teile (Seiten) erfordert das Speichern einer großen Datenmenge über die Platzierung von Seiten. Und da diese Daten im Speicher gespeichert sind, wird jedes Mal, wenn der Prozess auf den Speicher zugreift, ein weiterer zusätzlicher Zugriff ausgeführt.

Die Geräteunterstützung wird erneut zur Aufrechterhaltung der Leistung verwendet. Wie bei der Segmentierung unterstützen wir den Kernel bei der effizienten Umleitung mithilfe von Hardwaremethoden. Verwenden Sie dazu den TLB, der Teil der MMU ist und ein einfacher Cache für einige VPN-Weiterleitungen ist. TLB ermöglicht es dem Betriebssystem, nicht erneut auf den Speicher zuzugreifen, um die physikalische Adresse von der virtuellen abzurufen.

Eine Hardware-MMU wird bei jedem Speicherzugriff ausgelöst, extrahiert ein VPN aus der virtuellen Adresse und fragt den TLB, ob die Weiterleitung von diesem VPN darin gespeichert ist. Wenn ja, dann ist seine Rolle erfüllt. Wenn nicht, findet die MMU die gewünschte Tabelle der Prozessseiten und aktualisiert die Daten im TLB, wenn sie auf eine gültige Adresse verweisen, damit sie beim nächsten Zugriff bereitgestellt werden.

Wie Sie wissen, verlangsamt dies den Speicherzugriff, wenn der Cache nicht über die erforderlichen Umleitungen verfügt. Es kann davon ausgegangen werden, dass die Wahrscheinlichkeit, dass die erforderlichen Daten im TLB erscheinen, umso größer ist, je größer die Seitengröße ist. Aber dann werden wir mehr Speicher auf jeder Seite ausgeben. Hier ist also ein Kompromiss erforderlich. Moderne Kerne können Seiten unterschiedlicher Größe verwenden. Zum Beispiel kann Linux mit „riesigen“ 2-MB-Seiten anstatt mit herkömmlichen 4-KB-Seiten arbeiten.

Es wird auch empfohlen, Daten kompakt in benachbarten Speicheradressen zu speichern. Wenn Sie sie im gesamten Speicher verteilen, wird die erforderliche Weiterleitung im TLB häufiger nicht erkannt oder ist ständig voll. Dies wird als räumliche Lokalitätseffizienz bezeichnet: Die Daten, die sich direkt hinter Ihren im Speicher befinden, können auf derselben physischen Seite abgelegt werden. Dank des TLB erhalten Sie dann einen Leistungsgewinn.

Darüber hinaus speichert der TLB in jedem Datensatz die sogenannte ASID (Address Space Identifier). Es ist so etwas wie eine PID, eine Prozesskennung. Jeder zur Ausführung anstehende Prozess verfügt über eine eigene ASID, und der TLB kann den Zugriff eines beliebigen Prozesses auf den Speicher steuern, ohne dass das Risiko eines fehlerhaften Zugriffs durch andere Prozesse besteht.

Wir wiederholen noch einmal: Wenn der Benutzerprozess versucht, auf die falsche Adresse zuzugreifen, fehlt diese wahrscheinlich im TLB. Daher wird der Suchvorgang in der Prozessseitentabelle gestartet. Es speichert die Anrufweiterleitung, jedoch mit dem falschen Satz von Bits. X86-basierte Umleitungssysteme haben eine Größe von 4 KB, dh sie enthalten viele Bits. Dies bedeutet, dass es eine Chance gibt, das richtige Bit sowie andere Dinge zu finden, wie z. B. ein Änderungsbit ("Dirty Bit", "Dirty Bit"), Schutzbits (Schutzbit), Zugriffsbit (Referenzbit) usw. Wenn der Datensatz als falsch markiert ist, verwendet das Betriebssystem standardmäßig SIGSEGV, was zu einem Segmentierungsfehler führt, auch wenn keine Frage nach Segmenten besteht.

In der Tat ist der Paging-Speicher in modernen Betriebssystemen viel komplizierter als ich. Insbesondere werden mehrstufige Einträge in Seitentabellen, mehrseitige Größen und das Entfernen von Seiten, auch als "Austausch" bezeichnet, verwendet (der Kernel löscht Seiten aus dem Speicher auf die Festplatte und umgekehrt, wodurch die Effizienz der Hauptspeicherauslastung gesteigert und die Illusion unbegrenzter Prozesse in Prozessen erzeugt wird )

Fazit


Jetzt wissen Sie, was sich hinter der Meldung "Segmentierungsfehler" verbirgt. Bisher verwendeten Betriebssysteme Segmente, um den virtuellen Speicherplatz im physischen Raum zu platzieren. Wenn ein Benutzerprozess auf den Speicher zugreifen möchte, fordert er die MMU auf, ihn umzuleiten. Wenn die empfangene Adresse jedoch fehlerhaft ist, sich außerhalb des physischen Segments befindet oder das Segment nicht über die erforderlichen Rechte verfügt (Versuch, in das Nur-Lese-Segment zu schreiben), sendet das Betriebssystem standardmäßig ein SIGSEGV-Signal, das den Vorgang unterbricht und die Meldung „Segmentierung“ anzeigt Schuld “. In einigen Betriebssystemen kann dies eine "allgemeine Schutzverletzung" sein. Sie können den Linux-Quellcode für x86 / 64-Plattformen untersuchen, der für Speicherzugriffsfehler verantwortlich ist , insbesondere für SIGSEGV. Sie können auch sehen, wie die Segmentierung auf dieser Plattform implementiert ist . Sie werden interessante Punkte zur Paginierung entdecken, die viel mehr Möglichkeiten bieten als bei der Verwendung klassischer Segmente.

Jetzt auch beliebt: