- Cpusets son un subsistema de cgroups que restringe en qué CPUs y nodos de memoria pueden ejecutarse y reservar memoria los procesos.
- La configuración se realiza a través del sistema de ficheros cgroup, creando jerarquías de directorios y ajustando ficheros como cpuset.cpus y cpuset.mems.
- Controladores adicionales como cpu, memory o blkio permiten limitar y contabilizar CPU, RAM e I/O, integrando cpusets en una estrategia global de gestión de recursos.
- En cgroup v2, controladores como cpu y cpuset se combinan en un único árbol, permitiendo cuotas de CPU precisas mediante ficheros como cpu.max.

Si trabajas con servidores Linux cargados de servicios, seguro que alguna vez te has preguntado cómo repartir bien la CPU y la memoria entre procesos sin que unos ahoguen a otros. Las prioridades con nice y los límites clásicos ayudan, pero cuando todo el mundo quiere todos los recursos, el sistema se convierte en una jungla.
Ahí entran en juego dos piezas clave del kernel: cgroups y el subsistema cpuset. Gracias a ellos puedes decidir con precisión qué procesos usan qué CPUs y qué nodos de memoria, vigilar su consumo, limitarles el ancho de banda de CPU o de disco, e incluso agruparlos para crear «particiones blandas» dentro de un mismo servidor.
Qué son los cgroups y por qué importan
Los grupos de control (cgroups) son un mecanismo del kernel Linux para agrupar procesos y aplicarles políticas comunes de recursos: CPU, memoria, I/O de disco, dispositivos, red, etc. No sustituyen a los permisos clásicos, sino que los complementan desde el punto de vista de cuánto consume cada grupo, no de a qué tiene acceso.
Frente a herramientas tradicionales como nice, ionice o limits.conf, los cgroups permiten definir jerarquías de grupos en las que cada conjunto de procesos recibe una porción de recursos predefinida, independientemente de cuántos procesos haya dentro de cada grupo.
En kernels modernos, los cgroups se exponen a través de un sistema de ficheros virtual (cgroup v1 o cgroup v2) montado normalmente en /sys/fs/cgroup. Cada subdirectorio representa un grupo y los ficheros que hay dentro permiten consultar y cambiar su configuración.
cpusets: el subsistema para atar procesos a CPUs y nodos de memoria
Dentro de los cgroups, cpuset es el controlador (subsystem) encargado de restringir en qué CPUs y qué nodos de memoria puede ejecutar y reservar memoria un conjunto de tareas. Es especialmente útil en máquinas grandes con muchos cores y topología NUMA, pero también tiene sentido en servidores más modestos cuando quieres aislar cargas.
Cada cpuset define dos conjuntos fundamentales:
cpuset.cpus: lista de CPUs en las que las tareas del grupo pueden ejecutarse.cpuset.mems: lista de nodos de memoria en los que esas tareas pueden reservar memoria.
Cuando un proceso llama a sched_setaffinity(), mbind() o set_mempolicy(), el kernel filtra esas peticiones para que solo puedan usar CPUs y nodos incluidos en su cpuset actual. Además, el planificador no programará nunca esa tarea en una CPU que no esté en su máscara permitida, y el asignador de páginas no reservará memoria en nodos fuera de mems.
Por qué cpusets son tan útiles en sistemas grandes
En servidores con muchas CPUs y memoria distribuida en varios nodos, la colocación de procesos y memoria influye muchísimo en el rendimiento. Un acceso a memoria «lejana» en una máquina NUMA puede ser mucho más lento que a la memoria local del nodo donde corre la CPU.
Con cpusets puedes crear subconjuntos de la máquina (soft partitions) y asignar trabajos completos a esos subconjuntos: por ejemplo, un grupo de cores y nodos de memoria para una base de datos, otro para el frontal web, y otro para cargas de cálculo intensivo. Estos grupos se pueden ajustar dinámicamente según la carga del sistema, sin interferir con los trabajos que ya se están ejecutando en otras particiones.
Es una estrategia muy habitual en:
- Web servers que ejecutan varias instancias del mismo servicio.
- Máquinas mixtas con web, base de datos y otros demonios compartiendo hardware.
- Clusters NUMA y HPC que ejecutan aplicaciones científicas sensibles a la latencia de memoria.
Ejemplo práctico: domar un Apache glotón con cgroups y cpuset
Imagina un servidor con dos núcleos y un Apache que lanza procesos hijos dinámicamente. Aunque todos tengan el mismo nice, en la práctica la CPU que le queda al resto de servicios dependerá del número de procesos Apache activos en cada momento.
Con prioridades normales ocurren cosas como:
- Apache con 9 procesos y otro servicio con 1: el segundo recibe un 10% de CPU.
- Apache con 99 procesos y el otro con 1: el segundo cae a un 1% de CPU.
La prioridad no cambia, pero el número de procesos sí, y eso despedaza la equidad. Con cgroups y cpuset, puedes crear dos grupos: uno para Apache y otro para el resto, y decirle al kernel que a cada grupo le corresponde, por ejemplo, el 50% de la CPU, independientemente del número de procesos que monten dentro.
Montaje del sistema de ficheros cpuset (cgroup v1)
En muchas distribuciones actuales (Fedora, RHEL modernas, systemd) los cgroups se montan de serie y systemd ya agrupa servicios automáticamente. En otras más antiguas (por ejemplo, Ubuntu 12.04/14.04 con configuración clásica) puedes montar el subsistema cpuset a mano así:
mount -t tmpfs -o size=5M cgroup_root /sys/fs/cgroup
mkdir /sys/fs/cgroup/cpuset
mount -t cgroup -o cpuset cpuset /sys/fs/cgroup/cpuset
Con esto has creado un pequeño tmpfs para albergar las jerarquías, un directorio cpuset que hará de punto de montaje y, finalmente, has montado el sistema de ficheros cgroup limitado al controlador cpuset.
Si inspeccionas /sys/fs/cgroup/cpuset verás ficheros como:
tasksycgroup.procs: listas de tareas e IDs de grupos de hilos pertenecientes al grupo.cpuset.cpus: CPUs permitidas para este grupo.cpuset.mems: nodos de memoria permitidos.- Varios flags adicionales:
cpuset.cpu_exclusive,cpuset.mem_exclusive,cpuset.memory_migrate, etc.
Creación de subgrupos y asignación de CPUs/memoria
Cada subdirectorio que creas bajo /sys/fs/cgroup/cpuset es un nuevo cpuset hijo. Eliminarlo (si está vacío de tareas) se hace con rmdir. Por ejemplo, para partir el sistema en dos grupos, uno por núcleo:
cd /sys/fs/cgroup/cpuset
mkdir grupo-A grupo-B
echo 0 > grupo-A/cpuset.cpus
echo 1 > grupo-B/cpuset.cpus
echo 0 > grupo-A/cpuset.mems
echo 0 > grupo-B/cpuset.mems
Has creado dos cpusets, grupo-A y grupo-B, el primero usando solo la CPU 0 y el segundo la CPU 1. Ambos comparten el único nodo de memoria 0 de la máquina.
Asociar procesos a un cpuset
Para meter tareas en un cpuset, basta con escribir su PID en el fichero tasks de ese grupo. Un proceso solo puede pertenecer a un grupo dentro de una jerarquía concreta; cuando lo mueves, desaparece de la lista del padre.
Supón que abres dos shells nuevos, con PIDs 3435 y 3492. Estarán inicialmente en el cpuset raíz. Puedes enviarlos a los subgrupos así:
echo 3435 > grupo-A/tasks
echo 3492 > grupo-B/tasks
A partir de ese momento, todo lo que se lance desde esos bash heredará su cpuset. Si desde la shell 3435 ejecutas cuatro procesos consume_cpu (un binario que solo quema CPU en bucle), verás sus PIDs dentro de grupo-A/tasks y el núcleo 0 a tope mientras el núcleo 1 permanece casi ocioso.
Si luego quieres “rescatar” uno de esos procesos y darle un core entero, puedes mover su PID al cpuset hermano:
echo 3621 > /sys/fs/cgroup/cpuset/grupo-B/tasks
Al observar con top, verás cómo ambas CPUs empiezan a trabajar, y el proceso movido acapara la mayor parte de la CPU 1, mientras los otros tres consumen la CPU 0.
Otros controladores de cgroups disponibles
cpuset es solo una pieza del puzzle. Los cgroups incluyen más controladores que se pueden usar juntos para una gestión muy fina de recursos:
cpu: controla la proporción de tiempo de CPU asignada a cada grupo. Permite, por ejemplo, que un grupo tenga el 80% de la CPU, otro el 15% y otro el 5%.cpuacct: no limita, solo contabiliza el tiempo de CPU consumido por las tareas del grupo y sus descendientes.blkio: regula el ancho de banda de I/O en dispositivos de bloque, con cuotas proporcionales o límites duros.memory: impone límites de RAM y swap e informa del uso de memoria por grupo.devices: permite o deniega el acceso a dispositivos concretos (por ejemplo, bloquear un dispositivo de bloques a un contenedor).freezer: pausa o reanuda todas las tareas de un grupo.net_clsynet_prio: etiquetan tráfico de red o ajustan prioridades por interfaz para integrarse contc.ns: agrupa procesos en diferentes espacios de nombres, útil para virtualización ligera en combinación con namespaces.perf_event: permite monitorizar las tareas del cgroup con la herramientaperf.
Detalles internos de cpusets en el kernel
A nivel de kernel, cada tarea mantiene un puntero a la estructura de cgroup a la que pertenece. Los cpusets definen máscaras de CPUs y de nodos de memoria que se intersectan con las máscaras de afinidad y las políticas NUMA solicitadas por la tarea.
Algunos puntos clave de la implementación:
- El cpuset raíz contiene todos los cores y nodos de memoria del sistema.
- Cada cpuset hijo debe ser un subconjunto estricto de los recursos del padre.
- Se puede marcar un cpuset como exclusivo para CPU (
cpuset.cpu_exclusive) o memoria (cpuset.mem_exclusive): en ese caso, sus recursos no pueden solaparse con los de sus hermanos (sí con ancestros o descendientes). - No se añaden nuevas llamadas al sistema: todo se maneja vía sistema de ficheros cgroup y ficheros virtuales como
cpuset.cpus,tasks, etc.
El kernel engancha cpusets en varios puntos no críticos para el rendimiento:
- Init: inicializa el cpuset raíz al arrancar.
- fork/exit: para heredar y liberar la pertenencia a cpusets.
sched_setaffinity(): enmascara la afinidad con la máscara permitida por el cpuset.- Asignador de memoria: restringe las páginas a los nodos permitidos.
- Reclaim de memoria y migración de tareas: se respetan, en la medida de lo posible, las restricciones de cpuset.
Flags y ficheros importantes de cpuset
Cada cpuset tiene, además de cpuset.cpus y cpuset.mems, una serie de ficheros de configuración que controlan comportamientos avanzados:
cpuset.memory_migrate: si está a 1, al cambiar de cpuset o modificarmems, las páginas existentes se migran a los nuevos nodos respetando lo más posible la posición relativa.cpuset.mem_hardwallycpuset.mem_exclusive: cuando se activan, endurecen la barrera de memoria y restringen incluso ciertas reservas de kernel compartidas.cpuset.memory_pressureycpuset.memory_pressure_enabled: exponen una medida de la presión de memoria (reclaims directos por segundo) con media móvil, útil para orquestadores y batch schedulers.cpuset.memory_spread_pageycpuset.memory_spread_slab: si se activan, el kernel reparte páginas de caché de ficheros y ciertos slabs en modo round-robin por los nodos permitidos, en lugar de favorecer siempre el nodo local.cpuset.sched_load_balance: controla si el planificador intenta balancear carga entre las CPUs del cpuset.cpuset.sched_relax_domain_level: ajusta el alcance (socket, nodo, sistema completo) de ciertas operaciones de migración de tareas dentro de los dominios de planificación.
Además, en el cpuset raíz encontrarás cpuset.effective_cpus y cpuset.effective_mems, que reflejan los recursos realmente usables teniendo en cuenta eventos de hotplug de CPU/memoria. En modo especial cpuset_v2_mode, estos ficheros pueden diferir de cpuset.cpus y cpuset.mems para mantener un comportamiento más parecido al de cgroup v2.
Interacción con el planificador: sched_load_balance y sched_relax_domain_level
El planificador de Linux divide el sistema en dominios de planificación para minimizar el coste de balancear carga. Balancear entre muchos cores es caro, así que suele hacerse en grupos (por socket, por nodo, etc.).
El flag cpuset.sched_load_balance indica si las CPUs de ese cpuset deben estar en el mismo dominio para permitir que el scheduler mueva tareas libremente dentro de él. Si lo desactivas en el cpuset raíz y solo lo activas en algunos hijos, puedes evitar balanceos innecesarios en CPUs reservadas para tiempo real o cargas muy específicas.
El fichero cpuset.sched_relax_domain_level permite ajustar cuán lejos puede mirar el scheduler cuando:
- Se despierta una tarea y se intenta colocar en un core libre cercano.
- Una CPU queda sin trabajo y tira de tareas de CPUs más cargadas.
Valores típicos (dependientes de la arquitectura) van desde 0 (no buscar) hasta 5 (buscar a nivel de sistema completo en sistemas NUMA), con niveles intermedios para hermanos de HT, cores del mismo socket, nodos, etc. Es una herramienta fina que solo conviene tocar si tienes muy claro el impacto en latencias y cachés.
Cómo usar cpusets en la práctica: flujo típico
El flujo de trabajo para contener un “job” o servicio dentro de un cpuset concreto suele ser:
- Asegurarte de tener el sistema de ficheros cgroup/cpuset montado (v1 o v2).
- Crear el cpuset con
mkdiren la jerarquía correspondiente. - Configurar
cpuset.cpusycpuset.memsantes de añadir tareas. - Opcionalmente activar flags como
memory_migrateocpu_exclusive. - Arrancar un proceso “padre” de la carga y mover su PID al cpuset escribiéndolo en
tasksocgroup.procs. - Lanzar (o dejar que lance) los procesos hijos desde ese padre; heredarán su pertenencia al cpuset.
Si usas herramientas de espacio de usuario como cgroup-bin / libcgroup (en Debian/Ubuntu) o equivalentes en otras distros, puedes manejar esto de forma más cómoda con comandos tipo cgcreate, cgexec y cgclassify, o incluso con ficheros de configuración como /etc/cgconfig.conf y /etc/cgrules.conf para asignar grupos según usuario o comando.
cgroups v1 vs cgroups v2: controladores de CPU y cpuset en RHEL 8 y similares
En distribuciones modernas como RHEL 8, el kernel soporta simultáneamente cgroups v1 y v2. Por defecto, en RHEL 8 se monta v1 al arranque, aunque puedes forzar el uso del modo unificado (v2) con parámetros de kernel:
cgroup_no_v1=all: desactiva todos los controladores v1 en el arranque.systemd.unified_cgroup_hierarchy=1: indica a systemd que use cgroup v2 como jerarquía principal.
Tras el cambio y reinicio, puedes comprobar con mount o findmnt que ya no hay montajes de tipo cgroup clásicos (salvo los internos de systemd) y montar a mano un árbol v2, por ejemplo en /cgroups-v2:
mount -t cgroup2 none /cgroups-v2
En ese directorio raíz (grupo de control raíz) verás ficheros genéricos empezando por cgroup.* y otros específicos de controladores que estén activos ahí, como cpuset.cpus.effective o cpu.max.
Limitar CPU con cgroup v2: controlador cpu y cpuset juntos
En v2 se trabaja con un árbol único y los controladores se activan por subárbol usando el fichero cgroup.subtree_control. Un flujo típico para limitar CPU a un par de procesos sería:
- Activar los controladores
cpuycpusetpara los hijos directos del root escribiendo en/cgroups-v2/cgroup.subtree_controlalgo como+cpu +cpuset. - Crear un subgrupo, por ejemplo
/cgroups-v2/Example/, conmkdir. - Comprobar que en ese directorio aparecen ficheros como
cpu.maxycpuset.cpus. - Ajustar
cpuset.cpusycpuset.memspara asegurarte de que los procesos competirán en las mismas CPUs/nodos (el controladorcpusolo aplica si hay al menos dos procesos compitiendo en la misma CPU). - Configurar
cpu.maxcon una cuota y un periodo, por ejemplo:
echo "200000 1000000" > /cgroups-v2/Example/cpu.max
En este caso, todos los procesos del grupo solo podrán consumir en conjunto 0,2 s de CPU por cada segundo natural. El resto del tiempo quedarán estrangulados hasta el siguiente periodo.
Después solo queda añadir los PIDs de las aplicaciones deseadas al grupo escribiéndolos en /cgroups-v2/Example/cgroup.procs. Si, por ejemplo, hay dos procesos intensivos (PIDs 5439 y 5473) en ese grupo, cada uno acabará alrededor del 10% de CPU, porque están compartiendo la cuota del 20% que has fijado.
Otras herramientas para entender CPU, afinidad y hardware en Linux
Para trabajar con cpusets es muy útil entender bien la topología de CPUs y el estado de la CPU en tu sistema. Linux ofrece un montón de comandos y pseudo-ficheros y herramientas como CPU-X para Linux que dan información detallada:
lscpu: muestra de forma muy legible la arquitectura, número de CPUs lógicas y físicas, sockets, hyper-threading, cachés, soporte de virtualización, etc./proc/cpuinfo: expone información detallada por CPU lógica: modelo, familia, stepping, microcódigo, flags de características (SSE, AVX, VT-x, AMD-V, NX, etc.)./sys/devices/system/cpu/: estructura de directorios muy rica con un subdirectorio por CPU (cpu0,cpu1, …) y otros para cpufreq, cpuidle, microcode, topology, etc.
Dentro de /sys/devices/system/cpu/cpu0/cpufreq/ puedes ver, por ejemplo: (y para más detalles, cómo saber la frecuencia de la CPU).
cpuinfo_cur_freq: frecuencia actual.scaling_max_freqyscaling_min_freq: límites entre los que cpufreq puede escalar.scaling_governor: política activa (performance, powersave, ondemand, etc.).scaling_available_governorsyscaling_driver: modos y driver en uso (por ejemplo,intel_pstate).
En /sys/devices/system/cpu/cpu0/cpuidle/ verás estados de reposo (state0, state1, …) con latencias y consumos, manejados por el subsistema cpuidle. Este, junto con cpufreq y el planificador, decide cuándo apagar núcleos o bajar frecuencias para ahorrar energía según la carga.
En dispositivos móviles se empieza a usar Energy-Aware Scheduling (EAS), que unifica de forma más inteligente decisiones de cpuidle, cpufreq y el scheduler para evitar incoherencias como despertar cores apagados cuando hay otros activos disponibles.
Medir y vigilar el uso de CPU por proceso y por grupo
Cuando juegas con cpusets y controladores de CPU, necesitas herramientas para ver si lo que has configurado tiene efecto, por ejemplo si una VM como VirtualBox usa demasiada CPU. Algunas opciones muy habituales son:
top: vista dinámica del sistema con uso global de CPU, por CPU y por proceso. El campo %CPU te permite ver qué tareas están saturando el sistema.mpstat(del paquetesysstat): estadísticas por CPU y agregadas, útil para ver distribución de carga y tiempos de inactividad (idle, iowait, steal, etc.).ps: combinado consort, puedes listar los procesos que más CPU consumen, por ejemplo:
ps -eo pcpu,pid,user,args | sort -k 1 -r | head -10
Además, los ficheros de contabilidad como cpuacct.usage y cpuacct.usage_percpu (en v1) o los contadores integrados en cgroup v2 te permiten saber cuánta CPU ha consumido un grupo concreto desde que se creó, lo cual es ideal para facturación interna o para comparar el impacto de distintas aplicaciones.
Al final, cpusets y cgroups te dan la capacidad de modelar tu máquina como un conjunto de «islas» de recursos, asignarles trabajos y ajustar al vuelo cómo se reparten CPU, memoria e I/O. Conocer bien los ficheros de /sys/fs/cgroup y de /sys/devices/system/cpu, junto con las herramientas de monitorización, te permite pasar de «a ver qué tal va el servidor» a tener un control muy fino de qué corre dónde, cuánto consume y cómo puedes mejorar su comportamiento cuando la carga aprieta.
Redactor apasionado del mundo de los bytes y la tecnología en general. Me encanta compartir mis conocimientos a través de la escritura, y eso es lo que haré en este blog, mostrarte todo lo más interesante sobre gadgets, software, hardware, tendencias tecnológicas, y más. Mi objetivo es ayudarte a navegar por el mundo digital de forma sencilla y entretenida.