Ressourcen-Verwaltung mit Control Groups (cgroups)
Die sogenannten cgroups (Control Groups) dienen der Gruppierung von Prozessen. Dies ermöglicht dem Betriebssystem, einer Gruppe von definierten Prozessen ausgewählte Ressourcen zuzuweisen. Dabei werden auch Hierarchien unterstützt. Dieser Artikel zeigt an praktischen Beispielen, wie man cgroups nutzbringend verwendet.
Cpuset-Subsystem
Das Subsystem cpuset ist eventuell bereits vom gleichnamigen Pseudo-Dateisystem bekannt. Um es gleich vorweg klarzustellen: Das cpuset-Dateisystem wurde zu einem cgroup-Subsystem. Das reine cpuset-Dateisystem existiert weiterhin für Legacy-Unterstützung im Kernel, nutzt aber ausschließlich den Code des Control Group-Subsystems.
Dieses Subsystem ermöglicht es, Prozesse bestimmten Speicherknoten und Prozessoren zuzuweisen. cpuset ist eine definierte Liste von CPUs und Speicherknoten. Unter dem Begriff sind sowohl Hyperthreads als auch mehrere Kerne eines Prozessors zu verstehen (siehe getconf _NPROCESSORS_ONLN).
In vielen Systemen gibt es nur einen Speicherknoten. Mit der zunehmenden Verbreitung der Non-Uniform Memory Access (NUMA)-Architektur stehen dem Betriebssystem mehrere solcher Speicherknoten zur Verfügung. Die NUMA-Architektur unterschiedet zwischen lokalem und »fernem« Speicher, da hier der Speicher über unterschiedliche Busse verbunden ist. Hier unterscheidet sich die NUMA-Architekturen von SMP-Systemen, welche einen gemeinsamen Speicher-Bus für alle besitzen, und wo der Zugriff auf Speicher von allen CPUs aus gleich lange dauert, was allerdings mit steigender Anzahl an CPUs und Speicher nicht mehr skaliert. NUMA löst dieses Skalierungsproblem, indem mehrere Speicherbusse eingeführt wurden. Diese nehmen nur eine begrenzte Anzahl an Prozessoren auf. Zu einem Knoten gehört Speicher und die CPUs, die am selben Speicherbus angeschlossen sind.
NUMA-vertraute Linux-Kernel erlauben hier bestimmte Konfigurationen, um Performance-Vorteile bei Speicherzugriffen zu erhalten. So kann das Betriebssystem dafür sorgen, dass Prozesse möglichst immer »lokalen« Speicher beziehen, um die Zugriffszeit zu optimieren. Je nach Prozess/Anwendung kann es sogar passieren, dass ohne zusätzliche NUMA-Konfiguration die Anwendung auf einem NUMA-System schlechter läuft als auf einem SMP-System. Neben der Anwendung numactl kann auch das Control Groups Subsystem cpuset genutzt werden, um solche Einstellungen vorzunehmen. Als erstes sollte man allerdings mit Hilfe von numactl herausfinden, welche Nodes verfügbar sind und aus welchen Kombinationen aus Speicher und CPU diese bestehen. Zusätzlich ist wichtig, wie groß der Abstand zu anderen Nodes ist:
# numactl --hardware available: 2 nodes (0-1) node 0 cpus: 0 2 4 6 8 10 12 14 node 0 size: 6085 MB node 0 free: 5414 MB node 1 cpus: 1 3 5 7 9 11 13 15 node 1 size: 6144 MB node 1 free: 5725 MB node distances: node 0 1 0: 10 21 1: 21 10
Der erste Node besteht aus den CPUs mit geraden CPU-IDs, der zweite aus ungeraden. Dies sollte in der folgenden Konfiguration übers cpuset-Subsystem berücksichtigt werden:
# mount -t cgroup -o cpu,cpuset nodev /cgroup # cd /cgroup # mkdir students # cd students # echo $$ > tasks -bash: echo: write error: No space left on device [...]
In diesem Beispiel wird neben dem cpuset auch das Group CPU Scheduler-Subsystem geladen. Dieses ist allerdings nicht zwingend notwendig, um das cpuset zu laden. Ohne weitere Konfiguration kann allerdings einer neu erstellten Gruppe kein Prozess hinzugefügt werden. Der Grund dafür ist, dass das cpuset-Subsystem bestimmte Konfigurationen voraussetzt, bevor Prozesse in eine Control Group aufgenommen werden können.
[...] # ls -1 cpuset.* cpuset.cpu_exclusive cpuset.cpus cpuset.mem_exclusive [...] cpuset.mems [...]
Die cpuset-Subsystem-Einträge cpuset.cpus und cpuset.mems enthalten Listen über die zugelassenen CPUs und Speicherknoten in dieser Control Group und müssen gesetzt werden. Ohne CPU und Speicher kann schließlich kein Prozess ausgeführt werden.
# cat /cgroup/cpuset.cpus 0-15 # cat /proc/self/cgroup 1:cpu,cpuset:/ # taskset -c -p $$ pid 17182's current affinity list: 0-15 # cat /cgroup/cpuset.mems 0-1 # echo 1,3,5,7 > cpuset.cpus # echo 1 > cpuset.mems # echo $$ > tasks # cat /proc/self/cgroup 1:cpu,cpuset:/students # taskset -c -p $$ pid 17182's current affinity list: 1,3,5,7
Mit diesen Einstellungen wird der Control Group students eine CPU-Liste zugewiesen, wobei die CPUs Bestandteil des NUMA Node 1 sind. Entsprechend wird dieser Gruppe hier auch der Speicherknoten 1 zugeführt. Durch Zuweisung einer eingeschränkten CPU-Liste wird für alle Prozesse in dieser Gruppe eine CPU-Affinität gesetzt. Prozesse werden dann abhängig von ihrer CPU-Maske nur auf den entsprechenden Prozessoren gescheduled. Dies erlaubt das Portionieren von CPU- und Speicher-Ressourcen, um diese optimal zu nutzen. Durch sogenanntes CPU Shielding können CPU- und Speicher-Ressourcen einer Gruppe exklusiv zugewiesen werden.
Hiermit kann sichergestellt werden, dass keine anderen Prozesse aus anderen Gruppen eventuell CPU- oder Speicher-Ressourcen nutzen. Hierzu ist es allerdings auch notwendig, dass alle Prozesse aus der root-Gruppe in eine Gruppe verschoben werden, die nicht Zugriff auf solche CPU- und Speicher-Ressourcen besitzt:
# mkdir -p /cgroup/sysdefault # cd /cgroup/sysdefault # cd /cgroup/sysdefault # echo 0 > cpuset.mems # echo 0,2,4,6,9-15 > cpuset.cpus # for n in `cat ../tasks`; do echo $n > tasks ; done
Anschließend befinden sich nur noch Prozesse aus der Control Group students auf den exklusiven CPUs (mit Ausnahme von Kernel-Threads, die nicht ohne weiteres verschoben werden können bzw. verschoben werden sollten). CPU Shielding ist vor allem für Dienste interessant, die möglichst wenig Latenz erfahren und schnell reagieren sollen. Somit ist es auch eine gängige Praxis in Mehrprozessor-Echtzeitsystemen, welche den PREEMPT_RT Kernel inklusive Threaded Interrupts verwenden.

