martes, 2 de junio de 2026

Entrando en bucle 2 - Limitar el uso de CPU de un programa

NOTA: Ésta es la primera entrada "útil" de la serie. La he organizado con cada script partido en tres secciones: El script en sí para copiar y pegar, unas instrucciones de uso y, para los más curiosos, una explicación sobre cómo funcionan sus tripas. Al que no le interese la programación de scripts, puede saltarse la tercera sección. De todas formas, no es muy detallada: esto no es un curso de programación, ni pretende serlo.

Hoy veremos dos pequeños scripts que sirven para lanzar un programa con el uso de CPU limitado a nuestra voluntad. Todo empezó porque estoy recodificando algunos vídeos para que ocupen menos espacio en mis discos. Es un proceso pesado que usa la CPU al 100% durante horas, dejando el ordenador prácticamente inutilizable mientras tanto. Se me ocurrió limitar la capacidad de proceso del programa de codificación para dejar recursos disponibles para poder navegar por Internet, ver una película o incluso jugar a algo ligero (ahora estoy repitiendo con el primer Half-Life) mientras va comprimiendo sin prisa pero sin pausa.

Handbrake limitado al 35% de CPU, dejando recursos disponibles para el resto del sistema, en un modesto N100. El porcentaje de uso real varia entre el 32% y el 38%. 

He probado dos soluciones a este problema:
Uno de los scripts limita el número de núcleos asignados a un programa, asignando la afinidad de núcleos. Por ejemplo, en un equipo con 4 núcleos, podemos asignar 3 de ellos para una tarea pesada y dejar uno libre para el resto del sistema (sistema operativo, servicios en segundo plano y tareas ligeras como navegar o ver vídeos).
El otro utiliza el planificador de tareas del sistema para limitar un programa a un porcentaje del total del procesador.

Limitar núcleos es menos eficiente: es fácil acabar con varios núcleos funcionando al 100% y generando mucho calor, mientras el resto están infrautilizados. Por otro lado, limitar por porcentajes es menos "estable": hay más picos de uso (el planificador no es instantáneo), por lo que se puede notar que el ordenador da algunos tirones de vez en cuando. Usar uno u otro depende del uso que se les vaya a dar y de las preferencias personales de cada uno.

He usado Zenity para crear los diálogos gráficos. Es una herramienta fácil de usar, consume pocos recursos y se encuentra disponible y preinstalada en las distribuciones Linux mayoritarias. No es universal, pero casi. Hay sienes y sienes de manuales y tutoriales en Internet sobre esta herramienta. Yo he seguido éste: https://linuxconfig.org/how-to-use-graphical-widgets-in-bash-scripts-with-zenity

Afinardor - Establece afinidad entre procesos y núcleos

Script afinador.sh

#!/bin/bash
# -*- ENCODING: UTF-8 -*-
program="$1"
NumCores=$(nproc)
cores=()
i=0
while [[ $NumCores -gt $i ]]; do
((i++))
cores+=("a ")
cores+=($i)
done
final=$(zenity --list \
--width=600 \
--height=300 \
--title="Selecciona el número de núcleos a usar" \
--text "Número de núcleos a usar con $1" \
--radiolist \
--column="Select" --column="Option" \
"${cores[@]}" \
)
cores=
if [[ $? == 1 ]]; then
exit
fi
executor="taskset -c 0"
i=1
while [[ $i -lt $final ]]; do
executor="$executor,$i"
((i++))
done
executor="$executor $program"
eval "$executor"
exit

Copiar este texto, pegarlo en un archivo de texto plano, guardarlo y otorgarle permisos de ejecución.

Instrucciones de uso

Hay que lanzar el programa objetivo como parámetro del script. Una vez ejecutado, muestra un diálogo con el número de núcleos a usar, se selecciona la cantidad deseada y se pulsa en "Aceptar". Ejemplos:

./afinador.sh avidemux3_qt5
Muestra el diálogo para asignar núcleos y, al pulsar [Aceptar], lanza Avidemux limitado a esos núcleos.

./afinador.sh "/ruta/hasta/el programa en cuestión"
Lo mismo, para un ejecutable que requiera la ruta completa por no estar incluido en la variable $PATH. Nótese que se usan comillas para rutas con espacios o caracteres especiales.


Este script está pensado para usarse en modo gráfico mediante lanzadores estándar. Yo lo estoy usando con Handbrake, así que edité el lanzador del menú de inicio sustituyendo
ghb %f
por
/home/mi_usuario/afinador.sh ghb
Ahora, cuando voy a "Inicio - Sonido y vídeo - Handbrake", primero me aparece el diálogo del script y después se ejecuta el propio Handbrake con la afinidad que le haya establecido en dicho diálogo.

Cómo funciona

El script utiliza la herramienta "taskset", que permite asignar núcleos concretos a un programa. Por ejemplo:

taskset -c 0,1 audacious
ejecuta Audacious limitado a los núcleos 0 y 1 de la CPU (los dos primeros).

taskset -c 3 mplayer
ejecuta MPlayer exclusivamente en el núcleo número 3 (el 4º).

El script empieza almacenando el ejecutable objetivo desde el parámetro que se haya escrito ($1) en la variable $program. Continúa averiguando cuántos núcleos tiene la CPU mediante el comando nproc.
Luego genera la lista de opciones para el diálogo que mostrará Zenity (¿Cuántos núcleos quieres usar? ¿1, 2, 3...?) mediante un bucle while. Esta parte es un poco delicada, porque Zenity espera que cada opción contenga, al menos, dos valores separados por un espacio, uno para cada columna de la lista. Por eso le añade "a " a cada opción. En cada iteración del bucle se almacenan los valores "a " y el número de núcleos contados hasta ese momento dentro de la variable $cores usando el operador +=(); esta variable almacena un conjunto de valores, que después son extraídos en orden por Zenity mediante ${cores[@]}.
Con esta información disponible, por fin se puede crear el diálogo para el usuario. Zenity muestra un cuadro con una descripción, una lista con el número de núcleos a usar, un botón "Aceptar" y otro "Cancelar", y almacena la decisión de usuario en la variable $final. Si se pulsa "Cancelar", el script se cierra. De lo contrario, $final contiene el número de núcleos a usar.

El problema de taskset es que no se le puede decir simplemente "este proceso va a usar 3 cores" y dejar que él decida cuáles. Si queremos que asigne 3 cores, hay que "ensamblar" los parámetros, tipo taskset -c 0,1,2. Para eso se usa el segundo bucle while: En cada iteración va añadiendo núcleos al parámetro (0 - 0,1 - 0,1,2 - 0,1,2,3 ...) hasta alcanzar el número de núcleos esperados.

Al final, se "monta" el comando final (taskset -c 0,1,2,3... programa), almacenándolo como cadena de texto en $executor, y se lanza mediante el comando eval.

Limitador - Limita el porcentaje de CPU de un programa

Script limitador.sh

#!/bin/bash
# -*- ENCODING: UTF-8 -*-
program="$1"
percent=$(zenity --scale \
    --title="Límite CPU" \
    --text="Selecciona % de CPU" \
    --min-value=10 \
    --max-value=100 \
    --value=50)
if [[ $? == 1 ]]; then
    exit
fi
NumCores=$(nproc)
quota=$(($NumCores*$percent))
systemd-run --user --scope -p CPUQuota="${quota}%" "$program"
exit

Copiar este texto, pegarlo en un archivo de texto plano, guardarlo y otorgarle permisos de ejecución. 

Instrucciones de uso

Hay que lanzar el programa objetivo como parámetro del script. Una vez ejecutado, muestra un diálogo con una barra que representa el porcentaje de CPU a usar, se selecciona la cantidad deseada y se pulsa en "Aceptar". Ejemplos:

./limitador.sh avidemux3_qt5
Muestra el diálogo para asignar el porcentaje y, al pulsar [Aceptar], lanza Avidemux limitado a ese porcentaje .

./limitador.sh "/ruta/hasta/el programa en cuestión"
Lo mismo, para un ejecutable que requiera la ruta completa por no estar incluido en la variable $PATH. Nótese que se usan comillas para rutas con espacios o caracteres especiales.

 

Este script está pensado para usarse en modo gráfico mediante lanzadores estándar. Yo lo estoy usando con Handbrake, así que edité el lanzador del menú de inicio sustituyendo
ghb %f
por
/home/mi_usuario/limitador.sh ghb
Ahora, cuando voy a "Inicio - Sonido y vídeo - Handbrake", primero me aparece el diálogo del script y después se ejecuta el propio Handbrake con la limitación de CPU que le haya establecido en dicho diálogo.


Cómo funciona

En este caso, la función principal del programa la lleva a cabo systemd-run mediante el parámetro -p CPUQuota... y otros parámetros a los que he llegado por prueba y error. XD

Igual que en el script anterior, lo primero es almacenar el parámetro (el programa a lanzar) dentro de la variable $program.
Inmediatamente después se crea el diálogo con barra deslizante (en Zenity, con el parámetro --scale). El valor elegido se almacena en la variable $percent.

Aquí topamos con un problema curioso: CPUQuota no asume que el 100% es la CPU al completo; para él, el 100% es un núcleo. Así, en una CPU de 4 núcleos, establecer el 100% en realidad limitará el programa a un 25% de la CPU. Si queremos que use el 100%, habrá que establecer CPUCuota al 400%. Es muy poco intuitivo.
Así pues, para establecer el parámetro correcto, necesitamos multiplicar el porcentaje deseado por el usuario por el número de núcleos disponibles. Aquí entra de nuevo nuestro amigo nproc, que nos dice cuántos núcleos tiene la CPU.

Ya con el valor de porcentaje correcto, se lanza el comando final:
systemd-run --user --scope -p CPUQuota=porcentaje programa
Y fin.

Apéndice 

Estos scripts los he hecho para mi propio uso, en mi equipo funcionan bien y resuelven mi problema. No garantizo que funcionen en otros sistemas o para otros usos. Puede haber bugs que no he detectado (P.Ej.: Lanzar programas que requieren parámetros extra). Aunque no se utiliza ningún comando "peligroso", no me hago responsable de ningún daño derivado del uso de estos scripts.
Estaré encantado de recibir reportes de errores o posibles correcciones.
No hay licencia que valga: Cualquiera puede usar estos scripts, modificarlos, distribuirlos, usarlos para imprimir papel pintado o lo que le parezca. Ni siquiera espero que se mantenga la autoría. Al que le sean útiles, que los utilice como quiera.

 

P.D.: Sí, tengo un tema de Windows 98 en Cinnamon. Qué puedo decir, soy un nostálgico. :P 

No hay comentarios: