In dieser Reihe von Beiträgen werden wir einen der Hauptbestandteile des Containers - Namespaces - sorgfältig betrachten. Dabei erstellen wir einen einfacheren Klon des docker run
- unser eigenes Programm, das den Befehl (zusammen mit seinen Argumenten, falls vorhanden) an der Eingabe verwendet und den Container für seine Ausführung vom Rest des Systems isoliert erweitert, ähnlich wie Sie ihn ausführen würden docker run
, um von einem Image ausgeführt zu werden .
Was ist ein Namespace?
Der Linux-Namespace ist eine Abstraktion von Ressourcen im Betriebssystem. Wir können uns den Namespace als eine Box vorstellen. Dieses Feld enthält Systemressourcen, die vom Typ des Felds (Namespace) abhängen. Derzeit gibt es sieben Arten von Namespaces: Cgroups, IPC, Network, Mount, PID, User, UTS.
Der Netzwerk-Namespace enthält beispielsweise netzwerkbezogene Systemressourcen wie Netzwerkschnittstellen (z. B. wlan0
, eth0
), Routing-Tabellen usw. Der Mount-Namespace enthält Dateien und Verzeichnisse im System, die PID enthält Prozess-IDs usw. . Daher können zwei Instanzen des Netzwerk-Namespace A und B (die in unserer Analogie zwei Feldern desselben Typs entsprechen) unterschiedliche Ressourcen enthalten - möglicherweise enthält A wlan0
, während B eth0
und eine separate Kopie der Routing-Tabelle enthält.
Namespaces sind keine zusätzlichen Funktionen oder Bibliotheken, die Sie beispielsweise mithilfe des apt-Paketmanagers installieren müssen. Sie werden vom Linux-Kernel selbst bereitgestellt und sind bereits erforderlich, um einen Prozess auf dem System auszuführen. Zu jedem Zeitpunkt gehört jeder Prozess P zu genau einer Instanz des Namespace jedes Typs. Wenn er also "Aktualisieren der Routing-Tabelle im System" sagen muss, zeigt ihm Linux eine Kopie der Namespace-Routing-Tabelle, zu der er gerade gehört.
Wofür ist das?
Absolut umsonst ... natürlich habe ich nur Spaß gemacht. Eine der großartigen Eigenschaften der Boxen ist, dass Sie Dinge zur Box hinzufügen und daraus entfernen können. Dies hat keine Auswirkungen auf den Inhalt anderer Boxen. Dies ist die gleiche Idee bei Namespaces - der P- Prozess kann „verrückt werden“ und sudo rm –rf /
ausführen, aber der andere Q- Prozess, der einem anderen Mount-Namespace gehört, ist nicht betroffen, da er separate Kopien dieser Dateien verwendet.
Beachten Sie, dass die im Namespace enthaltene Ressource nicht unbedingt eine eindeutige Kopie ist. In einigen Fällen, die absichtlich oder aufgrund einer Sicherheitsverletzung aufgetreten sind, enthalten zwei oder mehr Namespaces dieselbe Kopie, z. B. dieselbe Datei. Somit sind die an dieser Datei in einem Mount-Namespace vorgenommenen Änderungen tatsächlich in allen anderen Mount-Namespaces sichtbar, die ebenfalls darauf verweisen. Daher werden wir unsere Schubladenanalogie aufgeben, da sich der Artikel nicht gleichzeitig in zwei verschiedenen Kartons befinden kann.
Einschränkung ist ein Problem
Wir können die Namespaces sehen, zu denen der Prozess gehört! In der Regel werden sie unter Linux als Dateien im Verzeichnis /proc/$pid/ns
dieses Prozesses mit der Prozess-ID $pid
:
$ ls -l /proc/$$/ns total 0 lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 cgroup -> cgroup:[4026531835] lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 ipc -> ipc:[4026531839] lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 mnt -> mnt:[4026531840] lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 net -> net:[4026531957] lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 pid -> pid:[4026531836] lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 user -> user:[4026531837] lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 uts -> uts:[4026531838]
Sie können ein anderes Terminal öffnen, denselben Befehl ausführen und dies sollte zu demselben Ergebnis führen. Dies liegt daran, dass der Prozess, wie bereits erwähnt, zu einem bestimmten Namespace (Namespace) gehören muss und Linux ihn standardmäßig zu Namespaces hinzufügt, bis wir explizit angeben, welcher.
Lassen Sie uns ein wenig darauf eingehen. Im zweiten Terminal können wir so etwas tun:
$ hostname iffy $ sudo unshare -u bash $ ls -l /proc/$$/ns lrwxrwxrwx 1 root root 0 May 18 13:04 cgroup -> cgroup:[4026531835] lrwxrwxrwx 1 root root 0 May 18 13:04 ipc -> ipc:[4026531839] lrwxrwxrwx 1 root root 0 May 18 13:04 mnt -> mnt:[4026531840] lrwxrwxrwx 1 root root 0 May 18 13:04 net -> net:[4026531957] lrwxrwxrwx 1 root root 0 May 18 13:04 pid -> pid:[4026531836] lrwxrwxrwx 1 root root 0 May 18 13:04 user -> user:[4026531837] lrwxrwxrwx 1 root root 0 May 18 13:04 uts -> uts:[4026532474] $ hostname iffy $ hostname coke $ hostname coke
Der Befehl unshare
startet das Programm (optional) im neuen Namespace. Das Flag -u
weist sie an, bash
im neuen UTS-Namespace auszuführen. Beachten Sie, dass unser neuer bash
Prozess auf eine andere uts
Datei verweist, während alle anderen gleich bleiben.
Das Erstellen neuer Namespaces erfordert normalerweise Superuser-Zugriff. unshare
gehen wir davon aus, dass sowohl das unshare
als auch unsere Implementierung mit sudo
durchgeführt werden .
Eine der Konsequenzen dessen, was wir gerade getan haben, ist, dass wir jetzt den Systemhostnamen von unserem neuen Bash-Prozess ändern können und dies keinen anderen Prozess im System beeinflusst. Sie können dies überprüfen, indem Sie den hostname
im ersten Terminal ausführen und feststellen, dass sich der Hostname dort nicht geändert hat.
Aber was ist zum Beispiel ein Container?
Hoffentlich haben Sie jetzt eine Vorstellung davon, was der Namespace tun kann. Sie können davon ausgehen, dass Container im Wesentlichen normale Prozesse mit Namespaces sind, die sich von anderen Prozessen unterscheiden, und Sie haben Recht. In der Tat ist dies eine Quote. Ein Container ohne Kontingente muss nicht zu einem eindeutigen Namespace jedes Typs gehören - er kann einige davon gemeinsam nutzen.
Wenn Sie beispielsweise docker run --net=host redis
Sie den docker run --net=host redis
an, keinen neuen Netzwerk-Namespace für den redis-Prozess zu erstellen. Und wie wir gesehen haben, wird Linux diesen Prozess wie jeden anderen regulären Prozess als Teilnehmer am Standard-Netzwerk-Namespace hinzufügen. Aus Netzwerksicht ist der Redis-Prozess also genau der gleiche wie bei allen anderen. Dies ist nicht nur eine Netzwerkkonfigurationsoption. Mit docker run
können Sie solche Änderungen für die meisten vorhandenen Namespaces vornehmen. Dies wirft die Frage auf, was ein Container ist. Gibt es einen Container, der einen Prozess verwendet, der alle bis auf einen gemeinsamen Namespace verwendet? ¯ \ _ (ツ) _ / ¯ Normalerweise werden Container mit dem Konzept der Isolation geliefert, das durch Namespaces erreicht wird: Je weniger Namespaces und Ressourcen der Prozess mit anderen teilt, desto mehr ist er isoliert und das ist alles, was wirklich zählt.
Isolierung
Im weiteren Verlauf dieses Beitrags legen wir den Grundstein für unser Programm, das wir als isolate
. isolate
nimmt den Befehl als Argument und startet ihn in einem neuen Prozess, der vom Rest des Systems isoliert und durch seine eigenen Namespaces begrenzt ist. In den folgenden Beiträgen werden wir uns mit der Unterstützung einzelner Namespaces für den Prozessbefehl befassen, mit dem Starts isolate
.
Je nach Anwendung konzentrieren wir uns auf Benutzer-, Mount-, PID- und Netzwerk-Namespaces. Der Rest wird nach Abschluss relativ trivial zu implementieren sein (tatsächlich werden wir hier bei der ersten Implementierung des Programms UTS-Unterstützung hinzufügen). Die Berücksichtigung von Cgroups geht beispielsweise über den Rahmen dieser Reihe hinaus (die Untersuchung von Cgroups, einer weiteren Komponente von Containern , mit denen gesteuert wird, wie viel Ressourcen ein Prozess verwenden kann).
Namespaces können sich als sehr schnell herausstellen und es gibt viele verschiedene Möglichkeiten, die Sie beim Erkunden der einzelnen Namespaces verwenden können. Wir können sie jedoch nicht alle gleichzeitig auswählen. Wir werden nur die Wege diskutieren, die für das Programm, das wir entwickeln, relevant sind. Jeder Beitrag beginnt mit einigen Experimenten in der Konsole des betreffenden Namespace, um die zum Konfigurieren dieses Namespace erforderlichen Schritte zu verstehen. Als Ergebnis haben wir bereits eine Vorstellung davon, was wir erreichen wollen, und dann wird die entsprechende Implementierung in isolate
folgen.
Um eine Codeüberladung von Posts zu vermeiden, werden keine Hilfsfunktionen berücksichtigt, die für das Verständnis der Implementierung nicht erforderlich sind. Den vollständigen Quellcode finden Sie hier auf Github .
Implementierung
Den Quellcode für diesen Beitrag finden Sie hier . Unsere isolate
Implementierung ist ein einfaches Programm, das eine Zeile mit einem Befehl von stdin liest und einen neuen Prozess klont, der sie mit den angegebenen Argumenten ausführt. Der geklonte Prozess mit dem Befehl wird in seinem eigenen UTS-Namespace auf die gleiche Weise ausgeführt wie zuvor beim unshare
. In den nächsten Beiträgen werden wir sehen, dass Namespaces nicht unbedingt von der Box funktionieren (oder zumindest eine Isolation bieten), und wir müssen nach dem Erstellen (aber vor dem tatsächlichen Ausführen des Befehls) einige Konfigurationen vornehmen, damit der Befehl wirklich isoliert ausgeführt wird.
Diese Namespace-Kombination zum Erstellen und Konfigurieren erfordert eine gewisse Interaktion zwischen dem isolate
und dem untergeordneten Prozess des auszuführenden Befehls. Daher besteht ein Teil der Hauptarbeit darin, den Verbindungskanal zwischen beiden Prozessen zu konfigurieren. In unserem Fall verwenden wir die Linux-Pipe aufgrund ihrer Einfachheit.
Wir müssen drei Dinge tun:
- Erstellen Sie einen grundlegenden
isolate
, der Daten aus stdin liest. - Klonen Sie einen neuen Prozess, der den Befehl im neuen UTS-Namespace ausführt.
- Konfigurieren Sie die Pipe so, dass der Befehlsausführungsprozess erst gestartet wird, nachdem vom Hauptprozess ein Signal empfangen wurde, dass die Namespace-Konfiguration abgeschlossen ist.
Hier ist der grundlegende Prozess:
int main(int argc, char **argv) { struct params params; memset(¶ms, 0, sizeof(struct params)); parse_args(argc, argv, ¶ms);
Beachten Sie die clone_flags
, die wir an unseren clone_flags
übergeben. Sehen Sie, wie einfach es ist, einen Prozess in einem eigenen Namespace zu erstellen? Alles, was wir tun müssen, ist ein Flag für den Namespace-Typ zu setzen (das CLONE_NEWUTS
Flag entspricht dem UTS-Namespace), und Linux kümmert sich um den Rest.
Als nächstes erwartet der Befehlsprozess ein Signal, bevor er startet:
static int cmd_exec(void *arg) {
Schließlich können wir versuchen, dies auszuführen:
$ ./isolate sh ===========sh============ $ ls isolate isolate.c isolate.o Makefile $ hostname iffy $ hostname coke $ hostname coke
Jetzt ist isolate
etwas mehr als ein Programm, das das Team einfach forciert (wir haben eine UTS, die für uns funktioniert). Im nächsten Beitrag werden wir einen weiteren Schritt unternehmen, indem wir die Benutzernamensräume untersuchen und isolate
um den Befehl in seinem eigenen Benutzernamensraum auszuführen. Dort werden wir sehen, dass wir tatsächlich etwas arbeiten müssen, um einen verwendbaren Namespace zu haben, in dem der Befehl ausgeführt werden kann.