Nachdem ich mit einigen Programmiersprachen gearbeitet habe, habe ich mich immer gefragt, warum der Thread-Stapel eine vordefinierte maximale Größe hat, anstatt sich automatisch nach Bedarf zu erweitern.
Im Vergleich dazu sind bestimmte, in den meisten Programmiersprachen häufig vorkommende Strukturen (Listen, Maps usw.) so konzipiert, dass sie nach Bedarf wachsen, während neue Elemente hinzugefügt werden. Ihre Größe wird nur durch den verfügbaren Speicher oder durch Recheneinschränkungen 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 begrenzt ist. Aus diesem Grund führt zu viel Rekursion sehr schnell zu einem allgegenwärtigen Stapelüberlauffehler / -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 Stack zur Laufzeit erreichen kann?
Antworten:
Es ist möglich, ein Betriebssystem zu schreiben, bei dem Stapel im Adressraum nicht zusammenhängend sein müssen. Grundsätzlich müssen Sie in der Aufrufkonvention ein bisschen herumspielen, um sicherzustellen, dass:
Wenn im aktuellen Stack-Bereich nicht genügend Speicherplatz für die aufgerufene Funktion vorhanden ist, erstellen Sie einen neuen Stack-Bereich und bewegen den Stack-Zeiger, um im Rahmen des Aufrufs auf den Anfang zu zeigen.
Wenn Sie von diesem Aufruf zurückkehren, kehren Sie zum ursprünglichen Stapelbereich zurück. Höchstwahrscheinlich behalten Sie die unter (1) erstellte Datei für die zukünftige Verwendung durch denselben Thread bei. Im Prinzip könnten Sie es freigeben, aber auf diese Weise liegen eher ineffiziente Fälle vor, in denen Sie in einer Schleife immer wieder über die Grenze hüpfen und für jeden Anruf eine Speicherzuweisung erforderlich ist.
setjmp
undlongjmp
, oder was auch immer Ihr Betriebssystem für die nicht-lokale Übertragung der Kontrolle gleichwertig ist, sind auf dem Laufenden und können bei Bedarf korrekt zum alten Stack-Umfang zurückkehren.Ich sage "Aufrufkonvention" - um genau zu sein, ich denke, es wird wahrscheinlich am besten in einem Funktionsprolog und nicht vom Aufrufer durchgeführt, aber meine Erinnerung daran ist verschwommen.
Der Grund, warum nicht wenige Sprachen eine feste Stapelgröße für einen Thread festlegen, ist, dass sie unter Betriebssystemen, die dies nicht tun, mit dem nativen Stapel arbeiten möchten . In den Antworten aller anderen heißt es, dass unter der Annahme, dass jeder Stapel im Adressraum zusammenhängend sein muss und nicht verschoben werden kann, für jeden Thread ein bestimmter Adressbereich reserviert werden muss. Das bedeutet, dass Sie im Voraus eine Größe auswählen müssen. Auch wenn Ihr Adressraum sehr groß 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 vermeintlichen 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 weitere Frage , die zum Glück bereits gestellt und beantwortet wurde.
quelle
Diese Datenstrukturen haben normalerweise Eigenschaften, die der Betriebssystemstapel nicht hat:
Verknüpfte Listen erfordern keinen zusammenhängenden Adressraum. So können sie ein Stück Gedächtnis hinzufügen, von 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 für ungültig erklären, wenn sie größer werden. Andererseits muss der Betriebssystemstapel Zeiger auf den Stapel gültig halten, bis die Funktion, zu deren Frame das Ziel gehört, zurückkehrt.
Eine Programmiersprache oder Laufzeitumgebung kann eigene Stapel implementieren, die entweder nicht zusammenhängend oder verschiebbar sind, um die Einschränkungen der Betriebssystemstapel zu umgehen. Golang verwendet solche benutzerdefinierten Stapel, um sehr viele Co-Routinen zu unterstützen, die ursprünglich als nicht zusammenhängender Speicher implementiert waren und jetzt dank Zeigerverfolgung über bewegliche Stapel (siehe hobbs Kommentar). Stackless Python, Lua und Erlang verwenden möglicherweise auch benutzerdefinierte Stacks, 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 sehr groß ist und der physische Speicher nur zugewiesen wird, wenn Sie ihn tatsächlich verwenden.
quelle
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 Ye Olde Days mit Single-Thread-Anwendungen und zusammenhängendem Speicher waren drei Komponenten eines Prozessadressraums: der Code, der Heap und der Stapel. Wie diese drei angeordnet waren, hing vom Betriebssystem ab, aber im Allgemeinen stand der Code an erster Stelle, beginnend am unteren Ende des Speichers, der Heap kam als nächstes und wuchs nach oben, und der Stack begann am oberen Ende 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, und je nachdem, was zuerst aktualisiert wurde, würden Sie entweder mit schlechten Daten arbeiten oder von einer Subroutine in einen beliebigen Teil des Speichers zurückkehren.
Die Speicherverwaltung hat dieses Modell ein wenig verändert: Aus Sicht des Programms hatten Sie 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 signalisierte dies Betriebssystem, wenn das Programm versucht hat, auf Speicher außerhalb eines Segments zuzugreifen. Sobald Sie über virtuellen Speicher verfügten, bestand keine Notwendigkeit oder kein Wunsch mehr, einem Programm Zugriff auf den gesamten Adressraum zu gewähren. So wurden den Segmenten feste Grenzen zugewiesen.
Unter der Annahme eines ausreichenden virtuellen Adressraums könnten Sie diese Segmente bei Bedarf erweitern, und das Datensegment (Heap) wächst tatsächlich mit der Zeit: Sie beginnen mit einem kleinen Datensegment, und wenn der Speicherzuordner wann mehr Speicherplatz anfordert es ist nötig. 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, wiederum mit fester Größe. Aber jetzt werden die Segmente nacheinander im virtuellen Adressraum angeordnet, sodass es keine Möglichkeit gibt, 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önnten 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 entlasten: 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 praktisch unendlich viele Stapel für praktisch unendlich viele Threads erstellen. Aber auch das ist nicht besonders wünschenswert: In fast allen Fällen weist ein Stapelüberlauf auf einen Fehler in Ihrem Code hin. Wenn Sie einen 1-GB-Stack bereitstellen, wird die Entdeckung dieses Fehlers lediglich verzögert.
quelle
Der Stapel mit einer festen Maximalgröße ist nicht allgegenwärtig.
Es ist auch schwierig, es richtig zu machen: Die Stapeltiefen folgen einer Potenzgesetzverteilung, was bedeutet, dass unabhängig von der Größe des Stapels 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: Welche Größe Sie auch wählen, es ist 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 dem Stack trotzdem erlauben, dynamisch zu wachsen, warum sollten Sie 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 zu implementieren:
Sobald Sie über leistungsfähige nicht-lokale Kontrollflusskonstrukte verfügen, geht die Idee eines einzelnen zusammenhängenden Stapels ohnehin aus dem Fenster: Wiederaufsetzbare Ausnahmen und Fortsetzungen beispielsweise "spalten" den Stapel auf, sodass Sie tatsächlich ein Netzwerk haben Stapel (z. B. mit einem Spaghetti-Stapel implementiert). Systeme mit erstklassigen modifizierbaren Stapeln, wie Smalltalk, erfordern außerdem Spaghetti-Stapel oder ähnliches.
quelle
Das Betriebssystem muss einen zusammenhängenden Block angeben, wenn ein Stapel angefordert wird. Dies ist nur möglich, wenn eine maximale Größe angegeben wird.
Angenommen, der Speicher sieht während der Anforderung folgendermaßen aus (Xs stehen für "used", "Os unused"):
XOOOXOOXOOOOOX
Bei einer Anforderung für eine Stapelgröße von 6 antwortet das Betriebssystem 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 Slots (Os) in einer Reihe.
Man kann auch die Schwierigkeit erkennen, Wachstum zuzulassen, wenn der nächste zusammenhängende Schlitz belegt ist.
Die anderen genannten Objekte (Listen usw.) werden nicht auf dem Stapel abgelegt, sondern landen in nicht zusammenhängenden oder fragmentierten Bereichen auf dem Haufen. Wenn sie also größer werden, benötigen sie nur Speicherplatz und keine zusammenhängenden Objekte wie sie sind anders gehandhabt.
Die meisten Systeme legen einen vernünftigen Wert für die Stapelgröße fest. Sie können ihn überschreiben, wenn der Thread erstellt wird, wenn eine größere Größe erforderlich ist.
quelle
Unter Linux ist dies lediglich eine Ressourcenbeschränkung, mit der außer Kontrolle geratene Prozesse abgebrochen werden, bevor sie schädliche Mengen der Ressource verbrauchen. Auf meinem Debian-System der folgende Code
erzeugt die Ausgabe
Beachten Sie, dass das Hard-Limit auf Folgendes festgelegt ist
RLIM_INFINITY
: Der Prozess kann das Soft-Limit auf einen beliebigen Wert 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 Beschränkung wird ein außer Kontrolle geratener Prozess (unbeabsichtigte unendliche Rekursion) lange Zeit abgebrochen, bevor er so viel Speicher belegt, 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 setzen.
Technisch gesehen wachsen Stacks 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 nie an ihre jeweiligen Soft-Limits gelangen. Vielmehr erkennt der Kernel Zugriffe unterhalb des Stapels und ordnet die Speicherseiten nach Bedarf zu. Die einzige wirkliche Beschränkung für die Stapelgröße ist daher der verfügbare Speicher auf 64-Bit-Systemen (die Adressraumfragmentierung ist bei einer Adressraumgröße von 16 Zebibyte eher theoretisch).
quelle
Die maximale Stapelgröße ist statisch, da dies die Definition von "maximal" ist . Jede Art von Maximum für irgendetwas ist ein fest vereinbarter Grenzwert. Wenn es sich wie ein sich spontan bewegendes Ziel verhält, ist es kein Maximum.
Stacks auf Betriebssystemen mit virtuellem Speicher wachsen tatsächlich dynamisch bis zum Maximum .
Apropos, es muss nicht statisch sein. Sie kann vielmehr pro Prozess oder pro Thread konfiguriert werden.
Wenn die Frage lautet "Warum gibt es eine maximale Stapelgröße" (eine künstlich auferlegte, normalerweise viel weniger als verfügbarer 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, möglicherweise ausgelöst durch einen unerwarteten Testfall. Angenommen, ein Parser für einen binären Infix-Operator arbeitet mit der Rekursion des rechten Operanden: erster Operand analysieren, Scan-Operator, Rest des Ausdrucks analysieren. Dies bedeutet , dass die Stapeltiefe , die die Länge des Ausdrucks proportional 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 erkannt.Ein weiterer Grund für eine feste maximale Stapelgröße ist, dass der virtuelle Speicherplatz für diesen Stapel über eine spezielle Art von Zuordnung reserviert und somit garantiert werden kann. Garantiert bedeutet, dass der Speicherplatz nicht einer anderen Zuordnung zugewiesen wird, mit der der Stapel kollidiert, bevor das Limit erreicht wird. Der Parameter für die maximale Stapelgröße ist erforderlich, um diese Zuordnung anzufordern.
Threads benötigen aus ähnlichen Gründen 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.
quelle