← Fundamentos de Sistemas Operativos Bloque 7 · Visión general de los servicios · Tema 29 de 34

Tema 29

Dispositivos: visión general

En el tema 8 miramos los dispositivos desde el hardware: registros, puertos, interrupciones. Ahora los miramos desde el otro extremo, el del programa que los usa. Veremos cómo convierte el kernel un circuito en un nombre dentro de /dev, qué es un driver por dentro, por qué una terminal es un dispositivo y qué ocurre en el sistema cuando se conecta una memoria USB.

1. El problema de la variedad

El tema 8 terminó con dos observaciones: los drivers son más de la mitad del código de un kernel, y cada modelo de controlador necesita el suyo. Hay miles. Si esa variedad llegara a los programas, cada aplicación tendría que saber con qué disco, qué teclado y qué tarjeta de red trabaja.

La tarea del sistema operativo en este terreno es impedirlo. Tiene que lograr dos cosas que tiran en direcciones opuestas:

Hacia arriba, a los programasHacia abajo, al hardware
Presentar pocas interfaces, simples y estables: todos los discos se usan igual.Hablar con cada controlador en su idioma particular.
Ocultar la lentitud: dormir al proceso que espera y despertarlo después.Atender las interrupciones cuando llegan, sin importar qué se estaba haciendo.
Proteger: decidir qué proceso puede usar cada dispositivo.Ser el único que toca el hardware.
Aceptar dispositivos que se conectan y desconectan en cualquier momento.Detectarlos, identificarlos y asignarles un driver.

En Unix, la respuesta a la primera columna la conocemos del tema 28: un dispositivo se presenta como un archivo. Se abre con open, se lee con read, se escribe con write. Este tema muestra qué hay debajo de esa idea. Mantenemos la convención del tema 8: controlador es el circuito, y driver, el programa del kernel que lo maneja.

2. Las capas de la entrada y salida

Entre la llamada read de un programa y el dispositivo hay varias capas de software. Solo la última conoce el hardware:

Capas de la entrada y salida: el programa en modo usuario; en modo kernel, el sistema de archivos virtual, luego los subsistemas de bloques y de caracteres, y debajo los drivers; por último el hardware, con sus controladores y dispositivos MODO USUARIO MODO KERNEL HARDWARE Programa: open · read · write · ioctl Sistema de archivos virtual: la misma interfaz para todo Archivos · caché · capa de bloques Terminales · entrada · sonido Driver de bloques Driver de caracteres Controlador y disco Controlador y teclado
Los drivers, resaltados, son la única capa que cambia de un equipo a otro. Todo lo que está por encima es igual en cualquier hardware.
CapaQué haceConoce el hardware
Sistema de archivos virtualRecibe la llamada y la dirige a quien corresponda, según el tipo de archivo.No
SubsistemasLa parte común a toda una clase de dispositivos: la caché y las colas para los discos; la edición de líneas para las terminales.No
DriversTraducen las operaciones genéricas a las órdenes concretas de un controlador, y atienden sus interrupciones.Sí

La capa intermedia es la que evita que cada driver repita el trabajo de los demás. Un driver de disco no implementa una caché ni decide en qué orden atender los pedidos: eso está escrito una vez, para todos. Solo sabe leer y escribir bloques en su controlador.

3. El directorio /dev

Los dispositivos tienen nombre en el árbol de directorios, y todos están en /dev:

terminal
$ ls -l /dev/nvme0n1* /dev/null /dev/tty /dev/pts/0
brw-rw---- 1 root  disk 259, 0 oct 12 09:14 /dev/nvme0n1
brw-rw---- 1 root  disk 259, 1 oct 12 09:14 /dev/nvme0n1p1
brw-rw---- 1 root  disk 259, 2 oct 12 09:14 /dev/nvme0n1p2
crw-rw-rw- 1 root  root   1, 3 oct 12 09:14 /dev/null
crw--w---- 1 diego tty  136, 0 oct 12 13:18 /dev/pts/0
crw-rw-rw- 1 root  tty    5, 0 oct 12 13:11 /dev/tty

Son los archivos de tipo b y c de la tabla del tema 28. Tienen dueño, grupo, permisos y fechas, como cualquier archivo, y donde debería figurar el tamaño aparecen dos números separados por una coma, que explicamos en la sección siguiente. No contienen datos: son puntos de acceso.

El directorio mismo tampoco está en el disco. Es un sistema de archivos en memoria, el devtmpfs que vimos en la salida de df, y se reconstruye en cada arranque. Sus entradas las crea el kernel a medida que los drivers detectan dispositivos; después, el servicio systemd-udevd del tema 22 ajusta sus permisos y añade nombres alternativos.

Los nombres siguen convenciones que permiten reconocer de qué se trata:

NombreDispositivo
nvme0n1, nvme0n1p2Un disco NVMe, y su segunda partición.
sda, sdb1Discos SATA y memorias USB: el primero, y la primera partición del segundo.
loop0Un archivo común tratado como si fuera un disco.
tty1Una consola de texto del equipo.
pts/0Una terminal de una ventana o de una conexión remota.
ttyS0, ttyUSB0Puertos serie.
input/event3Un dispositivo de entrada: teclado, ratón, panel táctil.
null, zero, urandomDispositivos sin hardware, de la sección 6.

4. Número mayor y número menor

Lo que vincula un archivo de /dev con un dispositivo no es su nombre. Son los dos números que ls -l muestra en el lugar del tamaño:

Número mayorNúmero menor
IdentificaEl driverUno de los dispositivos que maneja ese driver
Lo interpretaEl kernel, para saber a quién pasar el pedidoEl driver, a su manera
En /dev/nvme0n1p22592: la segunda partición
En /dev/null1: el driver de memoria3

Las tres entradas del disco comparten el número mayor, porque las atiende el mismo driver, y se distinguen por el menor. El kernel publica qué driver registró cada número mayor:

terminal (extracto)
$ cat /proc/devices
Character devices:
  1 mem
  4 tty
  5 /dev/tty
 13 input
136 pts
…
Block devices:
  7 loop
  8 sd
259 blkext

Hay dos listas, porque los dispositivos de caracteres y los de bloques se numeran por separado. Que el nombre es irrelevante se demuestra creando un archivo de dispositivo propio, con el comando mknod. Le damos un nombre cualquiera y los números de /dev/null:

terminal
$ sudo mknod agujero c 1 3          # de caracteres, mayor 1, menor 3
$ sudo chmod 666 agujero
$ ls -l agujero
crw-rw-rw- 1 root root 1, 3 oct 12 13:20 agujero
$ echo "esto desaparece" > agujero
$ cat agujero
$ rm -f agujero

El archivo agujero, creado en nuestro directorio, se comporta exactamente como /dev/null: acepta lo que se le escribe y no devuelve nada. Es el mismo dispositivo con otro nombre. Cuando un programa lo abre, el kernel lee en su inodo el tipo y los dos números, y con ellos llega al driver; la ruta y el nombre no intervienen.

Crear archivos de dispositivo requiere ser administrador, y se entiende por qué: quien pudiera crear un archivo con los números del disco y permisos amplios tendría acceso directo a su contenido.

5. Bloques y caracteres

En el tema 8 presentamos las dos clases según cómo se accede a los datos. Vistas desde el kernel, las diferencias son más amplias:

Dispositivo de bloquesDispositivo de caracteres
Los datos sonBloques numerados, de tamaño fijoUna corriente de bytes
AccesoA cualquier bloque, en cualquier ordenEn secuencia, a medida que llegan o salen
Cambiar de posición con lseekTiene sentidoPor lo general, no
Pasa por la caché de páginasSíNo
El kernel reordena y agrupa los pedidosSíNo
Uso habitualContener un sistema de archivosHablar directamente con un programa
EjemplosDiscos, particiones, memorias USBTerminales, teclado, ratón, sonido, puertos serie

La penúltima fila marca la diferencia de uso. Los programas casi nunca abren un dispositivo de bloques: abren archivos, y es el sistema de archivos quien lee y escribe los bloques por ellos. El acceso directo queda para las herramientas que trabajan por debajo de los archivos, como las de particionado y formato. Es lo que hizo mbr.c en el tema 20, al leer los primeros 512 bytes de /dev/nvme0n1.

Queda una tercera clase que no aparece en /dev: las interfaces de red. Una tarjeta de red no se abre ni se lee como un archivo, porque no entrega bytes sino paquetes dirigidos a distintos programas. Tiene nombre —enp3s0, wlan0—, se administra con el comando ip y se usa mediante sockets. Es la excepción más importante al lema de que todo es un archivo.

6. Dispositivos sin hardware

Varios de los archivos más usados de /dev no corresponden a ningún circuito. Son drivers que no manejan nada: su comportamiento está definido por completo en el código del kernel.

ArchivoAl leerAl escribirSe usa para
/dev/nullNo hay nada: fin de archivo inmediatoLo acepta y lo descartaDesechar una salida que no interesa
/dev/zeroBytes en cero, sin finLo descartaCrear archivos de un tamaño dado
/dev/fullBytes en ceroFalla con «disco lleno»Probar cómo reacciona un programa ante ese error
/dev/urandomBytes aleatorios, sin fin—Generar claves e identificadores
/dev/ttyLa terminal del proceso que lo abre, sea cual seaHablar con el usuario aunque la salida esté redirigida
terminal
$ head -c 16 /dev/urandom | xxd
00000000: 4e1c 9a03 d762 b580 2f6a e911 0c5d 73a8  N....b../j...]s.
$ head -c 1M /dev/zero | wc -c
1048576
$ wc -c < /dev/null
0

Los usamos varias veces: /dev/null en casi todos los temas, /dev/zero para crear archivos de prueba en los temas 14 y 20, y /dev/full en el tema 14, para provocar un error de escritura. El kernel usa además su generador de números aleatorios para el ASLR del tema 27.

Su interés aquí es que muestran, sin hardware que estorbe, qué es un driver. Este es el de /dev/null, tomado del código fuente de Linux con mínimas simplificaciones:

del código fuente de Linux, drivers/char/mem.c (simplificado)
static ssize_t read_null(struct file *archivo, char *bufer, size_t cantidad, loff_t *posicion)
{
    return 0;               /* cero bytes leídos: fin de archivo */
}

static ssize_t write_null(struct file *archivo, const char *bufer, size_t cantidad, loff_t *posicion)
{
    return cantidad;        /* «escribí todo», sin hacer nada */
}

static const struct file_operations null_fops = {
    .read  = read_null,
    .write = write_null,
};

Eso es todo el dispositivo. Dos funciones, y una estructura que las reúne. Cuando un programa hace write(fd, datos, 100) sobre /dev/null, el recorrido de la llamada al sistema termina en write_null, que devuelve 100. El programa recibe ese 100 como resultado y concluye que escribió cien bytes. Las funciones tienen la misma forma que las llamadas read y write del tema 14, y devuelven lo mismo que ellas.

7. Un driver, por dentro

La estructura file_operations del ejemplo es la pieza central. Es una tabla de punteros a funciones, una por cada operación que se puede hacer sobre un archivo:

CampoEl kernel lo llama cuando un programa…
.openabre el archivo de dispositivo.
.readllama a read.
.writellama a write.
.llseekllama a lseek.
.unlocked_ioctlllama a ioctl.
.mmappide mapear el dispositivo en su memoria.
.releasecierra el último descriptor que se refería a él.

Un driver de caracteres consiste, en esencia, en escribir esas funciones para un controlador concreto y registrar la tabla ante el kernel con un número mayor. El camino de una llamada queda así:

  1. El programa llama a open("/dev/algo"). El kernel recorre la ruta y encuentra un inodo de tipo dispositivo, con sus números mayor y menor. sistema de archivos virtual
  2. Busca qué driver registró ese número mayor y anota su tabla de operaciones en la entrada de archivo abierto. sistema de archivos virtual
  3. Llama a la función .open del driver, que prepara el dispositivo. driver
  4. El programa llama a read con el descriptor. El kernel toma la tabla anotada y llama a su función .read. driver
  5. La función del driver consulta el controlador. Si no hay datos, pone a dormir al proceso. driver
  6. Llega la interrupción. El manejador del driver recoge el dato y despierta al proceso, como en el tema 17. driver
  7. .read copia el dato al programa y devuelve la cantidad. vuelta a modo usuario

Un driver tiene, entonces, dos mitades que se ejecutan en momentos distintos. Una responde a los pedidos de los programas, a través de la tabla de operaciones. La otra responde al dispositivo, a través del manejador de interrupción. Entre ambas hay una cola de datos y un mecanismo para dormir y despertar procesos. Es la estructura que vimos en el recorrido de la tecla del tema 17, ahora con nombre.

Es también la tercera vez que encontramos el mismo recurso. La tabla de llamadas al sistema, la tabla de vectores de interrupción y esta tabla de operaciones resuelven el mismo problema: que una parte del sistema pueda invocar a otra sin conocerla, a través de un índice o de un conjunto de funciones acordado de antemano. Gracias a eso se puede añadir un driver, con los módulos del tema 24, sin tocar el resto del kernel.

8. La terminal

La terminal es el dispositivo de caracteres que más usamos, y su comportamiento explica varias cosas que observamos sin detenernos. El comando tty informa a qué archivo de dispositivo está conectada la terminal actual:

terminal
$ tty
/dev/pts/0
$ echo "hola, terminal" > /dev/pts/0
hola, terminal

Escribir en ese archivo es escribir en la pantalla. Es lo que hace cualquier programa cuando usa el descriptor 1, según vimos en la tabla de descriptores de la shell. Con dos ventanas abiertas, un echo dirigido desde una a /dev/pts/1 hace aparecer el texto en la otra.

Los nombres tienen historia. tty viene de teletype, las máquinas de escribir eléctricas que servían de terminal en los años sesenta. Las terminales actuales son ventanas, y el kernel las atiende con un par de dispositivos falsos llamado pseudoterminal: un extremo, el de /dev/pts, se comporta como una terminal de verdad ante los programas; el otro lo maneja el programa que dibuja la ventana, o el servicio de acceso remoto.

Entre el teclado y el programa, el kernel interpone una capa que procesa lo que se escribe, la disciplina de línea. Su configuración se consulta con stty:

terminal (salida abreviada)
$ stty -a
speed 38400 baud; rows 24; columns 80; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; start = ^Q; stop = ^S; susp = ^Z; …
… icanon … echo …
AjusteEfectoLo observamos en
icanonModo por líneas: el kernel retiene lo escrito, permite corregirlo con la tecla de borrar, y lo entrega al programa al pulsar Enter.Tema 14: read devolvía la línea completa.
echoEl kernel muestra en pantalla cada carácter que se escribe.—
intr = ^CAl recibir Ctrl+C, el kernel envía SIGINT al proceso en primer plano.Tema 26
susp = ^ZAl recibir Ctrl+Z, envía SIGTSTP.Tema 26
eof = ^DAl recibir Ctrl+D al comienzo de una línea, hace que read devuelva 0.Tema 14

La tabla resuelve varios misterios menores. El eco de lo que se escribe no lo hace el programa, sino el kernel. Ctrl+C no es una tecla que la shell detecte: es un carácter que la disciplina de línea convierte en señal. Y un programa que lee de la terminal no recibe nada hasta que se pulsa Enter, porque hasta entonces los caracteres están en un búfer del kernel, donde aún pueden corregirse.

Estos ajustes pueden cambiarse. Los programas que piden una contraseña desactivan el eco; los editores de texto desactivan el modo por líneas, para recibir cada tecla en el momento.

terminal
$ stty -echo; read secreto; stty echo     # lo que se escribe no se ve
$ echo "escribiste: $secreto"
escribiste: abracadabra

9. ioctl: lo que no cabe en leer y escribir

Cambiar el eco de una terminal no es leer ni escribir bytes. Tampoco lo es expulsar un disco, ajustar el volumen o consultar el tamaño de una ventana. Para todo lo que no encaja en las dos operaciones básicas existe una llamada al sistema de propósito general:

prototipo de ioctl
#include <sys/ioctl.h>

int ioctl(int fd, unsigned long operacion, ...);

Recibe un descriptor, un número que indica la operación y un argumento cuyo significado depende de ella. El kernel no interpreta nada: pasa los tres valores a la función .unlocked_ioctl del driver, y es el driver quien sabe qué hacer. Cada driver define sus propios números de operación. El siguiente programa pregunta al driver de la terminal cuántas filas y columnas tiene la ventana:

ventana.c
#include <stdio.h>
#include <sys/ioctl.h>
#include <unistd.h>

int main(void)
{
    struct winsize ventana;

    /* TIOCGWINSZ: «terminal, dame el tamaño de tu ventana» */
    if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ventana) == -1) {
        perror("ioctl");
        return 1;
    }

    printf("La terminal tiene %d filas y %d columnas\n", ventana.ws_row, ventana.ws_col);
    return 0;
}
terminal
$ gcc -Wall -Wextra -g ventana.c -o ventana
$ ./ventana
La terminal tiene 24 filas y 80 columnas
$ ./ventana > salida.txt
ioctl: Inappropriate ioctl for device

En la primera ejecución, el descriptor 1 es la terminal, y su driver responde. Así averiguan ls y top el ancho disponible para acomodar su salida. En la segunda, el descriptor 1 es un archivo regular: la operación no tiene sentido para él, y el kernel responde con el error ENOTTY.

Ese error tiene un uso inesperado. Es la forma en que un programa averigua si su salida es una terminal: intenta una operación propia de terminales y observa si falla. Es lo que hace la biblioteca de C, según vimos en el tema 15, para elegir entre el búfer por líneas y el búfer completo; y es la razón por la que ls usa colores y columnas en la pantalla y una lista simple cuando su salida va a una tubería.

ioctl es poco elegante: una sola llamada con miles de significados, sin comprobación de tipos. Es el precio de mantener pequeña la interfaz principal. Las alternativas modernas consisten en exponer los ajustes como archivos, que es lo que vemos a continuación.

10. /sys: el inventario

/dev contiene los puntos de acceso a los dispositivos: sirve para usarlos. Para saber qué dispositivos hay, cómo están conectados y cómo están configurados existe otro directorio, /sys, donde el kernel publica su inventario. Lo usamos en el tema 8 para leer los identificadores de un dispositivo PCI.

DirectorioOrganiza los dispositivos según
/sys/devicesSu conexión física: qué está enchufado a qué. Es el árbol real.
/sys/busEl bus al que pertenecen: pci, usb.
/sys/classSu función: net, input, sound, tty.
/sys/blockLos dispositivos de bloques.

Los tres últimos son vistas del primero, armadas con enlaces simbólicos. Cada dispositivo es un directorio, y cada uno de sus atributos, un archivo de una sola línea:

terminal
$ ls /sys/block
loop0  loop1  nvme0n1
$ cat /sys/block/nvme0n1/size              # tamaño, en sectores de 512 bytes
1000215216
$ cat /sys/block/nvme0n1/dev               # números mayor y menor
259:0
$ cat /sys/block/nvme0n1/queue/rotational  # ¿es un disco giratorio?
0
$ cat /sys/class/net/enp3s0/address        # la dirección física de la tarjeta de red
a8:5e:45:1c:9b:02

El tamaño coincide con el que informó fdisk en el tema 20. El archivo dev es el eslabón entre los dos directorios: contiene los números que llevará la entrada correspondiente de /dev. Y la última línea muestra que la tarjeta de red, que no tiene archivo en /dev, sí figura en el inventario.

Comandos como lsblk, lspci y lsusb obtienen de aquí lo que muestran. Algunos de estos archivos admiten escritura, y permiten cambiar un ajuste sin recurrir a ioctl. Recorremos /sys con más detalle en el tema 31.

11. Conectar un dispositivo en marcha

Casi todas las piezas de este tema intervienen cuando se enchufa una memoria USB. La secuencia completa une además varios temas anteriores:

  1. Se conecta la memoria. El controlador USB detecta el cambio eléctrico en el puerto y genera una interrupción. hardware
  2. El driver del controlador USB interroga al dispositivo y lee sus identificadores de fabricante, de producto y de clase. Es la enumeración del tema 8. kernel
  3. El kernel crea su directorio en /sys y emite un aviso con los datos del dispositivo. kernel
  4. systemd-udevd recibe el aviso y busca un módulo cuyo alias coincida con esos identificadores. Lo carga, como vimos en el tema 24. espacio de usuario
  5. El driver de almacenamiento USB toma el dispositivo. Lo registra como un disco, lee su tabla de particiones y crea los dispositivos de bloques correspondientes. kernel
  6. Aparecen /dev/sdb y /dev/sdb1. udev les asigna grupo y permisos, y crea enlaces con nombres estables. espacio de usuario
  7. El entorno de escritorio monta el sistema de archivos de la partición en un directorio, y muestra su icono. espacio de usuario

Los pasos del kernel quedan en su registro. Son las líneas que mostramos en el tema 11, sin poder explicarlas del todo:

terminal
$ sudo dmesg | tail -3
[ 8123.401222] usb 1-2: new high-speed USB device number 7 using xhci_hcd
[ 8123.553871] usb-storage 1-2:1.0: USB Mass Storage device detected
[ 8124.580113] sd 2:0:0:0: [sdb] Attached SCSI removable disk

Los «nombres estables» del paso 6 resuelven un problema práctico. El nombre sdb depende del orden de conexión: la misma memoria puede ser sdc mañana. Por eso udev mantiene directorios con enlaces que identifican a cada dispositivo por algo propio:

terminal
$ ls -l /dev/disk/by-uuid
lrwxrwxrwx 1 root root 15 oct 12 09:14 3f2a8c1e-7b4d-4e9a-b6c0-5d1f9e2a7c38 -> ../../nvme0n1p2
lrwxrwxrwx 1 root root 15 oct 12 09:14 A1B2-C3D4 -> ../../nvme0n1p1

El primer identificador es el de la partición raíz que aparecía en grub.cfg y en /proc/cmdline, en el tema 21. Este directorio es el lugar donde ese identificador se convierte en un dispositivo concreto.

La secuencia muestra además un reparto de tareas que ya es familiar. El kernel detecta, identifica y ofrece el mecanismo; la política —qué módulo cargar, qué permisos poner, dónde montar— la deciden programas de usuario. El comando udevadm monitor permite ver los avisos del paso 3 mientras se conecta y se desconecta un dispositivo.

12. La capa de bloques

En la figura de la sección 2, entre los sistemas de archivos y los drivers de disco hay una capa que merece un comentario propio. Los discos son, con mucha diferencia, los dispositivos más lentos de los que depende el rendimiento del sistema, y el kernel dedica un subsistema entero a usarlos lo menos y lo mejor posible. Hace tres cosas:

TécnicaEn qué consisteLa vimos en
CachéLos bloques leídos quedan en memoria; una segunda lectura no llega al disco.Tema 7
Escritura diferidaLo escrito queda en memoria y se envía al disco más tarde, agrupado.Temas 7 y 28
Planificación de pedidosLos pedidos pendientes se ordenan y se combinan antes de entregarlos al driver.—

La tercera es una forma de planificación, emparentada con la de la CPU: cuando varios procesos piden bloques a la vez, alguien decide en qué orden atenderlos. En los discos giratorios, el orden importaba muchísimo, porque mover el cabezal de un extremo a otro costaba milisegundos; con los discos de estado sólido importa menos, y a menudo el kernel no reordena nada.

terminal
$ cat /sys/block/nvme0n1/queue/scheduler
[none] mq-deadline

El valor entre corchetes es el planificador en uso. En este disco NVMe no hay ninguno: los pedidos pasan directamente al driver. La actividad se observa con vmstat, que usamos en el tema 19: sus columnas bi y bo son los bloques leídos y escritos por segundo, y wa, el porcentaje de tiempo en que los procesadores estuvieron ociosos esperando al disco.

Los dispositivos de caracteres no tienen nada parecido. Una tecla o un byte de un puerto serie se entregan en el orden y en el momento en que llegan.

13. Quién puede usar un dispositivo

Como los dispositivos son archivos, la protección no necesita un mecanismo aparte. Se aplican los permisos de siempre, y la primera salida de este tema ya los mostraba:

ArchivoPermisosDueño y grupoResultado
/dev/nullrw-rw-rw-root, rootCualquiera puede leerlo y escribirlo.
/dev/pts/0rw--w----diego, ttySolo el usuario de esa sesión lee lo que se escribe en su terminal.
/dev/nvme0n1rw-rw----root, diskSolo el administrador y el grupo disk.

El último caso es el más importante. Un usuario común no puede abrir el disco, y tiene que ser así: quien lee el dispositivo de bloques directamente obtiene los bytes de todos los archivos, sin que intervengan los permisos de ninguno. La protección de los archivos individuales descansa sobre la protección del dispositivo que los contiene. Por eso mbr.c necesitó sudo en el tema 20.

Para los dispositivos que un usuario sí debe poder usar, la solución habitual es el grupo. El sonido pertenece al grupo audio; los puertos serie, a dialout; las cámaras, a video. Añadir un usuario a uno de esos grupos le concede el acceso sin hacerlo administrador. En un equipo de escritorio, además, el servicio de sesiones concede esos permisos automáticamente a quien está sentado frente a la máquina.

Usuarios, grupos y permisos son el mecanismo de protección que recorre todo el sistema, y el contenido del tema 30.

14. Resumen

15. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué capa de la entrada y salida conoce los detalles del hardware?

Solo el driver. El sistema de archivos virtual y los subsistemas comunes, como la caché o la gestión de terminales, son iguales para cualquier hardware.

2. ¿Está /dev guardado en el disco?

No. Es un sistema de archivos en memoria. El kernel crea sus entradas a medida que los drivers detectan dispositivos, y se reconstruye en cada arranque.

3. ¿Qué identifican el número mayor y el número menor?

El mayor identifica el driver que atiende el dispositivo. El menor distingue, para ese driver, a cuál de sus dispositivos se refiere: por ejemplo, qué partición de un disco.

4. Se crea con mknod un archivo llamado pepe, de caracteres, con números 1 y 3. ¿Cómo se comporta?

Como /dev/null, que tiene esos mismos números. El kernel llega al driver por el tipo y los números guardados en el inodo; el nombre y la ubicación del archivo no cuentan.

5. Menciona tres diferencias entre un dispositivo de bloques y uno de caracteres.

El de bloques permite acceder a cualquier bloque en cualquier orden, y el de caracteres entrega los bytes en secuencia. El de bloques pasa por la caché de páginas y sus pedidos pueden reordenarse. Y el de bloques suele contener un sistema de archivos, mientras que el de caracteres se usa directamente.

6. ¿Por qué una tarjeta de red no aparece en /dev?

Porque no entrega una corriente de bytes que un programa pueda leer, sino paquetes destinados a distintos programas. Se usa mediante sockets y se administra con otras herramientas.

7. Un programa escribe 100 bytes en /dev/null. ¿Qué devuelve write, y por qué?

Devuelve 100. La función de escritura de ese driver no hace nada con los datos y devuelve la cantidad que se le pidió escribir.

8. ¿Qué es la estructura file_operations?

Una tabla de punteros a las funciones que implementan las operaciones sobre un archivo: abrir, leer, escribir, ioctl, cerrar. Cada driver registra la suya, y el kernel la usa para dirigir cada llamada al sistema a la función correspondiente.

9. ¿Cuáles son las dos mitades de un driver?

Las funciones de la tabla de operaciones, que se ejecutan cuando un programa hace una llamada al sistema, y el manejador de interrupción, que se ejecuta cuando el dispositivo avisa. Se comunican mediante una cola de datos y el mecanismo de dormir y despertar procesos.

10. ¿Quién muestra en pantalla los caracteres que se escriben en una terminal?

El kernel, en la capa llamada disciplina de línea, cuando el ajuste echo está activado. El programa que lee todavía no recibió nada.

11. ¿Cómo se convierte Ctrl+C en la terminación de un programa?

La disciplina de línea reconoce ese carácter como el de interrupción y, en lugar de entregarlo al programa, hace que el kernel envíe SIGINT al proceso que está en primer plano en esa terminal.

12. ¿Para qué existe ioctl?

Para las operaciones sobre un dispositivo que no consisten en leer ni escribir datos: consultar o cambiar su configuración, darle órdenes. El kernel pasa el pedido al driver, que define el significado de cada número de operación.

13. ¿Por qué ./ventana > salida.txt falla con «Inappropriate ioctl for device»?

Porque el descriptor 1 ya no es una terminal sino un archivo regular, que no admite la operación de consultar el tamaño de la ventana. El kernel devuelve ENOTTY.

14. ¿En qué se diferencian /dev y /sys?

/dev contiene los puntos de acceso para usar los dispositivos. /sys contiene información sobre ellos: cuáles hay, cómo están conectados y cómo están configurados.

15. ¿Por qué un usuario común no puede leer /dev/nvme0n1?

Porque leyendo el disco directamente obtendría el contenido de todos los archivos, salteando los permisos de cada uno. El archivo de dispositivo solo es accesible para el administrador y para un grupo reservado.

Para practicar

  1. Lista /dev con ls -l y cuenta cuántos archivos hay de cada tipo, con ls -l /dev | cut -c1 | sort | uniq -c.
  2. Averigua con /proc/devices qué drivers atienden a /dev/tty, a tu terminal y a tu disco.
  3. Crea con mknod un archivo propio que se comporte como /dev/zero, y otro como /dev/urandom. Consulta antes sus números con ls -l.
  4. Abre dos terminales, averigua el nombre de cada una con tty, y escribe en una desde la otra. Mira los permisos del archivo y deduce quién más podría hacerlo.
  5. Ejecuta ./leer, del tema 14, después de stty -icanon. ¿Cuántos bytes devuelve read ahora, y cuándo? Restablece el modo con stty icanon.
  6. Ejecuta ./ventana, cambia el tamaño de la ventana y vuelve a ejecutarlo. Prueba también ./ventana | cat.
  7. Modifica ventana.c para que, si ioctl falla con ENOTTY, escriba «la salida no es una terminal» en lugar del error.
  8. Recorre /sys/class/net y, para cada interfaz, muestra su dirección y su estado, que está en el archivo operstate.
  9. Ejecuta udevadm monitor y conecta una memoria USB. Luego busca en /dev/disk/by-id el nombre estable que recibió, y las líneas correspondientes en dmesg.
  10. Observa vmstat 1 mientras ejecutas cat sobre un archivo grande, dos veces seguidas. ¿Qué ocurre con la columna bi en la segunda lectura?