Festplatten-Tacho (Linux-Magazin, Oktober 2021)

Im Gegensatz zu alten Festplatten mit rotierenden Scheiben hören User von schnellen SSDs gar nichts mehr, wenn der darauf verfügbare Speicher schwindet. Speicherhungrige Applikationen wie Schneide-Software für Videos ziehen Plattenplatz ab wie mit einer Kreiselpumpe, und ehe der User sich's versieht, ist alles aufgebraucht. In solchen Situationen stürzt nicht sachgemäß programmierte Software oft ab, und die in das aktuelle Projekt investierte Zeit ist unwiederbringlich verloren. Wer vorher aufpasst, spart sich hinterher den Verdruss.

Wie wäre es mit einer ständig aktualisierten Anzeige des verbleibenden Platzes in einem tachoartigen Instrument auf dem Desktop, das der User aus dem Augenwinkel beobachten und zusehen kann, welchen Schwund eine gerade ausgelöste Aktion wie das Rendern eines Videos generiert? In Go wäre das doch schnell geschrieben, oder?

Ein Tacho wie im Auto

Ein Tacho im Auto zeigt zwei Dinge an: Einmal die aktuelle Geschwindigkeit, und andererseits die aufkumulierten zurückgelegten Kilometer. Auf Festplatten übertragen wird aus dem Tachostand des Autos der verbrauchte Plattenplatz, und die Nadel, die die aktuelle Geschwindigkeit des Fahrzeugs anzeigt, berichtet im Festplattenuniversum den pro Zeiteinheit neu verbrauchten Platz. Eine speicherhungrige Applikation entspricht also einem Temposünder im Verkehr.

Abbildung 1: Die Platte ist zu 39% voll und ein Schreibvorgang frisst noch mehr Platz.

Abbildung 2: Die grüne Tacho signalisiert einen Löschvorgang, der Plattenplatz freigibt.

Die Abbildungen 1 und zwei zeigen zweimal die Terminal-Ausgabe des fertigen Go-Programms. Oben illustriert jeweils ein Fortschrittsbalken den bislang verbrauchten Plattenplatz an (39% im vorliegenden Fall), und unten zeigt eine mittels eines Pie-Charts implementierte Tachonadel für arme Leute an, ob Platz schwindet (rot) oder zurückkommt (grün), und wie schnell das gerade vor sich geht. Bei einem simulierten Tempo von 0 bis 100 fängt die Nadel des etwas ungewöhnlichen Instruments unten im Kreis an, und bewegt sich dann entgegen dem Uhrzeigersinn erst nach oben. Abbildung 1 zeigt eine Schreibgeschwindigkeit von 35, Abbildung 2 eine Löschaktion mit Tempo 65. Bei Tempo 100 wäre des Kreis jeweils vollständig mit der entsprechenden Farbe gefüllt.

Listing 1: dftop.go

    01 package main
    02 
    03 import (
    04   "container/ring"
    05   "golang.org/x/sys/unix"
    06   "os"
    07   "time"
    08 )
    09 
    10 func main() {
    11   wd, err := os.Getwd()
    12   if err != nil {
    13     panic(err)
    14   }
    15 
    16   ui := NewUI()
    17   ui.Update(0, 0.0)
    18 
    19   uidone := ui.Run()
    20   defer ui.Close()
    21 
    22   r := ring.New(2)
    23 
    24   for {
    25     used, total, err := space(wd)
    26     if err != nil {
    27       panic(err)
    28     }
    29 
    30     r.Value = used
    31     p := used * 100 / total
    32     ui.Update(int(p), speed(r))
    33     r = r.Next()
    34 
    35     select {
    36     case <-uidone:
    37       return
    38     case <-time.After(
    39       1 * time.Second):
    40       continue
    41     }
    42   }
    43 }
    44 
    45 const maxSpeed = 500000
    46 
    47 func speed(r *ring.Ring) float64 {
    48   if r.Prev().Value == nil {
    49     return 0
    50   }
    51   s := float64(int(
    52     r.Value.(uint64)-
    53       r.Prev().Value.(uint64))) /
    54     maxSpeed
    55 
    56   if s > 1 {
    57     s = 1
    58   } else if s < -1 {
    59     s = -1
    60   }
    61 
    62   return s
    63 }
    64 
    65 func space(dir string) (
    66   uint64, uint64, error) {
    67   var stat unix.Statfs_t
    68 
    69   err := unix.Statfs(dir, &stat)
    70   return stat.Blocks -
    71     stat.Bfree, stat.Blocks, err
    72 }

Library statt Skript

Um den verbleibenden Platz auf einem Datenträger zu ermitteln, könnte das Programm immer wieder die Shell-Funktion df aufrufen, aber das ginge verschwenderisch mit den verfügbaren Resourcen um, da die Shell jedesmal einen neuen df-Prozess starten müsste. Die praktische Statfs-Programmierschnittstelle ([2]) auf Unix-Systemen gibt aber zum Glück auch ohne den Aufruf einer Shell-Utility zu einem vorgegebenen Verzeichnis die auf dem zugehörigen Mount insgesamt bereitgestellten, sowie die noch unbelegten Blöcke des Speichermediums an. Die Go-Schnittstelle dazu liefert mit Bfree() die Gesamtzahl der freien Blöcke und Bavail() die Untermenge der freien Blöcke, die der Nicht-Root-User noch belegen darf. Multipliziert mit der auf dem Speichermedium eingestellten Blockgröße (Bsize()) ergibt sich der verbleibende Speicherplatz in Tera-, Giga- oder was auch immer -bytes.

Die Funktion space() ab Zeile 65 in Listing 1 ermittelt so die Auslastung des Speichermediums und gibt Werte für die Anzahl der belegten Blöcke, sowie deren Gesamtzahl zurück. Tritt beim Ermitteln der Kapazität ein Fehler auf, reicht space() ihn als dritten Rückgabewert ans rufende Hauptprogramm hoch. Welche Festplatte misst das Programm eigentlich bei einem Rechner mit mehreren Speichermedien? Je nachdem, aus welchem Verzeichnis der User den Tacho aufruft, zeigt dieser den Platz der daran hängenden Festplatte an.

Speichern im Kreis

Die Geschwindigkeit, mit der sich die Festplatte füllt, ergibt sich aus der Differenz zweier Messungen des Füllpegels zu unterschiedlichen Zeitpunkten, dividiert durch die dazwischen verstrichene Zeit. Dazu muss das Programm eine oder mehrere Messungen aus der Vergangenheit vorrätig halten, um die Differenz zum aktuell gemessenen Wert zu ermitteln. Das könnte man mit extra Variablen machen, aber ein Ringpuffer (Abbildung 3) vom Typ container/ring aus Gos Standardbibliothek erledigt dies elegant ohne viel Code. Er speichert in r.Value neue Werte der Reihe nach in Punkten, die auf einer Kreislinie liegen. Mit r.Next() geht's zum nächsten Punkt, mit r.Prev() zurück zum vorherigen. Kommt der Algorithmus auf seiner Kreisbahn irgendwann wieder beim ersten Punkt an, überschreibt er diesen einfach. So kann ein Ringpuffer immer nur auf die N letzten Werte zugreifen, müllt aber nicht den Speicher mit irrelevanten Werten aus der Vergangenheit zu. Ein wie in Zeile 22 angelegter Ringpuffer mit nur zwei Einträgen nutzt sicher nicht das ganze Potential, aber wer will, kann den Puffer auch mit mehr Einträgen zur Mittelung und Beruhigung der Anzeige erweitern.

Abbildung 3: Ein Ringpuffer überschreibt alte und obsolete Werte automatisch (Quelle: Wikipedia)

Die Funktion speed() ab Zeile 47 errechnet mit dem erläuterten Verfahren die aktuelle Füllgeschwindigkeit des Speichermediums. Führt der Ringpuffer noch keine zwei Werte, steht die Geschwindigkeit noch nicht fest und Zeile 49 gibt den Wert Null zurück.

Vorzeichenlos

Den verbleibenden Plattenplatz gibt die Funktion Statfs() in Zeile 69 als uint64 zurück, also als 64-bit-Integer ohne Vorzeichen, der deswegen nie negative Werte annehmen kann. Die Differenz zwischen zweier dieser Werte kann allerdings sehr wohl negativ werden, und wer einfach beide Werte voneinander abzieht, wundert sich vielleicht, dass Go (wie auch andere Sprachen) absolute Mondwerte für die Differenz liefert, falls der Subtrahend größer ist als der Minuend. Das Ergebnis sollte in diesem Fall negativ sein, liefert aber sehr große positive Werte. Ohne Hilfestellung nimmt Go nämlich an, dass als Ergebnis einer Operation mit zwei vorzeichenlosen Integern ebenfalls ein vorzeichenloser Integer herauskommt. Die Lösung: ein Typecast mit int(x-y) macht Go klar, dass das Ergebnis der Differenz zweier uint64-Werte x und y ein Vorzeichen trägt.

Bevor Zeile 54 die Differenz allerdings durch einen empirisch ermittelten Wert von 100.000 teilt, damit bei durchschnittlicher Plattenperformance ein Fließkommawert zwischen 0 und 1 herauskommt, muss Zeile 51 den Typ des Ergebnisses in float64 umwandeln. Ist nun die Geschwindigkeit größer als 1 oder kleiner als -1, komprimiert das if-else-Konstrukt ab Zeile 56 es auf den Bereich zwischen -1 und +1.

Bleibt dem Hauptprogramm in Listing 1 nur noch, das aktuelle Verzeichnis zu ermitteln (Zeile 11), die UI zu starten (Zeile 19), und sie mit der defer-Anweisung in Zeile 20 auf jeden Fall zu schließen, sobald das main-Programm zu Laufen aufhört.

Auf den Schirm

Listing 2 zeigt, wie das Programm mit Hilfe des bewährten Github-Projekts termui die UI mit der Anzeige in ein Terminal zaubert. Es definiert dazu drei aufgestapelte Widgets: Ein Paragraph-Widgets zur Anzeige des Programmnamens "Disk Speedo v1.0", einen Fortschrittsbalken vom Typ Gauge, sowie eine Kuchengrafik vom Typ PieChart, die das Tachometer formt.

Listing 2: ui.go

    01 package main
    02 
    03 import (
    04   "math"
    05 
    06   tui "github.com/gizak/termui/v3"
    07   "github.com/gizak/termui/v3/widgets"
    08 )
    09 
    10 type UI struct {
    11   Gauge *widgets.Gauge
    12   Pie   *widgets.PieChart
    13   Head  *widgets.Paragraph
    14 }
    15 
    16 func NewUI() *UI {
    17   h := widgets.NewParagraph()
    18   h.TextStyle.Fg = tui.ColorBlack
    19   h.SetRect(6, 1, 30, 2)
    20   h.Text = "Disk Speedo v1.0"
    21   h.Border = false
    22 
    23   g := widgets.NewGauge()
    24   g.SetRect(2, 2, 28, 5)
    25   g.Percent = 0
    26   g.BarColor = tui.ColorRed
    27 
    28   p := widgets.NewPieChart()
    29   p.SetRect(-10, 5, 40, 20)
    30   p.Border = false
    31   p.Data = fract(0)
    32   p.AngleOffset = -1.5 * math.Pi
    33 
    34   return &UI{Gauge: g,
    35     Pie: p, Head: h}
    36 }
    37 
    38 func (ui UI) Run() chan bool {
    39   done := make(chan bool)
    40 
    41   err := tui.Init()
    42   if err != nil {
    43     panic("termui init failed")
    44   }
    45 
    46   go func() {
    47     events := tui.PollEvents()
    48     for {
    49       select {
    50       case e := <-events:
    51         switch e.ID {
    52         case "q", "<C-c>":
    53           done <- true
    54           return
    55         }
    56       }
    57     }
    58   }()
    59 
    60   return done
    61 }
    62 
    63 func (ui UI) Close() {
    64   tui.Close()
    65 }
    66 
    67 func (ui UI) Update(
    68   level int, speed float64) {
    69   ui.Gauge.Percent = level
    70   ui.Pie.Colors = []tui.Color{
    71     tui.ColorBlack, tui.ColorRed}
    72 
    73   if speed < 0 {
    74     ui.Pie.Colors[1] =
    75       tui.ColorGreen
    76     speed = -speed
    77   }
    78   ui.Pie.Data = fract(speed)
    79   tui.Render(ui.Head, ui.Gauge,
    80     ui.Pie)
    81 }
    82 
    83 func fract(
    84   val float64) []float64 {
    85   num := (1 - val) * 100
    86   denom := val * 100
    87   return []float64{num, denom}
    88 }

Der Konstruktor NewUI() ab Zeile 16 definiert die Widgets mit ihren Dimensionen, geometrischer Lage, sowie farblichen Extras, und reicht eine initialisierte Struktur vom Typ UI zurück, die ab Zeile 10 definiert ist. Diese nutzt das Hauptprogramm später, um methodenartige Funktionen wie Run() (ab Zeile 38) aufzurufen und ihnen den Kontext der vorher initialisierten Widgets mitzugeben. Objektorientierung nach Go-Art eben.

Auf Kanälen lauschen

Die Funktion Run() ab Zeile 38 startet die termui-UI in einer parallel laufenden Goroutine ab Zeile 46. Wie jede UI liefert auch sie laufend Events wie Tastendrücke oder Mausklicks, die es abzufangen und zu bearbeiten gilt, um umschönen Hängern vorzubeugen. Die Endlosschleife ab Zeile 48 wartet mit ihrer select-Anweisung deshalb darauf, dass der User entweder die Taste q oder die Kombination Ctrl-C drückt, und schickt in diesem Fall in einem neu angelegten Channel done den Wert true ans lauschende Hauptprogramm, damit dieses den Betrieb einstellt. Letzteres lauscht dazu in der select-Anweisung ab Zeile 35 gleichzeitig darauf, ob die UI das Ende des Programms meldet oder der Sekunden-Timer in Zeile 38 abläuft. Ist das der Fall, geht das Programm in die nächste Runde, holt neue Werte des Plattenfüllstands und zeigt diesen an.

Allgemein abstrahiert Listing 2 die Eigenheiten der termui-Library und schirmt das Hauptprogramm davon ab. Letzteres ruft zum Zusammenfalten der Terminal-UI lediglich die Funktion Close() ab Zeile 63 auf, die ihrerseits einen Close()-Call an das Github-Projekt termui weiterreicht.

Bunt gemalt

Neue Werte in die Anzeige schreibt die Funktion Update(), die Listing 2 ab Zeile 67 definiert. Sie nimmt zwei Werte entgegen: den Füllstand der Festplatte als Integer, sowie die gemessene Füllgeschwindigkeit speed als float64-Wert. Ersteren reicht sie in Zeile 69 ans Gauge-Widget der <termui>-Library weiter, letzteren an die Kuchengrafik in ui.Pie.

Damit das Pie-Chart positive Füllgeschwindigkeit in Rot malt und negative in Grün, setzt Zeile 71 die Farben der Kuchengrafik als Schwarz und Rot, und Zeile 74 modifiziert die Farbe im Falle einer negativen Geschwindigkeit auf Grün. Da die Grafik nur positive Werte verarbeitet, verkehrt Zeile 76 das Vorzeichen negativer Geschwindigkeiten. Die termui-Funktion Render malt in Zeile 79 alle drei Widgets ins Terminal, und frischt damit ihr Aussehen mit jedem sekündlichen Update auf.

Wie malt nun die Tortengrafik den Tachstand basierend auf einem Fließkommawert für die Geschwindigkeit zwischen 0 und 1? Sie ruft hierzu die Funktion fract() ab Zeile 83 auf, die mit der Formel (1-val)/val einen Bruch erzeugt, dessen Verhältnis von Zähler zu Nenner die Anteile des wachsenden farbigen Wertes (Grün oder Rot) zur beibehaltenen schwarzen Fläche angeben. Für millimetergenaue Auflösung multiplizieren die Zeile 85 und 86 die Werte von Zähler und Nenner auch noch mit 100. Als Ergebnis von fract() liefert ein Fließkommawert 0.35 zum Beispiel 0.65*100 und 0.35*100, also 65 und 35. Folglich belegt das dargestellte rote Tachosegment (wie anfangs in Abbildung 1 gezeigt) etwa ein Drittel des Gesamtkreises, während die restlichen zwei Drittel links schwarz bleiben.

Auf geht's beim Schichtl

Die ganze Chose kompilieren die Aufrufe

    $ go mod init dftop
    $ go mod tidy
    $ go build dftop.go ui.go

der die abhängige UI-Library termui von Github einholt und das Binary dftop erzeugt. Es produziert nach dem Aufruf die Ausgabe aus den Abbildungen 1 und 2. Wer es in einem Fenster am Bildschirmrand laufen lässt, behält die Festplatte im Auge und kann deren Überfüllung rechtzeitig vorbeugen.

Infos

[1]

Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2021/09/snapshot/

[2]

https://man7.org/linux/man-pages/man2/statfs.2.html

Michael Schilli

arbeitet als Software-Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen verschiedener Programmiersprachen. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.

POD ERRORS

Hey! The above document had some coding errors, which are explained below:

Around line 5:

Unknown directive: =desc