1. Qué vamos a construir
El programa se llama inspector. Al ejecutarlo muestra un informe del estado de la máquina en cuatro secciones:
$ ./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:
- Integrar. Cada línea del informe corresponde a un tema del curso. Escribir el programa obliga a recordar qué significa cada dato.
- Practicar la lectura de
/proccon las reglas del tema 31, en un programa de tamaño real. - Comparar dos herramientas. La misma tarea, resuelta con un script y con un programa compilado, muestra para qué sirve cada uno.
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:
| Dato | Fuente | Tipo | Tema |
|---|---|---|---|
| Nombre del equipo, kernel, arquitectura | uname | Llamada al sistema | 4, 11 |
| Distribución | /etc/os-release | Archivo en disco | 25 |
| Tiempo en marcha | /proc/uptime | Archivo virtual | 31 |
| Carga media | /proc/loadavg | Archivo virtual | 26 |
| Cantidad de procesadores | sysconf | Función de la biblioteca | 15 |
| Modelo del procesador | /proc/cpuinfo | Archivo virtual | 31 |
| Uso de CPU, interrupciones, cambios de contexto | /proc/stat, leído dos veces | Archivo virtual | 12, 17, 19 |
| Memoria total, disponible, en caché, intercambio | /proc/meminfo | Archivo virtual | 7, 27 |
| Lista de procesos | Los directorios numéricos de /proc | Directorio virtual | 26, 31 |
| Nombre, estado y memoria de cada proceso | /proc/PID/status | Archivo virtual | 26, 27 |
A eso se suman cuatro decisiones, que valen para las dos versiones:
- Cada archivo se lee una sola vez por medición, de principio a fin. Es la primera regla del tema 31.
- Los campos se buscan por su nombre, nunca por su posición.
- Un dato que falta no detiene el programa. Si un archivo no existe o un proceso desapareció, esa línea se omite y el informe continúa.
- Las tasas se obtienen por diferencia. El programa toma una muestra, espera un segundo y toma otra. Por eso tarda un segundo en ejecutarse.
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.
#!/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}'
$ 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ón | Qué hace |
|---|---|
. /etc/os-release | Ejecuta 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 < archivo | Lee 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:
- No tiene decimales. El uso de CPU aparece como «3 %» donde el valor real es 3,4. Con cifras pequeñas, la pérdida es grande.
- Depende de otros programas. Para la lista de procesos recurre a
ps, y para los textos, aawk,grep,cutysed. Cada uno es un proceso que hay que crear. - No cuenta los estados ni las interrupciones. Se podría, a costa de más procesos y de un script menos legible.
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 - 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.
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:
/* 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:
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.
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:
- Un
fopenque falla no es un error. Entre quereaddirentrega un nombre y que se abre su archivo, el proceso puede haber terminado. La función devuelve −1 y el bucle pasa al siguiente. - Los hilos del kernel quedan con cero. No tienen espacio de direcciones propio, y su archivo
statusno incluye la líneaVmRSS. Como el valor inicial es cero, nunca entran en la lista.
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.
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.
CC = gcc
CFLAGS = -Wall -Wextra -g
inspector: inspector.c
$(CC) $(CFLAGS) inspector.c -o inspector
clean:
rm -f inspector
$ 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 informe | Debe coincidir con |
|---|---|
| Kernel | uname -srm |
| En marcha, carga media | uptime |
| Procesadores | nproc |
| Uso de CPU | La tercera línea de top, sumando us y ni, y sy, hi y si |
| Interrupciones y cambios de contexto | Las columnas in y cs de vmstat 1 |
| Memoria total y disponible | Las columnas total y available de free -m |
| Total de procesos | ps -e --no-headers | wc -l |
| Mayor uso de memoria | ps -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:
$ 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:
| Prueba | Resultado esperado |
|---|---|
./inspector disco | El mensaje de uso, por la salida de errores, y código de salida 1. |
./inspector > informe.txt | El informe completo en el archivo. No hay nada que dependa de una terminal. |
./inspector procesos, mientras se crean y terminan procesos | Ningún error: los que desaparecen se omiten. |
| Ejecutarlo en WSL o en un contenedor | El 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:
$ 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.sh | inspector | |
|---|---|---|
| Líneas de código | Unas 50 | Unas 270 |
| Tiempo para escribirlo | Minutos | Horas |
| Programas que ejecuta, contándose a sí mismo | 16 | 1 |
| Llamadas al sistema | Unas 6000 | Unas 1500 |
| Tiempo de CPU, sin contar el segundo de espera | Decenas de milisegundos | Unos pocos milisegundos |
| Aritmética | Enteros | Con decimales |
| Necesita compilarse | No | Sí, para cada arquitectura |
| Depende de | awk, grep, ps, sed, cut, nproc | Solo 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 script es la herramienta para lo que se ejecuta de vez en cuando, cambia seguido o une programas que ya existen. Para un informe que se consulta a mano, la diferencia de rendimiento no se nota.
- El programa en C conviene cuando se ejecuta continuamente, cuando el costo importa o cuando hace falta algo que la shell no ofrece: estructuras de datos, aritmética, control fino de los errores. Un monitor que se ejecuta cada segundo en miles de servidores no puede crear dieciséis procesos por medición.
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ón | Pista | Tema |
|---|---|---|
| 1. Añadir a la sección del sistema los parámetros con que arrancó el kernel. | /proc/cmdline | 21 |
| 2. Mostrar el estado de cada proceso de la lista, y su usuario. | Las líneas State y Uid de status; getpwuid | 30 |
| 3. Mostrar el uso de cada procesador por separado. | Las líneas cpu0, cpu1… de /proc/stat | 31 |
| 4. Añadir una sección de red, con los bytes recibidos y enviados por segundo en cada interfaz. | /sys/class/net/*/statistics | 29 |
| 5. Añadir una sección de discos, con el espacio libre de cada sistema de archivos. | /proc/mounts y la función statvfs | 28 |
6. Añadir una opción -n segundos para repetir el informe a intervalos, como top. | Un bucle con sleep; conservar la muestra anterior | 19 |
| 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éntesis | 31 |
| 9. Ejecutarlo cada minuto como servicio, guardando el resultado en el registro. | Una unidad .service y una .timer; journalctl -u | 22 |
10. Usar PSS en lugar de RSS, y explicar por qué cambia el orden de la lista. | /proc/PID/smaps_rollup; requiere permisos | 27 |
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 proyecto | Concepto | Tema |
|---|---|---|
uname, fopen, readdir | Llamadas al sistema, envoltorios y funciones de biblioteca | 13, 14, 15 |
| La separación entre modo usuario y modo kernel | Los dos modos de la CPU | 12 |
Los tics de /proc/stat | El temporizador y la contabilidad del tiempo | 19 |
| Interrupciones por segundo | Las interrupciones de hardware | 17 |
| Cambios de contexto por segundo | La multitarea | 19 |
| Memoria disponible y caché | La jerarquía de memoria y la caché de disco | 7 |
VmRSS | Memoria virtual: reservar no es ocupar | 27 |
El recorrido de /proc, los estados | Procesos, y su directorio | 26, 31 |
| Los hilos del kernel sin memoria | El kernel y su espacio de direcciones | 11 |
| Archivos que se generan al leerlos | Sistemas de archivos virtuales | 28, 31 |
La verificación con strace | Observar las llamadas de un programa | 16 |
| Los dieciséis procesos del script | Lo que cuesta ejecutar un comando | 32 |
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
- El inspector informa el sistema, la CPU, la memoria y los procesos usando solo llamadas al sistema y archivos de
/proc. - El diseño empieza por decidir la fuente de cada dato, y por fijar reglas: leer cada archivo una vez, buscar por nombre, tolerar lo que falte, obtener las tasas por diferencia.
- La versión en Bash se escribe en minutos y sirve de prototipo. Se apoya en otros programas y solo maneja enteros.
- En C, dos funciones de apoyo encapsulan la lectura de archivos y devuelven −1 ante cualquier problema.
- El uso de CPU, las interrupciones y los cambios de contexto salen de dos muestras de
/proc/stat, cada una tomada en una sola lectura. /proc/meminfose recorre una vez, comparando cada línea con los campos buscados.- Los procesos se obtienen recorriendo
/procconopendiryreaddir. Un proceso que desaparece durante el recorrido no es un error. - Los cinco procesos de mayor memoria se mantienen en una lista ordenada de tamaño fijo, sin guardar todos.
- El resultado se verifica contra
uname,uptime,free,psyvmstat, y el comportamiento, constrace. - El script hace cuatro veces más llamadas al sistema y ejecuta dieciséis programas; la versión en C, uno. Cada herramienta tiene su ámbito.
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
- Escribe
inspector.sh, ejecútalo y verifica cada línea con la tabla de la sección 10. - Arma
inspector.ccon las seis partes, compílalo con elMakefiley comprueba que no produce advertencias. - Ejecuta el inspector mientras corre
./tiempos calcular, y luego./tiempos pedir, del tema 11. ¿Qué línea cambia en cada caso? - Ejecútalo mientras corren dos copias de
buclesobre un mismo procesador, del tema 19. ¿Qué ocurre con los cambios de contexto por segundo? - Lanza
./zombi &, del tema 26, y comprueba que el inspector cuenta el zombi. - 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. - Repite las mediciones de la sección 11 en tu equipo. ¿Cuántos programas ejecuta el script? Identifícalos en
procesos.txt. - Realiza al menos tres ampliaciones de la sección 12, una de cada tercio de la tabla.