cgroups - Grupos de Control en GNU/Linux

Los cgroups o grupos de control, son una característica del kernel Linux que permite que los procesos se organicen en grupos jerárquicos con el fin de limitar y monitorear el uso de varios tipos de recursos. Con cgroups cada proceso corre en su propio espacio del kernel y de la memoria. Cuando se tienen la necesidad, un administrador puede configurar fácilmente un cgroup para limitar los recursos que puede utilizar un proceso.

En concreto cgroups nos permite:

  • Establecer límites de recursos: Se pueden asignar límites de recursos como CPU, memoria, I/O,  etc. a cada cgroup
  • Realizar funciones de auditoría: Se puede medir el uso de los recursos de cada  cgroup
  • Priorizar recursos: Se puede dar mayor proporción de algunos recursos a determinados grupos de control
  • Etiquetar el tráfico de red con un identificador de clase para controlar el tráfico de red
  • Congelar grupos de procesos, sus puntos de control y reinicio

Como se indica en la documentación man, cgroups v1 se implementa en el kernel Linux desde la versión 2.6.24. Sus controladores se han ido actualizando y ampliando desde entonces, pero debido a inconsistencias entre ellos y a que la gestión de las jerarquías de cgroups se volvió bastante compleja, se desarrolló una segunda versión de cgroups (cgroups v2), que se incluye a partir de la versión 4.5 del kernel.

Aunque cgroups v2 está pensado como un reemplazo de cgroups v1, el sistema más antiguo sigue existiendo (y por razones de compatibilidad es poco probable que se elimine). Actualmente, cgroups v2 implementa solo un subconjunto de los controladores disponibles en cgroups v1. Los dos sistemas se implementan para que tanto los controladores v1 como los controladores v2 se puedan montar en el mismo sistema. Así, por ejemplo, es posible utilizar los controladores que son compatibles con la versión 2, mientras que también se utilizan controladores de la versión 1 donde la versión 2 aún no es compatible con esos controladores. La única restricción aquí es que un controlador no puede emplearse simultáneamente en una jerarquía de cgroups v1 y en la jerarquía de cgroups v2.

Tenemos los siguientes subsistemas disponibles para trabajar con cgroups v1

  • blkio Controla y monitoriza la entrada/salida con los dispositivos de bloques
  • cpu Utiliza el programador para determinar la proporción de uso de CPU
  • cpuacct No influye en la asignación de recursos, su finalidad es generar reportes del reparto de tiempo de CPU
  • cpuset Permite asignar núcleos y nodos de memoria a grupos de control
  • devices Permite/deniega el acceso a dispositivos concretos para aquellas tareas que estén en el cgroup
  • freezer Suspende o reactiva tareas en un cgroup
  • memory Fija límites a la memoria consumida por las tareas del cgroup y genera informes sobre dicho consumo
  • net_cls Etiqueta los paquetes de red con un identificador que permite al controlador de tráfico de GNU/Linux (tc) identificar los generados por un proceso de un cgroup concreto
  • net_prio Establece la prioridad del tráfico de red por interfaz
  • ns Agrupa los procesos en diferentes espacios de nombres (namespaces)
  • perf_event Identifica las tareas del cgroup para que se pueda monitorizar su rendimiento
  • pids Permite limitar el número de procesos que puede crear un grupo de control
  • rdma Permite controlar el número de recursos específicos RDMA/IB por cgroup

Uso de cgroups sin systemd

Podemos trabajar con los grupos de control y los subsistemas con los que se relacionan mediante comandos y utilidades de shell.  Es posible montar jerarquías y establecer parámetros de cgroups (de forma no persistente) utilizando comandos de shell y utilidades disponibles en cualquier sistema.

También podemos utilizar las herramientas incluidas en el paquete libcgroup, que contiene una serie de utilidades de línea de comandos relacionadas con cgroups y sus páginas de manual asociadas. El uso de las utilidades proporcionadas por libcgroup simplifica el trabajo con los cgroups y amplía sus capacidades.

Para instalar las herramientas libcgroup en entornos Devian/Ubuntu:
sudo apt install cgroup-tools

El paquete libcgroup está ahora en desuso. Para evitar conflictos, se recomienda no usar las herramientas libcgroup para los controladores de recursos predeterminados que ahora son un dominio exclusivo de systemd

Para probar el uso de cgroups vamos a crear un pequeño programa que crea procesos y los estresa calculando números primos. El código es el siguiente:

gasta_cpu.c
/*  
 * (c) Author: David Quiroga  
 * e-mail: david [at] clibre [dot] io  
 *  
 * SPDX-License-Identifier: GPL-3.0  
 */  
 
#include <stdio.h> 
#include <stdlib.h> 
#include <sys/types.h> 
#include <unistd.h> 
#include <sys/wait.h> 
 
#define PRI_LIMIT 200000 
#define NUM_PROC 7 
 
static void genera_primos(int limit) 
{ 
    int i, j, num = 2, isprime; 
 
    for (i = 4; i <= limit; i++) { 
        isprime = 1; 
        for (j = 2; j < limit / 2; j++) { 
         if ((i != j) && (i % j == 0)) { 
            isprime = 0; 
            break; 
         } 
        } 
        if (isprime) {  
         num++; 
         // printf("Primo: %d\n", i); 
        }  
    } 
} 
 
int main (int argc, char *argv[])  
{ 
    pid_t childPid; 
     
    printf("PID del proceso: %ld\n", (long) getpid());  
    printf("Pulsa ENTER para continuar: "); 
    getchar(); 
    printf("Gastando CPU...\n"); 
     
    for (int i=1; i<=NUM_PROC; i++) { 
        switch (childPid = fork()) { 
        case -1: 
         fprintf (stderr, "Error en fork()\n"); 
         break; 
        case 0: // child 
         genera_primos(PRI_LIMIT); 
         exit(EXIT_SUCCESS); 
        default: 
         printf("Creado proceso %d PID=%ld\n", i, (long) childPid);  
         break; 
        } 
    }  
     
    printf("Esperando que terminen los procesos...\n"); 
 
    while (waitpid(-1, NULL, 0) > 0) 
        continue; 
 
    printf("Todos los procesos finalizaron\n");  
     
    exit (EXIT_SUCCESS); 
} 

Construimos:

❯ gcc -Wall gasta_cpu.c -o gasta_cpu

En este caso lo corremos en una VM con 8 CPUs asignadas. En el código le indicamos que cree 7 procesos, si ejecutamos el programa:

❯ ./gasta_cpu
PID del proceso: 23468
Pulsa ENTER para continuar:
Gastando CPU...
Creado proceso 1 PID=23469
Creado proceso 2 PID=23470
Creado proceso 3 PID=23471
Creado proceso 4 PID=23472
Creado proceso 5 PID=23473
Creado proceso 6 PID=23474
Creado proceso 7 PID=23475
Esperando que terminen los procesos...

Podemos ver con top el uso de las CPUs:

Procesos sin cgroup

Como vemos el sistema distribuye los procesos por las CPUs de tal forma que tenemos 7 CPUs al 100% (o cercanas) durante el tiempo de proceso.

Ahora vamos a limitarle el uso de 2 CPU's al proceso usando cgroups.

Los cgroups se pueden manipular a través de un sistema de archivos virtual. En /sys/fs/cgroup se encuentra el directorio que contendrá los puntos de montaje de las diferentes jerarquías.

Para ver los subsistemas montados en la jerarquía existente usamos el comando lssubsys:

❯ lssubsys -am
cpuset /sys/fs/cgroup/cpuset
cpu,cpuacct /sys/fs/cgroup/cpu,cpuacct
blkio /sys/fs/cgroup/blkio
memory /sys/fs/cgroup/memory
devices /sys/fs/cgroup/devices
freezer /sys/fs/cgroup/freezer
net_cls,net_prio /sys/fs/cgroup/net_cls,net_prio
perf_event /sys/fs/cgroup/perf_event
hugetlb /sys/fs/cgroup/hugetlb
pids /sys/fs/cgroup/pids
rdma /sys/fs/cgroup/rdma

En este caso podemos ver que por defecto tenemos montado el subsistema cpuset, que es el que nos interesa en este ejemplo, en /sys/fs/cgroup/cpuset.

En el caso de no tener la jerarquía creada y los subsistemas montados, estos se pueden montar con mount. Para este caso concreto podría ser algo así:
mkdir /sys/fs/cgroup/cpuset
mount -t cgroup -o cpuset cpuset /sys/fs/cgroup/cpuset

Primero creamos un nuevo grupo de control que llamaremos group1:

❯ sudo cgcreate -g cpuset:/group1

Este comando es similar a mkdir /sys/fs/cgroup/cpuset/group1.

Ahora vamos a crear los parámetros del grupo donde le indicamos que use solo las CPUs 2-3 y el banco de memoria 0:

❯ sudo cgset -r cpuset.cpus=2-3 group1
❯ sudo cgset -r cpuset.mems=0 group1

Estos comandos son equivalentes a: 
echo 2-3 > /sys/fs/cgroup/cpuset/cpuset.cpus
echo 0 > /sys/fs/cgroup/cpuset/cpuset.mems

Nos queda mover los procesos que queramos al grupo de control. Para eso primero vamos a ejecutar gasta_cpu para ver el número de proceso (por comodidad el proceso nos lo indica por pantalla):

❯ ./gasta_cpu
PID del proceso: 25126
Pulsa ENTER para continuar:

Antes de pulsar ENTER movemos este proceso (y por ende los subprocesos que éste cree) al grupo de control:

❯ sudo cgclassify -g cpuset:group1 25126

Este comando es equivalente a echo 25126 /sys/fs/cgroup/cpuset/group1/task.

Pulsamos ENTER para continuar la ejecución de gasta_cpu y podemos ver con top el resultado:

Procesos con cgroup

Como vemos esta vez solo esta usando las CPUs que le hemos indicado: 2-3.

También podemos lanzar directamente un proceso en un grupo de control con cgexec:

❯ sudo cgexec -g cpuset:group1 ./gasta_cpu

 

Como he comentado antes, en sistemas que utilizan systemd el uso de del paquete libcgroup está recomendado solo para montar y manejar jerarquías para controladores que aún no son compatibles con systemd (principalmente net-prio).

Se recomienda no usar las herramientas libcgroup para modificar las jerarquías predeterminadas montadas por systemd con el fin de evitar comportamientos inesperados.

No sé si por evitar las jerarquías predeterminadas montadas por systemd en la documentación man de cpuset se monta el controlador en /dev/cpuset, que para nuestro caso sería algo así:

mkdir /dev/cpuset
mount -t cpuset cpuset /dev/cpuset
cd /dev/cpuset
mkdir Charlie
cd Charlie
/bin/echo 2-3 > cpuset.cpus
/bin/echo 0 > cpuset.mems
/bin/echo 'PID de gasta_cpu' > tasks

y el resultado es el mismo.

De cualquier forma si el sistema utiliza systemd lo recomendado es utilizar las herramientas que nos ofrece systemd para manjear cgroup, pero esto será el tema de un próximo post.

 

(1 Voto)

Deja un comentario

Asegúrese de introducir toda la información requerida, indicada por un asterisco (*). No se permite código HTML.