← Sistemas de Archivos y E/SBloque 12 · Planificación de disco · Tema 44 de 50

Tema 44

Planificadores de Linux, SSD y NVMe

En Linux, planificar E/S implica administrar concurrencia, prioridades y latencias, además de ordenar direcciones. Conectaremos los modelos de HDD con blk-mq, SSD y NVMe, y compararemos none, mq-deadline, Kyber y BFQ. Consultaremos sysfs desde C y diseñaremos un experimento reversible con fio, sin confundir más IOPS con mejor respuesta para todas las aplicaciones.

1. Qué cambia después del modelo de cilindros

Los temas 42 y 43 compararon recorridos con una cola estática, un cabezal y una dirección conocida. Ese modelo permite entender el coste mecánico del HDD, pero no describe toda la pila de almacenamiento de Linux.

Planificador de E/S de bloques: política del kernel que interviene en la selección y despacho de solicitudes de una cola. Concurrencia: cantidad de operaciones que pueden permanecer pendientes y progresar dentro de una frontera definida.

Un SSD elimina búsqueda y rotación, pero conserva recursos compartidos: controlador, acceso a flash, canales, memoria, enlace y trabajo interno. La fragmentación de la carga, las escrituras y la recolección de espacio pueden afectar la respuesta. El acceso aleatorio deja de pagar la búsqueda de un HDD; eso no implica rendimiento idéntico para cualquier patrón.

El objetivo también cambia según el usuario: una aplicación puede necesitar terminar un respaldo rápido, mientras otra intenta abrir un archivo sin quedar detrás de ese trabajo. Caudal agregado, respuesta interactiva y reparto entre competidores requieren mediciones distintas.

2. blk-mq: varias colas y varias capas

blk-mq es el mecanismo multicolas de la capa de bloques. Utiliza contextos de software asociados a CPU o nodos y contextos de despacho hacia el hardware. Su diseño permite aprovechar paralelismo y reducir la contención de una cola global. La aplicación puede originar BIOs que se integran en solicitudes del bloque.

Capas de envío de E/SVarias aplicaciones y CPU alimentan contextos de software. El planificador, si está activo, interviene antes de enviar a colas de hardware y al dispositivo. La finalización vuelve con una identidad de solicitud. Aplicaciones y CPUvarios emisores Contextos softwaresolicitudes y política Despacho hardwaredriver y recursos Controlador y dispositivocolas internas, medios y finalizacionesLa profundidad se observa en una capa; no es un único contador global.
Diagrama conceptual: las asociaciones entre colas dependen del driver y del dispositivo.

Si faltan recursos para despachar, pueden quedar solicitudes esperando. Los tags permiten identificar sus finalizaciones. El orden de terminación no queda garantizado por el orden de envío; las capas superiores implementan las dependencias necesarias. La documentación de blk-mq describe estas estructuras y el recorrido de solicitudes.

3. SSD, NVMe y profundidad de cola

SSD describe almacenamiento de estado sólido. NVMe es un protocolo de acceso a memoria no volátil; no es un sistema de archivos ni un planificador de Linux. Un SSD SATA y uno NVMe pueden usar flash y presentar capacidades y rutas de envío diferentes.

El modelo NVMe utiliza colas de envío y finalización. Su cantidad y profundidad efectiva dependen del controlador, el driver y los recursos del entorno. No hay una equivalencia fija entre «una aplicación», «una CPU» y «una cola física».

La profundidad que solicita un programa es un límite de concurrencia en su carga. No demuestra que ese número se mantenga siempre, ni que coincida con las solicitudes dentro del hardware. Un motor síncrono puede mantener una sola operación aunque se configure iodepth=32.

Relación observacional en régimen estable
L = λ × W
L: operaciones pendientes de media en la frontera elegida
λ: operaciones completadas por segundo
W: respuesta media en segundos

100000 operaciones/s × 0,00032 s = 32 operaciones pendientes de media

Aumentar concurrencia puede ocupar recursos que estaban libres. Después de alcanzar un cuello de botella, más cola puede agregar espera sin mejorar caudal. Esta relación no predice por sí sola la capacidad de un dispositivo ni permite sustituir L por cualquier límite configurado.

4. none: sin un planificador adicional

Con none no se incorpora una política adicional de ordenación del tipo de las otras tres estudiadas. Sigue existiendo la infraestructura de envío, control de recursos y finalización de la capa de bloques y del dispositivo.

Por lo tanto, none no significa «sin colas», «una operación a la vez» ni «orden FIFO global de todas las aplicaciones». El controlador y las capas posteriores todavía pueden procesar trabajo concurrente y completarlo en otro orden.

Es un candidato de referencia cuando interesa reducir trabajo de planificación en el host y el dispositivo maneja bien la concurrencia. Esto motiva probarlo en almacenamiento rápido, pero no lo convierte en ganador universal en NVMe ni garantiza buena latencia bajo competencia.

5. mq-deadline: posición y antigüedad

mq-deadline combina estructuras ordenadas por posición con listas por antigüedad. Organiza lotes y revisa expiraciones para evitar que atender por posición postergue indefinidamente determinadas solicitudes. También contempla el balance entre lecturas y escrituras y las prioridades de E/S.

Los nombres como read_expire, write_expire, fifo_batch y writes_starved expresan partes de esa política. Los atributos realmente presentes y sus valores se consultan en queue/iosched; no se copian números de otro kernel como si fueran constantes del equipo.

Una expiración es un criterio de despacho del planificador, no una garantía de finalización para la aplicación. Puede haber demora posterior en el driver, dispositivo, otras capas y recuperación de errores. El código de mq-deadline del kernel distingue los órdenes por posición y FIFO y trata estos límites como blandos.

No es el SCAN puro del tema 43: no recibe nuestra lista de cilindros físicos ni reproduce necesariamente sus rutas. Puede ser un candidato al estudiar lecturas sensibles a espera bajo otras cargas, tanto en HDD como en SSD.

6. Kyber: regular admisión con objetivos de latencia

kyber regula solicitudes para intentar cumplir objetivos de latencia. La documentación expone read_lat_nsec para lecturas y write_lat_nsec para escrituras síncronas, en nanosegundos.

El objetivo sirve al control del planificador: no transforma una carga superior a la capacidad del dispositivo en una carga con respuesta garantizada. Tampoco equivale a un percentil contractual observado desde cualquier aplicación.

Es un candidato para experimentar con la tensión entre concurrencia y respuesta bajo carga. Se debe verificar su disponibilidad, medir la carga relevante y registrar sus parámetros. La documentación oficial de Kyber especifica estos dos ajustes y su finalidad.

7. BFQ: presupuestos y reparto de servicio

bfq, Budget Fair Queueing, distribuye servicio entre colas y grupos mediante pesos y presupuestos de datos. Incluye mecanismos orientados a reducir latencia de tareas interactivas. El presupuesto no se interpreta simplemente como un turno de igual duración para todos.

Equidad no significa que cada proceso complete el mismo número de operaciones: si sus tamaños o pesos difieren, contar IOPS sin contexto oculta el reparto de bytes y la experiencia de cada aplicación.

BFQ puede introducir trabajo adicional de planificación y, según la carga y configuración, intercambiar parte del caudal por respuesta o garantías de servicio. También funciona con dispositivos multicolas; no es exclusivo de HDD. Las opciones de grupos dependen del soporte compilado y de la configuración del entorno.

La documentación de BFQ explica presupuestos, pesos y heurísticas de baja latencia. Un reparto ideal wA/(wA+wB) ayuda a entender pesos, pero no predice exactamente una ejecución con colas que se vacían, prioridades distintas o heurísticas activas.

8. Elegir candidatos según el objetivo

Primero identifica qué ofrece la cola y qué problema intentas resolver. La siguiente matriz propone hipótesis de ensayo; ninguna fila sustituye una medición en tu almacenamiento.

Objetivo o situaciónCandidatos a contrastar si están disponiblesEvidencia necesaria
Caudal de una aplicación en almacenamiento rápido.none como referencia y otras políticas disponibles.IOPS, bytes/s, CPU y latencia con la misma concurrencia.
Lecturas con espera elevada bajo otra E/S.mq-deadline y Kyber frente a la referencia.Distribución de respuesta de las lecturas y efecto sobre la otra carga.
Uso interactivo mientras corre trabajo de fondo.BFQ frente a la referencia y otros candidatos.Respuesta de la tarea interactiva y progreso del trabajo de fondo.
HDD con solicitudes dispersas.mq-deadline y BFQ según el objetivo.Patrón, mezcla, latencia y caudal; sin deducir cilindros de LBA.
Máquina virtual o almacenamiento apilado.Los que exponga la capa elegida.Mapeo a host, límites de observación y competencia externa.

Disponibilidad no equivale a conveniencia. El kernel, sus módulos, el driver y la cola determinan lo ofrecido. Un nombre ausente en una lista observada no se trata como seleccionable en ese instante. La distribución puede aportar políticas de configuración que modifiquen el estado después.

9. Identificar la cola y consultar sysfs

Empieza por el archivo de trabajo, su montaje y la pila de dispositivos. Un nombre como nvme0n1 identifica un espacio de nombres de bloques, no un archivo del sistema de archivos. Una partición y su disco padre tampoco son la misma cola de configuración.

Consultas de solo lectura · Bash en Linux
uname -r
findmnt -T .
lsblk -o NAME,KNAME,PKNAME,TYPE,ROTA,TRAN,SIZE,MOUNTPOINTS
# Elegir DEV despues de identificar la cola; sin /dev/ delante.
read -r -p 'Nombre de dispositivo de bloques a consultar: ' DEV
if [[ ! "$DEV" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]]; then
    printf 'Nombre invalido\n' >&2
else
    for atributo in scheduler rotational nr_requests logical_block_size physical_block_size; do
        ruta="/sys/block/$DEV/queue/$atributo"
        if [[ -r "$ruta" ]]; then
            printf '%s: ' "$atributo"
            cat -- "$ruta"
        else
            printf '%s: no disponible en esta cola\n' "$atributo"
        fi
    done
fi

Una salida ilustrativa es [none] mq-deadline: los corchetes indican la selección activa y los nombres restantes otras opciones expuestas. Otro equipo puede ofrecer una lista distinta. La documentación de selección del planificador describe esa interfaz.

ROTA=0 es lo que la capa informa: no demuestra por sí solo la tecnología física de un volumen virtual. nr_requests tampoco es el iodepth de una aplicación ni una lectura directa de todas las entradas del controlador.

Las rutas queue/iosched y el directorio mq, si existen, agregan información sobre ajustes y contextos. La ABI estable de sysfs documenta scheduler, rotational y otros atributos. En dispositivos apilados algunos archivos pueden no existir; se interpreta esa ausencia sin inventar un valor.

10. Cambio temporal y restauración del estado

Cambiar scheduler afecta a la cola del dispositivo y a los usuarios que la comparten. La práctica de cambio se realiza en un equipo de laboratorio y una cola identificada; la parte de consulta y análisis se puede completar sin modificarla.

La escritura requiere privilegios sobre sysfs. sudo echo ... > archivo no eleva la redirección que hace el shell; printf ... | sudo tee archivo ejecuta la escritura con el usuario autorizado. Tras escribir se vuelve a leer para verificar la selección.

Guarda con-planificador.sh. Recibe dispositivo, candidato disponible y un comando de prueba. Conserva el planificador original y lo restaura al salir normalmente o por señales capturables. No crea reglas persistentes.

con-planificador.sh · cambio temporal alrededor de un comando
#!/usr/bin/env bash
set -euo pipefail
if (( $# < 3 )); then
    printf 'Uso: %s DEV planificador comando [argumentos...]\n' "$0" >&2
    exit 2
fi
dev=$1
nuevo=$2
shift 2
[[ "$dev" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]] || exit 2
[[ "$nuevo" =~ ^[a-z0-9-]+$ ]] || exit 2
ruta="/sys/block/$dev/queue/scheduler"
linea=$(cat -- "$ruta")
activo() {
    local estado
    estado=$(cat -- "$ruta") || return 1
    [[ "$estado" =~ \[([a-z0-9-]+)\] ]] || return 1
    printf '%s\n' "${BASH_REMATCH[1]}"
}
original=$(activo)
disponible=0
for token in $linea; do
    token=${token#\[}
    token=${token%\]}
    if [[ "$token" == "$nuevo" ]]; then disponible=1; fi
done
(( disponible == 1 )) || { printf 'Candidato no ofrecido\n' >&2; exit 2; }
printf 'Original: %s; prueba: %s\n' "$original" "$nuevo" >&2
sudo -v
cambiado=0
restaurar() {
    local estado=$?
    trap - EXIT
    if (( cambiado )); then
        if ! printf '%s\n' "$original" | sudo -n tee "$ruta" >/dev/null; then
            printf 'Fallo al restaurar %s en %s\n' "$original" "$ruta" >&2
            exit 1
        fi
        if [[ "$(activo)" != "$original" ]]; then
            printf 'No se verifico la restauracion\n' >&2
            exit 1
        fi
        printf 'Restaurado: %s\n' "$original" >&2
    fi
    exit "$estado"
}
trap restaurar EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
cambiado=1
printf '%s\n' "$nuevo" | sudo tee "$ruta" >/dev/null
[[ "$(activo)" == "$nuevo" ]] || { printf 'Cambio no verificado\n' >&2; exit 1; }
"$@"

El registro imprime el original para una restauración manual si falla. Un apagado, SIGKILL, pérdida del dispositivo o intervención concurrente quedan fuera de lo que un trap puede garantizar. Por eso se vuelve a consultar el estado al terminar y se evita ejecutar dos pruebas que cambien simultáneamente la misma cola.

Si el dispositivo no ofrece una interfaz modificable o no cuentas con privilegios, conserva la consulta y ejecuta la carga con el estado actual. No se interpreta ese resultado como una comparación entre planificadores que no llegaron a activarse.

11. Qué medir y qué mantener constante

MedidaPregunta que respondeCondición a declarar
IOPS y bytes/s¿Cuánto trabajo completa la carga?Tamaño, mezcla y duración.
Media, p50, p95, p99¿Cómo se distribuye la respuesta?Frontera de medida y número de observaciones.
Concurrencia alcanzada¿Qué profundidad mantuvo el ensayo?Motor de E/S, trabajos y distribución observada.
Respuesta por aplicación¿Quién mejora y quién espera más?Competidores, prioridades y objetivos.
CPU y carga de fondo¿Cambió el coste del host o la interferencia?Equipo, versión y procesos presentes.

Una prueba A/B conserva archivo, rango, patrón, tamaño, motor, concurrencia, duración, opciones y preparación. Conviene repetir e intercalar el orden A/B/B/A, registrando variabilidad y condiciones del dispositivo. Cambiar de planificador y tamaño de bloque al mismo tiempo mezcla dos causas.

Una carga solitaria no comprueba reparto entre aplicaciones. Para estudiar BFQ o respuesta bajo competencia hacen falta cargas simultáneas y resultados separados. No se promedian los p99 de dos trabajos como si fueran el p99 de la unión de solicitudes.

Los contadores de bloques incluyen actividad de otros procesos y pueden reiniciarse. Sus incrementos ayudan a contextualizar, pero no coinciden necesariamente con operaciones de la aplicación debido a divisiones y fusiones. La documentación de estadísticas de E/S define esas fronteras y el significado de los tiempos acumulados.

Una lectura directa evita la ruta habitual de caché de páginas para datos cuando está soportada, pero no prueba ausencia de caché del dispositivo. Una escritura terminada tampoco significa persistencia sin definir sincronización: conserva las distinciones del tema 15.

12. Programa en C: leer atributos de una cola

Guarda leer-cola.c. Recibe la ruta de un directorio queue y abre una lista fija de atributos en lectura. scheduler es obligatorio; los otros pueden faltar. Devuelve 0 al completar la consulta, 1 ante error y 2 por uso incorrecto. Para probar sin sysfs también admite un directorio de archivos de texto del laboratorio.

leer-cola.c · consulta acotada, sin escrituras
#define _POSIX_C_SOURCE 200809L
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>

static int atributo(int dir, const char *nombre, int obligatorio)
{
    unsigned char b[4096];
    size_t usados = 0;
    int fd = openat(dir, nombre, O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK);
    if (fd == -1) {
        if (errno == ENOENT && !obligatorio) {
            printf("%s=NO_DISPONIBLE\n", nombre);
            return 0;
        }
        perror(nombre); return -1;
    }
    struct stat st;
    if (fstat(fd, &st) == -1) { perror("fstat"); close(fd); return -1; }
    if (!S_ISREG(st.st_mode)) {
        fprintf(stderr, "%s no es un atributo regular\n", nombre);
        close(fd); return -1;
    }
    for (;;) {
        ssize_t n = read(fd, b + usados, sizeof b - usados);
        if (n > 0) {
            usados += (size_t)n;
            if (usados == sizeof b) {
                fprintf(stderr, "%s excede el limite de lectura\n", nombre);
                close(fd); return -1;
            }
        } else if (n == 0) break;
        else if (errno == EINTR) continue;
        else { perror(nombre); close(fd); return -1; }
    }
    if (close(fd)) { perror("close atributo"); return -1; }
    if (usados && b[usados - 1] == '\n') --usados;
    if (!usados && obligatorio) {
        fprintf(stderr, "Atributo obligatorio vacio\n"); return -1;
    }
    printf("%s=", nombre);
    for (size_t i = 0; i < usados; ++i) {
        if (b[i] >= 32 && b[i] <= 126 && b[i] != '\\') putchar(b[i]);
        else printf("\\x%02X", (unsigned)b[i]);
    }
    putchar('\n');
    return 0;
}
int main(int argc, char **argv)
{
    if (argc != 2) { fprintf(stderr, "Uso: %s directorio-queue\n", argv[0]); return 2; }
    int dir = open(argv[1], O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
    if (dir == -1) { perror("open queue"); return 1; }
    const char *nombres[] = {"scheduler", "rotational", "nr_requests",
                            "logical_block_size", "physical_block_size"};
    int estado = 0;
    for (size_t i = 0; i < sizeof nombres / sizeof nombres[0]; ++i) {
        if (atributo(dir, nombres[i], i == 0)) { estado = 1; break; }
    }
    if (close(dir)) { perror("close queue"); estado = 1; }
    if (fflush(stdout) == EOF) { perror("stdout"); estado = 1; }
    return estado;
}

No se usa el tamaño reportado por stat para decidir cuánto leer: sysfs ofrece contenido generado. Se aceptan hasta 4095 bytes por atributo y se manejan lecturas cortas y EINTR. Los caracteres de control se representan en hexadecimal para no ejecutarlos como secuencias de terminal.

O_NOFOLLOW se aplica al directorio final y a cada atributo abierto, no a todos los componentes anteriores de la ruta. Es un lector de rutas conocidas, no un mecanismo de aislamiento frente a cambios de una jerarquía ajena. Sus consultas sucesivas tampoco constituyen una instantánea atómica si otro proceso cambia el estado.

El programa muestra la línea de scheduler sin convertirla en una afirmación sobre qué tecnología hay detrás. Un archivo opcional ausente se informa como tal; un error de permisos no se confunde con ausencia.

13. Laboratorio fio: lecturas a distintas profundidades

En Bash de Linux crea un directorio propio sobre el almacenamiento que deseas observar. Guarda leer-cola.c y con-planificador.sh allí. Se requieren compilador C y Python 3; para medir, fio con motor io_uring disponible y soporte de E/S directa en ese archivo.

Preparar un archivo de 64 MiB y herramientas
mkdir laboratorio44
cd laboratorio44
# Guardar los fuentes del tema antes de continuar.
cc -std=c11 -O2 -Wall -Wextra -Wpedantic leer-cola.c -o leer-cola
bash -n con-planificador.sh
python3 - <<'PY'
import os
with open('datos44.bin', 'xb') as f:
    for _ in range(64):
        f.write(os.urandom(1024 * 1024))
    f.flush()
    os.fsync(f.fileno())
PY
uname -r
findmnt -T datos44.bin
lsblk -o NAME,KNAME,PKNAME,TYPE,ROTA,TRAN,SIZE,MOUNTPOINTS
fio --version

Identifica DEV con la inspección de la sección 9 y consulta ./leer-cola "/sys/block/$DEV/queue". El archivo fue escrito para no depender de huecos dispersos; sus 64 MiB pueden caber en cachés del dispositivo. El ensayo no garantiza que cada lectura acceda a flash o a platos físicos.

Mismo patrón y tamaño, dos profundidades solicitadas · solo lectura
fio --readonly --name=lectura44 --filename=datos44.bin --allow_file_create=0 --rw=randread --bs=4k --size=64m --ioengine=io_uring --direct=1 --iodepth=1 --numjobs=1 --time_based=1 --runtime=10 --ramp_time=2 --randseed=44 --lat_percentiles=0 --clat_percentiles=1 --percentile_list=50:95:99 --output-format=json --output=actual-q1.json
fio --readonly --name=lectura44 --filename=datos44.bin --allow_file_create=0 --rw=randread --bs=4k --size=64m --ioengine=io_uring --direct=1 --iodepth=32 --numjobs=1 --time_based=1 --runtime=10 --ramp_time=2 --randseed=44 --lat_percentiles=0 --clat_percentiles=1 --percentile_list=50:95:99 --output-format=json --output=actual-q32.json

La documentación de fio define iodepth, motores, percentiles y salida JSON. Revisa errores y profundidad alcanzada. Si io_uring o direct=1 fallan, registra el fallo; sustituir el motor o cambiar a buffered define otro ensayo que debe compararse con su propia referencia.

Los percentiles elegidos corresponden a clat, el intervalo de finalización que mide fio, no automáticamente a la respuesta completa de una aplicación. El mismo randseed favorece reproducibilidad, pero ejecuciones por tiempo pueden completar cantidades distintas de operaciones.

Comparar un candidato y volver al original

Solo después de identificar la cola y comprobar que ofrece mq-deadline, el siguiente ejemplo ejecuta la carga Q=32 con ese candidato. El archivo de resultados es distinto para conservar la referencia. El wrapper solicita los privilegios de escritura, verifica el cambio y restaura el original.

Ejemplo de cambio temporal · DEV debe estar identificado
bash con-planificador.sh "$DEV" mq-deadline fio --readonly --name=lectura44 --filename=datos44.bin --allow_file_create=0 --rw=randread --bs=4k --size=64m --ioengine=io_uring --direct=1 --iodepth=32 --numjobs=1 --time_based=1 --runtime=10 --ramp_time=2 --randseed=44 --lat_percentiles=0 --clat_percentiles=1 --percentile_list=50:95:99 --output-format=json --output=deadline-q32.json
cat "/sys/block/$DEV/queue/scheduler"

Si mq-deadline ya era el original, esta ejecución es una repetición, no una comparación entre dos políticas. Conserva el nombre activo y los ajustes en un registro para cada resultado. Repite en otro orden antes de atribuir una variación al planificador.

Para estudiar mezcla o competencia, amplía después con trabajos separados y métricas por aplicación, manteniendo constante cada par comparado. Esta carga de lectura solitaria comprueba el método de medición, no las garantías de reparto o persistencia de escritura.

14. Analizador Python de resultados JSON

Guarda resumir-fio.py. Acepta archivos JSON de un único trabajo o grupo reportado, sin errores, con lecturas realizadas, sin escrituras y con clat_ns. Muestra caudal decimal y p99 de finalización en microsegundos; rechaza entradas incompletas en lugar de completar métricas con ceros.

resumir-fio.py · no combina percentiles de poblaciones distintas
import json
import math
import sys
from pathlib import Path

def numero(valor, nombre, positivo=False):
    if isinstance(valor, bool) or not isinstance(valor, (int, float)):
        raise ValueError('Campo numerico ausente o invalido: ' + nombre)
    if not math.isfinite(valor) or valor < 0 or (positivo and valor == 0):
        raise ValueError('Valor fuera de rango: ' + nombre)
    return valor

def resumen(documento):
    if not isinstance(documento, dict):
        raise ValueError('El documento debe ser un objeto JSON')
    trabajos = documento['jobs']
    if not isinstance(trabajos, list) or len(trabajos) != 1:
        raise ValueError('Se requiere un unico trabajo o grupo reportado')
    j = trabajos[0]
    if not isinstance(j, dict):
        raise ValueError('Trabajo invalido')
    if j.get('error') != 0:
        raise ValueError('El trabajo informa un error o no declara su estado')
    if numero(j['write']['io_bytes'], 'write.io_bytes') != 0:
        raise ValueError('Este analizador solo admite cargas de lectura')
    r = j['read']
    cantidad = numero(r['io_bytes'], 'read.io_bytes', True)
    duracion = numero(r['runtime'], 'read.runtime', True)
    iops = numero(r['iops'], 'read.iops', True)
    clat = r['clat_ns']
    media = numero(clat['mean'], 'clat_ns.mean') / 1000
    percentiles = clat['percentile']
    claves = [k for k in percentiles if float(k) == 99]
    if len(claves) != 1:
        raise ValueError('Se requiere un p99 de clat_ns unico')
    p99 = numero(percentiles[claves[0]], 'clat_ns.p99') / 1000
    mb_s = cantidad / duracion / 1000
    return iops, mb_s, media, p99

def main():
    if len(sys.argv) < 2:
        print('Uso: python3 resumir-fio.py resultado.json [...]', file=sys.stderr)
        return 2
    print('archivo\tiops\tMB_s\tclat_media_us\tclat_p99_us')
    for nombre in sys.argv[1:]:
        try:
            documento = json.loads(Path(nombre).read_text(encoding='utf-8'))
            valores = resumen(documento)
        except (OSError, ValueError, KeyError, TypeError, OverflowError) as e:
            print(f'Error en {nombre}: {e}', file=sys.stderr)
            return 1
        print(json.dumps(nombre, ensure_ascii=True) + '\t' +
              '\t'.join(f'{v:.6f}' for v in valores))
    return 0

if __name__ == '__main__':
    raise SystemExit(main())
Contrastar profundidad y, si se ejecutó, otro planificador
python3 resumir-fio.py actual-q1.json actual-q32.json
# Solo si se realizo la prueba con el candidato:
python3 resumir-fio.py actual-q32.json deadline-q32.json

El cálculo de MB/s usa bytes leídos y duración del trabajo en milisegundos. La media y p99 usan nanosegundos convertidos a microsegundos. No se redondean los datos a milisegundos antes de convertir.

El analizador no certifica igualdad de parámetros ni deduce el planificador activo del nombre del archivo. Revisa el registro de sysfs y las opciones incluidas por fio. Si hay varios trabajos, analiza sus poblaciones por separado o utiliza histogramas para construir una distribución conjunta.

15. Verificador con datos sintéticos

Este bloque verifica el lector y el analizador sin tocar sysfs ni cambiar planificadores. Requiere haber compilado leer-cola y guardado resumir-fio.py y con-planificador.sh. Crea un directorio temporal propio, comprueba ausencias, límites y caracteres de control, y contrasta conversiones con un JSON conocido.

Python 3 · lectura acotada y resultados sintéticos
python3 - <<'PY'
from pathlib import Path
from tempfile import TemporaryDirectory
import copy
import json
import math
import runpy
import subprocess

lector = str(Path('leer-cola').resolve())
analizador = str(Path('resumir-fio.py').resolve())
def ejecutar(args, estado=0):
    r = subprocess.run(args, capture_output=True, text=True)
    assert r.returncode == estado, (args, r.returncode, r.stderr)
    return r.stdout

ejecutar(['bash', '-n', 'con-planificador.sh'])
with TemporaryDirectory(prefix='cola44-') as d:
    cola = Path(d)
    (cola / 'scheduler').write_text('[none] mq-deadline\n')
    (cola / 'rotational').write_text('0\n')
    antes = {p.name: p.read_bytes() for p in cola.iterdir()}
    texto = ejecutar([lector, str(cola)])
    assert 'scheduler=[none] mq-deadline\n' in texto
    assert 'rotational=0\n' in texto
    assert 'nr_requests=NO_DISPONIBLE\n' in texto
    assert antes == {p.name: p.read_bytes() for p in cola.iterdir()}
    (cola / 'rotational').write_bytes(b'0\x1b[31m\n')
    texto = ejecutar([lector, str(cola)])
    assert 'rotational=0\\x1B[31m' in texto and '\x1b' not in texto
    (cola / 'scheduler').write_bytes(b'a' * 4096)
    ejecutar([lector, str(cola)], 1)
    (cola / 'scheduler').unlink()
    ejecutar([lector, str(cola)], 1)
    ejecutar([lector], 2)
    documento = {'jobs': [{'error': 0, 'write': {'io_bytes': 0}, 'read': {
        'io_bytes': 4096000, 'runtime': 1000, 'iops': 1000,
        'clat_ns': {'mean': 100000, 'percentile': {'99.000000': 250000}}}}]}
    ruta = cola / 'resultado.json'
    ruta.write_text(json.dumps(documento))
    salida = ejecutar(['python3', analizador, str(ruta)])
    valores = list(map(float, salida.splitlines()[1].split('\t')[1:]))
    assert valores == [1000, 4.096, 100, 250]
    resumen = runpy.run_path(analizador)['resumen']
    for alterar in ('error', 'write', 'percentile', 'runtime', 'jobs', 'nan'):
        malo = copy.deepcopy(documento)
        j = malo['jobs'][0]
        if alterar == 'error': j['error'] = 5
        if alterar == 'write': j['write']['io_bytes'] = 4096
        if alterar == 'percentile': j['read']['clat_ns']['percentile'] = {}
        if alterar == 'runtime': j['read']['runtime'] = 0
        if alterar == 'jobs': malo['jobs'].append(copy.deepcopy(j))
        if alterar == 'nan': j['read']['clat_ns']['mean'] = math.nan
        try:
            resumen(malo)
        except (ValueError, KeyError, TypeError):
            pass
        else:
            raise AssertionError('Se acepto un resultado invalido: ' + alterar)
    ruta.write_text('{incompleto')
    ejecutar(['python3', analizador, str(ruta)], 1)
print('OK: atributos, ausencias, limites, conversiones y rechazos; sin modificar sysfs')
PY

El verificador revisa la sintaxis del wrapper, pero no ejecuta cambios de planificador. La restauración sobre un dispositivo real se comprueba en la práctica controlada, leyendo su estado antes y después.

16. Simuladores de concurrencia y candidatos

Más profundidad hasta alcanzar un límite

Modelo idealizado en régimen estable, con Q operaciones siempre pendientes, latencia mínima W0 y capacidad máxima C. Se define λ=min(C, Q/W0) y luego W=Q/λ. Omite transitorios, dispersión, CPU y cachés. No reproduce none, Kyber ni otro algoritmo real.

Concurrencia, capacidad y respuesta media

Resultado y comprobación de L=λW
Q constanteIOPSRespuesta media, µsCaudal, MB/s

Convertir una lista de disponibilidad en un plan de ensayo

Copia una lista de scheduler con una opción activa entre corchetes. El filtro distingue disponibilidad de hipótesis; no declara un ganador ni cambia tu equipo. El perfil contextualiza el ensayo y el objetivo indica qué medir. Se aceptan nombres adicionales sin atribuirles comportamiento no estudiado.

Qué está disponible y qué comparar

PlanificadorEstado observadoHipótesis de ensayo
Condiciones que registrar

17. Diagnóstico y resumen

ObservaciónQué revisar
La lista solo ofrece none.Disponibilidad del kernel, módulos, driver y capa observada; no inventar otras opciones.
iodepth=32 se comporta como una operación pendiente.Motor, soporte, preparación y profundidad alcanzada.
Más profundidad sube p99 sin mejorar caudal.Saturación o competencia; más espera puede no aportar trabajo completado.
mq-deadline no cumple un tiempo máximo de aplicación.Sus criterios son de despacho y blandos; revisar también las capas posteriores.
Suben los IOPS agregados y empeora la aplicación interactiva.Medir por competidor; el agregado no describe reparto.
El archivo está en una partición, pero se cambia otra cola.Revisar disco padre y pila completa con findmnt y lsblk.
El planificador cambia de nuevo tras la prueba.Verificar restauración y posibles gestores externos de configuración.

18. Preguntas de repaso y práctica

1. ¿Qué problema mecánico desaparece en un SSD?

La búsqueda de cabezal y la espera rotacional. Permanecen otros costes y recursos compartidos.

2. ¿NVMe es un planificador de Linux?

No. Es un protocolo; none, mq-deadline, Kyber y BFQ son opciones de planificación de bloques.

3. ¿Qué aporta blk-mq?

Infraestructura multicolas para gestionar y despachar solicitudes aprovechando paralelismo del host y almacenamiento.

4. ¿none elimina todas las colas?

No. Continúan las colas y el control de recursos de la infraestructura y del dispositivo.

5. ¿La finalización conserva siempre el orden de envío?

No. Las dependencias necesarias se implementan en las capas correspondientes.

6. ¿Qué indican los corchetes de scheduler?

El planificador seleccionado en la lectura de esa cola.

7. ¿Se ofrece la misma lista en todos los equipos?

No. Depende del kernel, módulos, driver y cola.

8. ¿mq-deadline es SCAN puro?

No. Usa estructuras por posición y antigüedad, lotes y otras reglas; no sigue nuestro modelo de cilindros físicos.

9. ¿read_expire garantiza que read termine dentro de ese plazo?

No. Es un criterio blando de despacho, no una garantía temporal de aplicación.

10. ¿Qué ajusta Kyber?

Objetivos de latencia de lecturas y escrituras síncronas, usados para regular solicitudes.

11. ¿Qué reparte BFQ?

Servicio mediante presupuestos y pesos de colas o grupos, con mecanismos adicionales de baja latencia.

12. ¿El mismo número de IOPS prueba equidad?

No. Hay que considerar tamaños, bytes, pesos, respuesta y objetivos de cada competidor.

13. ¿iodepth=32 demuestra 32 solicitudes en el hardware?

No. Es un límite solicitado por la carga; motor y otras capas determinan lo realmente alcanzado.

14. ¿Más concurrencia siempre mejora el caudal?

No. Tras un cuello de botella puede aumentar la espera sin incrementar completaciones por segundo.

15. ¿Qué implica ROTA=0 en un volumen virtual?

Que esa capa se anuncia como no rotacional; no acredita la tecnología física subyacente.

16. ¿Qué significa clat p99 en este laboratorio?

Un percentil de latencia de finalización medido por fio, convertido desde nanosegundos; no toda respuesta de aplicación.

17. ¿Por qué no promediar p99 de dos trabajos?

Porque el percentil de la población conjunta depende de sus distribuciones y cantidades de observaciones.

18. ¿El wrapper crea una configuración persistente?

No. Guarda el estado observado, cambia temporalmente y trata de restaurarlo al salir.

19. ¿Un trap cubre apagado o SIGKILL?

No. Se registra el original y se consulta el estado final; la restauración puede requerir intervención manual.

20. ¿Una carga de lectura solitaria verifica reparto entre aplicaciones?

No. Se necesitan competidores y medidas separadas por aplicación.

Para practicar

  1. Relaciona las colas de aplicación, blk-mq y dispositivo sin asumir correspondencia uno a uno.
  2. Identifica el montaje y la pila del directorio de laboratorio y anota dónde aparece scheduler.
  3. Compila leer-cola y contrasta su salida con consultas manuales a los mismos atributos.
  4. Ejecuta el verificador sintético y explica qué casos distingue de una ausencia opcional.
  5. Obtén Q=1 y Q=32 con la misma carga y compara profundidad alcanzada, caudal y p99.
  6. Repite el ensayo conservando las opciones y registra la variabilidad antes de cambiar el planificador.
  7. Si el laboratorio lo permite, contrasta dos opciones ofrecidas y verifica la restauración.
  8. En el modelo ideal, usa Q=1,10,32,64 con W0=100 µs y C=100000 IOPS; identifica desde dónde la cola aumenta respuesta sin aumentar tasa.
  9. Explica por qué none puede ser una referencia útil sin ser un ganador universal.
  10. Diseña una prueba con dos competidores para estudiar BFQ y define métricas por aplicación.
  11. Calcula el promedio de operaciones pendientes con 50000 IOPS y 0,8 ms de respuesta media dentro de una misma frontera.
  12. Describe qué registros necesitarías para repetir una comparación después de actualizar el kernel.

En el tema 45 comenzaremos el bloque de dispositivos de bloque y de caracteres: archivos de /dev, números mayor y menor y herramientas para identificar los dispositivos del sistema.