Um den verbleibenden Plattenplatz im Auge zu behalten, können Sie während speicherintensiver Operationen auf einen in Go geschriebenen Tacho schielen. Mike Schilli übernimmt den Einbau.
Speicherhungrige Applikationen wie Schneidesoftware für Videos ziehen Plattenplatz ab wie mit einer Kreiselpumpe, und ehe es sich der User versieht, ist alles aufgebraucht. In solchen Situationen stürzt nicht sachgemäß programmierte Software oft ab, und die in das aktuelle Projekt investierte Zeit geht 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, anhand dessen der User aus dem Augenwinkel erkennen kann, welchen Schwund eine gerade ausgelöste Aktion wie das Rendern eines Videos verursacht? In Go lässt sich so etwas fix schreiben.
Ein Tacho wie im Auto
Ein Tacho im Auto zeigt die aktuelle Geschwindigkeit sowie die kumulierten zurückgelegten Kilometer an. Auf Festplatten übertragen, wird aus dem Kilometerstand des Autos der verbrauchte Plattenplatz. Die Nadel, die die aktuelle Geschwindigkeit des Fahrzeugs anzeigt, misst im Festplattenuniversum den pro Zeiteinheit neu verbrauchten Platz. Eine speicherhungrige Applikation entspricht also einem Temposünder im Verkehr.
In den Abbildungen 1 und**2 sehen Sie die Terminalausgabe des fertigen Go-Programms. Oben illustriert jeweils ein Fortschrittsbalken den bislang verbrauchten Plattenplatz (im vorliegenden Fall 39 Prozent). Unten signalisiert eine mittels eines Tortendiagramms implementierte Tachonadel, 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 gegen den Uhrzeigersinn nach oben. Abbildung 1 zeigt eine Schreibgeschwindigkeit von 35, Abbildung 2 eine Löschaktion mit Tempo 65. Bei Tempo 100 wäre der Kreis jeweils vollständig mit der entsprechenden Farbe gefüllt.
Library statt Skript
Um den verbleibenden Platz auf einem Datenträger zu ermitteln, könnte das Programm immer wieder die Shell-Funktion »df« aufrufen. Doch das ginge verschwenderisch mit den verfügbaren Ressourcen um, da die Shell dazu jedes Mal einen neuen »df«-Prozess starten müsste. Die praktische Statfs-Programmierschnittstelle [1] auf Unix-Systemen gibt aber zum Glück auch ohne 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 mit »Bavail()« die Untermenge der freien Blöcke, die der Nicht-Root-User noch belegen darf. Die Multiplikation mit der auf dem Speichermedium eingestellten Blockgröße »Bsize()« ergibt den verbleibenden Speicherplatz in Tera-, Giga- oder welchen Bytes auch immer.
Die Funktion »space()« ab Zeile 61 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 aufrufende Hauptprogramm hoch. Die Kapazität welcher Festplatte misst das Programm eigentlich bei einem Rechner mit mehreren Speichermedien? Je nachdem, aus welchem Verzeichnis der Benutzer den Tacho aufruft, zeigt dieser den Platz der entsprechenden Festplatte an.
Listing 1
dftop.go
package main
import (
"container/ring"
"golang.org/x/sys/unix"
"os"
"time"
)
func main() {
wd, err := os.Getwd()
if err != nil {
panic(err)
}
ui := NewUI()
ui.Update(0, 0.0)
uidone := ui.Run()
defer ui.Close()
r := ring.New(2)
for {
used, total, err := space(wd)
if err != nil {
panic(err)
}
r.Value = used
p := used * 100 / total
ui.Update(int(p), speed(r))
r = r.Next()
select {
case <-uidone:
return
case <-time.After(
1 * time.Second):
continue
}
}
}
const maxSpeed = 100000
func speed(r *ring.Ring) float64 {
if r.Prev().Value == nil {
return 0
}
s := float64(int(
r.Value.(uint64)-
r.Prev().Value.(uint64))) /
maxSpeed
if s > 1 {
s = 1
} else if s < -1 {
s = -1
}
return s
}
func space(dir string) (
uint64, uint64, error) {
var stat unix.Statfs_t
err := unix.Statfs(dir, &stat)
return stat.Blocks -
stat.Bfree, stat.Blocks, err
}
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 über gesonderte Variablen implementieren, aber ein Ringpuffer (Abbildung 3) des Typs »container/ring« aus der Go-Standardbibliothek erledigt das 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 es 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 ihn 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 19 angelegter Ringpuffer mit nur zwei Einträgen nutzt sicher nicht das ganze Potenzial, aber wer möchte, 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, CC BY-SA 4.0
Die Funktion »speed()« ab Zeile 44 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 46 gibt den Wert Null zurück.
Vorzeichenlos
Den verbleibenden Plattenplatz gibt die Funktion »Statfs()« in Zeile 64 als »uint64« zurück, also als 64-Bit-Integer ohne Vorzeichen, der nie negative Werte annehmen kann. Die Differenz zwischen zwei dieser Werte kann allerdings sehr wohl negativ ausfallen.
Wer also 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 ausfällt 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 Resultat einer Operation mit zwei vorzeichenlosen Integern ebenfalls eine vorzeichenlose Ganzzahl 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 51 die Differenz allerdings durch einen empirisch ermittelten Wert von 100 000 teilt, damit bei durchschnittlicher Platten-Performance ein Fließkommawert zwischen 0 und 1 herauskommt, muss Zeile 48 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 53 es auf den Bereich zwischen -1 und +1.
Bleibt dem Hauptprogramm in Listing 1 nur noch, das aktuelle Verzeichnis zu ermitteln (Zeile 10), die UI zu starten (Zeile 17) und sie mit der »defer«-Anweisung in Zeile 18 auf jeden Fall zu schließen, sobald das »main«-Programm zu laufen aufhört.
Auf den Schirm
Listing 2 zeigt, wie das Programm mithilfe des bewährten Github-Projekts Termui die UI mit der Anzeige in ein Terminal zaubert. Es definiert dazu drei aufgestapelte Widgets: ein Paragraph-Widget zur Anzeige des Programmnamens »Disk Speedo v1.0«, einen Fortschrittsbalken vom Typ »Gauge« sowie eine Tortengrafik vom Typ »PieChart«, die den Tacho formt.
Listing 2
ui.go
package main
import (
"math"
tui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
type UI struct {
Gauge *widgets.Gauge
Pie *widgets.PieChart
Head *widgets.Paragraph
}
func NewUI() *UI {
h := widgets.NewParagraph()
h.TextStyle.Fg = tui.ColorBlack
h.SetRect(6, 1, 30, 2)
h.Text = "Disk Speedo v1.0"
h.Border = false
g := widgets.NewGauge()
g.SetRect(2, 2, 28, 5)
g.Percent = 0
g.BarColor = tui.ColorRed
p := widgets.NewPieChart()
p.SetRect(-10, 5, 40, 20)
p.Border = false
p.Data = fract(0)
p.AngleOffset = -1.5 * math.Pi
return &UI{Gauge: g,
Pie: p, Head: h}
}
func (ui UI) Run() chan bool {
done := make(chan bool)
err := tui.Init()
if err != nil {
panic("termui init failed")
}
go func() {
events := tui.PollEvents()
for {
select {
case e := <-events:
switch e.ID {
case "q", "<C-c>":
done <- true
return
}
}
}
}()
return done
}
func (ui UI) Close() {
tui.Close()
}
func (ui UI) Update(
level int,
speed float64) {
ui.Gauge.Percent = level
ui.Pie.Colors = []tui.Color{
tui.ColorBlack, tui.ColorRed}
if speed < 0 {
ui.Pie.Colors[1] =
tui.ColorGreen
speed = -speed
}
ui.Pie.Data = fract(speed)
tui.Render(ui.Head, ui.Gauge,
ui.Pie)
}
func fract(
val float64) []float64 {
num := (1 - val) * 100
denom := val * 100
return []float64{num, denom}
}
Der Konstruktor »NewUI()« ab Zeile 13 definiert die Widgets mit ihren Dimensionen, ihrer geometrischen Lage sowie farblichen Extras und reicht eine initialisierte Struktur vom Typ »UI« zurück (ab Zeile 7 definiert). Die nutzt das Hauptprogramm später, um methodenartige Funktionen wie »Run()« (ab Zeile 32) aufzurufen und ihnen den Kontext der vorher initialisierten Widgets mitzugeben – Objektorientierung nach Go-Art eben.
Auf Kanälen lauschen
Die Funktion »Run()« ab Zeile 32 startet die Termui-Oberfläche in einer parallel laufenden Goroutine ab Zeile 39. 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 41 wartet mit ihrer Select-Anweisung deshalb darauf, dass der User entweder [Q] oder [Strg]+[C] drückt. Sie schickt in diesem Fall in einem neu angelegten Channel »done« den Wert »true« an das lauschende Hauptprogramm, damit das den Betrieb einstellt. Das Hauptprogramm beobachtet dazu in der Select-Anweisung ab Zeile 32 von Listing 1 gleichzeitig, ob die UI das Ende des Programms meldet oder der Sekunden-Timer (Listing 1, Zeile 35) abläuft. Ist Letzteres der Fall, geht das Programm in die nächste Runde, holt neue Werte des Plattenfüllstands und zeigt diese an.
Allgemein abstrahiert Listing 2 die Eigenheiten der Termui-Library und schirmt das Hauptprogramm davon ab. Das ruft zum Zusammenfalten der Terminal-UI lediglich die Funktion »Close()« (Listing 2 ab Zeile 55) auf, die ihrerseits einen »Close()«-Call an die Termui-Bibliothek weiterreicht.
Bunt gemalt
Neue Werte in die Anzeige schreibt die Funktion »Update()«, die Listing 2 ab Zeile 59 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 61 ans »Gauge«-Widget der Termui-Library weiter, Letzteren an die Tortengrafik in »ui.Pie«.
Damit das Pie-Chart eine positive Füllgeschwindigkeit in Rot malt und eine negative in Grün, setzt Zeile 63 die Farben des Tortendiagramms auf Schwarz und Rot, und Zeile 65 modifiziert die Farbe im Falle einer negativen Geschwindigkeit auf Grün. Da die Grafik nur positive Werte verarbeitet, verkehrt Zeile 67 das Vorzeichen negativer Geschwindigkeiten. Die Termui-Funktion »Render« malt in Zeile 70 alle drei Widgets ins Terminal und frischt damit die Anzeige mit jedem sekündlichen Update auf.
Wie malt nun die Tortengrafik den Tachostand basierend auf einem Fließkommawert für die Geschwindigkeit zwischen 0 und 1? Sie ruft dazu die Funktion »fract()« ab Zeile 74 auf, die mit der Formel »(1-val)/val« einen Bruch erzeugt, das Verhältnis des wachsenden farbigen Werts (Grün oder Rot) zur schwarzen Fläche angibt. Für eine präzise Auflösung multiplizieren die Zeilen 76 und 77 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 (siehe Abbildung 1) etwa ein Drittel des Gesamtkreises, während die restlichen zwei Drittel links schwarz bleiben.
Auf geht’s beim Schichtl!
Die ganze Chose kompilieren Sie mit den Aufrufen aus Listing 3. Sie holen die abhängige UI-Library Termui von Github ab und erzeugen das Binary »dftop«. Das produziert nach dem Aufruf die Ausgabe aus den Abbildungen 1 und**2. Lassen Sie Dftop in einem Fenster am Bildschirmrand laufen, behalten Sie damit die Festplatte im Auge und können einer Überfüllung rechtzeitig vorbeugen. (uba/jlu)
Listing 3
Kompilieren
$ go mod init dftop $ go mod tidy $ go build dftop.go ui.go
Infos
- Statfs: https://man7.org/linux/man-pages/man2/statfs.2.html
- Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2021/10/snapshot/








