8.1 Einführung und Übersicht Unter dem Begriff Betriebsmittel (BM, engl. resources) ist alles zusammengefasst, was ein Prozess zum Vorankommen benötigt. Probleme entstehen wenn BM nicht in beliebigem Umfang zur Verfügung stehen bzw. nicht simultan genutzt werden können. Betriebsmittel treten in den verschiedensten Formen auf und können auf verschiedene Art und Weise verwaltet werden. Beispiel 1: Kapitel 8 Betriebsmittelverwaltung und Verklemmungen (deadlocks) Prozesse benötigen Hauptspeicher zur Ablage ihrer Daten. Speicher steht als Betriebsmittel nur begrenzt zur Verfügung und muss zugeteil d. h. verwaltet werden Beispiel 2: Ein Prozess benötigt das Programm, das er ausführen soll, im Hauptspeicher, d. h. das Programm ist ein Betriebsmittel des Prozesse Da jedoch auch andere Prozesse dasselbe Programm simultan dazu ausführen können, steht dieses BM allen gleichzeitig zur Verfügung un muss nicht extra bewirtschaftet“ werden. ” Unkoordinierte Nutzung Koordination durch Verwaltungskomponente Bei unkoordinierter Nutzung eines Betriebsmittels können unerwünschte Effekte auftreten. Beispiel: Jeder von zwei Prozessen druckt einen Text zeilenweise aus Durch den Einsatz eines BM-Verwalters im Treiber kann eine exklusiv Nutzung des Druckers von jeweiligem Prozess erreicht werden Prozess x Prozess x Belegen Treiber Chaos! printline("xx...x") Prozess y Treiber printline(line) printline("xx...x") BM−Verwalter xxxxx yyyyy yyyyy xxxxx yyyyy Prozess y Freigeben Belegen printline(line) send line to printer send line to printer return return printline("yy...y") printline("yy...y") Freigeben 8-3 xxxxx xxxxx yyyyy yyyyy yyyyy Anschaulich: Worum geht es? Betriebsmittelprobleme im Alltag (Lösung 1) Akteure, z. B. Benutzer Prozess Thread Prozessor Netzwerkkarte Ein Straßenengpass als Betriebsmittel Betriebsmittel, z. B. Prozessor Speicher Bandbreite Datei Signal Nachricht Name Farbe Betriebsmittel sind knapp Benutzung erfolgt oft exklusiv Verwaltung ist sinnvoll Lösung 1: Zentrale Instanz entscheidet (BM-Verwalter), z. B. Ampeln, Schranken 8-5 Lösung 1: Betriebsmittelverwalter Betriebsmittelprobleme im Alltag (Lösung 2) P1 Nutzung des BM nur nach vorheriger Belegung möglich. (durch eine zwischengeschaltete Verwalter-Instanz erzwungen) P2 Ein Straßenengpass als Betriebsmittel BM−Verwalter BM Beispiele: Drucken Zugriff auf den Drucker ist nicht unmittelbar möglich, sondern nur über spezielle Software, den Treiber, der als Verwalter fungiert. Lösung 2: Verständigung der Teilnehmer (Regeln, Verhandlung, Protokoll), z. B. Verkehrszeichen, Berg- vor Talfahrt“, Handzeichen, Lichthupe ” Hauptspeicherverwaltung Zugriffe nur innerhalb der zugewiesenen Segmente möglich. 8-7 Lösung 2: Verständigung: Beispiele Betriebsmittelprobleme im Alltag (Lösung 3) P1 P2 Ein Straßenengpass als Betriebsmittel Die Bewerber um BMs stimmen sich ab (Protokoll) BM Beispiele: Kritischer Abschnitt Die Prozesse vereinbaren, den kritischen Abschnitt durch Nutzung von Sperren unter gegenseitigen Ausschluss zu stellen. Lösung 3: Keinerlei Maßnahmen Dezentrale Bus-Arbitrierung Der Zugriff auf den gemeinsamen Bus wird geregelt: die sendewilligen Komponenten legen mit einem Koordinationsprotokoll (Bus-Arbitrierung) fest, wer als nächster senden darf. Der Aufwand für seltene Kollisionsauflösung kann geringer sein als de permanente Aufwand für eine vorherige Abstimmung Einsatz dort, wo eine Kollision unwahrscheinlich, d. h. selten und der durch die Kollision entstandene Schaden reparabel“ ist. ” 8-9 Lösung 3: Unkoordinierte Nutzung: Beispiele Betriebsmittel: Begriffsbildung Optimistische Synchronisation von Transaktionen (Validierung) Transaktionen setzen keine Sperren, sondern greifen einfach zu. Die Zugriffe werden protokolliert (Log). Am Ende (Commit) wird überprüft, ob Operationen in Konflikt zu anderen standen (Validierung). Falls ja, wird die Transaktion abgebrochen (und neu gestartet). Lokale Netze: Kollisionsbehaftete Protokolle (Ethernet) Die sendewillige Station sendet, nachdem sie kurz die Leitung abgehört hatte. Senden zwei Stationen gleichzeitig, so kollidieren die Datenpakete und werden zerstört. Die beiden Stationen müssen dann erneut die Daten schicken. Existenzformen von Betriebsmitteln: real – logisch – virtuell reale Betriebsmittel sind physisch existent. Reale BM sind Voraussetzung für virtuelle und logische, die auf sie abgebildet werden. Beispiel: Hauptspeicher, Platte, Prozessor Virtuelle Betriebsmittel entstehen, wenn dem Benutzer eine größere Anzahl eines BM als real vorhanden vorgespiegelt wird. Die Nutzung erfolgt oft intermittierend: das virtuelle BM wird nur für kurze Zeitintervale auf das reale BM abgebildet (Multiplexing) Beispiel: Virtueller Speicher, Virtueller Prozessor Logische Betriebsmittel entstehen, wenn dem Benutzer im Vergleich zu realen eine höhere“, abstraktere, komfortablere, funktional angereicherte ” Schnittstelle zur Benutzung angeboten wird. Beispiel: Datei – Abstraktion der Platte, Fenster – des Bildschirms 8-11 8.2 Kandidatenauswahlstrategien Auswahlstrategien In der Folge der wartenden Anforderungen eines Betriebsmittels soll eine erfüllbare ausgewählt werden Grundsätzlich können hier verschiedene Strategien eingesetzt werden, die eine gute Auslastung des Betriebsmittels und/oder eine faire Behandlung der Interessenten zum Ziel haben. Sei nf (t) die Anzahl der zum Zeitpunkt t freien BM-Einheiten n(i) die Anzahl der von Prozess i geforderten Einheiten W (t) die Menge der wartenden Anforderungen/Prozesse. FIFO (First-In-First-Out) bzw. FCFS(First-Come-First-Served) Es wird nur die erste Anforderung i in der Warteschlange betrachtet. Gilt n(i) ≤ nf (t), so werden n(i) Einheiten belegt, sonst geschieht nichts Nachteil: Betriebsmittelauslastung kann schlecht sein, wenn erste Anforderung groß, d. h. n(i) > nf (t) Nachfolgende kleinere – und erfüllbare – Anforderungen bleiben unberücksichtigt. First-Fit-Request: Die Warteschlange wird von vorne beginnend durchsucht, bis eine Anforderung j gefunden wird, die erfüllt werden kann: n(j) ≤ nf (t). Best-Fit-Request: Die Warteschlange wird vollständig durchsucht, und es wird diejenige Anforderung j gewählt, für die gilt: min W (t) ist als Warteschlange aufzufassen, bei der Neuankömmlinge hinten“ eingefügt werden. ” j∈W (t)∧n(j)≤nf (t) {nf (t) − n(j)} d. h. die freie Restkapazität wird minimiert. 8-13 Iteration über Auswahlstrategien Nachteil bei First-Fit- und Best-Fit-Request: Programme mit großen Anforderungen können evtl. verhungern“ (starvation) ” Lösung des Problems des Verhungerns Verwendet man eine dynamische Fenstergröße, kann man das Verhungern einer großen Anforderung (bei First-Fit oder Best-Fit-Request) verhindern Iterative Anwendung Nach Freigabe einer größeren Anzahl von Einheiten können u. U. mehrere Anforderungen auf einmal erfüllt werden. Die obigen Strategien können daher iterativ angewendet werden, bis keine Belegung mehr möglich ist. Fenster Um Aufwand zu reduzieren, kann man sich auf ein Fenster der Größe L beschränken, d. h. lediglich die ersten L Positionen der Warteschlange werden betrachtet. Sei Lmax die maximale Fenstergröße (Initialwert). Bei jeder erfolgreichen Belegung wird die Fenstergröße L modifiziert: ( L − 1, falls L > 1 und die Anf. am Kopf der WS übergangen wurde L := Lmax , sonst Dadurch wird, nachdem die vorderste Anforderung höchstens L − 1 mal übergangen wurde, die Fenstergröße = 1, d. h. die vorderste Anforderung wird garantiert berücksichtigt: Fenster, L=3 Anforderung A5 A2 A6 A3 Belegung Warteschlange Für L → 1 konvergieren Best-Fit-Request und First-Fit-Request gegen FCF 8-15 8.3 Verklemmungen (Deadlocks) Beispiele der Verklemmungen Verklemmung im Alltag ( rechts hat Vorfahrt“): ” Beispiel Signalisierung P1 P2 SWAIT(SO1 ) SWAIT(SO2 ) SIGNAL(SO2 ) SIGNAL(SO1 ) Beispiel Kommunikation P1 P2 RECEIVE_S(CO1 ) RECEIVE_S(CO2 ) SEND_A(CO2 ) SEND_A(CO1 ) Beispiel Sperren P1 P2 LOCK(LO1 ) LOCK(LO2 ) LOCK(LO2 ) LOCK(LO1 ) Beispiel Betriebsmittel P1 P2 ALLOCATE(R1 ) ALLOCATE(R2 ) ALLOCATE(R2 ) ALLOCATE(R1 ) 8-17 Verklemmung Verklemmungsauflösung: Methoden notwendig Beispiel (mehrere Prozesse als Empfänger und Sender beteiligt): 8-19 Verklemmungsmodellierung: Wartegraph Verklemmung: notwendige & hinreichende Bedingungen Ein gerichteter Graph mit den Prozessen als Knoten und Wartebeziehungen als Pfeile heißt Wartegraph (wait-for graph). Für eine Verklemmung ist typisch, dass der Wartegraph einen Zyklus besitzt: zwischen sechs Prozessen Im Zusammenhang mit Betriebsmitteln gibt es folgende drei notwendige Bedingungen für das Auftreten einer Verklemmung 1. Wechselseitiger Ausschluss: BM werden exklusiv genutzt 2. Hold-and-wait: Prozess darf BM besitzen und auf andere BM warten 3. Keine Verdrängung: Prozess gibt BM nur freiwillig ab Diese Bedingungen sind teilweise sogar durchaus wünschenswert: wechselseitiger Ausschluss für die Konsistenz, keine zufällige Verdrängung um Zustand zu sichern, etc. zwischen zwei Prozessen Unter Bedingungen 1.–3. kann evtl. die folgende Bedingung eintreten: 4. Zyklische Wartesituation: Kette aufeinander wartender Prozesse, d.h. jeder besitzt ein BM, das vom nächsten Prozess in der Kette benötigt wird Satz. 1.–4. zusammen sind notwendige und hinreichende Bedingungen! 8-21 Betriebsmittelgraph Beachte: Während die Bedingungen 1.–3. vom Design des Systems abhängen, beschreibt die Bedingung 4. eine Situation, die durch die beteiligten Prozesse eintreten kann (Literatur: oft kein Unterschied) Betriebsmittelgraph: Eigenschaften und Operatione Betriebsmittelgraph ist noch eine Möglichkeit, Anforderungs- und Belegungssituationen formal darzustellen. Sei P die Menge der Prozesse, BM die Menge der BM-Typen. Ein BM-Graph spiegelt einen bestimmten Systemzustand wider, d. h. er repräsentiert eine Betriebsmittelsituation Ein gerichteter Graph (V, E) mit V = P ∪ BM und der folgenden Pfeilsemantik heißt Betriebsmittelgraph: Wie auch im Wartegraphen weist ein Zyklus auf eine potentielle Verklemmungssituation hin. (p, b) ∈ E ⇔ Prozess p fordert eine Einheit von BM-Typ b (b, p) ∈ E ⇔ Prozess p besitzt eine Einheit von BM-Typ b Der BM-Graph ist bzgl. der Knotenmengen P und BM bipartit, d. h. es gibt nur Kanten von P nach BM oder umgekehrt. Die Anzahl der verfügbaren Einheiten eines Betriebsmitteltyps bestimmt den maximalen Ausgangsgrad des BM-Knotens, graphisch dargestellt durch eine entsprechende Menge von Punkten innerhalb der Rechtecke. Allerdings ist ein Zyklus im BM-Graphen nur eine notwendige, aber keine hinreichende Bedingung für die Existenz einer Verklemmung (s Beispiel auf den nächsten zwei Folien) Jede Operation (Anfordern, Belegen, Freigeben) bedeutet eine Graphtransformation (Hinzufügen bzw. Entfernen von Kanten) Ein Prozess kann eine Operation ausführen, wenn er nicht blockiert ist B1 P3 B2 a) P1 Sind die Gesamtanforderungen eines Prozesses erfüllt, so kann er sein Arbeit beenden und alle von ihm belegten Betriebsmittel freigeben B3 P2 8-23 Betriebsmittelgraph: Reduktionen Beispiel(Fortsetzung) Ein Prozess reduziert den BM-Graphen bei Beendigung, durch Entfernen aller seiner Belegungskanten. Dadurch können andere Anforderungen erfüllt werden (entsprechende Kanten werden umgedreht) Ein BM-Graph heißt reduzierbar, wenn es einen Prozess gibt, der ihn reduzieren kann (d. h. der alle angeforderten BMs hält und somit nicht blockiert wird, und deshalb irgendwann endet und BMs freigibt) Der BM-Graph heißt vollständig reduzierbar, wenn es eine Folge von Reduktionen gibt, so dass am Ende alle Kanten des Graphen entfernt sind. Verklemmungstheorem: Eine Betriebsmittelsituation S ist verklemmt genau dann, wenn der dazugehörige BM-Graph nicht vollständig reduzierbar ist. Beispiel für eine Reduktion: der Graph ist vollständig reduzierbar B1 B1 P3 B2 b) P1 P3 B3 P2 B2 P1 c) P2 B1 B1 P3 B1 B3 P3 P3 B2 B2 a) P1 P1 B3 B2 P1 e) P2 B3 B3 d) P2 P2 8-25 Gegenmaßnahmen für Verklemmungen Verklemmungsvorbeugung (deadlock prevention) Vorbeugung in diesem Zusammenhang bedeutet ein präventives Vorgehen, die BM-Vergabe prinzipiell so restriktiv zu gestalten, dass eine der notwendigen Deadlock-Bedingungen nicht erfüllt ist. Grundsätzlich gibt es vier Strategien, Deadlocks zu behandeln: Summenbelegung (preclaiming) Sämtliche jemals benötigten BM werden einmalig zu Beginn angefordert (somit ist die hold-and-wait Bedingung nicht erfüllt) wartet auf Pi kann es nicht geben, da vorher nichts belegt Anmerkung: Verhindern/vorbeugen (prevention) Vermeiden (avoidance) Erkennen und beheben (detection) Ignorieren Beachte: In der Literatur werden die Begriffe Verhindern“ und ” Vermeiden“ unterschiedlich bzw. wechselnd verwendet ” In dynamischen Systemen ist der Gesamtbedarf eines Prozesses schwierig abzuschätzen. Verfahren ist unökonomisch, da BM viel zu lange belegt. 8-27 Verklemmungsvorbeugung (deadlock prevention) Verklemmungsvorbeugung (deadlock prevention) Totalfreigabe bei jeder Belegung Belegung gemäß vorgegebener Ordnung Belegen(BM1 , BM2 ) Die Betriebsmittel seien geordnet (BM1 , BM2 , BM3 , . . .). Betriebsmittel dürfen nur gemäß dieser Ordnung angefordert werden Freigeben(BM1 , BM2 ) P1 Belegen(BM3 ) Belegen(BM1 ) P2 evtl. zwangsweise Belegen(BM2 ) Belegen(BM3 ) Belegen(BM3 ) Belegen(BM5 ) Freigeben(BM3 ) Belegen(BM4 ) Belegen(BM1 , BM2 ) Auch hier wird eine Anforderung immer aus einem besitzlosen“ ” Zustand vorgenommen und dadurch eine zyklische Wartesituation vermieden. Dadurch werden Zyklen grundsätzlich vermieden. 8-29 Verklemmungsvermeidung (deadlock avoidance) Belegungstrajektorie (mit Verklemmung) P1 Die Verklemmungsdefinition betrifft eine aktuelle Belegungs- und Anforderungssituation. F Um in einer Anforderungssituation Verklemmungen vermeiden zu können, muss man die Restanforderungen der Prozesse kennen. Im ungünstigsten Fall werden alle diese Restforderungen augenblicklich (auf einmal) gestellt: BMy sicher F B BMx sicher unsicher sicher B könnten diese trotzdem erfüllt werden, so ist die Situation sicher andernfalls heißt die Situation unsicher. Eine unsichere Betriebsmittelsituation ist somit dadurch gekennzeichnet, dass es eine Teilmenge von Prozessen gibt, deren Restanforderungen nicht alle erfüllbar sind: sicher aktuelle Anforderungen können zwar evtl. noch erfüllt werden, aber wenn die Reihenfolge der Anforderungen und Freigaben ungünstig ist, wird es zur Verklemmung kommen. sicher B B 8-31 BMy sicher BMx F F P2 Sichere und Unsichere Zustände Sichere und Unsichere Zustände Beispiel eines unsicheren Zustandes: Beispiel eines sicheren Zustandes: Zustand (a) ist sicher, weil es eine Folge von BM-Zuteilungen gibt, so dass alle Prozesse garantiert zu Ende laufen können. Beweis: zunächst ausschließlich B ausführen (b), nach seiner Beendigung (c), dann C ausführen (d), nach seiner Beendigung (e). Das System kann also durch vorsichtiges Scheduling einen Deadlock vermeiden. Has Max Has Max Has Max Has Max 3 9 A 3 9 A 3 9 A 3 9 A 3 9 B 2 4 B 4 4 B 0 – B 0 – B 0 – 2 7 C 2 7 C 2 7 C 7 7 C 0 – C Free: 3 (a) Free: 1 (b) Free: 5 (c) Free: 0 (d) Beweis: wenn alle Prozesse ihre Restanforderungen auf einmal stellen, können wir zwar B beenden, siehe (c), (d), wo wir dann jedoch fest stecken Has Max Has Max A Wenn A eine weitere Ressource zugeteilt bekommt (b), dann ist somit ein Übergang in den unsicheren Zustand passiert. Has Max Has Max Has Max A 3 9 A 4 9 A 4 9 A 4 B 2 4 B 2 4 B 4 4 B — — C 2 7 C 2 7 C 2 7 C 2 7 Free: 3 (a) Free: 2 (b) Free: 0 (c) 9 Free: 4 (d) Beachte: es kann passieren, dass Prozess A eine Ressource freigibt bevor er versucht, weitere zu reservieren, ein Deadlock wäre gänzlich vermieden. Aber: man kann das nicht garantieren! Free: 7 (e) 8-33 Verklemmungsvermeidung (Bankier-Algorithmus) Bankier-Algorithmus: Anmerkungen Bankier-Algorithmus: steuert die Betriebsmittelvergabe so, dass keine unsichere Situation eintritt, d. h. Anforderungen werden nur gewährt, wenn sie in einen sicheren Zustand führen. Ein Zustand der Prozessmenge P = P1 , P2 , . . . , Pm heißt sicher, g. d. w. es eine Beendigungsreihenfolge der Prozesse gibt, so dass alle (auf einmal gestellten) Restanforderungen der laufenden Prozesse durch die BM-Freigaben beendeter Prozesse erfüllt werden können Der Bankier-Algorithmus [Dijkstra 1965] prüft bei jeder Anforderung, ob ihre Erfüllung in einen unsicheren Zustand führt; falls ja, wird diese Anforderung – obwohl erfüllbar – zurückgestellt. Beispiel [Tanenbaum, Abb. 3.11]: 10 BM-Einheiten vorhanden Der Bankier-Algorithmus wurde ursprünglich für den Fall eines Betriebsmittels entwickelt und dann für mehrere BMs erweitert, sieh z. B. [Tanenbaum,Chap. 3.5.4] Zahlreiche Artikel und Bücher befassten sich mit verschiedensten Aspekten der Komplexität und Implementierung des Algorithmus Jedoch ist der Algorithmus in Praxis nicht sehr hilfreich: es ist nur selten im Voraus bekannt, wieviele Betriebsmittel ein Prozes im Laufe der Ausführung brauchen wird die Anzahl der Prozesse ist oft nicht fest und ändert sich ständig auch Betriebsmittel können kommen und gehen Daher wird der Bankier-Algorithmus nur von sehr wenigen Betriebssystemen benutzt, um Verklemmungen zu vermeiden 8-35 Verklemmungsentdeckung (deadlock detection) Verklemmungsentdeckung Sind die Restanforderungen nicht bekannt, so kann keine Verklemmungsvermeidung durchgeführt werden. Man muss daher eine Verklemmungssituation wenigstens erkennen können. Dies gelingt indirekt durch die Bestimmung der Prozesse, die nicht in einer Verklemmung involviert sind. Eine Prozessmenge P1 , P2 , . . . , Pm heißt verklemmungsfrei, g. d. w. es gibt eine Beendigungsreihenfolge der Prozesse, so dass jede Anforderung durch das erfüllt werden kann, was von beendeten Prozessen freigegeben wird. Ein Vergleich der Definition von verklemmungsfrei“ mit der von ” sicher“ zeigt weitgehende Übereinstimmung: ” Bei Sicherheit“ wird eine Beendigungsreihenfolge gesucht, die ” ausführbar ist, wenn alle Restanforderungen auf einmal gestellt werden. Bei der Verklemmungsfreiheit“ wird das gleiche bezüglich der ” aktuellen Anforderungen getan. Wir können daher den Bankier-Algorithmus auch für die Verklemmungsentdeckung verwenden: Wir müssen nur die Restanforderungen durch die aktuellen Anforderungen ersetzen In Vergleich zur Verklemmungsvermeidung ist dieser Ansatz weniger restriktiv, da man nicht alle Belegungen in Voraus kennen muss – man muss jedoch mit einem evtl. auftretenden Deadlock rechnen Der Entdeckungsalgorithmus muss alle Prozesse einbeziehen und kan unterschiedlich aufgerufen werden: bei jeder Belegung periodisch im Leerlaufprozess bei Verdacht (evtl. manuell ausgelöst) Satz (Spezialfall Einexemplar-Betriebsmittel): Eine Verklemmung liegt genau dann vor, wenn der Wartegraph einen Zyklus enthält. Eine Verklemmungsentdeckung reduziert sich dann zur Zyklenentdeckung in einem Graphen (Tiefensuche), mit dem Zeitaufwand: O(|Kanten| + |Knoten|) 8-37 Verklemmungsauflösung (deadlock resolution) Der “Vogel-Strauss-Algorithmus” Ist ein (geordneter) BM-Entzug nicht möglich, so bleibt nur der Abbruch mindestens eines Prozesses Dieser Algorithmus wurde nicht von Herren Vogel und Strauss erfunden, sondern funktioniert nach dem Motto: den Kopf in den Sand stecken“ ” Zwei Sichten auf das Verklemmungsproblem: Theoretiker: um jeden Preis verhindern! Praktiker: wie oft tritt das Problem auf, insb. in Vergleich mit anderen Gründen für Abstürze, und wie schwerwiegend ist es? Es stellt sich die Frage, welchen Prozess man abbrechen soll Mögliche Kriterien: Bsp: Passiert ein Deadlock alle 5 Monate, stürzt System jedoch jede Woch aus anderen Gründen ab, dann ist der “Vogel-Strauss-Algorithmus” vernün Die Verklemmungsauflösung besteht darin, den Wartezyklus auf irgendeine Weise zu brechen Größe der Anforderung Umfang der belegten Betriebsmittel Dringlichkeit Benutzer- / Systemprozess Aufwand des Abbruchs Verlorengegangene Arbeit Restbedienzeit Beispiel: Das Erzeugen neuer Prozesse im System führt potentiell zu einem Deadlock, weil Prozesstabellen (wie alle BM) endlich sind. Das Problem tritt jedoch extrem selten auf, so dass man das dynamische Erzeugen von Prozessen nicht deswegen viel zu restriktiv gestalten will Deshalb ignorieren die meisten Betriebssysteme für breite Anwendergruppen (auch Unix und Windows) das Verklemmungsproblem fast vollständig In speziellen Betriebssystemen (für Roboter, Autos, Kernkraftwerke, etc.) werden dagegen relativ aufwendige Algorithmen verwendet 8-39
© Copyright 2024 ExpyDoc