--- title: "Namespaces y cgroups v2 en Linux: base de los contenedores" description: "Aprende cómo aíslan procesos los namespaces de Linux y cómo limitan CPU y memoria los cgroups v2 con unshare, ip netns y systemd-run en Ubuntu 24.04." category: "Temas Avanzados" language: es source: https://cubepath.com/es/docs/temas-avanzados/linux-namespaces-y-cgroups updated: 2026-09-25 --- # Namespaces y cgroups v2 en Linux: base de los contenedores Docker, Podman, LXC y Kubernetes no inventan el aislamiento: lo construyen sobre dos mecanismos del kernel. Los **namespaces** controlan qué ve un proceso (sus procesos, su red, su nombre de host, sus puntos de montaje) y los **cgroups** controlan cuánto puede consumir (CPU, memoria, procesos, disco). En este tutorial usarás ambos directamente desde la línea de comandos en Ubuntu 24.04 y terminarás montando a mano un "contenedor" mínimo con recursos limitados. ## Requisitos previos Para seguir esta guía necesitas: - Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath. Debian 12 y Rocky Linux 9 también usan cgroups v2 por defecto y los comandos son los mismos. - Un usuario no root con privilegios `sudo`. - Dos sesiones SSH abiertas, porque varios pasos comparan lo que se ve dentro y fuera de un namespace. ## Paso 1: Instalar las herramientas y revisar el sistema `unshare`, `nsenter` y `lsns` forman parte de `util-linux` e `ip` de `iproute2`, y ambos vienen instalados. Añade `stress-ng` para generar carga en las pruebas de límites: ```bash sudo apt update sudo apt install stress-ng ``` Cada proceso tiene un enlace por namespace en `/proc//ns/`. Míralo para tu propia shell: ```bash ls -l /proc/$$/ns ``` ```output lrwxrwxrwx 1 your_user your_user 0 ... cgroup -> 'cgroup:[4026531835]' lrwxrwxrwx 1 your_user your_user 0 ... ipc -> 'ipc:[4026531839]' lrwxrwxrwx 1 your_user your_user 0 ... mnt -> 'mnt:[4026531841]' lrwxrwxrwx 1 your_user your_user 0 ... net -> 'net:[4026531840]' lrwxrwxrwx 1 your_user your_user 0 ... pid -> 'pid:[4026531836]' ... lrwxrwxrwx 1 your_user your_user 0 ... user -> 'user:[4026531837]' lrwxrwxrwx 1 your_user your_user 0 ... uts -> 'uts:[4026531838]' ``` Dos procesos están en el mismo namespace si el número entre corchetes coincide. `lsns` muestra todos los namespaces del sistema: ```bash sudo lsns --type net ``` Comprueba también que el sistema usa cgroups v2 (la jerarquía unificada): ```bash stat -fc %T /sys/fs/cgroup ``` ```output cgroup2fs ``` Si la salida fuera `tmpfs`, el sistema estaría usando cgroups v1, algo que solo ocurre en distribuciones antiguas o configuradas a propósito. Esta guía asume cgroups v2. ## Paso 2: Aislar el árbol de procesos con un PID namespace Un PID namespace da a sus procesos una numeración propia: el primero es el PID 1 y no ven nada de fuera. Crea uno con una shell dentro: ```bash sudo unshare --pid --fork --mount-proc bash ``` - `--pid` crea el PID namespace. - `--fork` hace que `bash` se ejecute como hijo de `unshare`, que es necesario para que sea el PID 1 del nuevo namespace. - `--mount-proc` monta un `/proc` nuevo, porque `ps` lee de `/proc` y con el antiguo seguiría viendo todos los procesos del host. Dentro, lista los procesos: ```bash ps aux ``` ```output USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.1 9168 5376 pts/0 S 10:12 0:00 bash root 8 0.0 0.0 10884 4352 pts/0 R+ 10:12 0:00 ps aux ``` Desde la segunda sesión SSH, el mismo `bash` es un proceso normal con un PID alto: ```bash ps -ef | grep 'unshare --pid' ``` El aislamiento solo funciona en un sentido: el host ve los procesos del namespace, pero no al revés. Sal con `exit`. ## Paso 3: Cambiar el nombre de host con un UTS namespace El UTS namespace aísla el nombre de host. Así cada contenedor puede tener el suyo sin afectar al servidor: ```bash sudo unshare --uts bash hostname contenedor-demo hostname ``` ```output contenedor-demo ``` En la otra sesión, `hostname` sigue mostrando el nombre original del servidor. Sal con `exit`. ## Paso 4: Crear una pila de red aislada con un network namespace Un network namespace tiene sus propias interfaces, direcciones, rutas y reglas de firewall. `ip netns` los crea con nombre para poder reutilizarlos: ```bash sudo ip netns add demo sudo ip netns exec demo ip -br link ``` ```output lo DOWN 00:00:00:00:00:00 ``` El namespace nace solo con la interfaz `lo`, apagada. Para comunicarlo con el host se usa un par `veth`, un cable virtual con dos extremos: uno se queda en el host y el otro se mueve al namespace: ```bash sudo ip link add veth-host type veth peer name veth-demo sudo ip link set veth-demo netns demo ``` Asigna direcciones de una red privada que no uses (aquí `10.200.0.0/24`) y levanta las interfaces: ```bash sudo ip addr add 10.200.0.1/24 dev veth-host sudo ip link set veth-host up sudo ip netns exec demo ip addr add 10.200.0.2/24 dev veth-demo sudo ip netns exec demo ip link set veth-demo up sudo ip netns exec demo ip link set lo up ``` Comprueba la conectividad desde el namespace hacia el host: ```bash sudo ip netns exec demo ping -c 2 10.200.0.1 ``` ```output 64 bytes from 10.200.0.1: icmp_seq=1 ttl=64 time=0.051 ms 64 bytes from 10.200.0.1: icmp_seq=2 ttl=64 time=0.043 ms ``` El namespace no tiene ruta por defecto, así que no sale a Internet. Esto es lo que hace Docker con su bridge `docker0`, más NAT. La guía de redes avanzadas con iproute2 muestra cómo añadir un bridge y NAT. Por ahora, elimina el namespace (el par `veth` se borra con él): ```bash sudo ip netns delete demo ``` ## Paso 5: Ser root sin serlo con un user namespace El user namespace mapea identificadores de usuario de dentro a otros de fuera. Es lo que permite los contenedores sin privilegios (rootless): un proceso ve UID 0 dentro, pero para el kernel sigue siendo tu usuario normal. Ejecútalo **sin** `sudo`: ```bash unshare --user --map-root-user bash id ``` ```output uid=0(root) gid=0(root) groups=0(root),65534(nogroup) ``` Consulta el mapeo aplicado. Las columnas son: primer UID dentro, primer UID fuera y cuántos UID se mapean: ```bash cat /proc/$$/uid_map ``` ```output 0 1000 1 ``` Desde la otra sesión, `ps -o user,pid,cmd -C bash` muestra ese proceso como tu usuario, no como root. Sal con `exit`. > Nota: Ubuntu 24.04 restringe mediante AppArmor lo que pueden hacer los procesos sin privilegios dentro de un user namespace (`kernel.apparmor_restrict_unprivileged_userns = 1`). Crear el namespace funciona, pero operaciones como montar sistemas de archivos se deniegan salvo que el programa tenga un perfil de AppArmor que lo permita. No desactives esta protección solo para experimentar. ## Paso 6: Limitar recursos con cgroups v2 a mano En cgroups v2 todo es un sistema de archivos bajo `/sys/fs/cgroup`. Cada directorio es un cgroup y los archivos de dentro son sus límites y estadísticas. Mira qué controladores están disponibles para los cgroups hijos de la raíz: ```bash cat /sys/fs/cgroup/cgroup.subtree_control ``` ```output cpuset cpu io memory hugetlb pids rdma misc ``` Crea un cgroup de prueba. Al crear el directorio, el kernel rellena automáticamente sus archivos: ```bash sudo mkdir /sys/fs/cgroup/demo ls /sys/fs/cgroup/demo | grep -E '^(cpu|memory|pids)\.max$' ``` ```output cpu.max memory.max pids.max ``` Fija tres límites: la mitad de una CPU (50 000 µs de cada periodo de 100 000 µs), 200 MB de memoria sin swap y un máximo de 50 procesos: ```bash echo "50000 100000" | sudo tee /sys/fs/cgroup/demo/cpu.max echo 200M | sudo tee /sys/fs/cgroup/demo/memory.max echo 0 | sudo tee /sys/fs/cgroup/demo/memory.swap.max echo 50 | sudo tee /sys/fs/cgroup/demo/pids.max ``` Un proceso entra en un cgroup escribiendo su PID en `cgroup.procs`, y sus hijos heredan el cgroup. Lanza una carga de CPU dentro del cgroup: ```bash sudo sh -c 'echo $$ > /sys/fs/cgroup/demo/cgroup.procs && exec stress-ng --cpu 1 --timeout 30s' ``` Mientras se ejecuta, en la otra sesión observa el consumo con `top`: el proceso `stress-ng-cpu` no pasa del 50 % aunque tenga una CPU entera libre. El archivo `cpu.stat` confirma que el kernel lo ha estado frenando: ```bash grep -E 'nr_throttled|throttled_usec' /sys/fs/cgroup/demo/cpu.stat ``` ```output nr_throttled 297 throttled_usec 14823410 ``` Ahora prueba el límite de memoria intentando reservar 400 MB: ```bash sudo sh -c 'echo $$ > /sys/fs/cgroup/demo/cgroup.procs && exec stress-ng --vm 1 --vm-bytes 400M --timeout 15s' ``` El OOM killer del kernel mata el proceso al superar los 200 MB. Compruébalo en `memory.events`: ```bash cat /sys/fs/cgroup/demo/memory.events ``` ```output low 0 high 0 max 1523 oom 12 oom_kill 12 oom_group_kill 0 ``` Cuando el cgroup queda vacío, puedes borrarlo con `rmdir` (no con `rm -r`, porque sus archivos no se pueden eliminar): ```bash sudo rmdir /sys/fs/cgroup/demo ``` ## Paso 7: Usar cgroups a través de systemd En Ubuntu, systemd es el dueño del árbol de cgroups: cada servicio vive en su propio cgroup dentro de `system.slice`. Crear cgroups a mano sirve para aprender, pero en un servidor real conviene pedírselos a systemd, que además los limpia al terminar. Visualiza el árbol: ```bash systemd-cgls --no-pager | head -n 20 ``` `systemd-run` ejecuta un comando en un cgroup temporal con los límites que le indiques. El mismo experimento del paso anterior queda así: ```bash sudo systemd-run --scope -p CPUQuota=50% -p MemoryMax=200M -p MemorySwapMax=0 -p TasksMax=50 stress-ng --cpu 1 --timeout 20s ``` ```output Running scope as unit: run-r3f1c2a9d8e7b4c6a.scope ``` Mientras se ejecuta, `systemd-cgtop` muestra el consumo de CPU y memoria por cgroup en tiempo real: ```bash sudo systemd-cgtop ``` Para aplicar límites permanentes a un servicio existente usa `systemctl set-property`, que guarda la configuración en un archivo drop-in. Por ejemplo, para limitar `cron` a 256 MB: ```bash sudo systemctl set-property cron.service MemoryMax=256M systemctl show cron.service -p MemoryMax ``` ```output MemoryMax=268435456 ``` Para quitar el límite, ejecuta `sudo systemctl set-property cron.service MemoryMax=infinity`. ## Paso 8: Construir un contenedor mínimo Ahora combina todo: una shell con su propio árbol de procesos, nombre de host, red e IPC, dentro de un cgroup con memoria y CPU limitadas: ```bash sudo systemd-run --scope -p MemoryMax=256M -p CPUQuota=100% \ unshare --pid --fork --mount-proc --uts --net --ipc bash ``` Dentro, comprueba el aislamiento: ```bash hostname mini-contenedor ps aux ip -br link cat /proc/self/cgroup ``` ```output USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.1 9168 5504 pts/0 S 10:40 0:00 bash root 9 0.0 0.0 10884 4480 pts/0 R+ 10:40 0:00 ps aux lo DOWN 00:00:00:00:00:00 0::/system.slice/run-r8b2e5d0c1a9f4e37.scope ``` La shell solo se ve a sí misma, no tiene red y su cgroup es el scope creado por systemd. Lo que le falta para ser un contenedor como los de Docker es un sistema de archivos raíz propio (con `pivot_root` y un mount namespace), un user namespace para no ejecutarse como root real y filtros como seccomp y capabilities. Sal con `exit`. Si tienes Docker instalado, puedes comprobar que usa exactamente estos mecanismos: ```bash docker run -d --name limite --memory 256m --cpus 0.5 nginx:alpine cat /sys/fs/cgroup/system.slice/docker-$(docker inspect -f '{{.Id}}' limite).scope/memory.max docker rm -f limite ``` ```output 268435456 ``` ## Solución de problemas **`unshare: unshare failed: Operation not permitted`**: la mayoría de tipos de namespace requieren privilegios. Usa `sudo`, salvo para `--user`, que debe ejecutarse sin él. **`tee: /sys/fs/cgroup/demo/cpu.max: No such file or directory`**: el controlador `cpu` no está habilitado para los hijos del cgroup padre. Comprueba `cgroup.subtree_control` del padre y actívalo con `echo "+cpu" | sudo tee /sys/fs/cgroup/cgroup.subtree_control`. **`rmdir: failed to remove '/sys/fs/cgroup/demo': Device or resource busy`**: aún queda algún proceso dentro. Compruébalo con `cat /sys/fs/cgroup/demo/cgroup.procs` y espera a que terminen o muévelos a otro cgroup. **El proceso no respeta el límite de memoria y el servidor usa swap**: falta `memory.swap.max` a `0` (o `MemorySwapMax=0` en systemd). Sin él, el cgroup puede seguir creciendo en swap. ## Conclusión Has usado PID, UTS, network y user namespaces para aislar lo que ve un proceso, y cgroups v2 (directamente y mediante systemd) para limitar cuánto consume. Con esto entiendes qué hacen por debajo opciones como `docker run --memory` o `--network none`. Como siguientes pasos puedes: - Profundizar en la red de los namespaces con bridges, NAT y control de tráfico usando iproute2. - Aplicar `MemoryMax`, `CPUQuota` y `TasksMax` a tus propios servicios de systemd para que un fallo no tumbe el servidor. - Probar Podman en modo rootless para ver los user namespaces en un entorno de contenedores completo.