In Fortsetzung des vorherigen Artikels über KVM veröffentlichen wir eine neue Übersetzung und verstehen die Funktionsweise von Containern am Beispiel der Ausführung eines Busybox-Docker-Images.
Dieser Artikel über Container ist eine Fortsetzung des vorherigen Artikels über KVM. Ich möchte Ihnen genau zeigen, wie Container funktionieren, indem Sie ein Busybox-Docker-Image in unserem eigenen kleinen Container ausführen.
Im Gegensatz zur virtuellen Maschine ist der Container sehr vage und vage. Was wir normalerweise als Container bezeichnen, ist ein eigenständiges Codepaket mit allen erforderlichen Abhängigkeiten, die zusammen ausgeliefert und in einer isolierten Umgebung innerhalb des Host-Betriebssystems ausgeführt werden können. Wenn Sie der Meinung sind, dass dies eine Beschreibung einer virtuellen Maschine ist, gehen wir tiefer und sehen, wie Container implementiert werden.
BusyBox Docker
Unser Hauptziel wird es sein, ein reguläres Busybox-Image für Docker auszuführen, jedoch ohne Docker. Docker verwendet btrfs als Dateisystem für seine Images. Versuchen wir, das Bild herunterzuladen und in ein Verzeichnis zu entpacken:
mkdir rootfs
docker export $(docker create busybox) | tar -C rootfs -xvf -
Wir haben jetzt das Busybox-Image-Dateisystem in den rootfs- Ordner entpackt . Natürlich können Sie ./rootfs/bin/sh ausführen und eine funktionierende Shell erhalten, aber wenn wir uns die Liste der Prozesse, Dateien oder Netzwerkschnittstellen ansehen, können wir feststellen, dass wir Zugriff auf das gesamte Betriebssystem haben.
Versuchen wir also, eine isolierte Umgebung zu schaffen.
Klon
Da wir steuern möchten, auf was der untergeordnete Prozess Zugriff hat, verwenden wir Klon (2) anstelle von Fork (2) . Clone macht fast dasselbe, erlaubt jedoch das Übergeben von Flags, die angeben, welche Ressourcen Sie (mit dem Host) teilen möchten.
Folgende Flags sind erlaubt:
- CLONE_NEWNET - isolierte Netzwerkgeräte
- CLONE_NEWUTS - Host- und Domänenname (UNIX-Timesharing-System)
- CLONE_NEWIPC - IPC-Objekte
- CLONE_NEWPID - Prozesskennungen (PID)
- CLONE_NEWNS - Mountpunkte (Dateisysteme)
- CLONE_NEWUSER - Benutzer und Gruppen.
In unserem Experiment werden wir versuchen, Prozesse, IPC, Netzwerk- und Dateisysteme zu isolieren. So lass uns anfangen:
static char child_stack[1024 * 1024];
int child_main(void *arg) {
printf("Hello from child! PID=%d\n", getpid());
return 0;
}
int main(int argc, char *argv[]) {
int flags =
CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWNET;
int pid = clone(child_main, child_stack + sizeof(child_stack),
flags | SIGCHLD, argv + 1);
if (pid < 0) {
fprintf(stderr, "clone failed: %d\n", errno);
return 1;
}
waitpid(pid, NULL, 0);
return 0;
}
Der Code muss mit Superuser-Berechtigungen ausgeführt werden, da sonst das Klonen fehlschlägt.
Das Experiment liefert ein interessantes Ergebnis: Die untergeordnete PID ist 1. Wir sind uns bewusst, dass der Init-Prozess normalerweise die PID 1 hat. In diesem Fall erhält der untergeordnete Prozess jedoch eine eigene isolierte Prozessliste, in der er zum ersten Prozess wurde.
Arbeitsschale
Um das Erlernen einer neuen Umgebung zu vereinfachen, starten wir eine Shell im untergeordneten Prozess. Lassen Sie uns beliebige Befehle wie Docker ausführen :
int child_main(void *arg) {
char **argv = (char **)arg;
execvp(argv[0], argv);
return 0;
}
Wenn Sie nun unsere Anwendung mit dem Argument / bin / sh starten, wird eine echte Shell geöffnet, in die wir Befehle eingeben können. Dieses Ergebnis zeigt, wie falsch wir waren, als wir über Isolation sprachen:
# echo $$
1
# ps
PID TTY TIME CMD
5998 pts/31 00:00:00 sudo
5999 pts/31 00:00:00 main
6001 pts/31 00:00:00 sh
6004 pts/31 00:00:00 ps
Wie wir sehen können, hat der Shell-Prozess selbst eine PID von 1, aber tatsächlich kann er alle anderen Prozesse des Hauptbetriebssystems sehen und darauf zugreifen. Der Grund ist, dass die Prozessliste aus procfs gelesen wird , das noch vererbt wird.
Also, mounten Sie procfs :
umount2("/proc", MNT_DETACH);
Jetzt brechen die Befehle ps , mount und andere beim Starten der Shell, da procfs nicht gemountet ist. Dies ist jedoch immer noch besser als das übergeordnete Prozessleck.
Chroot
Normalerweise wird chroot verwendet, um das Stammverzeichnis zu erstellen , aber wir werden die Alternative pivot_root verwenden . Dieser Systemaufruf verschiebt das aktuelle Systemstammverzeichnis in ein Unterverzeichnis und weist dem Stammverzeichnis ein anderes Verzeichnis zu:
int child_main(void *arg) {
/* Unmount procfs */
umount2("/proc", MNT_DETACH);
/* Pivot root */
mount("./rootfs", "./rootfs", "bind", MS_BIND | MS_REC, "");
mkdir("./rootfs/oldrootfs", 0755);
syscall(SYS_pivot_root, "./rootfs", "./rootfs/oldrootfs");
chdir("/");
umount2("/oldrootfs", MNT_DETACH);
rmdir("/oldrootfs");
/* Re-mount procfs */
mount("proc", "/proc", "proc", 0, NULL);
/* Run the process */
char **argv = (char **)arg;
execvp(argv[0], argv);
return 0;
}
Es ist sinnvoll, tmpfs in / tmp , sysfs in / sys zu mounten und ein gültiges / dev-Dateisystem zu erstellen . Der Kürze halber werde ich diesen Schritt jedoch überspringen.
Jetzt sehen wir nur Dateien aus dem Busybox-Image, als ob wir eine Chroot verwenden würden :
/ # ls
bin dev etc home proc root sys tmp usr var
/ # mount
/dev/sda2 on / type ext4 (rw,relatime,data=ordered)
proc on /proc type proc (rw,relatime)
/ # ps
PID USER TIME COMMAND
1 root 0:00 /bin/sh
4 root 0:00 ps
/ # ps ax
PID USER TIME COMMAND
1 root 0:00 /bin/sh
5 root 0:00 ps ax
Im Moment sieht der Container ziemlich isoliert aus, vielleicht sogar zu viel. Wir können nichts anpingen und das Netzwerk scheint überhaupt nicht zu funktionieren.
Netzwerk
Das Erstellen eines neuen Netzwerk-Namespace war nur der Anfang! Sie müssen ihm Netzwerkschnittstellen zuweisen und diese so konfigurieren, dass Pakete korrekt weitergeleitet werden.
Wenn Sie die br0-Schnittstelle nicht haben, müssen Sie sie manuell erstellen (brctl ist Teil des Bridge-Utils-Pakets in Ubuntu):
brctl addbr br0
ip addr add dev br0 172.16.0.100/24
ip link set br0 up
sudo iptables -A FORWARD -i wlp3s0 -o br0 -j ACCEPT
sudo iptables -A FORWARD -o wlp3s0 -i br0 -j ACCEPT
sudo iptables -t nat -A POSTROUTING -s 172.16.0.0/16 -j MASQUERADE
In meinem Fall war wlp3s0 die Haupt-WiFi-Netzwerkschnittstelle und 172.16.xx das Netzwerk für den Container.
Unser Container-Launcher muss zwei Schnittstellen erstellen, veth0 und veth1, diese mit br0 verknüpfen und das Routing innerhalb des Containers einrichten.
In der main () -Funktion führen wir diese Befehle vor dem Klonen aus:
system("ip link add veth0 type veth peer name veth1");
system("ip link set veth0 up");
system("brctl addif br0 veth0");
Wenn der Aufruf von clone () endet, fügen wir dem neuen untergeordneten Namespace veth1 hinzu:
char ip_link_set[4096];
snprintf(ip_link_set, sizeof(ip_link_set) - 1, "ip link set veth1 netns %d",
pid);
system(ip_link_set);
Wenn wir nun den IP-Link in einer Container-Shell ausführen , sehen wir eine Loopback-Schnittstelle und eine veth1 @ xxxx-Schnittstelle. Aber das Netzwerk funktioniert immer noch nicht. Lassen Sie uns einen eindeutigen Hostnamen im Container festlegen und Routen konfigurieren:
int child_main(void *arg) {
....
sethostname("example", 7);
system("ip link set veth1 up");
char ip_addr_add[4096];
snprintf(ip_addr_add, sizeof(ip_addr_add),
"ip addr add 172.16.0.101/24 dev veth1");
system(ip_addr_add);
system("route add default gw 172.16.0.100 veth1");
char **argv = (char **)arg;
execvp(argv[0], argv);
return 0;
}
Mal sehen, wie es aussieht:
/ # ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
47: veth1@if48: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue qlen 1000
link/ether 72:0a:f0:91:d5:11 brd ff:ff:ff:ff:ff:ff
/ # hostname
example
/ # ping 1.1.1.1
PING 1.1.1.1 (1.1.1.1): 56 data bytes
64 bytes from 1.1.1.1: seq=0 ttl=57 time=27.161 ms
64 bytes from 1.1.1.1: seq=1 ttl=57 time=26.048 ms
64 bytes from 1.1.1.1: seq=2 ttl=57 time=26.980 ms
...
Funktioniert!
Fazit
Den vollständigen Quellcode finden Sie hier . Wenn Sie einen Fehler finden oder einen Vorschlag haben, hinterlassen Sie bitte einen Kommentar!
Natürlich kann Docker noch viel mehr! Es ist jedoch erstaunlich, wie viele geeignete APIs der Linux-Kernel hat und wie einfach es ist, sie für die Virtualisierung auf Betriebssystemebene zu verwenden.
Ich hoffe, Ihnen hat der Artikel gefallen. Sie können die Projekte des Autors auf Github finden und Twitter folgen, um die Nachrichten zu verfolgen, sowie über RSS .