Erstellen eines Container-Unterprozesses
Gehen Sie in das Verzeichnis ~/project
und erstellen Sie eine Datei namens docker.hpp
. In dieser Datei werden wir zunächst einen Namespace namens docker
erstellen, der von unserem externen Code aufgerufen werden kann.
//
// docker.hpp
// cpp_docker
//
// Header-Dateien für Systemaufrufe
#include <sys/wait.h> // waitpid
#include <sys/mount.h> // mount
#include <fcntl.h> // open
#include <unistd.h> // execv, sethostname, chroot, fchdir
#include <sched.h> // clone
// C-Standardbibliothek
#include <cstring>
// C++-Standardbibliothek
#include <string> // std::string
#define STACK_SIZE (512 * 512) // Definieren Sie die Größe des Kindprozess-Speicherbereichs
namespace docker {
//.. hier beginnt die Docker-Magie
}
Lassen Sie uns zunächst einige Variablen definieren, um die Lesbarkeit zu verbessern:
// Innerhalb des `docker`-Namespaces definiert
typedef int proc_status;
proc_status proc_err = -1;
proc_status proc_exit = 0;
proc_status proc_wait = 1;
Bevor wir die Container-Klasse definieren, lassen Sie uns die Parameter analysieren, die für die Erstellung eines Containers erforderlich sind. Wir werden uns vorerst nicht um die netzwerkbezogene Konfiguration kümmern. Um einen Docker-Container aus einem Image zu erstellen, müssen wir nur den Hostnamen und den Speicherort des Images angeben. Daher:
// Docker-Container-Startkonfiguration
typedef struct container_config {
std::string host_name; // Hostname
std::string root_dir; // Root-Verzeichnis des Containers
} container_config;
Jetzt definieren wir die container
-Klasse und lassen sie die erforderlichen Konfigurationen für den Container im Konstruktor durchführen:
class container {
private:
// Verbessert die Lesbarkeit
typedef int process_pid;
// Kindprozess-Stack
char child_stack[STACK_SIZE];
// Container-Konfiguration
container_config config;
public:
container(container_config &config) {
this->config = config;
}
};
Bevor wir über die spezifischen Methoden in der container
-Klasse nachdenken, lassen Sie uns zunächst darüber nachdenken, wie wir diese container
-Klasse verwenden würden. Dazu erstellen wir eine main.cpp
-Datei im ~/project
-Ordner:
//
// main.cpp
// cpp_docker
//
#include "docker.hpp"
#include <iostream>
int main(int argc, char** argv) {
std::cout << "...start container" << std::endl;
docker::container_config config;
// Konfigurieren Sie den Container
//...
docker::container container(config);// Erstellen Sie den Container basierend auf der Konfiguration
container.start(); // Starten Sie den Container
std::cout << "stop container..." << std::endl;
return 0;
}
In main.cpp
, um den Containerstart kompakt und verständlich zu gestalten, nehmen wir an, dass der Container mit einer start()
-Methode gestartet wird. Dies bildet die Grundlage für das Schreiben der docker.hpp
-Datei später.
Jetzt kehren wir zu docker.hpp
zurück und implementieren die start()
-Methode:
void start() {
auto setup = [](void *args) -> int {
auto _this = reinterpret_cast<container *>(args);
// Führen Sie die relevanten Konfigurationen für den Container durch
//...
return proc_wait;
};
process_pid child_pid = clone(setup, child_stack+STACK_SIZE, // Gehen Sie zum unteren Ende des Stacks
SIGCHLD, // Senden Sie ein Signal an den Elternprozess, wenn der Kindprozess beendet wird
this);
waitpid(child_pid, nullptr, 0); // Warten Sie auf das Beenden des Kindprozesses
}
Die docker::container::start()
-Methode verwendet den clone()
-Systemaufruf in Linux. Um das docker::container
-Instanzobjekt an die Callback-Funktion setup
zu übergeben, können wir es über das vierte Argument von clone()
übergeben. Hier übergeben wir den this
-Zeiger.
Was die setup
-Funktion betrifft, erstellen wir dafür einen Lambda-Ausdruck. In C++ kann ein Lambda-Ausdruck mit einer leeren Capture-Liste als Funktionszeiger übergeben werden. Daher wird setup
zur Callback-Funktion, die an clone()
übergeben wird.
Sie können auch eine statische Memberfunktion in der Klasse anstelle eines Lambda-Ausdrucks verwenden, aber das würde den Code weniger elegant gestalten.
Im Konstruktor dieser container
-Klasse definieren wir eine Kindprozess-Behandlungsfunktion, die vom clone()
-Systemaufruf aufgerufen wird. Wir verwenden typedef
, um den Rückgabetyp dieser Funktion in proc_status
zu ändern. Wenn diese Funktion proc_wait
zurückgibt, wird der von clone()
geklonte Kindprozess auf das Beenden warten.
Dies reicht jedoch nicht aus, da wir keine Konfiguration innerhalb des Prozesses durchgeführt haben. Infolgedessen wird unser Programm sofort beendet, da es nichts weiter zu tun hat, sobald der Prozess gestartet wird. Wie wir wissen, können wir in Docker, um einen Container am Laufen zu halten, verwenden:
docker run -it ubuntu:14.04 /bin/bash
Dies bindet STDIN an das /bin/bash
des Containers. Lassen Sie uns also der docker::container
-Klasse eine start_bash()
-Methode hinzufügen:
private:
void start_bash() {
// Konvertieren Sie sicher einen C++-std::string in einen C-Style-String char *
// Ab C++14 ist diese direkte Zuweisung verboten: `char *str = "test";`
std::string bash = "/bin/bash";
char *c_bash = new char[bash.length()+1]; // +1 für '\0'
strcpy(c_bash, bash.c_str());
char* const child_args[] = { c_bash, NULL };
execv(child_args[0], child_args); // Führen Sie /bin/bash im Kindprozess aus
delete []c_bash;
}
Und rufen Sie es innerhalb von setup
auf:
auto setup = [](void *args) -> int {
auto _this = reinterpret_cast<container *>(args);
_this->start_bash();
return proc_wait;
}
Jetzt können wir die folgenden Aktionen sehen:
labex:project/ $ hostname
iZj6cboigynrxh4mn2oo16Z
labex:project/ $ g++ main.cpp -std=c++11
labex:project/ $./a.out
...start container
labex@iZj6cboigynrxh4mn2oo16Z:~/project$ mkdir test
labex@iZj6cboigynrxh4mn2oo16Z:~/project$ ls
a.out docker.hpp main.cpp test
labex@iZj6cboigynrxh4mn2oo16Z:~/project$ exit
exit
stop container...
In den obigen Schritten überprüfen wir zunächst den aktuellen hostname
, kompilieren den bisher geschriebenen Code, führen ihn aus und treten in unseren Container ein. Wir können sehen, dass sich der Bash-Prompt nach dem Eintritt in den Container ändert, was wir erwartet haben.
Es fällt jedoch leicht auf, dass dies nicht das gewünschte Ergebnis ist, da es genau das gleiche wie unser Hostsystem ist. Alle Operationen, die innerhalb dieses "Containers" ausgeführt werden, wirken sich direkt auf das Hostsystem aus.
Hier führen wir die erforderlichen Namespaces in der clone
-API ein.