← Sistemas de Archivos y E/SBloque 13 · Dispositivos de bloque y de caracteres · Tema 45 de 50

Tema 45

Dispositivos de bloque y de caracteres

Una ruta de /dev puede representar un disco, una terminal o un generador de bytes. Estudiaremos qué guarda ese archivo especial, cómo se identifica el dispositivo y qué operaciones ofrece su controlador. Dos programas en C, un laboratorio de consultas y dos simuladores permitirán distinguir metadatos, datos, ceros y fin de archivo.

1. Un nombre de archivo como entrada al dispositivo

El tema 44 mostró dispositivos de bloque y sus colas en sysfs. Ahora cambiaremos de perspectiva: ¿qué significa abrir una ruta como /dev/sda, /dev/tty o /dev/null? Aunque aparezcan en el árbol de archivos, no son archivos regulares con contenido almacenado de la misma forma.

Archivo especial de dispositivo: nodo del sistema de archivos cuyo tipo y número de dispositivo permiten dirigir operaciones al controlador correspondiente. Su nombre es una ruta de acceso; no define por sí solo el comportamiento.

La aplicación puede usar open, read, write y close, pero el controlador determina qué significa leer o escribir. Una operación puede obtener datos, descartarlos, esperar, informar un error o requerir otra interfaz, como ioctl.

Del nombre al controladorLa ruta identifica un nodo. Sus metadatos incluyen tipo y número de dispositivo. El kernel dirige las operaciones al controlador y su recurso, que puede ser físico o virtual. Ruta de acceso/dev/null Nodo especialcarácter · mayor 1 · menor 3 Controladoroperaciones del dispositivoEl nodo tiene metadatos; el controlador aporta la semántica.El recurso no necesita ser un componente físico.
Un nombre distinto puede conducir al mismo tipo y número de dispositivo.

2. Bloques y caracteres: dos contratos de acceso

AspectoDispositivo de bloqueDispositivo de caracteres
Tipo observadoS_ISBLK; letra b en ls -l.S_ISCHR; letra c en ls -l.
Modelo habitualEspacio direccionable por bloques o sectores.Interfaz definida por el controlador: flujo, terminal, fuente o control.
EjemplosDiscos, particiones, volúmenes y dispositivos loop.Terminales, null, zero y urandom.
Uso de un sistema de archivosPuede contenerlo y servir como origen de un montaje.No se presupone un volumen montable de bloques.
Acceso por posiciónHabitual; las reglas dependen de modo, alineación y dispositivo.No está garantizado por pertenecer a la categoría.

«De caracteres» no significa texto ni un byte por llamada: una lectura puede devolver muchos bytes binarios. «De bloque» tampoco exige que cada read de usuario mida exactamente un sector; la pila puede adaptar o rechazar solicitudes según la operación y sus restricciones.

No todos los dispositivos de caracteres son secuenciales, no todos esperan entrada y no todos admiten las mismas operaciones. El tipo clasifica la interfaz del kernel; el contrato concreto se consulta para cada dispositivo.

Un dispositivo loop es de bloque aunque su almacenamiento esté respaldado por un archivo regular. A la inversa, una imagen ext4 en un archivo regular no se convierte en nodo de bloque solo por contener un sistema de archivos.

3. Nodo, controlador y gestión de /dev

El nodo contiene identidad, tipo, permisos y otros metadatos. El controlador implementa operaciones. El recurso puede existir aunque falte un nombre accesible en nuestro espacio de nombres; también puede existir un nodo sin un dispositivo actualmente utilizable detrás.

En sistemas Linux habituales, devtmpfs proporciona nodos de dispositivos y udev responde a eventos para administrar permisos y enlaces de identificación. Un contenedor puede presentar un subconjunto distinto de /dev y aplicar restricciones adicionales. La documentación de udev explica eventos, reglas y enlaces.

Las rutas /dev/disk/by-id o /dev/disk/by-uuid suelen ser enlaces simbólicos. Ayudan a identificar recursos sin depender solo del orden de enumeración, pero aún hay que resolver qué objeto representan. El nombre /dev/sda no es una identidad permanente universal.

Crear, renombrar o eliminar un nodo no equivale a conectar, renombrar o destruir el hardware. Por ejemplo, borrar un alias retira esa ruta; no borra los sectores del dispositivo representado.

4. Tipo, número mayor y número menor

El número de dispositivo se representa con dev_t. Conceptualmente contiene un número mayor, asociado al ámbito o familia del controlador, y un número menor, que distingue instancias o funciones dentro de ese ámbito. La interpretación exacta la establece la asignación del kernel.

Para interpretar un nodo hay que conservar también su tipo: el espacio de números de bloque y el de caracteres no se identifican sin más entre sí. Dos pares numéricos iguales con tipos distintos no constituyen el mismo destino de operaciones.

Linux · composición y extracción mediante la API
#include <sys/types.h>
#include <sys/sysmacros.h>

dev_t numero = makedev(1, 3);
unsigned int mayor = major(numero);
unsigned int menor = minor(numero);

Se usan major, minor y makedev en vez de suponer una codificación como mayor << 8 | menor. La representación de dev_t no se trata como un formato portátil de ocho bits por componente.

En Linux, /dev/null usa carácter 1:3, /dev/zero carácter 1:5 y /dev/urandom carácter 1:9. No generalices esos ejemplos para adivinar la asignación de un disco o una terminal: consulta sus metadatos.

5. st_dev y st_rdev responden preguntas distintas

CampoPreguntaUso
st_mode¿Qué tipo de objeto se consultó?S_ISREG, S_ISDIR, S_ISCHR, S_ISBLK, S_ISLNK, etc.
st_dev¿En qué sistema de archivos reside este nodo?Identidad del sistema de archivos que contiene el inodo.
st_ino¿Qué inodo es dentro de ese sistema de archivos?Se interpreta junto con st_dev.
st_rdev¿Qué número de dispositivo representa el nodo especial?Solo se interpreta como tal para bloques o caracteres.

Al consultar /dev/null, st_dev corresponde al sistema de archivos donde reside su nodo, mientras st_rdev contiene 1:3. Varios nodos de /dev pueden compartir st_dev y tener distintos st_rdev. Una imagen ext4 regular tiene st_dev, pero no un st_rdev de bloque por el contenido de sus bytes.

La referencia inode(7) describe estos campos. Un tamaño st_size igual a cero en un nodo no prueba que una lectura devuelva EOF: /dev/zero es el contraejemplo del laboratorio.

lstat consulta el enlace simbólico final; stat sigue ese enlace y consulta el destino. Para describir el objeto que realmente se abrió se utiliza fstat sobre el descriptor. Consultar una ruta y abrirla más tarde no elimina una carrera entre comprobación y uso.

6. Reconocer dispositivos con lsblk y blkid

Consultas de inventario y metadatos
ls -l /dev/null /dev/zero /dev/urandom
lsblk -o NAME,KNAME,TYPE,MAJ:MIN,SIZE,FSTYPE,UUID,MOUNTPOINTS
findmnt -T .
stat -c 'tipo=%F fs=%d inodo=%i rdev_hex=%t:%T' /dev/null
readlink -f /sys/dev/char/1:3

ls -l muestra c o b y el par mayor,menor para nodos especiales. En el comando stat de GNU, %t y %T expresan los componentes de st_rdev en hexadecimal; no compares esos textos con decimales sin convertirlos. El programa C de este tema mostrará ambos componentes en decimal.

lsblk presenta dispositivos de bloque y relaciones de la pila, con columnas elegidas explícitamente. La ausencia de /dev/null en ese listado es esperable: es de caracteres. Un disco, su partición y un volumen lógico no se confunden porque aparezcan relacionados.

blkid identifica firmas y atributos del contenido, como tipo de sistema de archivos o UUID. Eso es distinto de clasificar un nodo como bloque o carácter. En modo de sondeo también puede inspeccionar una imagen regular.

La consulta habitual de blkid puede depender de caché y privilegios; para el ensayo con una imagen propia usaremos blkid -p -o export imagen. Una firma reconocida no acredita integridad del volumen, y una salida ausente o ambigua requiere interpretar el estado de la herramienta.

7. Qué crea mknod y qué no crea

mknod crea un nodo con tipo y número indicados. No fabrica el dispositivo, no instala un controlador y no crea un sistema de archivos dentro de un disco. El nombre elegido tampoco modifica la función del número representado.

Sintaxis conceptual · comandos para un directorio propio, no para reconstruir /dev
mknod -m 600 nombre c MAYOR MENOR
mknod -m 600 nombre b MAYOR MENOR

/* Forma en C; modo y dev se combinan con las API correspondientes. */
mknod(ruta, S_IFCHR | 0600, makedev(mayor, menor));

La llamada mknod(2) documenta creación, errores y privilegios. Crear un nodo de bloque o caracteres suele requerir CAP_MKNOD; usarlo es una comprobación adicional, condicionada por permisos, políticas de dispositivos, montaje nodev y disponibilidad del controlador.

Un alias de carácter 1:3 puede representar null bajo otro nombre, incluso fuera de /dev, si el entorno permite crearlo y usarlo. El laboratorio opcional demostrará esa relación en un directorio temporal privado. No se reemplaza ningún nodo del sistema.

8. /dev/null: descartar escrituras y devolver EOF

/dev/null es un dispositivo de caracteres. Las escrituras válidas descartan los bytes; las lecturas de una cantidad positiva devuelven cero, que representa fin de archivo. Ese retorno no significa «se leyó un byte cuyo valor era cero».

Descarte y lectura sin datos
printf 'mensaje que no se conserva\n' > /dev/null
head -c 16 /dev/null | wc -c
# Resultado: 0 bytes.

Redirigir a /dev/null no conserva una copia ni un registro recuperable de lo descartado. En cambio, redirigir un descriptor de salida no impide automáticamente errores en otro descriptor: stdout y stderr son canales distintos.

El descarte no elimina los errores posibles de apertura o uso: una ruta inaccesible o un buffer inválido sigue siendo un problema. La semántica normal de null y zero está documentada en null(4).

9. /dev/zero: datos con valor cero

/dev/zero entrega bytes binarios cuyo valor es 0x00. Para la aplicación son datos recibidos y cuentan en el retorno de read. No son los caracteres '0' y no indican fin de archivo.

Lectura de tamaño explícito · mostrar bytes binarios en hexadecimal
head -c 16 /dev/zero | od -An -tx1
# Dieciseis valores 00.
head -c 16 /dev/zero | wc -c
# Resultado: 16 bytes.

Una fuente de ceros no tiene una longitud finita como un archivo regular de 16 bytes. El ensayo termina porque head impone un límite. Leer hasta EOF de una fuente sin final natural no es un criterio de terminación útil.

Las escrituras en zero también descartan datos, pero eso no convierte a zero y null en fuentes equivalentes. Para representarlos en una terminal usamos od o hexadecimal, en vez de enviar bytes de control o NUL directamente a la pantalla.

Crear un archivo regular mediante escrituras de ceros y crear uno disperso con truncate pueden dar el mismo contenido lógico en un rango, pero distinta asignación. Ninguno se convierte por ello en el dispositivo /dev/zero.

10. /dev/urandom y el origen de los bytes aleatorios

/dev/urandom es un dispositivo de caracteres que entrega bytes generados por el generador del kernel. No son necesariamente texto imprimible ni un archivo con un conjunto fijo que se recorre hasta agotarlo. Se lee una cantidad explícita.

Muestra binaria acotada
head -c 16 /dev/urandom | od -An -tx1
# La salida contiene 16 bytes; sus valores no se fijan como resultado esperado.

El caso del arranque temprano importa: /dev/urandom puede producir datos antes de que se haya inicializado la fuente. Para aplicaciones Linux que necesitan bytes del generador inicializado, getrandom con flags=0 ofrece una interfaz que espera esa inicialización y no requiere abrir un nodo de /dev.

Las referencias random(4) y getrandom(2) detallan estas diferencias. No se presupone que /dev/random deba bloquear por cada lectura durante toda la vida del sistema ni se usa esa simplificación para elegir una API.

En el laboratorio verificaremos longitud y representación de la muestra, no exigiremos que dos lecturas difieran ni que todos sus bytes sean distintos. Esas condiciones no constituyen una prueba de calidad del generador.

11. Permisos, desplazamientos y operaciones admitidas

El acceso depende del nodo, directorios, credenciales y políticas de la capa de dispositivos. Poder consultar stat no garantiza poder abrir. Un descriptor abierto además se verifica con fstat antes de interpretar su identidad.

La presencia de read, write o lseek en la API general no obliga a todo controlador a admitirlos. Una terminal puede no permitir desplazamiento y devolver ESPIPE. Algunos dispositivos de caracteres implementan posicionamiento u operaciones propias; no se generaliza el error a toda la categoría. Véase lseek(2).

Incluso para dispositivos conocidos se tratan lecturas cortas, EINTR y EOF de acuerdo con su contrato. read(fd, buffer, 0) no sirve para detectar el final de una fuente: el ensayo de EOF usa un tamaño solicitado positivo.

Leer o escribir un nodo de bloque accede al contenido del dispositivo, no a un archivo ordinario de su montaje. Este tema consulta sus metadatos y concentra las lecturas en null, zero y urandom. El tema 46 desarrollará la lectura de bloques e ioctl con una imagen de laboratorio.

12. Programa en C: inspeccionar sin abrir el dispositivo

Guarda inspeccionar-nodo.c. Recibe lstat|stat y una o más rutas. Solo consulta metadatos: no abre terminales, FIFO ni discos. Las rutas se imprimen como bytes hexadecimales para representar incluso nombres con controles. Devuelve 0 si todas las consultas resultan válidas, 1 si alguna falla y 2 por uso incorrecto.

inspeccionar-nodo.c · tipo, sistema contenedor y dispositivo representado
#define _POSIX_C_SOURCE 200809L
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>

static const char *tipo(mode_t m)
{
    if (S_ISREG(m)) return "regular";
    if (S_ISDIR(m)) return "directorio";
    if (S_ISCHR(m)) return "caracter";
    if (S_ISBLK(m)) return "bloque";
    if (S_ISLNK(m)) return "enlace";
    if (S_ISFIFO(m)) return "fifo";
    if (S_ISSOCK(m)) return "socket";
    return "otro";
}
int main(int argc, char **argv)
{
    if (argc < 3 || (strcmp(argv[1], "lstat") && strcmp(argv[1], "stat"))) {
        fprintf(stderr, "Uso: %s lstat|stat ruta [ruta ...]\n", argv[0]);
        return 2;
    }
    int seguir = !strcmp(argv[1], "stat"), estado = 0;
    for (int i = 2; i < argc; ++i) {
        struct stat st;
        int r = seguir ? stat(argv[i], &st) : lstat(argv[i], &st);
        if (r == -1) { perror("consulta de metadatos"); estado = 1; continue; }
        printf("PATH_HEX=");
        const unsigned char *p = (const unsigned char *)argv[i];
        for (; *p; ++p) printf("%02X", (unsigned)*p);
        printf("\nTIPO=%s\n", tipo(st.st_mode));
        printf("FS_MAYOR=%u\nFS_MENOR=%u\n", major(st.st_dev), minor(st.st_dev));
        printf("INODO=%" PRIuMAX "\n", (uintmax_t)st.st_ino);
        printf("TAMANO_INFO=%" PRIdMAX "\n", (intmax_t)st.st_size);
        if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode)) {
            printf("RDEV_MAYOR=%u\nRDEV_MENOR=%u\n", major(st.st_rdev), minor(st.st_rdev));
        } else {
            puts("RDEV_MAYOR=NA\nRDEV_MENOR=NA");
        }
        puts("FIN");
    }
    if (fflush(stdout) == EOF) { perror("stdout"); estado = 1; }
    return estado;
}

RDEV=NA indica que no se interpreta el campo como identidad de dispositivo para ese tipo. TAMANO_INFO conserva el valor informado sin usarlo como límite de lectura. Dos alias de dispositivo pueden tener distintos inodos y el mismo tipo y st_rdev; no necesitan ser hard links.

Una consulta fallida no impide continuar con las rutas siguientes, pero el estado final será 1. Si el objetivo fuera usar el objeto después de verificarlo, habría que abrirlo con las restricciones adecuadas y comprobar el descriptor, en vez de asumir que esta consulta congeló la ruta.

13. Programa en C: leer tres fuentes conocidas con límite

Guarda leer-fuente.c. Solo admite los nombres null, zero o urandom y de 1 a 256 bytes. Las rutas y números Linux están fijados en el código. Abre en lectura y verifica el descriptor con fstat: un archivo regular colocado bajo el nombre esperado no supera esa comprobación.

leer-fuente.c · identidad del descriptor y lectura acotada
#define _POSIX_C_SOURCE 200809L
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>

static int cantidad(const char *s, size_t *valor)
{
    unsigned n = 0;
    if (!*s || strlen(s) > 3) return -1;
    for (; *s; ++s) {
        if (*s < '0' || *s > '9') return -1;
        n = n * 10 + (unsigned)(*s - '0');
    }
    if (n < 1 || n > 256) return -1;
    *valor = n;
    return 0;
}
int main(int argc, char **argv)
{
    const char *nombres[] = {"null", "zero", "urandom"};
    const char *rutas[] = {"/dev/null", "/dev/zero", "/dev/urandom"};
    const unsigned menores[] = {3, 5, 9};
    size_t pedido;
    if (argc != 3 || cantidad(argv[2], &pedido)) {
        fprintf(stderr, "Uso: %s null|zero|urandom bytes(1..256)\n", argv[0]); return 2;
    }
    int elegido = -1;
    for (int i = 0; i < 3; ++i) if (!strcmp(argv[1], nombres[i])) elegido = i;
    if (elegido == -1) { fprintf(stderr, "Fuente no admitida\n"); return 2; }
    int fd = open(rutas[elegido], O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
    if (fd == -1) { perror("open"); return 1; }
    struct stat st;
    if (fstat(fd, &st) == -1) { perror("fstat"); close(fd); return 1; }
    if (!S_ISCHR(st.st_mode) || st.st_rdev != makedev(1, menores[elegido])) {
        fprintf(stderr, "Tipo o numero de dispositivo inesperado\n"); close(fd); return 1;
    }
    unsigned char buffer[256];
    size_t usados = 0;
    int eof = 0;
    while (usados < pedido) {
        ssize_t n = read(fd, buffer + usados, pedido - usados);
        if (n > 0) usados += (size_t)n;
        else if (n == 0) { eof = 1; break; }
        else if (errno == EINTR) continue;
        else { perror("read"); close(fd); return 1; }
    }
    if (close(fd)) { perror("close"); return 1; }
    if (elegido != 0 && usados != pedido) {
        fprintf(stderr, "EOF inesperado en la fuente\n"); return 1;
    }
    printf("SOLICITADOS=%zu\nLEIDOS=%zu\nEOF=%s\nHEX=", pedido, usados, eof ? "si" : "no");
    for (size_t i = 0; i < usados; ++i) printf("%02X", (unsigned)buffer[i]);
    putchar('\n');
    if (fflush(stdout) == EOF) { perror("stdout"); return 1; }
    return 0;
}

El programa devuelve 0 al completar el ensayo esperado, 1 ante fallo de apertura, identidad o lectura y 2 ante uso incorrecto. Para null devuelve LEIDOS=0 y EOF=si. Para zero y urandom completa el límite y no realiza una lectura adicional para buscar EOF.

O_NOFOLLOW rechaza un enlace simbólico en el componente final. La práctica usa las rutas conocidas de /dev de un entorno confiable; no pretende aislarse de cambios maliciosos en toda la jerarquía. El programa no acepta rutas arbitrarias ni abre dispositivos de bloque.

14. Laboratorio: metadatos, bytes y un alias opcional

En Bash de Linux crea un directorio nuevo y guarda los dos fuentes. La parte principal requiere compilador, Python 3 y acceso de lectura a los tres dispositivos conocidos, sin privilegios administrativos.

Compilar y observar objetos distintos
mkdir laboratorio45
cd laboratorio45
# Guardar aqui inspeccionar-nodo.c y leer-fuente.c.
cc -std=c11 -O2 -Wall -Wextra -Wpedantic inspeccionar-nodo.c -o inspeccionar-nodo
cc -std=c11 -O2 -Wall -Wextra -Wpedantic leer-fuente.c -o leer-fuente
printf 'archivo ordinario\n' > nota.txt
ln -s /dev/null enlace-null
mkfifo tuberia45
./inspeccionar-nodo lstat nota.txt . tuberia45 enlace-null /dev/null /dev/zero /dev/urandom
./inspeccionar-nodo stat enlace-null
./leer-fuente null 16
./leer-fuente zero 16
./leer-fuente urandom 16
printf 'descartar este mensaje\n' > /dev/null

Consultar el FIFO no lo abre y no espera a un escritor. lstat identifica enlace-null como enlace; stat muestra el destino de caracteres 1:3. El lector muestra 32 dígitos hexadecimales para las dos fuentes de 16 bytes y ninguno para null.

Identificar contenido sin convertir el archivo en dispositivo

Si están disponibles truncate, mkfs.ext4 y blkid, crea una imagen propia y sin montar. El programa de formato recibe únicamente el archivo recién creado del directorio de laboratorio.

Imagen regular con firma de sistema de archivos
if [[ -e imagen45.img || -L imagen45.img ]]; then
    printf 'La ruta ya existe; no se modifica\n' >&2
else
    truncate -s 32M imagen45.img &&
    mkfs.ext4 -q -F imagen45.img &&
    ./inspeccionar-nodo lstat imagen45.img &&
    blkid -p -o export imagen45.img
fi

TIPO=regular y TYPE=ext4 no se contradicen: el primero clasifica el nodo y el segundo el contenido reconocido. El UUID tampoco es el número mayor o menor del dispositivo. No se requiere montar la imagen ni configurar un loop para este sondeo.

Ampliación opcional: crear un segundo nombre para null

En un entorno de laboratorio que permita CAP_MKNOD y uso de nodos, este bloque crea un alias privado. El modo 600 deja el acceso al usuario que ejecuta mknod con sudo; las consultas de metadatos no requieren leer el contenido. Si la política del entorno rechaza la creación o apertura, registra ese resultado y conserva el resto de la práctica.

Nodo propio y limpieza de esa ruta exacta
(
set -euo pipefail
LAB45=$(mktemp -d "${TMPDIR:-/tmp}/nodos45.XXXXXX")
limpiar() {
    sudo rm -f -- "$LAB45/null-alias"
    rmdir -- "$LAB45"
}
trap limpiar EXIT
chmod 700 "$LAB45"
sudo mknod -m 600 "$LAB45/null-alias" c 1 3
./inspeccionar-nodo lstat "$LAB45/null-alias" /dev/null
sudo head -c 16 "$LAB45/null-alias" | wc -c
# Resultado esperado si la apertura esta permitida: 0 bytes.
)

El alias tiene su propio inodo y el st_dev del sistema de archivos temporal, pero representa el mismo tipo y st_rdev que null. Suprimirlo retira solamente ese nombre. No se modifica /dev/null ni se crean alias de discos para esta ampliación.

15. Verificador de tipos, identidades y lecturas

Desde laboratorio45 ejecuta este bloque. Crea archivos, enlaces y un FIFO temporales propios, compara la salida C con los metadatos que observa Python y comprueba las tres fuentes. No crea nodos especiales ni necesita ejecutar la ampliación con sudo.

Python 3 · contrastar metadatos, EOF y longitud binaria
python3 - <<'PY'
from pathlib import Path
from tempfile import TemporaryDirectory
import os
import stat
import subprocess

inspector = str(Path('inspeccionar-nodo').resolve())
lector = str(Path('leer-fuente').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

def campos(texto):
    return dict(linea.split('=', 1) for linea in texto.splitlines() if '=' in linea)

def comprobar(ruta, seguir, esperado):
    salida = ejecutar([inspector, 'stat' if seguir else 'lstat', str(ruta)])
    c = campos(salida)
    st = os.stat(ruta) if seguir else os.lstat(ruta)
    assert c['PATH_HEX'] == os.fsencode(ruta).hex().upper()
    assert c['TIPO'] == esperado
    assert int(c['FS_MAYOR']) == os.major(st.st_dev)
    assert int(c['FS_MENOR']) == os.minor(st.st_dev)
    assert int(c['INODO']) == st.st_ino
    if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
        assert int(c['RDEV_MAYOR']) == os.major(st.st_rdev)
        assert int(c['RDEV_MENOR']) == os.minor(st.st_rdev)
    else:
        assert c['RDEV_MAYOR'] == c['RDEV_MENOR'] == 'NA'

with TemporaryDirectory(prefix='tema45-') as d:
    base = Path(d)
    regular = base / 'nota\ncontrol.txt'
    regular.write_bytes(b'ABC')
    enlace = base / 'enlace'
    enlace.symlink_to('/dev/null')
    fifo = base / 'fifo'
    os.mkfifo(fifo, 0o600)
    comprobar(regular, False, 'regular')
    comprobar(base, False, 'directorio')
    comprobar(fifo, False, 'fifo')
    comprobar(enlace, False, 'enlace')
    comprobar(enlace, True, 'caracter')
    roto = base / 'roto'
    roto.symlink_to(base / 'no-existe')
    comprobar(roto, False, 'enlace')
    ejecutar([inspector, 'stat', str(roto)], 1)
    assert regular.read_bytes() == b'ABC'

for nombre, menor in [('null', 3), ('zero', 5), ('urandom', 9)]:
    ruta = Path('/dev') / nombre
    comprobar(ruta, False, 'caracter')
    st = ruta.stat()
    assert st.st_rdev == os.makedev(1, menor)
    for n in (1, 16, 256):
        r = campos(ejecutar([lector, nombre, str(n)]))
        assert int(r['SOLICITADOS']) == n
        b = bytes.fromhex(r['HEX'])
        assert int(r['LEIDOS']) == len(b)
        if nombre == 'null':
            assert b == b'' and r['EOF'] == 'si'
        else:
            assert len(b) == n and r['EOF'] == 'no'
            if nombre == 'zero': assert b == bytes(n)
for args in ([], ['otro', '16'], ['zero', '0'], ['zero', '257'],
             ['null', '-1'], ['urandom', '1.5'], ['zero', '']):
    ejecutar([lector, *args], 2)
ejecutar([inspector], 2)
ejecutar([inspector, 'otro', '/dev/null'], 2)
print('OK: tipos, st_dev, st_rdev, enlaces, EOF y lecturas acotadas')
PY

El verificador requiere los nodos estándar accesibles de Linux; si el entorno los oculta o restringe, la ejecución informa el fallo y no fabrica un éxito. La muestra urandom solo se revisa por longitud y codificación. Los archivos temporales se eliminan al cerrar el ensayo.

16. Simuladores de metadatos y lecturas acotadas

Consultar un nodo o seguir su enlace

Los ejemplos son datos locales de enseñanza. Los valores st_dev se han inventado para distinguir el sistema de archivos contenedor; no describen tu equipo. Las identidades Linux de null, zero y urandom sí corresponden a los ejemplos del tema.

¿Qué objeto describen los metadatos?

DatoValor del ejemploInterpretación
Conclusión sobre identidad y contenido

Terminar por límite y distinguir EOF de un byte cero

En este modelo cada lectura entrega como máximo el tamaño de tramo elegido. null devuelve EOF en la primera solicitud positiva. zero entrega ceros y urandom usa una muestra hexadecimal fija de demostración, que no proviene del kernel ni sirve como generador aleatorio. Se omiten errores y señales para observar el bucle.

Una fuente no es necesariamente un archivo finito

LlamadaSolicitadosRetornoAcumuladoInterpretación
Bytes y motivo de terminación

17. Diagnóstico y resumen

ObservaciónQué revisar
Una imagen ext4 se clasifica como regular.El tipo del nodo y la firma de su contenido son propiedades distintas.
Dos nodos de /dev tienen igual st_dev.Pueden residir en el mismo sistema de archivos y representar distintos st_rdev.
Un nodo tiene tamaño cero, pero entrega datos.No usar st_size de un dispositivo como longitud de un archivo regular.
La lectura muestra valores 00.Son bytes recibidos, no el retorno cero que señala EOF.
mknod funciona, pero open falla.Revisar permisos, nodev, políticas de dispositivos y controlador disponible.
lstat ve un enlace y stat ve caracteres.Se consultó el enlace final y luego su destino.
lsblk no muestra /dev/urandom.Es de caracteres; lsblk enumera dispositivos de bloque.

18. Preguntas de repaso y práctica

1. ¿Un archivo de /dev guarda necesariamente datos regulares?

No. Puede ser un nodo especial que dirige operaciones a un controlador o incluso un enlace a otro objeto.

2. ¿Qué letras identifican bloque y caracteres en ls -l?

b para bloque y c para caracteres.

3. ¿Un dispositivo de caracteres solo devuelve texto?

No. Puede devolver bytes binarios y muchos bytes por llamada.

4. ¿Un dispositivo loop es un archivo regular?

Su nodo es de bloque; puede usar un archivo regular como respaldo.

5. ¿Qué representa st_dev?

La identidad del sistema de archivos en el que reside el nodo consultado.

6. ¿Qué representa st_rdev?

El número de dispositivo representado por un nodo especial de bloque o caracteres.

7. ¿Basta con mayor y menor sin el tipo?

No. Debe conservarse también si el nodo es de bloque o de caracteres.

8. ¿Cómo se compone un dev_t en C?

Con makedev; major y minor extraen sus componentes.

9. ¿Dos alias de null necesitan compartir inodo?

No. Pueden ser nodos distintos que representen el mismo tipo y número 1:3.

10. ¿mknod instala un controlador?

No. Crea el nodo; disponer de un controlador y poder usarlo son condiciones adicionales.

11. ¿Por qué lsblk no enumera null?

Porque enumera dispositivos de bloque y null es de caracteres.

12. ¿blkid mostrando ext4 convierte una imagen en bloque?

No. Identifica el contenido; el nodo de la imagen sigue siendo regular.

13. ¿Qué devuelve una lectura positiva de null?

Retorno cero: EOF, sin bytes entregados.

14. ¿Qué significa recibir dieciséis bytes 00 de zero?

Que se recibieron dieciséis bytes de datos cuyo valor es cero. No es EOF.

15. ¿Por qué limitar una lectura de zero?

Porque es una fuente sin final natural y esperar EOF no define cuándo terminar.

16. ¿st_size=0 demuestra EOF en cualquier dispositivo?

No. El tamaño de metadatos no define de forma general los datos que entrega el controlador.

17. ¿Qué ventaja aporta getrandom con flags=0 al inicio?

Espera a que la fuente esté inicializada y no necesita abrir un nodo de /dev.

18. ¿Dos muestras iguales prueban que urandom falló?

No. La igualdad o diferencia de una muestra pequeña no constituye una prueba de calidad.

19. ¿Qué diferencia lstat de stat con un enlace?

lstat describe el enlace final; stat sigue el enlace y describe su destino.

20. ¿Qué comprueba fstat en el lector del tema?

Que el descriptor abierto corresponde a un dispositivo de caracteres con el número esperado antes de leer.

Para practicar

  1. Consulta los tres nodos estándar y separa st_dev de st_rdev en cada resultado.
  2. Identifica un dispositivo de bloque con lsblk y consulta únicamente sus metadatos con el inspector C.
  3. Crea dos enlaces a null en un directorio propio y compara lstat con stat.
  4. Explica por qué dos nodos pueden tener distinto inodo y representar el mismo dispositivo.
  5. Lee 1, 16 y 256 bytes de las tres fuentes y distingue longitud de valor de los bytes.
  6. En el simulador, divide 16 bytes en tramos de 6 e identifica la última solicitud de 4 bytes.
  7. Justifica por qué el lector no agrega una llamada para buscar EOF después de completar el límite.
  8. Compila los dos programas con las advertencias indicadas y ejecuta el verificador.
  9. Si dispones de las herramientas, contrasta TIPO=regular con TYPE=ext4 en la imagen propia.
  10. Si el laboratorio lo permite, crea el alias privado de null y compara su sistema contenedor con el de /dev/null.
  11. Describe qué políticas pueden impedir usar un nodo aunque mknod haya tenido éxito.
  12. Explica qué validaciones y unidades adicionales necesitaría una lectura de un dispositivo de bloque.

En el tema 46 estudiaremos lectura de dispositivos de bloque e ioctl: tamaño de sector, capacidad y estructuras de particionado sobre imágenes de laboratorio.