Creación de un subproceso de contenedor
Ingresa al directorio ~/project
y crea un archivo llamado docker.hpp
. En este archivo, primero crearemos un espacio de nombres (namespace) llamado docker
que puede ser llamado por nuestro código externo.
//
// docker.hpp
// cpp_docker
//
// Archivos de cabecera para llamadas al sistema
#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
// Biblioteca estándar de C
#include <cstring>
// Biblioteca estándar de C++
#include <string> // std::string
#define STACK_SIZE (512 * 512) // Define el tamaño del espacio del proceso hijo
namespace docker {
//.. donde comienza la magia de Docker
}
Comencemos definiendo algunas variables para mejorar la legibilidad:
// Definidas dentro del espacio de nombres `docker`
typedef int proc_status;
proc_status proc_err = -1;
proc_status proc_exit = 0;
proc_status proc_wait = 1;
Antes de definir la clase de contenedor, analicemos los parámetros necesarios para crear un contenedor. Por ahora, no consideraremos la configuración relacionada con la red. Para crear un contenedor Docker a partir de una imagen, solo necesitamos especificar el nombre de host y la ubicación de la imagen. Por lo tanto:
// Configuración de arranque del contenedor Docker
typedef struct container_config {
std::string host_name; // Nombre de host
std::string root_dir; // Directorio raíz del contenedor
} container_config;
Ahora, definamos la clase container
y hagamos que realice la configuración necesaria para el contenedor en el constructor:
class container {
private:
// Mejora la legibilidad
typedef int process_pid;
// Pila del proceso hijo
char child_stack[STACK_SIZE];
// Configuración del contenedor
container_config config;
public:
container(container_config &config) {
this->config = config;
}
};
Antes de pensar en los métodos específicos de la clase container
, primero pensemos en cómo usaríamos esta clase container
. Para ello, creemos un archivo main.cpp
en la carpeta ~/project
:
//
// 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;
// Configurar el contenedor
//...
docker::container container(config);// Construir el contenedor basado en la configuración
container.start(); // Iniciar el contenedor
std::cout << "stop container..." << std::endl;
return 0;
}
En main.cpp
, para hacer el arranque del contenedor conciso y fácil de entender, asumamos que el contenedor se inicia utilizando un método start()
. Esto proporciona una base para escribir el archivo docker.hpp
más adelante.
Ahora, volvamos a docker.hpp
e implementemos el método start()
:
void start() {
auto setup = [](void *args) -> int {
auto _this = reinterpret_cast<container *>(args);
// Realizar configuraciones relevantes para el contenedor
//...
return proc_wait;
};
process_pid child_pid = clone(setup, child_stack+STACK_SIZE, // Mover al final de la pila
SIGCHLD, // Enviar una señal al proceso padre cuando el proceso hijo finaliza
this);
waitpid(child_pid, nullptr, 0); // Esperar a que el proceso hijo finalice
}
El método docker::container::start()
utiliza la llamada al sistema clone()
de Linux. Para pasar el objeto de instancia docker::container
a la función de devolución de llamada setup
, podemos pasarlo utilizando el cuarto argumento de clone()
. Aquí, pasamos el puntero this
.
En cuanto a la función setup
, creamos una expresión lambda para ella. En C++, una expresión lambda con una lista de captura vacía se puede pasar como un puntero a función. Por lo tanto, setup
se convierte en la función de devolución de llamada pasada a clone()
.
También puedes usar una función miembro estática definida en la clase en lugar de una expresión lambda, pero eso haría que el código fuera menos elegante.
En el constructor de esta clase container
, definimos una función de manejo de proceso hijo que será llamada por la llamada al sistema clone()
. Usamos typedef
para cambiar el tipo de retorno de esta función a proc_status
. Cuando esta función devuelve proc_wait
, el proceso hijo clonado por clone()
esperará a salir.
Sin embargo, esto no es suficiente porque no hemos realizado ninguna configuración dentro del proceso. Como resultado, nuestro programa saldrá inmediatamente ya que no hay nada más que hacer una vez que el proceso se inicia. Como sabemos, en Docker, para mantener un contenedor en ejecución, podemos usar:
docker run -it ubuntu:14.04 /bin/bash
Esto enlaza STDIN con el /bin/bash
del contenedor. Entonces, agreguemos un método start_bash()
a la clase docker::container
:
private:
void start_bash() {
// Convertir de forma segura std::string de C++ a cadena de estilo C char *
// A partir de C++14, esta asignación directa está prohibida: `char *str = "test";`
std::string bash = "/bin/bash";
char *c_bash = new char[bash.length()+1]; // +1 para '\0'
strcpy(c_bash, bash.c_str());
char* const child_args[] = { c_bash, NULL };
execv(child_args[0], child_args); // Ejecutar /bin/bash en el proceso hijo
delete []c_bash;
}
Y llámalo dentro de setup
:
auto setup = [](void *args) -> int {
auto _this = reinterpret_cast<container *>(args);
_this->start_bash();
return proc_wait;
}
Ahora, podemos ver las siguientes acciones:
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...
En los pasos anteriores, primero verificamos el hostname
actual, compilamos el código que hemos escrito hasta ahora, lo ejecutamos y entramos en nuestro contenedor. Podemos ver que después de entrar en el contenedor, el indicador de bash cambia, lo cual era lo esperado.
Sin embargo, es fácil notar que este no es el resultado que queremos, ya que es exactamente el mismo que nuestro sistema host. Cualquier operación realizada dentro de este "contenedor" afectará directamente al sistema host.
Aquí es donde introducimos los espacios de nombres necesarios en la API clone
.