En esta serie de publicaciones, consideraremos cuidadosamente uno de los ingredientes principales del contenedor : los espacios de nombres. En el proceso, crearemos un clon más simple del docker run
: nuestro propio programa que tomará el comando (junto con sus argumentos, si los hay) en la entrada y expandirá el contenedor para su ejecución, aislado del resto del sistema, de forma similar a cómo ejecutaría docker run
para correr desde una imagen .
¿Qué es el espacio de nombres?
El espacio de nombres de Linux es una abstracción de recursos en el sistema operativo. Podemos pensar en el espacio de nombres como un cuadro. Este cuadro contiene recursos del sistema que dependen del tipo de cuadro (espacio de nombres). Actualmente hay siete tipos de espacios de nombres: Cgroups, IPC, Network, Mount, PID, User, UTS.
Por ejemplo, el espacio de nombres de red incluye recursos del sistema relacionados con la red, tales como interfaces de red (por ejemplo, wlan0
, eth0
), tablas de enrutamiento, etc., el espacio de nombres Mount incluye archivos y directorios en el sistema, PID contiene ID de proceso, etc. . Por lo tanto, dos instancias del espacio de nombres de red A y B (correspondientes a dos cuadros del mismo tipo en nuestra analogía) pueden contener diferentes recursos: quizás A contiene wlan0
, mientras que B contiene eth0
y una copia separada de la tabla de enrutamiento.
Los espacios de nombres no son una característica o biblioteca adicional que necesite instalar, por ejemplo, usando el administrador de paquetes apt. Los proporciona el núcleo de Linux y ya son una necesidad para ejecutar cualquier proceso en el sistema. En cualquier momento dado, cualquier proceso P pertenece exactamente a una instancia de espacio de nombres de cada tipo. Por lo tanto, cuando necesita decir "actualizar la tabla de enrutamiento en el sistema", Linux le muestra una copia de la tabla de enrutamiento del espacio de nombres a la que pertenece en ese momento.
¿Para qué es esto?
Absolutamente por nada ... por supuesto, solo estaba bromeando. Una de las grandes propiedades de los cuadros es que puede agregar y quitar elementos del cuadro y esto no afectará el contenido de otros cuadros. Esta es la misma idea con espacios de nombres: el proceso P puede "volverse loco" y ejecutar sudo rm –rf /
, pero otro proceso Q que pertenezca a otro espacio de nombres Mount no se verá afectado, ya que usan copias separadas de estos archivos.
Tenga en cuenta que el recurso contenido en el espacio de nombres no es necesariamente una copia única. En algunos casos que ocurrieron intencionalmente o debido a una violación de seguridad, dos o más espacios de nombres contendrán la misma copia, por ejemplo, el mismo archivo. Por lo tanto, los cambios realizados en este archivo en un espacio de nombres de Mount serán visibles en todos los demás espacios de nombres de Mount, que también se refieren a él. Por lo tanto, abandonaremos nuestra analogía del cajón, ya que el artículo no puede estar en dos cajas diferentes al mismo tiempo.
La restricción es una preocupación
¡Podemos ver los espacios de nombres a los que pertenece el proceso! Por lo general, para Linux, aparecen como archivos en el /proc/$pid/ns
de este proceso con el id del proceso $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]
Puedes abrir otra terminal, ejecutar el mismo comando y esto debería darte el mismo resultado. Esto se debe a que, como mencionamos anteriormente, el proceso debe pertenecer a un determinado espacio de nombres (espacio de nombres) y hasta que especifiquemos explícitamente cuál, Linux lo agrega a los espacios de nombres de forma predeterminada.
Vamos a involucrarnos un poco en esto. En la segunda terminal, podemos hacer algo como esto:
$ 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
El comando unshare
inicia el programa (opcional) en el nuevo espacio de nombres. La bandera -u
le dice que ejecute bash
en el nuevo espacio de nombres UTS. Tenga en cuenta que nuestro nuevo proceso bash
apunta a otro archivo uts
, mientras que todos los demás siguen siendo los mismos.
Crear nuevos espacios de nombres generalmente requiere acceso de superusuario. unshare
, asumiremos que tanto unshare
como nuestra implementación se realizan utilizando sudo
.
Una de las consecuencias de lo que acabamos de hacer es que ahora podemos cambiar el nombre de host del sistema de nuestro nuevo proceso bash y esto no afectará a ningún otro proceso en el sistema. Puede verificar esto ejecutando hostname
en la primera terminal y viendo que el nombre de host no ha cambiado allí.
Pero, ¿qué es, por ejemplo, un contenedor?
Esperemos que ahora tenga alguna idea de lo que puede hacer el espacio de nombres. Puede suponer que los contenedores son procesos esencialmente ordinarios con espacios de nombres que son diferentes de otros procesos, y tendrá razón. De hecho, esta es una cuota. No se requiere que un contenedor sin cuotas pertenezca a un espacio de nombres único de cada tipo; puede compartir algunos de ellos.
Por ejemplo, cuando escribe docker run --net=host redis
, todo lo que debe hacer es decirle al docker que no cree un nuevo espacio de nombres de red para el proceso de redis. Y, como hemos visto, Linux agregará este proceso como participante en el espacio de nombres de red predeterminado, como cualquier otro proceso regular. Por lo tanto, desde el punto de vista de la red, el proceso de redis es exactamente el mismo que el de todos los demás. Esta no es solo una opción de configuración de red, la docker run
permite realizar dichos cambios para la mayoría de los espacios de nombres existentes. Esto plantea la pregunta, ¿qué es un contenedor? ¿Hay un contenedor que usa un proceso que usa todos menos uno del espacio de nombres común? ¯ \ _ (ツ) _ / ¯ Normalmente, los contenedores vienen con el concepto de aislamiento logrado a través de espacios de nombres: cuanto menor sea el número de espacios de nombres y recursos que el proceso comparte con otros, más está aislado y eso es todo lo que realmente importa.
Aislamiento
En el resto de esta publicación, sentaremos las bases para nuestro programa, que llamaremos isolate
. isolate
toma el comando como argumentos y lo inicia en un nuevo proceso, aislado del resto del sistema y limitado por sus propios espacios de nombres. En las siguientes publicaciones, veremos cómo agregar soporte para espacios de nombres individuales para el comando de proceso que inicia los isolate
.
Dependiendo de la aplicación, nos centraremos en los espacios de nombres de usuario, montaje, PID y red. El resto será relativamente trivial para implementar después de que terminemos (de hecho, agregaremos soporte UTS aquí en la implementación inicial del programa). Y la consideración, por ejemplo, de Cgroups, está más allá del alcance de esta serie (el estudio de cgroups, otro componente de los contenedores utilizados para controlar la cantidad de recursos que puede utilizar un proceso).
Los espacios de nombres pueden resultar muy rápidos y hay muchas formas diferentes que puede usar al explorar cada espacio de nombres, pero no podemos seleccionarlos todos a la vez. Discutiremos solo aquellas formas que son relevantes para el programa que estamos desarrollando. Cada publicación comenzará con algunos experimentos en la consola en el espacio de nombres en cuestión para comprender los pasos necesarios para configurar este espacio de nombres. Como resultado, ya tendremos una idea de lo que queremos lograr, y luego seguirá la implementación correspondiente isolate
.
Para evitar la sobrecarga de código de las publicaciones, no incluiremos elementos tales como funciones auxiliares que no son necesarias para comprender la implementación. Puede encontrar el código fuente completo aquí en Github .
Implementación
El código fuente de esta publicación se puede encontrar aquí . Nuestra implementación isolate
será un programa simple que lee una línea con un comando de stdin y clona un nuevo proceso que lo ejecuta con los argumentos especificados. El proceso clonado con el comando se ejecutará en su propio espacio de nombres UTS de la misma manera que lo hicimos con unshare
. En las próximas publicaciones veremos que los espacios de nombres no necesariamente funcionan (o al menos proporcionan aislamiento) del cuadro y necesitaremos realizar alguna configuración después de crearlos (pero antes de ejecutar el comando), de modo que el comando realmente se ejecute de forma aislada.
Esta combinación de creación de espacio de nombres requerirá cierta interacción entre el proceso de isolate
principal y el proceso secundario del comando a ejecutar. Como resultado, parte del trabajo principal aquí será configurar el canal de conexión entre ambos procesos; en nuestro caso, utilizaremos la tubería de Linux debido a su simplicidad.
Necesitamos hacer tres cosas:
- Cree un proceso de
isolate
básico que lea datos de stdin. - Clone un nuevo proceso que ejecutará el comando en el nuevo espacio de nombres UTS.
- Configure la tubería para que el proceso de ejecución del comando comience su lanzamiento solo después de recibir una señal del proceso principal de que la configuración del espacio de nombres se ha completado.
Aquí está el proceso básico:
int main(int argc, char **argv) { struct params params; memset(¶ms, 0, sizeof(struct params)); parse_args(argc, argv, ¶ms);
Tenga en cuenta los clone_flags
que pasamos a nuestra llamada de clone
. ¿Ves lo fácil que es crear un proceso en su propio espacio de nombres? Todo lo que necesitamos hacer es establecer un indicador para el tipo de espacio de nombres (el indicador CLONE_NEWUTS
corresponde al espacio de nombres UTS), y Linux se encargará del resto.
A continuación, el proceso de comando espera una señal antes de comenzar:
static int cmd_exec(void *arg) {
Finalmente, podemos intentar ejecutar esto:
$ ./isolate sh ===========sh============ $ ls isolate isolate.c isolate.o Makefile $ hostname iffy $ hostname coke $ hostname coke
Ahora isolate
es un poco más que un programa que simplemente abandona al equipo (tenemos un UTS que funciona para nosotros). En la próxima publicación, daremos un paso más al examinar los espacios de nombres de Usuario y hacer que isolate
ejecute el comando en su propio espacio de nombres de Usuario. Allí veremos que realmente necesitamos hacer algo de trabajo para tener un espacio de nombres utilizable en el que se pueda ejecutar el comando.