Schlecht dokumentierte Linux-Funktionen

    Seufzend sagte sie:
    „Wie lange habe ich geschlafen!“
    BildAls ich Unix kennenlernte, war ich einmal fasziniert von der logischen Harmonie und Vollständigkeit des Systems. Einige Jahre später studierte ich wütend die Geräte- und Systemaufrufe des Kernels und las alles, was ich bekommen konnte. Nach und nach wurde mein Hobby zunichte gemacht, und es wurden weitere dringende Fragen gestellt. Ab einiger Zeit entdeckte ich das eine oder andere Merkmal, von dem ich vorher nichts wusste. Der Prozess ist natürlich, aber zu oft sind solche Vorfälle durch eine Sache verbunden - das Fehlen einer maßgeblichen Dokumentationsquelle. Häufig erfolgt die Antwort in Form des dritten Kommentars zum StapelüberlaufOft müssen Sie zwei oder drei Quellen zusammenführen, um die Antwort auf genau die Frage zu erhalten, die Sie gestellt haben. Ich möchte hier eine kleine Sammlung derart schlecht dokumentierter Merkmale vorstellen. Keiner von ihnen ist neu, einige sind sogar nicht sehr neu, aber für jeden habe ich mehrere Stunden auf einmal getötet und kenne oft noch keine systematische Beschreibung.

    Alle Beispiele beziehen sich auf Linux, obwohl viele davon für andere * nix-Systeme gültig sind. Ich habe nur das am aktivsten entwickelnde Betriebssystem zugrunde gelegt, abgesehen von dem, das vor meinen Augen liegt und in dem ich den vorgeschlagenen Code schnell überprüfen kann.

    Bitte beachten Sie, dass ich in der Überschrift "schlecht dokumentiert" und nicht "wenig bekannt" geschrieben habe, sodass diejenigen, die Bescheid wissen, bitte in den Kommentaren Links zu artikulieren posten Dokumentation füge ich gerne am Ende der Liste hinzu.

    Возвращается ли освобожденная память обратно в ОС?


    Этот вопрос, заданный вполне мною уважаемым коллегой, послужил спусковым крючком для этой публикации. Целых полчаса после этого я смешивал его с грязью и обзывал сравнительными эпитетами, обьясняя что еще классики учили — память в Unix выделяется через системный вызов sbrk(), который просто увеличивает верхний лимит доступных адресов; выделяется обычно большими кусками; что конечно технически возможно понизить лимит и вернуть память в ОС для других процессов, однако для аллокатора очень накладно отслеживать все используемые и неиспользуемые фрагменты, поэтому возвращение памяти не предусмотрено by design. Dieser klassische Mechanismus funktioniert in den meisten Fällen einwandfrei, mit der Ausnahme, dass ein Server stunden- / monatelang still steht, plötzlich viele Seiten zur Verarbeitung eines Ereignisses auffordert und wieder einschlaft (in diesem Fall hilft er jedoch beim Auswechseln). Nachdem ich meine FAQ erfüllt hatte, bestätigte ich als ehrliche Person meine Meinung im Internet und stellte überrascht fest, dass Linux ab 2.4 je nach angeforderter Größe sowohl sbrk () als auch mmap () zur Speicherzuweisung verwenden kann. Darüber hinaus kehrt der durch mmap () zugewiesene Speicher nach dem Aufruf von free () / delete vollständig zum Betriebssystem zurück. Nach einem solchen Schlag hatte ich nur einenZwei - entschuldige dich demütig und finde heraus, wo genau diese mysteriöse Grenze liegt. Da ich keine Informationen fand, musste ich mit den Händen messen. Es stellte sich heraus, dass auf meinem System (3.13.0) - nur 120 Bytes. Der Linealcode für diejenigen, die messen möchten, ist hier .

    Was ist das Mindestintervall, in dem ein Prozess / Thread schlafen kann?


    Dasselbe lehrte Maurice Bach : Der Scheduler von Prozessen im Kernel wird durch jeden Interrupt aktiviert; Nach Erhalt der Kontrolle durchläuft der Scheduler die Liste der Schlafvorgänge und überträgt diejenigen, die aufgewacht sind (die angeforderten Daten von einer Datei oder einem Socket erhalten haben, das Schlafintervall () abgelaufen ist usw.), in die Liste "Bereit zur Ausführung" und beendet dann den Interrupt in den aktuellen Prozess. Wenn der Systemtimer unterbrochen wird, was einmal alle 100 ms geschah, setzt der Scheduler nach Erhöhung der CPU-Geschwindigkeit alle 10 ms den aktuellen Prozess an das Ende der Liste „Bereit zur Ausführung“ und startet den ersten Prozess am Anfang dieser Liste. Also, wenn ich Schlaf anrufe (0)oder sogar aus irgendeinem Grund für einen Moment eingeschlafen, so dass mein Prozess von der "Ready to Run" -Liste auf die "Preempted" -Liste umgestellt wurde, hat er keine Chance, in weniger als 10 ms wieder zu verdienen, selbst wenn er alleine im System ist. Im Prinzip kann der Kernel durch Verringern dieses Intervalls wiederhergestellt werden. Dies verursacht jedoch unangemessen hohe CPU-Kosten, sodass dies keine Option ist. Diese bekannte Einschränkung vergiftet seit vielen Jahren das Leben von Entwicklern schnell reagierender Systeme und hat die Entwicklung von Echtzeitsystemen und nicht blockierenden ( schlossfreien ) Algorithmen stark stimuliert .

    Und irgendwie wiederholte ich dieses Experiment (ich war tatsächlich an subtileren Momenten wie der Wahrscheinlichkeitsverteilung interessiert) und sah plötzlich, dass der Prozess danach aufwachtSchlaf (0) nach 40 μs, 250 mal schneller. Dasselbe gilt nach Aufrufen von yield (), std :: mutex :: lock () und allen anderen blockierenden Aufrufen. Was ist los?!

    Die Suche führte ziemlich schnell zu dem seit 2.6.23 eingeführten Completely Fair Scheduler. Lange konnte ich jedoch nicht nachvollziehen, wie genau dieser Mechanismus zu einem so schnellen Wechsel führt. Wie ich schließlich herausgefunden habe, liegt der Unterschied genau im Standard-Scheduler-Klassenalgorithmus selbst, unter denen standardmäßig alle Prozesse gestartet werden. Im Gegensatz zur klassischen Implementierung hat dabei jeder Arbeitsprozess / Thread eine dynamische Priorität, so dass der Arbeitsprozess eine Priorität hat, die gegenüber anderen ausstehenden Ausführungen schrittweise reduziert wird. Auf diese Weise kann der Scheduler entscheiden, sofort einen anderen Prozess zu starten, ohne auf das Ende des festgelegten Intervalls zu warten, und der Algorithmus zum Aufzählen von Prozessen O (1) ist jetzt viel einfacher und kann häufiger ausgeführt werden.

    Diese Änderung führt zu überraschend weitreichenden Konsequenzen, nämlich der Lücke zwischen Echtzeit und das übliche System ist fast verschwunden, die vorgeschlagene Verzögerung von 40 Mikrosekunden ist für die meisten Anwendungen wirklich klein genug, dasselbe gilt für nicht blockierende Algorithmen - die klassischen blockierenden Datenstrukturen auf Mutexen sind sehr wettbewerbsfähig geworden.

    Und was sind diese Klassen für Planungsrichtlinien ?


    Dieses Thema ist mehr oder weniger beschrieben, ich werde es nicht wiederholen, und trotzdem werden wir ein und das zweite maßgebliche Buch auf der entsprechenden Seite öffnen und miteinander vergleichen. Es gibt an einigen Stellen fast wörtliche Wiederholungen voneinander sowie einige Unterschiede zu dem, was man -s2 sched_setscheduler sagt . Ein Symptom.

    Lass uns einfach eine Weile mit dem Code herumspielen. Ich erstelle mehrere Threads mit unterschiedlichen Prioritäten, hebe sie alle im Mutex auf und wecke sie alle auf einmal auf. Ich erwarte natürlich, dass sie in strikter Übereinstimmung mit ihrer Priorität aufwachen:

    iBolit# ./sche -d0 -i0 -b0 -f1 -r2 -f3 -i0 -i0 -i0 -d0
    6 SCHED_FIFO[3]
    5 SCHED_RR[2]
    4 SCHED_FIFO[1]
    1 SCHED_OTHER[0]
    2 SCHED_IDLE[0]
    3 SCHED_BATCH[0]
    7 SCHED_IDLE[0]
    8 SCHED_IDLE[0]
    9 SCHED_IDLE[0]
    10 SCHED_OTHER[0]
    

    Die Zahl am Anfang der Zeile gibt die Reihenfolge an, in der die Threads erstellt wurden. Wie Sie sehen, haben die beiden Prioritätsklassen SCHED_FIFO und SCHED_RR immer Vorrang vor den drei üblichen Klassen SCHED_OTHER, SCHED_BATCH und SCHED_IDLE, und werden streng nach Priorität geordnet, was erforderlich ist. Die Tatsache, dass alle drei Benutzerklassen zu Beginn gleich sind, wird jedoch an keiner Stelle erwähnt, auch SCHED_IDLE, dessen Rechte im Vergleich zu der Standardeinstellung SCHED_OTHER weit unterlegen sind, wird vorangestellt, wenn es sich um die erste Benutzerklasse in der Warteschlange des Mutex handelt. Na ja, zumindest funktioniert alles, aber
    Solaris hat an dieser Stelle ein Loch
    Vor einigen Jahren habe ich diesen Test unter Solaris ausgeführt und festgestellt, dass Thread-Prioritäten vollständig ignoriert werden und Threads in einer völlig zufälligen Reihenfolge aufwachen. Ich habe dann den technischen Support von Sun kontaktiert, aber eine überraschend verschwommene und bedeutungslose Antwort erhalten (zuvor haben sie bereitwillig mit uns zusammengearbeitet). Zwei Wochen später war Sun weg . Ich hoffe aufrichtig, dass nicht meine Bitte der Grund war.

    Für diejenigen, die mit Prioritäten und Klassen verhandeln möchten, befindet sich der Quellcode an derselben Stelle .

    Verzögerte TCP-Pakete


    Wenn die vorhergehenden Beispiele als angenehme Überraschung angesehen werden können, dann ist diese kaum angenehm.
    Die Geschichte begann vor einigen Jahren, als wir plötzlich feststellten, dass einer unserer Server, der Clients einen kontinuierlichen Datenstrom sendet, periodische Verzögerungen von 40 Millisekunden aufweist. Dies geschah selten, aber wir konnten uns einen solchen Luxus nicht leisten, so dass ein ritueller Tanz mit einem Schnüffler und anschließender Analyse durchgeführt wurde. Achtung , wenn im Internet diskutiert wird, hängt dieses Problem normalerweise mit dem Nagle-Algorithmus zusammen. Unseren Ergebnissen zufolge ist es falsch , dass das Problem unter Linux auftritt, wenn verzögertes ACK und langsamer Start interagieren . Erinnern wir uns an einen anderen Klassiker,Richard Stevens , um die Erinnerung aufzufrischen.
    delayed ACK ist ein Algorithmus, der das Senden der ACK an das empfangene Paket um einige zehn Millisekunden verzögert , in der Erwartung, dass ein Antwortpaket sofort gesendet wird und die ACK in dieses Paket eingebaut werden kann, um den Verkehr leerer Datagramme über das Netzwerk zu reduzieren. Dieser Mechanismus funktioniert in einer interaktiven TCP-Sitzung und war 1994, als TCP / IP Illustrated herauskam , bereits ein Standardbestandteil des TCP / IP-Stacks. Was für das weitere Verständnis wichtig ist, kann die Verzögerung insbesondere durch das Eintreffen des nächsten Datenpakets unterbrochen werden, wobei die kumulative ACK für beide Datagramme sofort gesendet wird.
    Langsamer Start- Ein nicht weniger alter Algorithmus zum Schutz von Intermediate-Routern vor übermäßig aggressiven Quellen. Der sendende Teilnehmer zu Beginn der Sitzung kann nur ein Paket senden und muss auf die Bestätigung des Empfängers warten, wonach er zwei, vier usw. senden kann, bis er gegen andere Regulierungsmechanismen verstößt. Dieser Mechanismus funktioniert offensichtlich bei starkem Datenverkehr und wird deutlich zu Beginn der Sitzung und nach jeder erzwungenen Weiterleitung des verlorenen Datagramms aktiviert.
    TCP-Sitzungen können in zwei große Klassen unterteilt werden - interaktive (z. B. Telnet ) und umfangreiche ( Bulk-Verkehr, z. B. FTP)) Es ist leicht zu bemerken, dass die Anforderungen an verkehrsregelnde Algorithmen in diesen Fällen häufig entgegengesetzt sind, insbesondere widersprechen sich die Anforderungen "ACK halten" und "ACK abwarten" offensichtlich. Im Fall einer stabilen TCP-Sitzung wird die oben erwähnte Bedingung behoben - der Empfang des nächsten Pakets unterbricht die Verzögerung und die ACK wird an beide Segmente gesendet, ohne auf ein durchlaufendes Paket mit Daten zu warten. Wenn jedoch eines der Pakete plötzlich verloren geht, leitet die sendende Seite sofort einen langsamen Start ein - sendet ein Datagramm und wartet auf eine Antwort, die empfangende Seite empfängt ein Datagramm und verzögert die ACK, da als Antwort keine Daten gesendet werden, bleibt die gesamte Vermittlung 40 ms lang hängen. Voilà.
    Der Effekt tritt gerade bei Linux-Linux-TCP-Verbindungen auf, bei anderen Systemen habe ich das nicht gesehen, es sieht nach etwas in deren Umsetzung aus. Und wie geht man damit um? Nun, im Prinzip bietet Linux die (nicht standardmäßige) TCP_QUICKACK- Option an , mit der die verzögerte ACK deaktiviert wird. Diese Option ist jedoch instabil. Sie wird automatisch deaktiviert , sodass Sie das Flag vor jedem Lesen () / Schreiben () setzen müssen . Es gibt auch / proc / sys / net / ipv4 , insbesondere / proc / sys / net / ipv4 / tcp_low_latency , aber es ist nicht bekannt, ob sie das tut, was ich vermute, dass sie tun sollte. Außerdem gilt dieses Flag für alle TCP-Verbindungen auf diesem Computer, nicht gut.
    Was sind die Vorschläge?

    Aus der Dunkelheit der Jahrhunderte


    Und schließlich der allererste Vorfall in der Geschichte von Linux, nur um das Bild zu vervollständigen.
    Linux hatte von Anfang an einen nicht standardmäßigen Systemaufruf - clone () . Es funktioniert genauso wie fork () , erstellt also eine Kopie des aktuellen Prozesses, wobei der Adressraum jedoch weiterhin gemeinsam genutzt wird. Es ist leicht zu erraten, wofür es erfunden wurde, und tatsächlich hat diese elegante Lösung Linux bei der Implementierung von Multithreading sofort an die Spitze des Betriebssystems gebracht. Es gibt jedoch immer eine Einschränkung ...

    Tatsache ist, dass beim Klonen eines Prozesses alle Dateideskriptoren, einschließlich Sockets, ebenfalls geklont werden. Wenn es zuvor ein ausgearbeitetes Schema gab: Ein Socket wird geöffnet, es wird an andere Flows übertragen, alle arbeiten zusammen, senden und empfangen Daten, einer der Flows entscheidet, den Socket zu schließen, alle anderen sehen sofort, dass der Socket geschlossen ist, und am anderen Ende der Verbindung (im Falle von TCP) sehen sie auch, dass der Socket geschlossen; was passiert jetzt Wenn einer der Threads beschließt, seinen Socket zu schließen, wissen die anderen Threads nichts darüber, da es sich tatsächlich um separate Prozesse handelt und sie über eigene Kopien dieses Sockets verfügen und weiterarbeiten. Darüber hinaus betrachtet das andere Ende der Verbindung die Verbindung auch als offen. Es gehört der Vergangenheit an, aber als diese Innovation das Muster vieler Netzwerkprogrammierer durchbrach, musste der Code unter Linux so ziemlich neu geschrieben werden.

    Literatur


    1. Maurice J. Bach. Das Design des UNIX-Betriebssystems.
    2. Robert Love. Linux-Kernel-Entwicklung
    3. Daniel P. Bovet, Marco Cesati. Grundlegendes zum Linux-Kernel
    4. Richard Stevens. TCP / IP illustriert, Band 1: Die Protokolle
    5. Richard Stevens. Unix-Netzwerkprogrammierung
    6. Richard Stevens. Erweiterte Programmierung in der UNIX-Umgebung
    7. Uresh Vahalia. UNIX-Interna: Die neuen Grenzen

    Hier könnte Ihr Link zu den behandelten Themen stehen.


    Die ersten Schwalben:


    Und ich bin wirklich gespannt, wie viel ich noch verschlafen habe und wie weit ich hinter dem Leben zurückbleibe. Lassen Sie mich eine kleine Umfrage einschalten.

    Nur registrierte Benutzer können an der Umfrage teilnehmen. Bitte komm rein .

    Wie wenig werden die Fragen aufgeworfen?

    • 2,4% Alles ist trivial und bekannt, genau so sollte es funktionieren. 28
    • 2,2% Grundsätzlich ist alles bekannt, aber das sind wirklich Abweichungen vom klassischen Unix 26
    • 53,4% haben etwas Neues gelernt. Interessant 621
    • 19,5% So etwas habe ich noch nie gehört 227
    • 9,9% Ich verwende hochrangige Bibliotheken und Frameworks. Solche Probleme nie auftreten 115
    • 1,2% In welcher Sprache ist es geschrieben? Verwenden Sie <....> und es wird keine Probleme geben 14
    • 11,1% Was ist Linux? 130

    Jetzt auch beliebt: