Die Verwaltung von Grenzen für den Speicherplatzverbrauch basiert auf einem recht simplen Konzept. Anstatt jedem Benutzer zu erlauben, den gesamten verfügbaren Speicherplatz in einem Dateisystem (oder weniger technisch: einer Festplatte) aufzubrauchen, wird der Speicherplatz je Benutzer durch sogenannte Quotas limitiert. Diese Quotas definieren, wie viel Speicherplatz ein einzelner Benutzer oder eine Benutzergruppe verwenden darf.
In seiner ansonsten umfangreichen Standardbibliothek ist derzeit in Go noch kein Support für die Verwaltung von Grenzen für den Speicherplatzverbrauch inkludiert. Leider gibt es auch von Drittanbietern noch keine Bibliotheken, die diese Funktionalität beinhalten. Eines unserer aktuellen Projekte in der Anexia R&D Abteilung macht eine Speicherplatzbegrenzung in Go erforderlich, somit haben wir selbst eine Lösung entwickelt: Eine native Implementierung in Go.
Ich möchte diese Gelegenheit nutzen, um zu erläutern, wie Go mit dem zugrundeliegenden Betriebssystem interagiert – und zwar im Low-Level Bereich – dem Syscall-Level. Diese Schritte werden während der Entwicklung unserer Lösung nochmals erörtert. Dahingehend wird erklärt, auf welche Weise die notwendige Grundlagenrecherche durchgeführt wurde und wie wir unsere Quota-Bibliothek entwickelt haben.
In Linux unterstützt uns libc bei der Speicherplatzbegrenzung mithilfe seiner quotactl Funktion. musl-libc zeigt, dass quotactl als dünner Wrapper um den zugrundeliegenden System-Call mit dem selben Namen dienen kann:
int quotactl(int cmd, const char *special, int id, char *addr) { return syscall(SYS_quotactl, cmd, special, id, addr); }
Diese Funktion, gepaart mit einigen in musl’s include/sys/quota.h definierten C Präprozessormakros, bietet eine vollumfängliche Quota-Lösung. Wie man sieht, war für die Verwendung von Quotas in Go nur ein dünner Wrapper, wie die in musl-libc, notwendig – und hier beginnen wir mit der Implementierung dieser Funktionalität in Go.
Bevor man mit dem Testing beginnen kann, benötigt man ein sogenanntes Testbed. Wir wählen hier die Erstellung eines neuen ext4-Dateisystems aus, aktivieren die Quotas und mounten diese Datei mithilfe eines Loop Devices:
$ truncate -s 1G /tmp/test.ext4 $ /sbin/mkfs.ext4 /tmp/test.ext4 $ sudo mkdir -p /mnt/quota_test $ sudo mount -o usrjquota=aquota.user,grpjquota=aquota.group,jqfmt=vfsv1 /tmp/test.ext4 /mnt/quota_test $ sudo quotacheck -vucm /mnt/quota_test $ sudo quotacheck -vgcm /mnt/quota_test $ sudo quotaon -v /mnt/quota_test
Eventuell bedarf es zunächst der Installation der Quota-Pakete. In Debian und Ubuntu heißt das benötigte Paket quota. Nach erfolgreicher Durchführung der oben beschriebenen Schritte, erhalten wir ein neues Dateisystem mit Begrenzungen für Benutzer und Gruppen unter /mnt/quota_test. Achte auf den zuletzt aufgerufenen Befehl. Er sollte den Präfix /dev/loopN haben, wobei N eine beliebige Zahl ist. Diese Bezeichnung wird später noch benötigt.
Die Standardbibliothek in Go bietet zahlreiche Funktionen, um direkt über das syscalls Paket mit Systemaufrufen zu arbeiten. Glücklicherweise definiert dieses Paket auch die SYS_QUOTACTL Konstante – und beinhaltet die erforderliche Syscall-Nummer. Zusätzlich bietet das Paket einige sehr praktische Hilfsfunktionen, wie zum Beispiel BytePtrFromString – zu denen wir später noch kommen werden.
Des Weiteren beinhaltet es die Syscall und Syscall6 Funktionen, welche die Systemaufrufe auslösen. Die erste Variante kann bis zu drei Argumente aufrufen und Syscall6 unterstützt – wie der Name schon sagt – bis zu sechs. Da man für den quotactl Systemaufruf fünf Argumente benötigt, fällt die Wahl auf Syscall6.
Auf der man-page quotactl(2) wird die Schnittstelle detailliert beschrieben. Für den Anfang benötigen wir allerdings lediglich die Basisfunktionen, nämlich die Möglichkeit, Quotas abzufragen und festzulegen. Diese Funktionalität ist über „Subcommands“ umgesetzt. Einer dieser Subcommands, Q_GETQUOTA, welches eine Abfrage durchführt, erwartet einen Pointer auf eine Datenstruktur namens dqblk, welche wir in Go umsetzen müssen.
Ein weiteres interessantes Detail, das näher betrachtet werden muss, ist, wie das cmd Argument für den quotactl Aufruf berechnet wird. Auf den man-pages wird das QCMD(subcmd, type) Makro beschrieben, welches in sys/quota.h definiert ist und einen „Subcommand“ sowie einen „Quota-Typ“ benötigt und durch ein bitweises Shiften und eine Maskierung durch ein logisches “AND” den benötigten Wert festlegt.
#define QCMD(cmd, type) (((cmd) << SUBCMDSHIFT) | ((type) & SUBCMDMASK))
Liegen alle benötigten Informationen vor, definieren wir einen Wrapper, welcher dem ähnelt, den wir in musl-libc in Go vorgefunden haben:
func quotactl(cmd int, special string, id int, target unsafe.Pointer) (err error) { var deviceNamePtr *byte if deviceNamePtr, err = syscall.BytePtrFromString(special); err != nil { return } if _, _, errno := syscall.RawSyscall6(syscall.SYS_QUOTACTL, uintptr(cmd), uintptr(unsafe.Pointer(deviceNamePtr)), uintptr(id), uintptr(target), 0, 0); errno != 0 { err = os.NewSyscallError("quotactl", errno) } return }
Da unsafe.Pointer wieder und wieder verwendet wird, sollten wir uns diese Funktion einmal genauer ansehen. unsafe.Pointer lässt sich wohl am besten mit einem void-Pointer in C vergleichen. Der Typ ist unwichtig, wir benötigen nur einen Pointer auf eine Speicherstelle.
Zudem sollte man, wenn man in so tiefe Ebenen einsteigt, sich die Funktion syscall.BytePtrFromString genauer ansehen. Dies ist notwendig, da der String-Typ von Go nicht mit nullterminierten Strings, wie in Kernel oder C verwendet, kompatibel ist. syscall.BytePtrFromString wandelt einen Go-String in einen solchen nullterminierten String um, und liefert einen Pointer auf das erste Byte dieses Strings zurück.
Das target-Argument ist absichtlich ein unsafe.Pointer, da quotactl auch andere Datenstrukturen als die erwähnten dqblk befüllen kann. Dieser Ansatz ermöglicht später eine einfachere Erweiterung. Was uns direkt zu dqblk führt:
type Dqblk struct { DqbBHardlimit uint64 DqbBSoftlimit uint64 DqbCurSpace uint64 DqbIHardlimit uint64 DqbISoftlimit uint64 DqbCurInodes uint64 DqbBTime uint64 DqbITime uint64 DqbValid uint32 }
Wie man sieht, können die Feldnamen beliebig geändert werden, jedoch stellen wir sicher, dass die Feldreihenfolge bestehen bleibt. Die erforderlichen Konstanten sowie eine Funktion zur Implementierung des QCMD-Makros in Go zu definieren, wird dem Leser als Übung überlassen.
Jetzt, da wir die Datenstruktur definiert haben und die C-typische Snake-Case-Schreibweise in eine Go-ähnlichere Camel-Case-Schreibweise umgewandelt haben, können wir die erste öffentliche Funktion zum Abrufen von Quotas implementieren:
func GetQuota(typ int, special string, id int) (result *Dqblk, err error) { result = &Dqblk{} if err = quotactl(qCmd(qGetQuota, typ), special, id, unsafe.Pointer(result)); err != nil { result = nil } return }
Ein möglicher Aufruf wäre beispielsweise GetQuota(UsrQuota, “/dev/loop0”, 0), welche die Informationen eines Benutzers, die „User Quota“, für UID 0 von „/dev/loop0“ abfragt. Ersetze /dev/loop0 mit der Bezeichnung, die beim Erstellen des Testbeds zurückgeliefert wurde, und du wirst sehen, dass der Prototyp funktioniert.
In diesem Artikel wurden die notwendigen Schritte beschrieben, um Low-Level Linux Kernel Funktionen von Go zu verwenden. Informationen zu Speicherplatzgrenzen aus Dateisystemen abzurufen, ist mit geringem Programmieraufwand umsetzbar. Dieser Ansatz – die Funktion nativ in Go zu implementieren – erlaubt die Erstellung von statisch gelinkten Binaries ohne zusätzliche Abhängigkeiten, was ein Deployment insgesamt stark vereinfacht.
Den Code zu diesem Artikel findest du unter: https://github.com/anexia-it/wad2018-quotactl. Eine vollständigere Go-Bibliothek, welche auf den Recherchen basiert, die auch zu diesem Artikel geführt haben, findest du unter: https://github.com/anexia-it/fsquota.
Ich hoffe diese Reise in die Tiefen von Go und Syscalls war genauso spannend und lustig, wie diesen Artikel zu schreiben.