← Fundamentos de Sistemas Operativos Bloque 8 · Integración y cierre · Tema 33 de 34

Tema 33

Proyecto: un inspector del sistema en C y Bash

A lo largo del curso escribimos una treintena de programas pequeños, cada uno para observar una sola cosa. En este tema los reunimos en una herramienta completa: un programa que informa qué kernel se ejecuta, cuán ocupados están los procesadores, cuánta memoria queda y qué procesos la usan. Lo hacemos dos veces, en Bash y en C, para comparar.

1. Qué vamos a construir

El programa se llama inspector. Al ejecutarlo muestra un informe del estado de la máquina en cuatro secciones:

el resultado final
$ ./inspector
== Sistema ==
Equipo:        equipo
Kernel:        Linux 6.8.0-45-generic (x86_64)
Distribucion:  Ubuntu 24.04.1 LTS
En marcha:     5 h 52 min
Carga media:   0.42 0.31 0.27 (con 8 procesadores)

== CPU ==
Modelo:        AMD Ryzen 3 5300G with Radeon Graphics
Procesadores:  8
Uso:           3.4 % en modo usuario, 1.3 % en modo kernel
Por segundo:   412 interrupciones, 655 cambios de contexto

== Memoria ==
Total:         15871 MB
Disponible:    10675 MB (67 %)
En cache:      9924 MB
Intercambio:   0 MB en uso, de 2047 MB

== Procesos ==
Total:         288 (1 en ejecucion, 287 dormidos, 0 zombis)
Mayor uso de memoria:
     2204     402 MB  gnome-shell
     5120     318 MB  firefox
     5344     204 MB  Web Content
     3310      59 MB  gnome-terminal-
     1120      41 MB  gdm3

Es, en pequeño, lo que hacen top, free y uptime juntos. No usa ninguna biblioteca especial ni ejecuta otros comandos: todo lo que muestra lo obtiene directamente del kernel, por las dos vías que distinguimos en el tema 31, llamadas al sistema y archivos de /proc.

El proyecto tiene tres objetivos:

2. El diseño

Antes de escribir código conviene decidir de dónde saldrá cada dato. Para casi todos hay más de una fuente, y la tabla siguiente fija la elegida:

DatoFuenteTipoTema
Nombre del equipo, kernel, arquitecturaunameLlamada al sistema4, 11
Distribución/etc/os-releaseArchivo en disco25
Tiempo en marcha/proc/uptimeArchivo virtual31
Carga media/proc/loadavgArchivo virtual26
Cantidad de procesadoressysconfFunción de la biblioteca15
Modelo del procesador/proc/cpuinfoArchivo virtual31
Uso de CPU, interrupciones, cambios de contexto/proc/stat, leído dos vecesArchivo virtual12, 17, 19
Memoria total, disponible, en caché, intercambio/proc/meminfoArchivo virtual7, 27
Lista de procesosLos directorios numéricos de /procDirectorio virtual26, 31
Nombre, estado y memoria de cada proceso/proc/PID/statusArchivo virtual26, 27

A eso se suman cuatro decisiones, que valen para las dos versiones:

3. Primera versión, en Bash

Empezamos por el script. En Bash, leer un archivo y extraer un campo es cuestión de una línea, de modo que sirve como prototipo: permite comprobar que las fuentes elegidas dan lo que se espera antes de invertir esfuerzo en C.

inspector.sh
#!/bin/bash
# inspector.sh - informe del estado del sistema, con herramientas de la shell.

# Muestra el valor de un campo de /proc/meminfo, convertido a MB.
mem_mb() {
    awk -v campo="$1:" '$1 == campo {print int($2 / 1024)}' /proc/meminfo
}

echo "== Sistema =="
. /etc/os-release                               # define PRETTY_NAME, entre otras
echo "Equipo:        $(uname -n)"
echo "Kernel:        $(uname -sr) ($(uname -m))"
echo "Distribucion:  $PRETTY_NAME"

read -r segundos resto < /proc/uptime           # la primera palabra del archivo
segundos=${segundos%.*}                         # quitar los decimales
echo "En marcha:     $((segundos / 3600)) h $((segundos / 60 % 60)) min"

read -r c1 c5 c15 resto < /proc/loadavg
echo "Carga media:   $c1 $c5 $c15 (con $(nproc) procesadores)"

echo
echo "== CPU =="
echo "Modelo:        $(grep -m1 'model name' /proc/cpuinfo | cut -d: -f2- | sed 's/^ //')"
echo "Procesadores:  $(nproc)"

# Dos lecturas de la primera línea de /proc/stat, separadas por un segundo.
read -r _ us1 ni1 sy1 id1 wa1 hi1 si1 st1 _ < /proc/stat
sleep 1
read -r _ us2 ni2 sy2 id2 wa2 hi2 si2 st2 _ < /proc/stat

total=$(( (us2+ni2+sy2+id2+wa2+hi2+si2+st2) - (us1+ni1+sy1+id1+wa1+hi1+si1+st1) ))
usuario=$(( (us2+ni2) - (us1+ni1) ))
kernel=$(( (sy2+hi2+si2) - (sy1+hi1+si1) ))
echo "Uso:           $((100 * usuario / total)) % en modo usuario, $((100 * kernel / total)) % en modo kernel"

echo
echo "== Memoria =="
total_mb=$(mem_mb MemTotal)
disp_mb=$(mem_mb MemAvailable)
echo "Total:         $total_mb MB"
echo "Disponible:    $disp_mb MB ($((100 * disp_mb / total_mb)) %)"

echo
echo "== Procesos =="
echo "Total:         $(ls -d /proc/[0-9]* | wc -l)"
echo "Mayor uso de memoria:"
ps -eo pid,rss,comm --sort=-rss |
    awk 'NR > 1 && NR <= 6 {printf "  %7d  %6d MB  %s\n", $1, $2 / 1024, $3}'
terminal (extracto)
$ chmod +x inspector.sh
$ ./inspector.sh
== Sistema ==
Equipo:        equipo
Kernel:        Linux 6.8.0-45-generic (x86_64)
Distribucion:  Ubuntu 24.04.1 LTS
En marcha:     5 h 52 min
Carga media:   0.42 0.31 0.27 (con 8 procesadores)

== CPU ==
Modelo:        AMD Ryzen 3 5300G with Radeon Graphics
Procesadores:  8
Uso:           3 % en modo usuario, 1 % en modo kernel
…

Cincuenta líneas, y el informe está casi completo. Tres construcciones del script merecen un comentario:

ConstrucciónQué hace
. /etc/os-releaseEjecuta el archivo como si fuera parte del script. Sus líneas son asignaciones, así que quedan definidas como variables. Lo usamos en el tema 25.
read -r a b c < archivoLee la primera línea del archivo y reparte sus palabras entre las variables. La última recibe todo lo que sobre; el nombre _ se usa para lo que se descarta.
$(( … ))Aritmética de la shell. Solo con números enteros.

La versión en Bash tiene límites, y son los que justifican la segunda:

4. La versión en C: las funciones de apoyo

El programa en C es un solo archivo, inspector.c, que presentamos en seis partes. Copiadas una a continuación de otra, en el orden en que aparecen, forman el programa completo. La primera contiene las cabeceras y dos funciones que usan todas las secciones.

inspector.c (parte 1 de 6)
/* inspector.c - informe del estado del sistema, a partir de llamadas al sistema y de /proc. */

#include <ctype.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/utsname.h>
#include <unistd.h>

#define MAX_LINEA 512
#define TOP 5                           /* cuántos procesos mostrar */

/* Lee la primera línea de un archivo, sin el salto final. Devuelve 0, o -1 si falla. */
static int leer_linea(const char *ruta, char *destino, size_t tam)
{
    FILE *f = fopen(ruta, "r");

    if (f == NULL)
        return -1;
    if (fgets(destino, (int)tam, f) == NULL) {
        fclose(f);
        return -1;
    }
    fclose(f);

    destino[strcspn(destino, "\n")] = '\0';
    return 0;
}

/* Busca la línea que empieza con `clave` y copia lo que sigue, sin separadores ni comillas. */
static int buscar_texto(const char *ruta, const char *clave, char *destino, size_t tam)
{
    FILE *f = fopen(ruta, "r");
    char linea[MAX_LINEA];
    size_t largo = strlen(clave);
    int resultado = -1;

    if (f == NULL)
        return -1;

    while (fgets(linea, sizeof(linea), f) != NULL) {
        if (strncmp(linea, clave, largo) == 0) {
            const char *valor = linea + largo;

            valor += strspn(valor, " \t:=\"");          /* saltar «: », «=» y la comilla */
            snprintf(destino, tam, "%s", valor);
            destino[strcspn(destino, "\"\n")] = '\0';   /* cortar en la comilla o el salto */
            resultado = 0;
            break;
        }
    }
    fclose(f);
    return resultado;
}

Las dos funciones encapsulan las decisiones de diseño. Abren el archivo, lo leen y lo cierran en una sola llamada, y devuelven −1 ante cualquier problema, de modo que quien las usa puede omitir el dato sin más. buscar_texto sirve para formatos distintos: en /proc/cpuinfo las líneas tienen la forma model name : valor, y en /etc/os-release, PRETTY_NAME="valor". Saltar los separadores y cortar en la comilla resuelve ambos.

Las funciones se declaran static, que en C limita su alcance a este archivo. Es una buena costumbre para todo lo que no necesita verse desde afuera.

5. La sección del sistema

La primera sección combina las tres clases de fuente de la tabla: una llamada al sistema, un archivo guardado en el disco y dos archivos virtuales.

inspector.c (parte 2 de 6)
static void seccion_sistema(void)
{
    struct utsname u;
    char texto[MAX_LINEA];

    printf("== Sistema ==\n");

    if (uname(&u) == 0) {                         /* llamada al sistema */
        printf("Equipo:        %s\n", u.nodename);
        printf("Kernel:        %s %s (%s)\n", u.sysname, u.release, u.machine);
    }

    if (buscar_texto("/etc/os-release", "PRETTY_NAME", texto, sizeof(texto)) == 0)
        printf("Distribucion:  %s\n", texto);

    if (leer_linea("/proc/uptime", texto, sizeof(texto)) == 0) {
        double segundos = atof(texto);            /* el primer número del archivo */

        printf("En marcha:     %d h %d min\n", (int)(segundos / 3600), (int)(segundos / 60) % 60);
    }

    if (leer_linea("/proc/loadavg", texto, sizeof(texto)) == 0) {
        double c1, c5, c15;

        if (sscanf(texto, "%lf %lf %lf", &c1, &c5, &c15) == 3)
            printf("Carga media:   %.2f %.2f %.2f (con %ld procesadores)\n",
                   c1, c5, c15, sysconf(_SC_NPROCESSORS_ONLN));
    }
}

La llamada uname es la del programa entorno.c con que empezamos en el tema 4. Devuelve en una estructura los mismos datos que el kernel publica en /proc/sys/kernel: es un caso en que las dos vías dan lo mismo, y elegimos la llamada porque es más directa.

La carga media se muestra junto con la cantidad de procesadores, porque sola no dice nada, como vimos en el tema 26. Cada dato está dentro de un if propio: si falta /etc/os-release, el informe sigue con la línea siguiente.

6. La sección de la CPU

Es la sección más elaborada, porque sus tres datos dinámicos —el uso de los procesadores, las interrupciones y los cambios de contexto— son contadores acumulados. Hay que tomar dos muestras y restar. Una estructura reúne lo que interesa de cada muestra, y una función la llena leyendo /proc/stat una vez:

inspector.c (parte 3 de 6)
/* Una muestra de /proc/stat: contadores acumulados desde el arranque. */
struct muestra {
    unsigned long long usuario, kernel, total;      /* tiempo de CPU, en tics */
    unsigned long long interrupciones, cambios;
};

static int leer_stat(struct muestra *m)
{
    unsigned long long us, ni, sy, id, wa, hi, si, st;
    char palabra[32];
    int hallados = 0;
    FILE *f = fopen("/proc/stat", "r");

    if (f == NULL)
        return -1;

    /* la primera línea: los tiempos de todos los procesadores juntos */
    if (fscanf(f, "cpu %llu %llu %llu %llu %llu %llu %llu %llu",
               &us, &ni, &sy, &id, &wa, &hi, &si, &st) == 8) {
        m->usuario = us + ni;
        m->kernel = sy + hi + si;
        m->total = us + ni + sy + id + wa + hi + si + st;
        hallados++;
    }

    /* el resto, palabra por palabra, hasta dar con los otros dos contadores */
    while (hallados < 3 && fscanf(f, "%31s", palabra) == 1) {
        if (strcmp(palabra, "intr") == 0 && fscanf(f, "%llu", &m->interrupciones) == 1)
            hallados++;
        else if (strcmp(palabra, "ctxt") == 0 && fscanf(f, "%llu", &m->cambios) == 1)
            hallados++;
    }

    fclose(f);
    return (hallados == 3) ? 0 : -1;
}

static void seccion_cpu(void)
{
    struct muestra antes, despues;
    char modelo[MAX_LINEA];

    printf("\n== CPU ==\n");

    if (buscar_texto("/proc/cpuinfo", "model name", modelo, sizeof(modelo)) == 0)
        printf("Modelo:        %s\n", modelo);
    printf("Procesadores:  %ld\n", sysconf(_SC_NPROCESSORS_ONLN));

    /* dos muestras separadas por un segundo: la diferencia es lo ocurrido en ese segundo */
    if (leer_stat(&antes) == -1)
        return;
    sleep(1);
    if (leer_stat(&despues) == -1 || despues.total == antes.total)
        return;

    double intervalo = (double)(despues.total - antes.total);

    printf("Uso:           %.1f %% en modo usuario, %.1f %% en modo kernel\n",
           100.0 * (double)(despues.usuario - antes.usuario) / intervalo,
           100.0 * (double)(despues.kernel - antes.kernel) / intervalo);
    printf("Por segundo:   %llu interrupciones, %llu cambios de contexto\n",
           despues.interrupciones - antes.interrupciones,
           despues.cambios - antes.cambios);
}

leer_stat une dos técnicas que ya usamos por separado: el fscanf sobre la línea cpu de usocpu.c, del tema 31, y la búsqueda palabra por palabra de interrupciones.c, del tema 17. Hacerlo en una sola pasada garantiza que los cinco valores correspondan al mismo instante.

La separación entre modo usuario y modo kernel es la del tema 12. El tiempo de usuario incluye el de los procesos con cortesía; el de kernel, el dedicado a las interrupciones. Lo que no entra en ninguno de los dos es tiempo ocioso.

La comprobación despues.total == antes.total evita una división por cero, que sería posible si las dos muestras coincidieran. No hace falta para que el programa no falle —en el tema 18 vimos que la división entre números reales no produce ninguna excepción—, sino para no mostrar un resultado sin sentido.

7. La sección de la memoria

/proc/meminfo tiene muchas líneas y necesitamos seis. En lugar de abrir el archivo seis veces, lo recorremos una sola y comparamos cada línea con los seis nombres:

inspector.c (parte 4 de 6)
static void seccion_memoria(void)
{
    long long total = 0, disponible = 0, bufer = 0, cache = 0, swap_total = 0, swap_libre = 0;
    char linea[MAX_LINEA];
    FILE *f = fopen("/proc/meminfo", "r");

    printf("\n== Memoria ==\n");
    if (f == NULL) {
        perror("/proc/meminfo");
        return;
    }

    /* una sola pasada: cada sscanf solo tiene efecto en la línea que empieza con su nombre */
    while (fgets(linea, sizeof(linea), f) != NULL) {
        sscanf(linea, "MemTotal: %lld", &total);
        sscanf(linea, "MemAvailable: %lld", &disponible);
        sscanf(linea, "Buffers: %lld", &bufer);
        sscanf(linea, "Cached: %lld", &cache);
        sscanf(linea, "SwapTotal: %lld", &swap_total);
        sscanf(linea, "SwapFree: %lld", &swap_libre);
    }
    fclose(f);

    if (total == 0)
        return;

    /* los valores del archivo están en kB */
    printf("Total:         %lld MB\n", total / 1024);
    printf("Disponible:    %lld MB (%.0f %%)\n",
           disponible / 1024, 100.0 * (double)disponible / (double)total);
    printf("En cache:      %lld MB\n", (bufer + cache) / 1024);
    printf("Intercambio:   %lld MB en uso, de %lld MB\n",
           (swap_total - swap_libre) / 1024, swap_total / 1024);
}

La técnica del bucle es la de memoria.c, del tema 27. sscanf compara el texto de la línea con el formato desde el primer carácter: si la línea no empieza con MemTotal:, no asigna nada y la variable conserva su valor. Por eso la línea SwapCached: no se confunde con Cached:.

El informe muestra la memoria disponible, no la libre, por la razón que dimos en el tema 7: la memoria que ocupa la caché de disco se recupera en cuanto hace falta, y contar solo la que está vacía haría parecer que el equipo está siempre al límite.

8. La sección de los procesos

Falta lo que en el script le encargamos a ps. En C hay que recorrer el directorio /proc, y para eso existen tres funciones de la biblioteca: opendir abre un directorio, readdir devuelve sus entradas de a una, y closedir lo cierra. Por debajo usan la llamada al sistema getdents64, la que vimos en la traza de ls en el tema 10.

inspector.c (parte 5 de 6)
struct proceso {
    int pid;
    char nombre[64];
    char estado;
    long rss;                           /* memoria física, en kB */
};

/* Lee /proc/PID/status. Devuelve -1 si el proceso ya no existe. */
static int leer_proceso(int pid, struct proceso *p)
{
    char ruta[64], linea[MAX_LINEA];
    FILE *f;

    snprintf(ruta, sizeof(ruta), "/proc/%d/status", pid);
    f = fopen(ruta, "r");
    if (f == NULL)
        return -1;                      /* terminó mientras recorríamos: no es un error */

    p->pid = pid;
    p->nombre[0] = '\0';
    p->estado = '?';
    p->rss = 0;                         /* los hilos del kernel no tienen la línea VmRSS */

    while (fgets(linea, sizeof(linea), f) != NULL) {
        sscanf(linea, "Name: %63[^\n]", p->nombre);
        sscanf(linea, "State: %c", &p->estado);
        sscanf(linea, "VmRSS: %ld", &p->rss);
    }
    fclose(f);
    return 0;
}

static void seccion_procesos(void)
{
    struct proceso mayores[TOP] = { 0 };
    struct proceso p;
    struct dirent *entrada;
    int total = 0, ejecutando = 0, dormidos = 0, zombis = 0;
    DIR *dir = opendir("/proc");

    printf("\n== Procesos ==\n");
    if (dir == NULL) {
        perror("/proc");
        return;
    }

    while ((entrada = readdir(dir)) != NULL) {
        if (!isdigit((unsigned char)entrada->d_name[0]))
            continue;                   /* solo los directorios con nombre numérico */
        if (leer_proceso(atoi(entrada->d_name), &p) == -1)
            continue;

        total++;
        if (p.estado == 'R')
            ejecutando++;
        else if (p.estado == 'Z')
            zombis++;
        else if (p.estado == 'S' || p.estado == 'I' || p.estado == 'D')
            dormidos++;

        /* insertar en la lista de los que más memoria ocupan, que se mantiene ordenada */
        for (int i = 0; i < TOP; i++) {
            if (p.rss > mayores[i].rss) {
                for (int j = TOP - 1; j > i; j--)
                    mayores[j] = mayores[j - 1];
                mayores[i] = p;
                break;
            }
        }
    }
    closedir(dir);

    printf("Total:         %d (%d en ejecucion, %d dormidos, %d zombis)\n",
           total, ejecutando, dormidos, zombis);
    printf("Mayor uso de memoria:\n");
    for (int i = 0; i < TOP && mayores[i].rss > 0; i++)
        printf("  %7d  %6ld MB  %s\n", mayores[i].pid, mayores[i].rss / 1024, mayores[i].nombre);
}

La estructura general es la de procesos.sh, del tema 31, con dos diferencias. En lugar de mostrar cada proceso, los cuenta por estado, con las letras de la tabla del tema 26. Y conserva los cinco que más memoria ocupan.

Para lo segundo no hace falta guardar todos los procesos y ordenarlos. Basta con una lista de cinco lugares, ordenada de mayor a menor: cada proceso leído se compara con ella y, si supera a alguno, se inserta en su posición y los siguientes se desplazan, con lo que el último queda fuera. El programa usa la misma memoria con trescientos procesos que con treinta mil.

Dos detalles del código corresponden a observaciones del curso:

La memoria que se muestra es RSS, con la limitación que explicamos en el tema 27: cuenta completas las páginas compartidas, de modo que sirve para comparar procesos entre sí, y no para sumarlos.

9. El programa principal, y a compilar

La última parte decide qué secciones mostrar. Sin argumentos las muestra todas; con argumentos, solo las nombradas, en el orden indicado.

inspector.c (parte 6 de 6)
int main(int argc, char *argv[])
{
    if (argc == 1) {                    /* sin argumentos: el informe completo */
        seccion_sistema();
        seccion_cpu();
        seccion_memoria();
        seccion_procesos();
        return 0;
    }

    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "sistema") == 0)
            seccion_sistema();
        else if (strcmp(argv[i], "cpu") == 0)
            seccion_cpu();
        else if (strcmp(argv[i], "memoria") == 0)
            seccion_memoria();
        else if (strcmp(argv[i], "procesos") == 0)
            seccion_procesos();
        else {
            fprintf(stderr, "uso: %s [sistema] [cpu] [memoria] [procesos]\n", argv[0]);
            return 1;
        }
    }
    return 0;
}

Para compilar, y para no tener que recordar las opciones, añadimos un Makefile. Es más simple que el del módulo del tema 24: indica que el ejecutable depende del archivo fuente y cómo se obtiene a partir de él.

Makefile (la línea del comando empieza con un tabulador)
CC = gcc
CFLAGS = -Wall -Wextra -g

inspector: inspector.c
	$(CC) $(CFLAGS) inspector.c -o inspector

clean:
	rm -f inspector
terminal
$ make
gcc -Wall -Wextra -g inspector.c -o inspector
$ make
make: 'inspector' is up to date.
$ ./inspector memoria procesos

== Memoria ==
Total:         15871 MB
Disponible:    10675 MB (67 %)
En cache:      9924 MB
Intercambio:   0 MB en uso, de 2047 MB

== Procesos ==
Total:         288 (1 en ejecucion, 287 dormidos, 0 zombis)
Mayor uso de memoria:
     2204     402 MB  gnome-shell
…

La segunda llamada a make no hace nada. Compara la fecha de modificación del ejecutable con la del fuente —dos de las fechas que guarda el inodo, según el tema 28— y concluye que no hace falta recompilar. El programa debe compilar sin ninguna advertencia; si aparece alguna, hay un error al copiar.

Las secciones de memoria y de procesos responden al instante. La de CPU es la que introduce la espera de un segundo.

10. Comprobar el resultado

Un programa que informa números necesita ser verificado, y aquí hay con qué: las herramientas del sistema leen las mismas fuentes. Cada línea del informe se puede contrastar con un comando conocido.

Línea del informeDebe coincidir con
Kerneluname -srm
En marcha, carga mediauptime
Procesadoresnproc
Uso de CPULa tercera línea de top, sumando us y ni, y sy, hi y si
Interrupciones y cambios de contextoLas columnas in y cs de vmstat 1
Memoria total y disponibleLas columnas total y available de free -m
Total de procesosps -e --no-headers | wc -l
Mayor uso de memoriaps -eo pid,rss,comm --sort=-rss | head -6

Los valores dinámicos no coincidirán exactamente, porque las dos herramientas no miden el mismo segundo; deben ser del mismo orden. El total de procesos puede diferir en uno: cada herramienta se cuenta a sí misma.

La segunda verificación es sobre lo que el programa hace. Con strace se comprueba que usa las fuentes de la tabla de diseño, y ninguna otra:

terminal
$ strace -e trace=uname,openat ./inspector sistema 2>&1 | grep -E 'uname|/etc/os|/proc|/sys'
uname({sysname="Linux", nodename="equipo", ...}) = 0
openat(AT_FDCWD, "/etc/os-release", O_RDONLY) = 3
openat(AT_FDCWD, "/proc/uptime", O_RDONLY) = 3
openat(AT_FDCWD, "/proc/loadavg", O_RDONLY) = 3
openat(AT_FDCWD, "/sys/devices/system/cpu/online", O_RDONLY|O_CLOEXEC) = 3
$ strace -c ./inspector procesos 2>&1 | grep -E 'openat|getdents|total'
  0.00    0.000000           0       291           openat
  0.00    0.000000           0         2           getdents64
100.00    0.000000           0      1482           total

La primera traza muestra, en orden, las cuatro fuentes de la sección y una quinta que no escribimos. Es la función sysconf: para saber cuántos procesadores hay, la biblioteca de C lee un archivo de /sys, el mismo que consultamos a mano en el tema 31. Es un ejemplo de lo dicho en el tema 15: una función de biblioteca que parece un simple cálculo puede esconder llamadas al sistema.

La segunda cuenta las llamadas de la sección de procesos: 291 aperturas, una por proceso más el directorio y las bibliotecas, y dos lecturas del directorio. Unas cinco llamadas por cada proceso examinado: abrir, consultar, leer, leer hasta el final y cerrar.

Queda una tercera prueba, la de las condiciones adversas. Conviene comprobar que el programa se comporta con sensatez cuando algo falta:

PruebaResultado esperado
./inspector discoEl mensaje de uso, por la salida de errores, y código de salida 1.
./inspector > informe.txtEl informe completo en el archivo. No hay nada que dependa de una terminal.
./inspector procesos, mientras se crean y terminan procesosNingún error: los que desaparecen se omiten.
Ejecutarlo en WSL o en un contenedorEl informe, quizá sin alguna línea.

11. Bash frente a C

Las dos versiones producen casi el mismo informe. Lo que cuesta producirlo es muy distinto, y se puede medir con las herramientas del curso:

terminal
$ strace -f -c -o cuenta.txt ./inspector.sh > /dev/null; tail -1 cuenta.txt
100.00    0.041230           6      6214       402 total
$ strace -f -c -o cuenta.txt ./inspector > /dev/null; tail -1 cuenta.txt
100.00    0.004870           3      1561         1 total
$ strace -f -e trace=execve -o procesos.txt ./inspector.sh > /dev/null; grep -c execve procesos.txt
16
inspector.shinspector
Líneas de códigoUnas 50Unas 270
Tiempo para escribirloMinutosHoras
Programas que ejecuta, contándose a sí mismo161
Llamadas al sistemaUnas 6000Unas 1500
Tiempo de CPU, sin contar el segundo de esperaDecenas de milisegundosUnos pocos milisegundos
AritméticaEnterosCon decimales
Necesita compilarseNoSí, para cada arquitectura
Depende deawk, grep, ps, sed, cut, nprocSolo la biblioteca de C

Las cifras exactas dependen de la cantidad de procesos del equipo; la proporción es estable. El script ejecuta dieciséis programas, y cada uno recorre el camino completo del tema 32: fork, execve, enlazador dinámico, decenas de llamadas antes de hacer nada útil. Más de la mitad de sus seis mil llamadas son esa preparación repetida.

Ninguna de las dos versiones es «la correcta». Sirven para cosas distintas:

El orden en que lo hicimos es también una recomendación: empezar por el script para aclarar qué se quiere y de dónde sale cada dato, y pasar a C solo si hay una razón.

12. Ampliaciones

El inspector cubre lo esencial y deja mucho por hacer. Las siguientes ampliaciones están ordenadas por dificultad, y cada una indica el tema que conviene repasar. Son los ejercicios de este tema.

AmpliaciónPistaTema
1. Añadir a la sección del sistema los parámetros con que arrancó el kernel./proc/cmdline21
2. Mostrar el estado de cada proceso de la lista, y su usuario.Las líneas State y Uid de status; getpwuid30
3. Mostrar el uso de cada procesador por separado.Las líneas cpu0, cpu1… de /proc/stat31
4. Añadir una sección de red, con los bytes recibidos y enviados por segundo en cada interfaz./sys/class/net/*/statistics29
5. Añadir una sección de discos, con el espacio libre de cada sistema de archivos./proc/mounts y la función statvfs28
6. Añadir una opción -n segundos para repetir el informe a intervalos, como top.Un bucle con sleep; conservar la muestra anterior19
7. Terminar con código 2 si la memoria disponible baja de un porcentaje dado, y usarlo desde un script que avise.El valor de return de main; $?26
8. Listar los procesos que más CPU consumen, además de los que más memoria ocupan.Dos lecturas de /proc/PID/stat por proceso; atención al nombre entre paréntesis31
9. Ejecutarlo cada minuto como servicio, guardando el resultado en el registro.Una unidad .service y una .timer; journalctl -u22
10. Usar PSS en lugar de RSS, y explicar por qué cambia el orden de la lista./proc/PID/smaps_rollup; requiere permisos27

La octava es la más instructiva, y la que acerca el programa a un top de verdad. El uso de CPU de un proceso, como el del sistema, se obtiene por diferencia: hay que recordar, para cada PID, el tiempo que llevaba consumido en la muestra anterior. Y hay que prever que entre una muestra y otra aparezcan procesos nuevos, desaparezcan otros, y que un PID se reutilice.

Para todas vale el método del tema 31: si no se sabe de dónde sale un dato, se ejecuta bajo strace el comando que lo muestra, y se mira qué archivo abre.

13. Lo que el proyecto usó del curso

El inspector es un programa corto, y para escribirlo y entender lo que informa hizo falta casi todo el recorrido:

En el proyectoConceptoTema
uname, fopen, readdirLlamadas al sistema, envoltorios y funciones de biblioteca13, 14, 15
La separación entre modo usuario y modo kernelLos dos modos de la CPU12
Los tics de /proc/statEl temporizador y la contabilidad del tiempo19
Interrupciones por segundoLas interrupciones de hardware17
Cambios de contexto por segundoLa multitarea19
Memoria disponible y cachéLa jerarquía de memoria y la caché de disco7
VmRSSMemoria virtual: reservar no es ocupar27
El recorrido de /proc, los estadosProcesos, y su directorio26, 31
Los hilos del kernel sin memoriaEl kernel y su espacio de direcciones11
Archivos que se generan al leerlosSistemas de archivos virtuales28, 31
La verificación con straceObservar las llamadas de un programa16
Los dieciséis procesos del scriptLo que cuesta ejecutar un comando32

La primera vez que vimos una salida parecida fue en el tema 1, con cinco comandos que consultaban «información que solo el kernel conoce». Entonces eran cifras. Ahora cada una tiene detrás un mecanismo que conocemos, y un programa nuestro que la obtiene.

Con el proyecto termina el contenido del curso. El tema 34 hace el balance y presenta lo que sigue en la ruta.

14. Resumen

15. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Por qué el inspector tarda un segundo en ejecutarse?

Porque el uso de CPU, las interrupciones y los cambios de contexto son contadores acumulados desde el arranque. Para saber cuánto ocurrió por segundo hay que leerlos dos veces, con una pausa en el medio, y restar.

2. ¿Por qué leer_stat obtiene los cinco valores en una sola lectura del archivo?

Para que correspondan al mismo instante. El contenido se genera al leer; si se abriera el archivo una vez por cada valor, cada uno reflejaría un momento distinto.

3. En seccion_memoria, ¿por qué sscanf(linea, "Cached: %lld", …) no toma el valor de la línea SwapCached?

Porque sscanf compara el formato con el texto desde el primer carácter. La línea empieza con «Swap», no coincide, y la función no asigna nada.

4. ¿Por qué el informe muestra la memoria disponible y no la libre?

Porque la memoria que no se usa para otra cosa la ocupa la caché de disco, que el kernel libera en cuanto hace falta. La memoria libre es casi siempre poca, sin que eso indique ningún problema.

5. ¿Qué ocurre si un proceso termina mientras el inspector recorre /proc?

La apertura de su archivo status falla, leer_proceso devuelve −1 y el bucle pasa a la entrada siguiente. El proceso no se cuenta, y el informe continúa.

6. ¿Por qué los hilos del kernel no aparecen nunca en la lista de mayor uso de memoria?

No tienen espacio de direcciones propio, y su archivo status no incluye la línea VmRSS. Su valor queda en cero, y la lista solo admite valores mayores que los que ya contiene.

7. ¿Cómo logra el programa conservar los cinco procesos mayores sin guardarlos todos?

Mantiene una lista de cinco lugares, ordenada. Cada proceso leído se compara con ella: si supera a alguno, se inserta en esa posición y los siguientes se desplazan, perdiéndose el último.

8. La traza de ./inspector sistema muestra la apertura de un archivo de /sys que el código no menciona. ¿Quién lo abre?

La función sysconf de la biblioteca de C, que para averiguar la cantidad de procesadores lee /sys/devices/system/cpu/online.

9. ¿Por qué el script hace cuatro veces más llamadas al sistema que el programa en C?

Porque ejecuta dieciséis programas. Cada uno requiere crear un proceso, cargar el ejecutable y sus bibliotecas e inicializarse antes de hacer su trabajo, y todo eso son llamadas al sistema.

10. ¿En qué situación elegirías cada versión?

El script, para una consulta ocasional o para algo que cambia seguido: se escribe y se modifica en minutos. El programa en C, para algo que se ejecuta continuamente o en muchos equipos, donde el costo de crear procesos importa, o cuando hacen falta decimales y estructuras de datos.

Para practicar

  1. Escribe inspector.sh, ejecútalo y verifica cada línea con la tabla de la sección 10.
  2. Arma inspector.c con las seis partes, compílalo con el Makefile y comprueba que no produce advertencias.
  3. Ejecuta el inspector mientras corre ./tiempos calcular, y luego ./tiempos pedir, del tema 11. ¿Qué línea cambia en cada caso?
  4. Ejecútalo mientras corren dos copias de bucle sobre un mismo procesador, del tema 19. ¿Qué ocurre con los cambios de contexto por segundo?
  5. Lanza ./zombi &, del tema 26, y comprueba que el inspector cuenta el zombi.
  6. Ejecuta ./memoria, del tema 27, modificado para que haga una pausa de diez segundos después de escribir, y búscalo en la lista del inspector.
  7. Repite las mediciones de la sección 11 en tu equipo. ¿Cuántos programas ejecuta el script? Identifícalos en procesos.txt.
  8. Realiza al menos tres ampliaciones de la sección 12, una de cada tercio de la tabla.