 Also Treiberentwicklung ist viele Jahre in C gemacht worden und es wird auch heute hauptsächlich noch so gemacht. C hat grundsätzliche Probleme und es gibt bessere Alternativen. Die kämpfen immer dagegen, dass sie wohl langsam sein sollen und schwierig zu benutzen sein. Das Team hier heute erklärt euch, dass das nicht wahr ist, dass ihr Treiber mit Hochsprachen schreiben könnt und dass sie sehr gut funktionieren. Dass sie eine sehr hohe Performance haben. Paul, Simon und Sebastian. Ich bin Paul und mit mir sind Simon und Sebastian. Ich rede schnell und entschuldigung zu den Übersetzern. Auf der rechten Seite könnt ihr den Playback-Speed verzögern. Auf YouTube kann man das wohl auch machen. Vielleicht habt ihr schon gesehen, dass wir hier drei Namen haben. Als viele Namen haben, das sind alle Leute, die hier mitgemacht haben. Das sind meine Studenten. Heute habe ich hier Simon und Sebastian und ich bin Forscher in der Uni München für Paketverarbeitung. Heute reden wir über Netzwerktreiber. Wir haben Netzwerktreiber als Studie verwendet, weil es unser Forschungsbereich ist. Ich habe letztes Jahr auch schon über Netzwerktreiber geredet. Hier ist nochmal eine Zusammenfassung vom letzten Jahr. Ich habe das XE Projekt vorgestellt. Das ist was, wo ich angefangen habe und zeigen wollte, dass man im Userspace-Tribe schreiben kann, die verständlich und schnell und lesbar sind. Also nicht mehr Tausende von Zeilen, von C-Codes, die Vollreferenzen und Perspektivikationen sind. Wir haben seitdem die Bedürfnisung für Wirteiro gemacht und auf geht, ob wir das anschauen. Damals habe ich den C geschrieben. Warum würde man den C schreiben? Das ist eine offensichtliche Frage. Ja, warum nicht? Das sind doch die meisten. Aber wenn man natürlich Ausbildung betreiben will, dann kann man das so machen wie alle anderen auch. Damit jeder das auch lesen kann. Ich denke, in ein paar Fällen kann es ja auch schön sein. In ein paar Fällen. Wer kann jetzt den C lesen? Das sieht so fast aus wie jeder. Lass uns mal ein bisschen C-Code anschauen. Das ist echt der Code von unserem Treiber, was ein Student hinzugefügt hat. Ich habe einen Polar Quest gekriegt und ich habe ihm gesagt, nee, du kannst das so nicht machen. Wir wollen ein lesbaren Code haben. Und dann haben wir es diskutiert und am Schluss haben wir es doch hinzugefügt, weil es keinen besseren Weg gibt. Wer kennt das und weiß, was es tut? Ich sehe eine Hand. Zwei Hände, mehrer Hände. Nicht schlecht. Wenn ich euch den Namen zeige, wisst ihr dann, was es ist. Wer kennt es jetzt? Ja, ein paar mehr. Das ist in Treibern um zu abstrahieren und es wird häufig genutzt um verschiedene Treiber zu abstrahieren. Ich habe es auf dem Bildungsplanen geholt und ich habe 15.000 Betutzungen vor diesem Makro gesehen und es ist nicht wirklich ungewöhnlich, dass man das so machen kann. Da gibt es einen Kommentar zu einem Blog-Post, der erklärt, was es tut. Aber es zeigt auch, dass viele Leute sie lesen können und dann sehen sie so was. Und plötzlich kann es nur hässlich sein, wie es aussieht. Es kann auch hässlich sein, wenn man die Security Issues anschaut. Hier ist ein Screenshot von den Fehlerberichten. Das sind alle Linux Kernelbugs über den letzten 20 Jahre. Warum ist diese Sprache schuld? Man kann doch so an jeder Sprache schlecht machen. Jetzt müssen wir durch alle Bugs durchgehen und schauen, ob wir die durch den Besser oder speichersichere Sprache verbessern könnten. Aber jemand hat es tatsächlich gemacht. Patler hat so ein Paper geschrieben und er hat ein Betriebssystem in Go entwickelt und er hat den Code Extrusion Bugs in Linux angeschaut und er hat geschaut, ob das vermindet werden könnte durch ein anderer Verprogrammiersprache. Und hier sieht man die Prozentzahlen 17, 21, 12 und 49 für die verschiedenen Fehler, ob die verhindert werden können oder nicht. Das sind 40 vermeidbare Fehler. Wir haben so angeschaut, wie viele davon in Treibern waren und wie viele in Kernel waren. 39 waren in Treibern und der andere war in Bluetooth. Und von den Treibern hatte der Qualcomm Treibern. 13 Fehler. Wer hätte das gedacht? Sollte ich in 2019 neuen Code in C schreiben? Wahrscheinlich nicht, wenn du eine Wahl hast, aber manchmal hast du keine Wahl. Man kann ja versucht in Kernelmodulen in Rast schreiben. Man kann auch versuchen, Kernelmodulen in JavaScript zu schreiben. Und wenn es funktioniert, ist es trotzdem eine schlechte Idee. Und das ist warum wir auf User Space Treiber schrauben, weil die in jeder Sprache geschrieben werden können. Und jetzt ist die Frage, sind alle Sprachen gleichgute Möglichkeiten? Welche Sprache sollte ich wählen? Wenn jetzt ist ein Git Compiler oder ein Garbage-Collector ein Problem in einem Treiber. Und ich habe gesagt, wir sollten Treiber in allen Treibern fragen. Und ich spreche aber nicht alle Sprachen. Und deswegen habe ich ein paar Studenten gebeten. Hier ist ein screenshot von meiner Webseite, wo ich die Dissertation aufliste und da lasse ich die Studenten in allen möglichen Sprachen diese Treiber schreiben. Meine Kollegen haben auch mich geschaut. Meinst du das wirklich? Es ist immer wieder alles gleiche. Und manche wissen immer noch nicht, ob ich es ernsten meine, aber ich meine es ernst. Und ich habe mit 30 Studenten gesprochen, die alle was damit machen wollten. Und die beiden hier waren die Ersten. Und ich habe versucht, sie wegzuschicken. Es wird schwierig werden und du kannst irgendwann was einfacheres machen. Du musst viele low-level Details wissen. Und 20 habe ich geschafft, wegzuschicken. Und wir haben zehn Stück machen lassen. Und wir haben schöne Ergebnisse. Ein paar laufen noch. Und dann haben wir jetzt zehn verschiedene Sprachen bald. Wir haben jetzt ungefähr sechs oder sieben Mal beschlossen, je nachdem, wie man zählt. Und hier ein Vortrag zu halten ist natürlich auch schön, weil ich hier neue Studenten kriege, weil viele Studenten hier meinen Vortrag gesehen haben und mich hinterher kontaktiert haben. Und wie bräuchte ich die jetzt dazu in Treiber zu schreiben? Ich habe denn die Grundlagen erzählt, wie man mit einem modernen Gehalt spricht und es gibt drei verschiedene Möglichkeiten mit dem Ding zu reden. Wir ignorieren ein paar alte Sachen, wie man benutzen kann. Man kann also mit dem Gerät über MemoryMap AI aussprechen. Das ist eine besondere Speicher gegen die in meinem Prozess wohnt und die direkt an das Gerät geht. Und wenn ich lese oder schreibe auf diesen Bereich, dann kriegt das Gerät meine Anfragen und kann darauf antworten. Und in Unix kann ich also ein besonderes File über das AI auf subsystemansprechend. Das zweite ist Direct Memory Access, wie das Gerät mit mir redet. Da kann das Gerät beliebige Speicherbereiche lesen und schreiben. Und für User Space Treiber muss ich nur schauen, wie ich das Gerät anweise, das an der richtigen Stelle zu schreiben. Und das dritte ist mit Interrupts. Die nutzen wir hier nicht. Die brauchen wir nicht für High Speed Netzwerk Treiber. Aber das ist hier drauf, weil die Leute sagen, man kann Interrupts nicht von User Space nutzen, aber man kann es doch mit dem WFIO Framework. Ich habe meinen Studentenfolgendes erzählt, wie sie Treiber schreiben sollen. Sie sollten den Kernel Treiber entfernen. Sie sollten einen Magic-Imagischen-Map aufrufen machen und dann die physikalischen Adressen für das DMA herausfinden und dann den Treiber schreiben. Wir haben viel Hardware auf der Universität und wir geben den Zugang zu Servern. Hier haben wir eine 10 Gigabit Netzwerkarte von Intel. Wir sind häufig an Bord oder die sind manchmal sogar in den CPUs drin. Und da gibt es schöne Datenblätter, die sind öffentlich fügbar. Wir haben einfache Funden. Wir haben einfache Funden gegen die Datenblätter zu programmieren als gegen die WIRTIRO-Spezifikation. Diese Netzwerkarte ist auch schon wieder 10 Jahre alt, aber es ist ein schöner Low-Level. Da tauscht man Messages mit einer Firmware aus und das ist langweilig. Aber hier hat man einen schönen Low-Level-Zugang zu der Karte. Wenn man mit der Firmware redet, dann fühlt man sich nicht, als ob man ein Treiber selber schreibt. Das war dann quasi so die Basics, wie ich meine. Jetzt gebe ich an Sebastian ab, der euch ein bisschen C-Code zeigen wird, wie man den Treiber ins Zug schreiben würde. Und dann geht es weiter mit dem Hochsprachen. Danke Paul. Wir haben euch gerade gezeigt, was wir so im Allgemeinen machen müssen, um den Treiber zu schreiben. Wir gehen noch mal ein bisschen ins Detail. Wir müssen natürlich erstmal rausfinden, wo die PCI-Adressen von den Geräten sind, dass man mit LSPCI und dann bekommt man eine Liste von den PCI-Geräten und dann sucht man im Prinzip einfach nur nach Intel Corporation 80509. Irgendwas aussieht wie das hier. Und ja, dann steht da die Adresse am Anfang der Zeile. Sieht ziemlich ähnlich aus wie eine Mac-Adresse, aber das ist tatsächlich die Adresse, die wir brauchen. Und ja, super. Jetzt haben wir die Adresse. Wir können dann mit diesem Kommando den Treiber aus dem Kernel entfernen und im nächsten Schritt dann die PCI-Express-Register in unseren Adressraum einblenden. Dafür benutzen wir M-Map. Das heißt, wir öffnen wieder diesen magischer Teil. Man sieht die Adresse im Fahrt und machen dann M-Map auf einem File-Deskriptor. Im Prinzip braucht man diese Möglichkeit in jeder wirklichen Hochsprache, indem man das dann verwenden will, braucht man irgendwie M-Map, um das machen zu können. Ja, dann ist die nächste Sache, sich um die Geräte-Register zu kümmern. Das hier ist ein Extract aus einem der Datenretter. Und man hat dann hier immer die Registernamen, Register-Offsets. Und das heißt, man kann einfach durch das ganze Dokument gehen und die Register suchen, die man braucht und dann lesen und schreiben, was man eben braucht und treibbar. Ein kleines Beispiel. Die LEDs blinken lassen. LEDs sind ja an den ganzen Netzwerkkarten immer dran und wir können die blinken lassen. Das geht im Prinzip so, wir besorgen uns die Baseladresse von unseren Registern und dann jeweils das Offset, das wir im Wartenblock gefunden haben und dann ändern wir einfach das Bit und das macht die LED an oder aus. Ja, hier vielleicht so als Randbemerkung, das ist einer der wenigen berechtigten Nutzen von Volatile in C, denn wir wissen hier halt, Compileroptimierung verhindern, damit wir hier garantiert auch den Speicherzugriff haben, um tatsächlich dann auch die LEDs bringen zu lassen. So, sie kümmern wir uns um Paketbehandlungen mit DMA. Pakete werden über sogenannte Cues, also Schlangenübertragung, ich werde es in Forme Cues nennen und die werden dann in so einer ringartigen Struktur organisiert und diese Ringe werden über Memorymap.io konfiguriert und dann über DMA zugegriffen. DMA heißt Direct Memory Access, direkter Zugriff auf den Speicher. Diese Ringe enthalten dann Pointer zu den Paketen, die man dann auch wieder über DMA zugreifen kann. Die Details variieren hier etwas zwischen den verschiedenen Geräten, aber grundsätzlich ist das sehr ähnlich für jedes PC Express Device, also Gerät, also das ist gar nicht spezifisch für die jeweiligen Netzwerke, für Netzwerker hat es meistens so. Was ist schwieriger in Hochsprachen? Also wir haben gesehen, wir brauchen diesen M-Map aufruf mit den richtigen Flex und eine andere Sache ist, dass wir in der Sprache irgendwie extern anizierten Speicher behandeln müssen, also zum Beispiel den eingeblendeten M-Map-Speicher. Wir müssen das Memory-Layout, also Speichelayout genau kontrollieren und wir müssen Speicherzugriffs-Demantiken kontrollieren können, also zum Beispiel Wörterteil, was wir im NC gesehen haben oder Speicherbarrieren und es ist meistens unvermeidbar, dass man irgendwo unsicheren, also Unsafe Code schreibt, weil ein paar Operationen einfach inneren Unsafe sind. Und das Ziel ist es im Prinzip diesen Unsafe Code an so wenigen Stellen wie möglich, so klein wie möglich zu halten, um eben weiter die Vorteile der Hochsprache und die Garantie in der Hochsprach-Handel zu zu können. Jetzt gebe ich noch mal zurück zu Paul zurück und der sagt jetzt noch mal was zu implementieren, unsere Implementierung. Also ja genau, das war im Prinzip das, was ich meinen Studis gesagt habe zur Implementierung, was ich haben möchte. Ich wollte das same Feature Set haben, wie der C-Triver, der als Referenzimplementierung galt und der sollte ungefähr die selbe Struktur haben, wie der C-Triver, aber gleichzeitig sollte das trotzdem idiomatischer Code sein. Also sollte so ausländisch für die Sprache geschrieben worden. Und dann ist immer die Frage, wie viele Sprachfeatures können wir jetzt nutzen, wie viele Sicherheitsfeatures der Sprache können wir jetzt nutzen, ohne die Performance zu verlieren, die wir haben wollen. Und ja, die Idee ist schon, dass wir nachher dann drei Beimplementierungen nachher in den verschiedenen Hochsprachen haben, die wir quantitativ vergleichen können und dann gibt es nochmal separate Details, so Speicherheit, Sicherheit und so weiter, was ist alles garantiert für die jeweilige Implementierung. Und jetzt schauen wir uns ein paar von diesen Sprachen an. Spezifisch red ich immer nur, immer nur ein bis zwei Minuten über die Implementierung in den Sprachen von den Studien, die sie nicht hier sind. Und dann schauen wir uns etwas detaillierter Code und was an. Also, wie gesagt, jetzt ein schneller Überblick, C-Sharp. Wir haben ein Studi gefunden dafür, also haben wir es auch gemacht. Wir haben jetzt keinen Driver für Windows entwickelt, sondern es gibt ja Microsoft Core CLA für Linux, also das funktioniert auch super. Für die, die die C-Sharp nicht kennen. C-Sharp ist eine just-in-time-kompilierte Sprache. Es gibt Garbage Collection und sie ist speicher-sicher. Also, hat Speicher-Sicherheit garantieren, aber sie hat eine etwas obskure Unterstützung für Unsafe Code und das heißt, man kann da Code schreiben, dass er eben hier aus der WC und man muss dem C-Sharp-Kompiler dann einfach nur sagen, hey, ich werde Unsafe Code benutzen. Aber wie kann man einen externen Speicher zugreifen? Es gibt da eben ein paar schöne Rapper, also zum Beispiel den Unmanaged Memory Stream, aber die Typen sind leider viel zu langsam für uns und deswegen haben wir ein Unsafe-Teil benutzt. Man sieht dann hier, das ist Unsafe Keyboard und das sieht im Prinzip jetzt alles ziemlich so aus WC und führt sich auch so an WC und ja, ist halt eben eine schöne Sprache, um die Treiber zu schreiben und der Vorteil ist aber, dass der Unsafe Code an einigen wohl bekannten und durch das Schlüsselwort markierten Stellen ist, das heißt, es ist sehr viel der Einziger einfach zu auditieren als ein ganzen Haufen C-Code, wo er der Unsafe Code von der Zeit über ist. Dann eine andere untypische Sprache ist Swift. Swift wollte auch ein Studio machen. Okay, habe ich gar nicht so lange gedacht. Okay, können wir ja machen. Und ja, also wir haben auch kein MacOS oder IOS-Triver geschrieben, aber es gibt Swift halt auch für Linux. Swift deskompiliert mit LLVM und Speicherverwaltung ist über Referenzzählen implementiert und die ist sparenprinzipiell auch Memory Safe. Jetzt müssen wir wieder mit Pointeren irgendwie rumgehen, das heißt, es gibt dann etwas, das heißt, Unsafe-Buffer-Pointer und so, dass man das dafür verwenden kann. Und ja, dann nutzen wir eben genau das, um zum Beispiel DMR zu machen. Und hier sehen wir dann hier, wie Speicher in einem Unsafe-Buffer-Pointer-Rapper eingepackt wird. Und das heißt, ja, da schreibt man eben auch rein, wie groß dieser Buffer ist und dann macht er eben genau die nötigen Bound Checks, also check, dass man nicht auf Speicher zugreift der Go ist als der Go des Buffers. Ja, wenn man jetzt C, wenn man es jetzt ein bisschen mit C sharp und so vergleicht oder C, dann ist das alles ein bisschen elaborierter der Code dafür. Aber ja, es geht. Also auch, also man kann mit Swift auch drei Verschreiblichungen. Dann gibt es noch für die funktionale Programmierung. Da haben wir eine voll funktionierende Implementierung in OCaml. OCaml ist auch eine komplette Sprache, gibt auch Speicherverwaltung mit Garbage Collection da drin, Speicher Safe und ein sehr cooles Feature C-Struck Library, die erlaubt, dass man das Speicher Layout so spezifiziert, wie man das hier sieht. Das sieht halt genauso aus wie ein C-Struck, also ein C-Struktur-Deklaration. Und ja, das generiert dann halt eben einen innovaten Struktur automatisch mit den richtigen Offsets. Das ist sehr, sehr viel angenehmer als zum Beispiel in Swift, wo man die ganzen Offsets hartcoden muss, wie der C-Compiler das halt dann machen würde. Ja, und dann der Code in OCaml sieht dann halt ein bisschen anders aus, als man das sich vielleicht vorstellt bei dem Vertreiber-Code, dass hier ist eine Funktion, die zählt, wie viele Pakete empfangen wurden, indem man Flags vom Recife-Ring checkt. Das ist in der GetRxWb-Status-Routine, die eben dann so diesen Status-Wert holt, den Flag-Datron checkt und dann was aufruft, also den Wert implementiert und das zurückgeht. Noch mehr funktionale Programmierungen haben wir auch noch. Eine funktionale Programmierung in Haskell ist eine kompetente Sprache, auch wieder Garbage Collection und Speicher-Sicher und Funktionale. Und ja, schöne Eigenschaft von Haskell ist, dass ihr vielleicht nicht kennt, dass man SystemPausex.memory-Package mit dem kann man im Vergleich zu OCaml tatsächlich sehr viel mehr in Haskell machen, als in OCaml, OCaml mussten wir ein bisschen C-Glucco schreiben dafür und das andere Ding, was wir in Haskell sehr gerne benutzt haben, waren die Sum-Types, denn was es in Haskell gibt sind, ja, ihr kennt vielleicht Unions in C, wo man dann einen Speicherbereich mehrfach benutzt, z.B. für Lesen oder Schreiben, weil man weiß, es wird immer in einem Kontext verwendet und im Vergleich dazu bei den Sum-Types funktioniert das anders. Ja, bei Sum-Types hat man im Prinzip was wie C-Unions nur, dass es tatsächlich typisch ist. So, das waren die Sprachen von Studenten, die nicht hier sind. Und ihr könnt alle Implementierung auf GitHub anschauen. Und jetzt gebe ich weiter an Sebastian, der Go-Implementierung vorstellt. Ihr habt ein paar Fragen gesprochen, wir sehen und jetzt kommt Go. Was ist denn Go? Go ist eine kompellierte Programmiersprache von Google, die ist für alle Anwendungsbereiche und häufig für verteilte Systeme. Ein Treiber ist kein verteiltes System, warum sollten wir Go benutzen? Go bietet verschiedene interessante Sachen an. Ein Garbage-Collector, ein Typen und Speichersicherheit und hat eine sehr große Standardbibliothek. Also wir brauchen keinen anderen Code außer der Standardbibliothek. Und wie macht man das in Go? In vielen Fällen ist es so wie C. Die großen Unterschiede sind, es gibt keine Punkte arithmetik, aber wir haben arithmetik. Und das brauchen wir für DMA-Speicher. Und wir haben keinen Volatil-Befilm für Speicherbarrieren. Was sollen wir sonst machen? Wir verwalten den DMA-Speicher über Slices, über Scheiben. Und wir haben Unsafe Pointers, das sind beliebige Pointer. Und die gehen um die Run Time außen rum, das heißt wir müssen sehr vorsichtig sein. Und das benutzen wir für physikalische Adressberechnungen und Registerzugriffe. Und es gibt eine Menge Regeln für Unsafe Pointers, das heißt die sind immer noch grültig. So, wir verwalten den DMA-Speicher in Pools. Hier sieht man, wie man den Mempool initialisiert. Und Mempool.buff ist eine gesamte, gemärbte Bereich. Und hier sage ich, ich möchte den Packetbuffer, das hast du, das ist relativ einfach. Und unten kommt der Unsafe Pointer, wo ich die physikalische Adresse berechne. Wir müssen unsere virtuellen Adressen in physikalische Adressen umwandeln, die die Netzwerkkarte für das tatsächliche Send und Empfang von Paketen verwenden kann. Die Run Time überprüft viele Sachen. Und hier muss man typen explizit umwandeln. Und hier sieht man, wie ich den Pointer in einen Unsafe Pointer umwandle. Und dann kann ich ihn in einen integer type umwandeln. Und der UND-Pointer ist was wir nutzen. Etwas, was ich noch brauchte, ist ein Volatile, weil ich mit der Netzwerkkarte Register teile. Und ich brauche eine Compiler-Speicherbarriere, was für ihn gehört. Die Sync und Atomic Funktionen geben einige, die da garantieren. Und es kostet uns hier an dieser Stelle keine Performance. Und deswegen nutzt das einfach. Das wir nutzen Atomic Store und Atomic Load für Integers. Und damit haben wir unsere Speicherbarrieren zusammenfassend. Habe ich gedacht, Go ist eigentlich sehr schön. Die Sicherheit ist besser geworden. Und Cutler, so haben wir auch einen Kernel da drin geschrieben. Es gibt ja einige Sicherheitsgarantien. Das ist meine persönliche Meinung. Aber es sieht aus wie C-Code und ist trotzdem hübsch. Im schlimmsten Fall ist es 10%, langsamer als C. In den optimaleren Fällen ist es noch schlechter. Und der Zugriff auf die Deskriptoren kann hässlich sein. Und jetzt kommen wir zu Simon, der das in Rust gemacht hat. Was ist Rust? Auf der Website steht, das ist eine sichere Sprache, die praktisch zu benutzen ist und Fokus auf Konkurrenz hat. Und auf Systementwicklung. Rust unterscheidet sich ein bisschen von Go. Rust hat keinen Garbage-Collector, hat ein ziemlich einzigartiges Ownership-System. Also, man verfolgt, welcher Teil von Code gerade besitzt eine Variable hat. Entsprechende Move und Boring-Regeln. Und es hat einen unsafe Modus wie in anderen Sprachen und z.B. wenn sie sich hat. Was ist das Ownership-System? Das ist im Prinzip das Kernfeature von Rust. Es besteht eigentlich aus drei einfachen Regeln, nämlich Regel 1. Jeder Wert hat eine Variable, die gerade ihr Besitzer oder ihr Owner ist. Und es darf immer nur einen Besitzer zu dem möglichen Zeitpunkt geben. Und wenn der Besitzer out of scope geht, d.h. wenn die Variable lokal irgendwie weggeht oder so, dann wird der Wert freigegeben. Und diese Regeln werden zur Compile-Zeit durchgesetzt. Wir haben keine Performance-Strafen oder Performance-Probleme zur Laufzeit, keine Performance-Zerlaufzeit. Und das Ownership-System ist dann eben für die Speicher-Zugriff-Sicherheit da. Es gibt Package-Straks für unsere Netzwerkpakete, die dann für DMA benutzt werden. Und die werden inzwischen zum User-System den Treiber hin und her gereicht. Und die Implementierung ist so, dass die Ownership eben dann eben auch durch die Gegend gegeben wird, also zwischen User und Treiber. Und d.h. es kann nicht passieren, dass sie da, also durch die Sprachgarantäne kann es passieren, dass es nicht passieren, dass das Paket gleichzeitig von User oder vom Treiber benutzt wird. Hier sieht man dann, wie man den Treiber benutzen kann. Hier ist zum Beispiel das Treiber-Interface. Also man sieht, dass hier einfach keine Möglichkeit da ist, Fehler zu machen, systematisch. Und wenn Pakete dann out of scope gehen, werden sie zurück an den Speicher-Pull von unserem Treiber gegeben. Und das ist absolut safe an der Stelle. Unglücklicherweise gibt es aber auch Unsafe-Code im Treiber, wenn, wie gesagt, man kann nicht alles in Sicherung-Restcode machen. Also zum Beispiel Funktionen, die außerhalb von Rust sind, also um WC-Funktionen aufrufen. Und wo Pointer zu de-referenzieren, ist per Definition Unsafe. Und ja, dann kann man das eben so markieren mit diesem Unsafe-Statement. Hier sieht das dann unserem Treiber aus. Wir haben die SetRegister-Methode, die genutzt wird, um Register von dem Gerät zu setzen. Und weil wir benutzen da Pointer WriteVolatile, um eben dann an diesem Adresse zu schreiben. Und bevor wir das machen, haben wir Assertions in unserem Code die checken, dass die Adresse, auf die wir schreiben, ist tatsächlich in dem Speicher-Bereich ist, der gemappt wurde. Ja, also haben wir jetzt hier tollen Code, läuft er auch schnell. Um das rauszufinden, haben wir einen Testbed aufgebaut. Nehmen wir dann unsere Treiber-Benchmarken-Konten. Wir haben zwei Server, einen Paketgenerator und das Gerät, das wir gerade testen. Das ist ein Direktional verbunden mit 10-Gear-Bit-Verbindungen. Und wir benutzen MoonGen, das ist ein Paketgenerator, der von Paul geschrieben wurde. Natürlich, weil das ist natürlich der beste Paketgenerator. Und das Gerät und das Test, also das Gerät, das wir gerade testen, da haben wir eben einen, der Anwendungsfall, den wir dann auf unseren Treiber drauf implementiert haben, war, wie Direktional da Paket weiterleitet. Das heißt, einfach nur Pakete auf der einen Seite reinkommen, auf der anderen Seite rausgeben. Dass wir es der Durchsatz von unserem Vorworder auf der X-Achse, sehen wir das, die CPU-Geschwindigkeit auf der Y-Achse, die Anzahl von Paketen pro Sekunde. Wir schauen uns Pakete pro Sekunde an, weil der Hauptoverhalt pro Paket ist und nicht pro Byte, das wir transportieren. Und was wir tatsächlich schaffen sind, also die Obergrenze jetzt in den Grafen sind 30 Millionen Pakete pro Sekunde. Und das ist quasi das, was überhaupt möglich ist bei minimaler Paketgröße. Man sieht die Plots für die verschiedenen Sprachen, das Linie auskaliert mit verschiedenen CPU-Geschwindigkeiten. Man sieht halt, wie die Performance ist, z.B. bei Rust. Man nennst ja normalerweise die CPU-Geschwindigkeiten nicht normalerweise, sonst ist es halt automatisch. Und dann gibt es irgendwas anderes, was wir machen können noch. Man kann z.B. die Batch-Große ändern, also wie viele Pakete man auf einmal sendet, wie viele Pakete man auf einmal in das PCR-Express gereizendet. Und da gibt es dann eben halt Overhead, den man damit reduzieren kann. Ein Kernel wird normalerweise eine Batch-Große von 1 verwendet und höhere Batch-Großen sind dann normalerweise eine der Gründe, warum Users-based-Triber schneller sind, weil die höhere Batch-Große verwenden. 64 Pakete pro Batch ist normalerweise eine sehr gute Batch-Große. Denn ab größerer Batch-Großen hat man normalerweise dann mehr Cashmiss, dann geht die Performance wieder runter. Ja, Swiss-Perform ziemlich schlecht und Paul erklärt euch gleich auch, warum das funktioniert, warum das so schlecht performt. Was macht man? Man lässt einen Profiler drüber laufen, man kriegt viele Daten und man visualisiert die. Und häufig macht man das mit einem Flamegraph. Die X-Achse ist die Zeit, die in Funktion verbracht wird und die Y-Achse ist die Tiefe des Crossbacks, das sind hier nur die Funktionsnamen. Wenn man sich die Obersten anschaut, dann schaut man die Blätter an und dann kann man die charakterisieren. Man kann schauen, was macht es da drin. Und wir haben herausgefunden, es hängt an einer Speicherverwaltung. Anders ruft jedes Mal den Referenzzähler auf. Und wenn man eine UI schreibt, ist das kein Problem. Aber wenn man ein Driver schreibt, was die ganze Zeit durch Millionen von Paketen an Funktionen geht, dann verbringt das 76% seiner Zeit in diesem Release-Retain. Und es wäre besser, wenn es den Speicher anders verwandeln würde. In Go verbringen wir weniger als ein halbes Prozent der Zeit im Garbage-Collector. Der große Vorteil von Schwifts Referenzzählen ist, dass es keine unvorhergesehenen Pausenzeiten gibt. Das heißt also, der stoppt nie. Und jetzt ist die Frage, ist es eine gute Idee, braucht man ein Garbage-Collector? Und wir haben eine weitere Anwendung. Und da haben wir die Latents der Pakete gemessen. Und die Latents wird gemessen bei 16 Millionen Paketen pro Sekunden. Und hier ist Verteilungsfunktion aus Rust. Und es ist fast eine normale Verteilung. Die G hängt ungefähr auf acht Mikrosekunden. Und ein Hardware-Sitch braucht normalerweise eine Mikrosekunde. Aber acht Mikrosekunden für den Software-Fordering ist echt schön und schnell. Jetzt schauen wir uns die anderen an. Go ist ein bisschen ähnlich. C-Sharp ist ein bisschen langsamer oben. Aber dieser Graph, der sieht ein bisschen komisch aus. Deswegen lasst mich das erklären. Wenn er nicht mit den CDS verteilt sein. Wir schauen auf 0,50 Prozent. Dann gehen wir zur Sprache rüber. Und dann gehen wir nach unten zur X-Achse. Und das heißt also, 50 Prozent der Pakete brauchen weniger als 8,9 Mikrosekunden. Und die anderen 50 Prozent brauchen länger. Wenn man jetzt die Latents anschaut, wo komische Spitzen drin sind, hier das 99 Prozent für C-Sharp. Also 1 Prozent, weil C-Sharp brauchen länger als 30 Mikrosekunden. Und 1 Prozent ist eine ganze Menge. Das ist 1 in 100. Und wenn man in Millionen pro Sekunde macht, dann passiert da echt was. Wir wollen aber jetzt schauen, wenn man jetzt reinzoomt, dann ändert man häufig die Achse zu logarithmisch. Wir wollen nur ein Teil des Graphen sehen. Jetzt sehen wir hier die komplementäre Verteilungsfunktionen logarithmisch. Und die steht jetzt auf dem Kopf. Wir wollen aber jetzt das 99.999 Prozent anschauen. Und die steht jetzt auf dem Kopf. Aber das ist, was man in der akademischen Publikation über Latents sieht. Der ist zwar ein bisschen verwirrend, aber man sieht die Prozente. Okay, ganz unten. Das ist ein Paket in der Million. Wir können die Achsenbeschreibung stellen und jetzt drehen. Vorher war es die Y-Achse und jetzt ist die Y-Achse die Latents und die X-Achse das Prozent. Und jetzt können wir 99.999 anschauen. Jetzt können wir schauen, welche Latents haben die an dieser Stelle. Da kann man sehen, ob das Latents spitzen hat. Wir können sehen, dass Leute die Latents messen und dann die durchschnittliche Latents zeigen. Die meisten wissen es wahrscheinlich nicht besser. Aber wenn du es messen willst, dann bitte publiziere sowas hier. Es gibt ein HDR-Histogramm, das kann diese Grafen generieren von einem Latentsmesser. Und dann kann man eine schöne Beschreibung machen. Wir haben ein Driver, der ist schön und schnell und hat relativ niedrige Latents. Aber wir haben die Sicherheit und noch nicht angeschaut, die über die Sprache hinausgeht. Unser Driver läuft als Ruht. Und die meisten User-Spaces-Driver laufen als Ruht. Warum ist das so? Wir haben ein Driver, die initialisieren, die braucht einfach Routrechte, wie zum Beispiel PCIe, Ressourcen zuzugreifen. Das braucht das. Der DMA war verbraucht für die Belegung. Speicherlocken braucht Ruht. Aber es gibt noch alles, die initialisiert werden müssen. Die offensichtliche Idee ist, wenn man ein kleines Programm machen könnte, was diese Sachen für uns macht und dann die Privilegien wegschmeißen, können wir das machen. Das kann man einfach machen. Aber das ist noch nicht sicher. Um das zu erklären, müssen wir High-Level drauf schauen, wie High-Level-Speicherzugriff funktioniert auf einem modernen System, dass es wie ein modernes System aussieht. Darauf haben wir die CPU. Da läuft die Anwendung links unten. Das PCIe Express-Device. Das hat ein bisschen Speicher. Wenn wir das Gerät zurück machen wollen, dann geht es einmal durch die Memory Management Unit. Die übersetzt die virtuellen Adressen in meinem Programm zu den physikalischen Adressen, die vom Memory Controller benutzt werden können. Die Isolierung zwischen Prozessen wird durch die MMU garantiert und kontrolliert. Wenn wir das Gerät zugreifen wollen, dann gehen wir auch durch die MMU. Dann weiß die MMU, dass es nicht zum Speicher geht, sondern es redet mit dem Gerät. Das ist auch wunderbar. Das ist alles, was wir brauchen. Wo ist das Problem? Das Problem ist, wenn wir dem Gerät sagen, dass wir etwas mit Speicher machen sollen, dann nehmen wir physikalische Adresse. Das Gerät geht nicht durch die MMU. Es kennt nicht das Konzept, dass es so gebraucht wird. Das hat vollen Zugang zum ganzen Speicher. Wenn du das Programm manipulierst, dann ist es eine triviale Übung von wichtigen Bereichen, auf die man nicht zugreifen soll. Also jede Anwendung, die auf ein PCI Express Gerät draufpasst, zugreift, hat Routrechte. Egal, ob das Betriebsystem das so sieht oder nicht. Deswegen gibt es in der Mitte die IO MMU. Das ist auf jeder modernen CPU. Das hat Hardware-Virtualisierung. Das ist auf jeder modernen CPU. Das hat Hardware-Virtualisierung. Weil es häufig dafür verwendet wird, um PCI Express-Devices an virtuellen Maschinen weiterzureichen. Wir müssen dem Clan Hotel erzählen, dass die Einschränkungen richtig eingestellt sein müssen. Wir müssen es halt so einstellen, dass die IO MMU, dass das Smartphone, dass das Smartphone, dass das das richtig Rechte drauf hat, und dann ist es sicher, dass selbst, wenn mein Programm übernommen wird, dann kann er nichts außerhalb dessen machen, was es machen soll. Nun, ich hab also irgendwann mal den Server gekillt, als ich bestimmte Bereiche überschrieben hab. Und wenn ich! die Bereiche überschrieben haben und wenn ich von Anfang an die IOMMU genutzt hätte, dann hätte ich halt nicht diese Bereiche überschrieben und die nicht den 12er neu installieren müssen. In Linux gibt es das Virtual IOMMU Subsystem und das muss man einmal als Rot vorbereiten. Dann kann man das Gerät an diesen Treiber dranhängen. Man kann den Benutzer ändern, kann einen unprivilegierten Benutzer draus machen. Dann kann man diesem Benutzer das Recht geben, Memory zu belegen und zu locken für MDMA. Und dann kann der Rest, als dieser unprivilegierte User gemacht werden, der MDMA besser drauf aufrufen kann, MDMA auf diesem neuen Gerät aufrufen und kann dem Kernel über IO Controls sagen, was es tun soll. Und danach kann man das Gerät so benutzen, wie man es zuvor benutzt hat und wie es innerhalb der Parameter aufgesetzt worden ist. Die IOMMU überprüft die Zugriffe und von dem Benutzer kann man diese IMUs nicht umkonfigurieren und nur die Bereiche zugreifen, die man auch zugreifen darf. Wir haben das in unserem C-Treiber umgesetzt, der Student, der das umgesetzt hat, der ist hier, aber der wollte hier nicht auf die Bühne kommen, aber ihr könnt hier kontaktieren. Und jetzt haben wir einen tollen Treiber, der ist sicher und alles. Und jetzt kommen viele Leute, die sagen, ja, ich habe schon ein Treiber im Kernel, ich brauche keinen User Space Treiber. Ich hatte viel Spaß, warum sollte man das nicht machen? Dann brauchst du vielleicht nur einen Quick and Dirty Treiber für ein komisches Gerät. Du möchtest schnelle Entwickler zyklen, wo du nicht die ganze Zeit Neuboten möchtest, wo man vielleicht entwickel ich ein spezialisiertes Gerät oder ein FPGA. Und dann muss ich Kernel machen. Oder ich brauche ein Feature, das ist noch nicht im Device Treiber, aber das ist schon auf der Hardware. Zum Beispiel konnte man IPsec auf Loading machen, das war nicht im Open Source Treiber. Und jetzt gibt es Hardware Time Stamping. Die Latentsmessungen, da wollten wir Timestamps von Timestamps auf Nanoseck und Genauigkeit machen. Und dafür musste man, wir haben net FPGAs in der Vergangenheit benutzen. Aber das ist sehr teuer. Und aus Benutzer Sicht möcht man ja eigentlich nur ein paar Timestamps haben. Wir wollten das mit billigen Commodity-Netzwerkarten machen. Und einige von den Neueren haben ein Hardware-Feature, wo man ein Timestamp zu dem Buffer hinzufügen können. Aber keiner der aktuellen Treiber unterstützt das. Aber nachdem ich jetzt, nachdem ich euch gezeigt habe, wie man in der Register schreibt, kann man die ersten Schritte überspringen, braucht man den Timer nicht rausladen. Wahrscheinlich geht der TCP-Stack dabei, kaputt, wenn ich da einfach den Wert reinschreibe. Und die Karte da immer in den Timestamp hinten dran schreibt. Und sich dann nicht weiter darum kümmert. Wir haben die eingebauten Nick auf einem XND verwendet. Und haben an einem fiberoptic Splitter alle Timestamps bevor und danach mit einer Timestamp angeschaut. Und das war ein einfacher Use Case, warum man so einen Treiber wollen könnte. Zusammenfassend denke ich, ich denke Treiber sollten in höheren Sprachen geschrieben werden. Man sollte keine neuen User Space-Schreiber in C schreiben. Es gibt ein Netzwerk User Space-Dreiber, der es in C, weil er hauptsächlich aus dem Tunnel-Dreiber kopiert ist. Das FTDK, das ist auch in C geschrieben. Und Snap hat Treiber in Lua. Und unsere Implementierungen, die haben verschiedene, wenn man die miteinander vergleichen möchte. Man kann hier diesen QR-Code scannen oder man kann auf uns aufgehtab für XC langes Suchen. Die Slides, alles hat einen Link zu diesem Talk. Und ich empfehle euch, dass also euer eigener Treiber keinen kleinen Code braucht. Vielen Dank für eure Zeit. Thank you very much Paul, Simon und Sebastian. We do have time for questions, please line up at the microphone. And to get it started, please a question from our signal angel from the internet. So the IRC, first of all, was wondering why was the BASH-Propos... Das IRC wollte wissen. Warum ist das mit BASH nur als eine Bachelor-Arbeit angeboten worden? Ja, also bei der BASH-Frage. Ich habe zufällige Sprachen hinzugefügt. Also, ja, ich habe da eben halt einfach alle Sprachen hinzugefügt, die ich mir gedacht habe. Und in BASH da hätte man ein bisschen zu viel C-Code schreiben müssen. Ich habe ein kleines C-Programm geschrieben, das M-Map-Ding aufrufen würde und dann einfach warten würde. Und die Idee war, dass man den Adressraum von dem Programm, das gerade das M-Map aufrufen hat, über das PROCFS aufruft. Und dann kann man mit DD da Faust lesen und raus schreiben. Aber an irgendeinem Punkt geht das dann kaputt, weil es kann passieren, dass wenn man irgendwann was ins PROCFS reinschreibt, dann geht das nicht unbedingt direkt ans BCA Express. Und als Bachelor-Accessor habe ich das nur aufgeschrieben, weil Master-Accessor sollte dann schon irgendwie ein bisschen weitergehen. Wie macht ihr das mit strikten Timing-Anforderungen und Interrupts? Wenn ich strikte Timing-Anforderungen habe, dann werde ich keine Interrupts verwenden. Sondern dann polle ich das Gerät einfach. Ich frage es einfach immer wieder. So funktionieren eigentlich alle Usus-Bass-Treiber. Die Fragen einfach, das Gerät gibt es ein Paket immer wieder, ungefähr eine Million Mal pro Sekunde. Interrupts sind für sowas überhaupt nicht geeignet, weil das Empfang von dem Interrupt ein Kontextswitch verursacht, Kontextswitch zurück vom Interrupt-Templar woanders sind. Und dann muss man das Gerät trotzdem pollen, weil der Interrupt sagt ja mir nur das, was passiert ist, aber nicht was. Und wenn man sich tatsächlich jetzt über Latents-Sorgen macht, dann pollt man das Gerät eh die ganze Zeit. Und für Usus-Bass-Interrupts kann man sich das VfIO-Framework anschauen, wenn man das wirklich alles braucht. Okay, let's go to the whole questions. Please keep your questions to one sentence only and only ask questions because there's many of them. Microphone number two please. So, when you compare different user space drivers to different converges user space implementations, why was slower than slower than slower than slower than slower than slower. And we save these compile time. Oh, das ist doch eigentlich alles compile time. We have few more memory operations because of the safety. So on the pre we have to move the packet structures Wir müssen nämlich die Paket-Datenstruktur von innerhalb des Treibers an den User weitergeben und da passieren Kopien und deswegen ist das tatsächlich ein bisschen langsamer. Aber man kann es tatsächlich bestimmt nochmal deutlich besser optimieren und das war halt immer noch nur eine Bachelorarbeit. Das heißt, ihr hattet jetzt nicht so viel Zeit. Also man könnte es bestimmt noch irgendwie schneller machen. Aber meine Vermutung ist, dass es trotzdem immer noch liebster schneller bleibt. Und natürlich macht mein C3 überhaupt nur keine Bounce-Jacks, schon dann das Ding keiner schneller. Habt Haskell erwähnt? Und das war nicht im Vergleich? Haskell wurde überhaupt nichts auf Performance optimiert und das heißt, wäre im Vergleich jetzt unfair gewesen. Es war auf jeden Fall ziemlich langsam bis jetzt. Aber wie gesagt, wurde auch nicht optimiert. Thank you very much. Have you considered using total programming languages like Idris or Cog where your compiler can check the logic of your driver? Habt ihr andere Programmiersprachen wie Idris oder Cog angedacht, wo man den Treiber checken kann? Dafür hätte ich einfach keine Studie-Szene, das implementiert hätte. With Garbage Collector included, including Go. So my question is, how often do the GCs stop the war happens and what is the general heap sizes? Wie oft stoppt der Garbage Collector die Welt, wie ist die heap Größe? Ich weiß jetzt nicht genau, wie häufig, im Latentsdiagramm hat man gesehen, wie lange es dauert. Aber die Statistiken, wie häufig das jetzt genau mit den Garbage Collector passiert, habe ich nicht. Es gibt in den zugehörigen Papers, gerade auch diesen verlinkten Paper, wo der Typ das Betriebszement Go implementiert hat. Wie lange so was passiert, kannst du mich da an was erinnern? Ja, mit der heap size haben sie, glaube ich, was gemessen. Und wie häufig der Garbage Collector pausiert. Ich bin mir gerade aber nicht zu sicher, weil das Go Profiling diesen Knoten einfach komplett weggeworfen hat, weil es quasi keine Zeit verbraucht hat im Vergleich zu den ganzen anderen Knoten. Das heißt, bis auf die Latentsdi, das ist eigentlich irrelevant. Zwei Sprachen vermisst OpenCL. Also, was die GPU tatsächlich mit dem Netzwerk redet, da gibt es ein Paket, das heißt Packet Shader. Es gibt ein Paper mit diesem Titel und sie machen tatsächlich genau das, was ihr denkt. Es gibt auch ein anderes Paper. Das heißt Raising the Bar for GPU Packet Processing. Sie sagen, dass das im Prinzip keine gute Idee ist, denn wenn man die Pakete zwischen der Netzwerkkarte und der GPU transportiert, dann muss man gigantisch große Batchgrößen verwenden, damit das besser wird der Durchsatz und dann wird aber die Latents schlechter. Und dann haben sie in dem Paper eben auch keine ordentliche Latentsmessung, was das Ganze so ein bisschen unterstreicht. Wie geht denn mit der Reihenfolge um IO-Ordering? Die Reihenfolge, in der die CPU die IO-Zugreffer ausgibt, ist nicht die gleiche. Wenn ihr das mit UIO das macht, dann habt ihr diese Garantien nicht. Ja, also zum Memory-Ordering, also zum Speicherzugreinfolge, dann ist das ziemlich gerettet, spezifisch welche Spezifische Mantekwander da verwendet. Aber bei dem Intel-Gerät, da gibt es eine Stelle, wo ich mir ziemlich sicher bin, dass ich Release-Ordermemory, Release-Ordermemory brauche, wo ein Flag gesetzt wird, was dazu führt, dass noch ein anderes Flag gelesen wird. Und ja, ich finde aber in den ganzen Treiberimplementierungen da draußen keine Release-Ordermemory-Barriere, also brauchen wir es wahrscheinlich nicht, für Go und Rust haben wir eben die Low-Level-Primitive, die Adware-Memory-Barrieren da einfügen, für andere Sprachen, zum Beispiel wenn man sich hier in Snap-Triber anschaut, der in Lua geschrieben ist, da gibt es eben so einen kleinen C-Stub, der aufgerufen wird immer, wenn ein Speicherbarriere gebraucht wird, dann empfängt es auf. Die Zusammenfassung von einem Talk hat erwähnt, das ist eine benutzerliche Space. Das C-Implementierung ist 6-8-mal schneller als die Kernel-Implementierung. Was war der Grund? Das ist vor allem wegen der Bettgrößen, es gibt auch eine andere Implementierung, die XDP verwendet, also mit XDP oder ohne XDP, da sind wir ca. 30% schneller als die Version bei Kerneln mit XDP. Das wäre mit Sicherheit interessant. Das ist bestimmt eine schöne Forschungsfrage, aber wir beschäftigen uns mit Netzwerk, weil das Moment ziemlich verbreitet ist und auch total populär ist, da dran zu arbeiten. Man kann da ja TCP und so weiter drauf machen. Vielen Dank. Beinflusst IOMMU eure Performance? Da sind wir uns nicht so ganz sicher. Ihr könnt den da vorne fragen. Wie gesagt, er hat angefangen zu evaluieren. Gerade braucht es nicht zu irgendwelchen Performance-Einbrüchen. Es gibt auch ein Taper, das heißt PCIe-Bench. Das ist ein Paper. Ich glaube, es wurde auf der Sitcom-Listia veröffentlicht. Es gibt definitiv irgendwelche Effekte. Wir haben das aber in unserem Setup noch nicht gemessen. Wenn er die Ringbuffer zugreift, ist er normalerweise ein einmal Zugriffs-Macro in C, weil C in der Lage ist, die Symantik so zu ändern, das Speicher zu lesen in der Variable. C darf das zweimal lesen. Und habt ihr da einen Macro, um das zu verhindern? Wir hatten eine Million, wo dieser Bug uns das kaputt gemacht hat. Wie sichert ihr einen einzelnen speicher Zugriff? Enforce ist durchsetzend. Es ist irgendwie immer ein ziemlich hartes Constraint. Was die ... Was die ... Was die Programmiersparen halt alle machen müssen, ist Diskriptoren, also im Prinzip Pointer, auf eigentliche Buffern aus diesen Regen auf der Netzwerkkarte rausgeben. Das müssen die Implementierungen machen. Das sind tatsächlich immer nur so 16 Byte und so was. Ich hoffe einfach, dass das tatsächlich dann auch immer Kopien sind. Ich bin mir da eigentlich ziemlich sicher. Wir hatten Atomic Read, das auch schon mal laufen soll. Und ich würde das jetzt als Bug-and-Go betrachten, wenn Atomic Read zu zwei Read-Institutionen führen würde. Wie können wir andere Leute, Business-Leute und Entwickler davon überzeugen, dass sie neue Sprache lernen und die Zeit investieren, drinnen zu entwickeln? Ja, da habe ich keine Ahnung. Es ist für mich ein absolutes Rätsel, warum Leute immer noch Sachen in der See schreiben. Keine Ahnung, sorry. Thank you very much to Paul Salman and Sebastian. Vielen Dank an Paul und Sebastian.