it-swarm.com.de

Warum hat der Aufrufstapel eine statische maximale Größe?

Nachdem ich mit einigen Programmiersprachen gearbeitet habe, habe ich mich immer gefragt, warum der Thread-Stack eine vordefinierte maximale Größe hat, anstatt bei Bedarf automatisch zu erweitern.

Im Vergleich dazu sind bestimmte sehr häufige Strukturen auf hoher Ebene (Listen, Karten usw.), die in den meisten Programmiersprachen zu finden sind, so konzipiert, dass sie nach Bedarf wachsen, während neue Elemente hinzugefügt werden. Die Größe wird nur durch den verfügbaren Speicher oder durch Rechengrenzen begrenzt ( zB 32-Bit-Adressierung).

Mir sind jedoch keine Programmiersprachen oder Laufzeitumgebungen bekannt, in denen die maximale Stapelgröße nicht durch eine Standard- oder Compileroption vorgegeben ist. Aus diesem Grund führt zu viel Rekursion sehr schnell zu einem allgegenwärtigen Stapelüberlauffehler/einer allgegenwärtigen Stapelüberlauf-Ausnahme, selbst wenn nur ein minimaler Prozentsatz des für einen Prozess verfügbaren Speichers für den Stapel verwendet wird.

Warum legen die meisten (wenn nicht alle) Laufzeitumgebungen eine maximale Grenze für die Größe fest, die ein Stapel zur Laufzeit vergrößern kann?

46
Lynn

Es ist möglich, ein Betriebssystem zu schreiben, für das keine Stapel im Adressraum zusammenhängend sein müssen. Grundsätzlich müssen Sie in der Aufrufkonvention etwas mehr herumspielen, um Folgendes sicherzustellen:

  1. wenn im aktuellen Stapelbereich nicht genügend Speicherplatz für die aufgerufene Funktion vorhanden ist, erstellen Sie einen neuen Stapelbereich und bewegen den Stapelzeiger so, dass er im Rahmen des Aufrufs auf den Anfang zeigt.

  2. wenn Sie von diesem Aufruf zurückkehren, kehren Sie zum ursprünglichen Stapelumfang zurück. Höchstwahrscheinlich behalten Sie die unter (1) erstellte für die zukünftige Verwendung durch denselben Thread bei. Im Prinzip könnten Sie es freigeben, aber auf diese Weise liegen ziemlich ineffiziente Fälle, in denen Sie in einer Schleife immer wieder über die Grenze hin und her springen und jeder Aufruf eine Speicherzuweisung erfordert.

  3. setjmp und longjmp oder was auch immer Ihr Betriebssystem für die nicht-lokale Übertragung der Kontrolle hat, sind aktiv und können bei Bedarf korrekt zum alten Stapelumfang zurückkehren.

Ich sage "Calling Convention" - um genau zu sein, ich denke, es wird wahrscheinlich am besten in einem Funktionsprolog und nicht vom Anrufer gemacht, aber meine Erinnerung daran ist verschwommen.

Der Grund, warum einige Sprachen eine feste Stapelgröße für einen Thread angeben, besteht darin, dass sie mit dem nativen Stapel auf Betriebssystemen arbeiten möchten, die nicht mach das. Wie die Antworten aller anderen sagen, müssen Sie unter der Annahme, dass jeder Stapel im Adressraum zusammenhängend sein muss und nicht verschoben werden kann, einen bestimmten Adressbereich für die Verwendung durch jeden Thread reservieren. Das bedeutet, eine Größe im Voraus zu wählen. Auch wenn Ihr Adressraum sehr groß ist und die von Ihnen gewählte Größe sehr groß ist, müssen Sie ihn auswählen, sobald Sie zwei Threads haben.

"Aha", sagen Sie, "was sind diese angeblichen Betriebssysteme, die nicht zusammenhängende Stapel verwenden? Ich wette, es ist ein obskures akademisches System, das für mich keinen Nutzen hat!". Nun, das ist eine andere Frage das wird zum Glück schon gestellt und beantwortet.

13
Steve Jessop

Diese Datenstrukturen haben normalerweise Eigenschaften, die der Betriebssystemstapel nicht hat:

  • Verknüpfte Listen benötigen keinen zusammenhängenden Adressraum. So können sie ein Stück Erinnerung hinzufügen, wo immer sie wollen, wenn sie wachsen.

  • Sogar Sammlungen, die zusammenhängenden Speicher benötigen, wie der Vektor von C++, haben einen Vorteil gegenüber Betriebssystemstapeln: Sie können alle Zeiger/Iteratoren bei jedem Wachstum für ungültig erklären. Andererseits muss der Betriebssystemstapel Zeiger auf den Stapel so lange gültig halten, bis die Funktion, zu deren Frame das Ziel gehört, zurückkehrt.

Eine Programmiersprache oder Laufzeit kann wählen, ob sie ihre eigenen Stapel implementieren möchten, die entweder nicht zusammenhängend oder beweglich sind, um die Einschränkungen der Betriebssystemstapel zu vermeiden. Golang verwendet solche benutzerdefinierten Stapel, um eine sehr hohe Anzahl von Co-Routinen zu unterstützen, die ursprünglich als nicht zusammenhängender Speicher und jetzt über bewegliche Stapel dank Zeigerverfolgung implementiert wurden (siehe Kommentar von hobb). Stapellose Python, Lua und Erlang verwenden möglicherweise auch benutzerdefinierte Stapel, aber das habe ich nicht bestätigt.

Auf 64-Bit-Systemen können Sie relativ große Stapel mit relativ geringen Kosten konfigurieren, da der Adressraum ausreichend ist und der physische Speicher nur dann zugewiesen wird, wenn Sie ihn tatsächlich verwenden.

36
CodesInChaos

In der Praxis ist es schwierig (und manchmal unmöglich), den Stapel zu vergrößern. Um zu verstehen, warum dies erforderlich ist, müssen Sie den virtuellen Speicher verstehen.

In den alten Tagen der Single-Thread-Anwendungen und des zusammenhängenden Speichers waren drei drei Komponenten eines Prozessadressraums: der Code, der Heap und der Stapel. Wie diese drei angeordnet waren, hing vom Betriebssystem ab, aber im Allgemeinen kam der Code zuerst, beginnend am unteren Ende des Speichers, der Heap kam als nächstes und wuchs nach oben, und der Stapel begann am oberen Rand des Speichers und wuchs nach unten. Es war auch etwas Speicher für das Betriebssystem reserviert, aber das können wir ignorieren. Programme hatten damals etwas dramatischere Stapelüberläufe: Der Stapel stürzte in den Heap ab, und je nachdem, welche zuerst aktualisiert wurden, arbeiteten Sie entweder mit fehlerhaften Daten oder kehrten von einer Unterroutine in einen beliebigen Teil des Speichers zurück.

Die Speicherverwaltung hat dieses Modell etwas geändert: Aus Sicht des Programms hatten Sie immer noch die drei Komponenten einer Prozessspeicherzuordnung, und sie waren im Allgemeinen auf die gleiche Weise organisiert, aber jetzt wurde jede der Komponenten als unabhängiges Segment verwaltet und die MMU signalisiert dem Betriebssystem, wenn das Programm versucht, auf Speicher außerhalb eines Segments zuzugreifen. Sobald Sie über virtuellen Speicher verfügten, war es nicht erforderlich oder gewünscht, einem Programm Zugriff auf seinen gesamten Adressraum zu gewähren Den Segmenten wurden also feste Grenzen zugewiesen.

Warum ist es nicht wünschenswert, einem Programm Zugriff auf seinen vollständigen Adressraum zu gewähren? Weil diese Erinnerung eine "Festschreibungsgebühr" gegen den Tausch darstellt; Zu jedem Zeitpunkt muss möglicherweise ein Teil oder der gesamte Speicher eines Programms geschrieben werden, um Platz für den Speicher eines anderen Programms zu schaffen. Wenn jedes Programm möglicherweise 2 GB Swap verbrauchen könnte, müssten Sie entweder genügend Swap für alle Ihre Programme bereitstellen oder die Chance nutzen, dass zwei Programme mehr benötigen, als sie bekommen könnten.

Unter der Annahme eines ausreichenden virtuellen Adressraums können Sie diese Segmente bei Bedarf könnten erweitern, und das Datensegment (Heap) wächst tatsächlich im Laufe der Zeit: Sie beginnen mit einem kleinen Datensegment und wann Der Speicherzuweiser fordert bei Bedarf mehr Speicherplatz an. Zu diesem Zeitpunkt wäre es mit einem einzelnen Stapel physikalisch möglich gewesen, das Stapelsegment zu erweitern: Das Betriebssystem könnte den Versuch abfangen, etwas außerhalb des Segments zu verschieben und mehr Speicher hinzuzufügen. Dies ist aber auch nicht besonders wünschenswert.

Geben Sie Multithreading ein. In diesem Fall hat jeder Thread ein unabhängiges Stapelsegment mit wiederum fester Größe. Jetzt werden die Segmente jedoch nacheinander im virtuellen Adressraum angeordnet, sodass es nicht möglich ist, ein Segment zu erweitern, ohne ein anderes zu verschieben. Dies ist nicht möglich, da das Programm möglicherweise Zeiger auf den im Stapel befindlichen Speicher enthält. Sie können alternativ etwas Platz zwischen den Segmenten lassen, aber dieser Platz würde in fast allen Fällen verschwendet. Ein besserer Ansatz bestand darin, den Anwendungsentwickler zu belasten: Wenn Sie wirklich tiefe Stapel benötigen, können Sie dies beim Erstellen des Threads angeben.

Mit einem virtuellen 64-Bit-Adressraum könnten wir heute effektiv unendliche Stapel für effektiv unendlich viele Threads erstellen. Aber auch das ist nicht besonders wünschenswert: In fast allen Fällen weist ein Stapelüberlauf auf einen Fehler mit Ihrem Code hin. Wenn Sie einen 1-GB-Stack bereitstellen, wird die Entdeckung dieses Fehlers einfach verzögert.

26
kdgregory

Der Stapel mit einer festen maximalen Größe ist nicht allgegenwärtig.

Es ist auch schwierig, das Richtige zu finden: Stapeltiefen folgen einer Potenzgesetzverteilung, was bedeutet, dass unabhängig davon, wie klein Sie die Stapelgröße machen, immer noch ein erheblicher Teil der Funktionen mit noch kleineren Stapeln vorhanden ist (Sie verschwenden also Platz). und egal wie groß Sie es machen, es wird immer noch Funktionen mit noch größeren Stapeln geben (so erzwingen Sie einen Stapelüberlauffehler für Funktionen, die keinen Fehler haben). Mit anderen Worten: Unabhängig von der gewählten Größe ist sie immer gleichzeitig zu klein und zu groß.

Sie können das erste Problem beheben, indem Sie zulassen, dass Stapel klein anfangen und dynamisch wachsen, aber dann haben Sie immer noch das zweite Problem. Und wenn Sie zulassen, dass der Stapel trotzdem dynamisch wächst, warum sollten Sie ihm dann eine willkürliche Grenze setzen?

Es gibt Systeme, in denen Stapel dynamisch wachsen können und keine maximale Größe haben: Erlang, Go, Smalltalk und Scheme. Es gibt viele Möglichkeiten, so etwas umzusetzen:

  • bewegliche Stapel: Wenn der zusammenhängende Stapel nicht mehr wachsen kann, weil etwas anderes im Weg ist, verschieben Sie ihn an einen anderen Speicherort mit mehr freiem Speicherplatz
  • nicht zusammenhängende Stapel: Anstatt den gesamten Stapel in einem einzigen zusammenhängenden Speicherbereich zuzuweisen, weisen Sie ihn mehreren Speicherbereichen zu
  • heap-zugewiesene Stapel: Anstatt separate Speicherbereiche für Stapel und Heap zu haben, weisen Sie einfach den Stapel auf dem Heap zu. Wie Sie selbst bemerkt haben, haben Heap-zugewiesene Datenstrukturen keine Probleme, bei Bedarf zu wachsen und zu schrumpfen
  • verwenden Sie überhaupt keine Stapel. Dies ist auch eine Option, z. Lassen Sie die Funktion eine Fortsetzung an den Angerufenen weitergeben, anstatt den Funktionsstatus in einem Stapel zu verfolgen

Sobald Sie über leistungsstarke nicht-lokale Kontrollflusskonstrukte verfügen, geht die Idee eines einzelnen zusammenhängenden Stapels ohnehin aus dem Fenster: Wiederaufnahmefähige Ausnahmen und Fortsetzungen "verzweigen" beispielsweise den Stapel, sodass Sie tatsächlich ein Netzwerk haben von Stapeln (zB mit einem Spaghetti-Stapel implementiert). Auch Systeme mit erstklassigen modifizierbaren Stapeln wie Smalltalk erfordern Spaghetti-Stapel oder ähnliches.

4
Jörg W Mittag

Das Betriebssystem muss einen zusammenhängenden Block angeben, wenn ein Stapel angefordert wird. Dies kann nur erreicht werden, wenn eine maximale Größe angegeben wird.

Angenommen, der Speicher sieht während der Anforderung so aus (X steht für verwendet, Os für nicht verwendet):

XOOOXOOXOOOOOX

Wenn eine Anforderung für eine Stapelgröße von 6 vorliegt, antwortet die Antwort des Betriebssystems mit Nein, auch wenn mehr als 6 verfügbar sind. Wenn Sie einen Stapel der Größe 3 anfordern, ist die Antwort des Betriebssystems einer der Bereiche mit 3 leeren Steckplätzen (Os) in einer Reihe.

Man kann auch die Schwierigkeit erkennen, Wachstum zuzulassen, wenn der nächste zusammenhängende Schlitz besetzt ist.

Die anderen Objekte, die erwähnt werden (Listen usw.), werden nicht auf dem Stapel abgelegt. Sie landen in nicht zusammenhängenden oder fragmentierten Bereichen auf dem Haufen. Wenn sie also wachsen, greifen sie nur nach Speicherplatz und benötigen keine zusammenhängenden Objekte, wie sie sind anders gehandhabt.

Die meisten Systeme legen einen angemessenen Wert für die Stapelgröße fest. Sie können diesen überschreiben, wenn der Thread erstellt wird, wenn eine größere Größe erforderlich ist.

1
Jon Raynor

Unter Linux ist dies lediglich eine Ressourcenbeschränkung, die vorhanden ist, um außer Kontrolle geratene Prozesse zu beenden, bevor sie schädliche Mengen der Ressource verbrauchen. Auf meinem Debian-System der folgende Code

#include <sys/resource.h>
#include <stdio.h>

int main() {
    struct rlimit limits;
    getrlimit(RLIMIT_STACK, &limits);
    printf("   soft limit = 0x%016lx\n", limits.rlim_cur);
    printf("   hard limit = 0x%016lx\n", limits.rlim_max);
    printf("RLIM_INFINITY = 0x%016lx\n", RLIM_INFINITY);
}

erzeugt die Ausgabe

   soft limit = 0x0000000000800000
   hard limit = 0xffffffffffffffff
RLIM_INFINITY = 0xffffffffffffffff

Beachten Sie, dass das harte Limit auf RLIM_INFINITY Gesetzt ist: Der Prozess kann sein weiches Limit auf any Betrag erhöhen. Solange der Programmierer keinen Grund zu der Annahme hat, dass das Programm wirklich ungewöhnlich viel Stapelspeicher benötigt, wird der Prozess abgebrochen, wenn er eine Stapelgröße von acht Mebibyte überschreitet.

Aufgrund dieser Begrenzung wird ein außer Kontrolle geratener Prozess (unbeabsichtigte unendliche Rekursion) lange Zeit abgebrochen, bevor er so viel Speicher verbraucht, dass das System gezwungen ist, mit dem Auslagern zu beginnen. Dies kann den Unterschied zwischen einem abgestürzten Prozess und einem abgestürzten Server ausmachen. Programme mit einem legitimen Bedarf an einem großen Stapel werden jedoch nicht eingeschränkt. Sie müssen lediglich das Soft-Limit auf einen geeigneten Wert festlegen.


Technisch gesehen wachsen Stapel dynamisch: Wenn das Soft-Limit auf acht Mebibyte festgelegt ist, bedeutet dies nicht, dass diese Speichermenge tatsächlich noch zugeordnet wurde. Dies wäre ziemlich verschwenderisch, da die meisten Programme niemals in die Nähe ihrer jeweiligen Soft-Limits gelangen. Vielmehr erkennt der Kernel Zugriffe unterhalb des Stapels und ordnet die Speicherseiten nach Bedarf zu. Daher ist die einzige wirkliche Einschränkung der Stapelgröße der verfügbare Speicher auf 64-Bit-Systemen (die Adressraumfragmentierung ist bei einer Adressraumgröße von 16 Zebibyten eher theoretisch).

Die Stapelgröße Maximum ist statisch, da dies die Definition von "Maximum" ist. Jede Art von Maximum für irgendetwas ist eine feste, vereinbarte Grenzzahl. Wenn es sich wie ein sich spontan bewegendes Ziel verhält, ist es kein Maximum.

Stapel auf Betriebssystemen mit virtuellem Speicher funktionieren tatsächlich wachsen dynamisch bis zum Maximum.

Apropos, es muss nicht statisch sein. Vielmehr kann es sogar pro Prozess oder pro Thread konfigurierbar sein.

Wenn die Frage lautet "Warum ist gibt es eine maximale Stapelgröße" (eine künstlich auferlegte, normalerweise viel weniger als der verfügbare Speicher)?

Ein Grund dafür ist, dass die meisten Algorithmen keinen enormen Stapelspeicherplatz benötigen. Ein großer Stapel ist ein Hinweis auf eine mögliche außer Kontrolle geratene Rekursion. Es ist gut, die außer Kontrolle geratene Rekursion zu stoppen, bevor der gesamte verfügbare Speicher zugewiesen wird. Ein Problem, das wie eine außer Kontrolle geratene Rekursion aussieht, ist die entartete Stapelverwendung, die möglicherweise durch einen unerwarteten Testfall ausgelöst wird. Angenommen, ein Parser für einen binären Infix-Operator rekursiviert den rechten Operanden: Parse first operand, scan operator, parse rest des Ausdrucks. Dies bedeutet, dass die Stapeltiefe proportional zur Länge des Ausdrucks ist: a op b op c op d .... Ein großer Testfall dieser Form erfordert einen großen Stapel. Wenn Sie das Programm abbrechen, wenn es ein angemessenes Stapellimit erreicht, wird dies behoben.

Ein weiterer Grund für eine feste maximale Stapelgröße besteht darin, dass der virtuelle Speicherplatz für diesen Stapel über eine spezielle Art der Zuordnung reserviert und somit garantiert werden kann. Garantiert bedeutet, dass der Speicherplatz nicht einer anderen Zuordnung zugewiesen wird, mit der der Stapel dann kollidiert, bevor das Limit erreicht wird. Der Parameter für die maximale Stapelgröße ist erforderlich, um diese Zuordnung anzufordern.

Threads benötigen aus einem ähnlichen Grund eine maximale Stapelgröße. Ihre Stapel werden dynamisch erstellt und können nicht verschoben werden, wenn sie mit etwas kollidieren. Der virtuelle Speicherplatz muss im Voraus reserviert werden, und für diese Zuordnung ist eine Größe erforderlich.

0
Kaz