Para descargar el Sistema Operativo tOSh, podés hacerlo desde la sección Resources de este mismo sitio.
Partes
Esta serie de artículos tiene las siguientes partes:
- Parte I - Bootloader
- Parte II - Stage 1
- Parte III - Saltando a C
- Parte IV - Interrupciones
- Parte V - Sistema de Archivos y Driver de Disquetera <— Estás acá
- Parte VI - Booteando en diferentes tipos de máquinas
fuck wINdOzE gO tOSh!
Programar un Driver
¿Alguna vez te preguntaste que verga es un driver?
Es una verga, eso es lo que es. ;-( – Pero es una verga que tenemos que programar, sino, no funciona una verga nada. 3;-D !
Pasaron una banda de años y la vida detuvo el desarrollo de tOSh, el Sistema Operativo revolucionario que le va a cambiar la vida a no se quien, pero la mia, por seguro que lo hizo.
Barrenamos el terreno para poder empezar a hacer algo útil –¡después de años!– con nuestro sistema operativo. Y una de las cosas útiles que una computadora hace, es efectivamente, guardar archivos.
En tOSh por ahora ni tenemos disco rígido. Ni en el mismo disquette en el que booteamos podemos guardar un archivo porque primero, no estamos manipulando la disquetera ni mucho menos tenemos un sistema de archivos para guardar nada!
En x86, en algún momento pensé que controlar el floppy iba a ser mas fácil que armar un driver de disco rígido. Como tomé la decision de ir por el floppy porque ME GUSTAN LOS DISQUETTES no se bien por ahora si hubiera sido mejor manejar HDDs o la disquetera, pero hacer un driver funcionando para disquetera fue soprendentemente más difícil de lo que esperaba.
Mientras desarrollo tOSh generalmente uso qemu y bochs para probar que las cosas estén funcionando, incluyendo la disquetera. Pero cuando pruebo alguna versión del sistema operativo en hardware real como mi [Máquina de 1997] o algunas otras del hackerspace en el que soy miembro o amigos que tienen hardware realmente viejo las cosas se van complicando aún más.
Acompañenmé en esta aventura para programar lo más genéricamente posible, un driver para controladoras de disquetteras, FDC (Floppy Disk Controller) y un sistema de archivos para escribir datita sin parar, 70s style.
Driver de Disquetera
Arranquemos con unos #dEfInEz:
#define FDC_DOR 0x3F2 /* Digital Output Register */
#define FDC_MSR 0x3F4 /* Main Status Register */
#define FDC_FIFO 0x3F5 /* Data FIFO */
#define FDC_CCR 0x3F7 /* Configuration Control Register */
- FDC_DOR prende y apaga el motor, selecciona el drive activo y habilita IRQ/DMA.
- FDC_MSR es de solo lectura y te dice en qué estado está el controlador.
- FDC_FIFO es el buffer de 1 byte por donde entran los comandos y salen los resultados. Todo el protocolo del FDC pasa por este único puerto, un byte a la vez.
- FDC_CCR define la "velocidad" de transferencia.
Protocolo
El protocolo tiene tres partes o fases.
- command phase, mandás bytes.
- execution phase el controlador ejecuta cosas importantísimas
- result phase el controlador te devuelve status de la operación anterior.
El MSR te dice en qué parte estás mirando los bits RQM (Request For Master) y DIO (Data Input/Output):
static bool fdc_wait_write_ready(void)
{
uint32_t start = pit_get_ticks();
while ((pit_get_ticks() - start) < 100) {
uint8_t msr = inb(FDC_MSR);
if ((msr & 0xC0) == 0x80)
return true;
inb(0x80); /* I/O Delay for ISA hardware */
}
terminal_writestring("FLOPPY: FDC write timeout\n");
return false;
}
(msr & 0xC0) == 0x80 es RQM=1, DIO=0. El controlador espera un byte. Para leer un resultado, la condición cambia a (msr & 0xD0) == 0xD0, que agrega CB=1 (controller busy) a RQM=1, DIO=1: el FDC tiene un byte listo y procesando un comando (por eso el busy).
Todo esto lo hago con polling con timeout. Si el FDC no responde el Kernel va a colgarse para siempre. Cada fdc_write / floppy_read_byte tiene 100 ticks de margen (a 100Hz, un segundo), y si no lo logra, lo reporta y aborta.
El inb(0x80) lo necesitamos para –en hardware real– hacer un I/O delay en ISA. El bus necesita tiempo X entre polls.
*Tose*
Hablemos de emuladores.
¿Vieron que a veces algunas personas –fukken hermosas personas en el puto mundo– están obsesionadas con emular arquitecturas enteras con precisión de clock?. Bueno, qemu y bochs no son esos.
Ahora justo estamos hablando de x86, pero en otras arquitecturas o consolas, como Super Nintendo o no se, Playstation hay varias formas de encarar la emulación. Quizás con la SNES, como es una ROM no es tan claro pero con CDROM donde hay un motor involucrado (como con un floppy) las cosas cambian.
Si yo programo un controlador de cualquier verga que esta emulada con qemu, me estoy perdiendo todas las particularidades físicas que el motor y los RPM del CD o la lámina magnética del disquette tienen. En este caso, estamos hablando de una disquetera, donde hay un motor (!) spinneando y tardando algunos milisegundos en alcanzar una velocidad de revoluciones estable.
Cuando estuve programando este driver y el Sistema Operativo en general, en qemu andaba y cuando lo quise correr en hardware real, no andaba ni a palos.
Por esto es increiblemente importante seguir los datasheets de lo que sea que estés programando, y si tenés suerte, poder recabar en información que otras personas hayan investigado anteriormente, como por ejemplo Josh Cole de floppy.cafe que recopiló practicamente un 90% de todo lo que necesité para poder desarrollar el driver, en un solo lugar. Un capo.
En tiempos de Inteligencia Artificial, saber donde encontrar la documentación de la cual se alimentó, es quizás hasta mejor que pedirle ciegamente la implementación. Se los aseguro.
Reset e inicialización
La secuencia de arranque del FDC en floppy_init() sigue el orden del datasheet:
1. Unmask IRQ6 en el PIC
2. Configurar data rate (CCR = 0x00)
3. DOR = enable + irq, motor off
4. Reset: DOR = 0x00
5. Delay de I/O (4x escrituras a 0x80)
6. DOR = enable + irq (fin del reset)
7. Esperar IRQ6 (el reset del FDC genera una IRQ)
8. 4x SENSE INTERRUPT
9. SPECIFY
10. CONFIGURE
11. Motor on -> RECALIBRATE -> motor off
12. Lectura de prueba (sector 0, head 0, sector 1)
Lo único relevante es porque hacer 4 Sense Interrupt después del reset. Esto es porque dejé el controlador en modo de "polling de drives": cuando el FDC hace un reset con ese modo activo, genera un status pendiente por cada uno de los 4 drives que puede llegar a tener (A, B, C ó D floppies), aunque solo tengas uno conectado. Si no drenás esos 4 resultados con floppy_sense_interrupt(), el controlador queda con porquería en la cola y tenés que agotar los datos de la misma.
Estuve bastante tiempo tratando de entender por qué se me colgaba el drive, y una de las razones era por esto.
Specify
static bool floppy_specify(void)
{
if (!fdc_write(FDC_CMD_SPECIFY))
return false;
if (!fdc_write(0x8f)) /* SRT=8, HUT=15 */
return false;
if (!fdc_write(0x1e)) /* HLT=15, NDMA=0 */
return false;
return true;
}
SPECIFY no tiene result phase ni genera IRQ, así que una vez mandados los 3 bytes, listo. Los valores:
-
Byte 1 (
0x8f): nibble alto = SRT (Step Rate Time, cuánto tarda cada paso del cabezal, en unidades inversas – valores más altos = pasos más rápidos), nibble bajo = HUT (Head Unload Time, cuánto espera antes de "soltar" el cabezal tras una operación). -
Byte 2 (
0x1e): bits altos = HLT (Head Load Time, cuánto tarda en asentar el cabezal antes de leer/escribir), bit bajo = NDMA.NDMA=0es el que importa: le dice al FDC que vamos a usar DMA para la transferencia de datos, no PIO.
Uso transferencias DMA porque (creo que esto está comentado en floppy.h) porque bochs no soporta PIO transfers, solo DMA.
Si te olvidás este bit, el FDC espera que vos leas cada byte manualmente del FIFO y todo el pipeline de DMA que armamos más abajo no sirve para nada.
Como el reset limpia esta configuración, hay que volver a mandarla en cada floppy_init().
Configure
static bool floppy_configure(void)
{
if (!fdc_write(0x13)) /* CONFIGURE */
return false;
if (!fdc_write(0x00)) /* reserved byte */
return false;
if (!fdc_write(0x57)) /* EIS=1, EFIFO=0, POLL=1, FIFOTHR=7 */
return false;
if (!fdc_write(0x00)) /* PRETRK */
return false;
return true;
}
0x57 activa tres cosas: EIS (Enable Implied Seek, el FDC hace el seek solo antes de un read/write sin que se lo pidas explícitamente), EFIFO=0 (que, contraintuitivamente, activa el FIFO interno de 16 bytes del controlador en vez de desactivarlo – el nombre del bit es al revés de lo que uno esperaría) y FIFOTHR=7, el umbral de 8 bytes que dispara un acceso a DMA/memoria. Esto le da margen al sistema para no perder bytes si hay un poco de latencia en el bus.
CONFIGURE, como SPECIFY, tampoco genera IRQ ni result phase.
Motor y timing
static void floppy_motor_on(void)
{
outb(FDC_DOR, FDC_DOR_ENABLE | FDC_DOR_IRQ | FDC_DOR_MOTOR_A | FDC_DRIVE_A);
pit_wait_ms(500);
}
500ms es más de lo que documenta osdev (que sugiere 300ms para 3.5" y hasta dice que 50ms alcanza). En mi setup real, 50ms no era suficiente y tenía fallos intermitentes de seek. Preferí ir a lo seguro con medio segundo en vez de perseguir el mínimo teórico – el costo de esperar de más es insignificante comparado con el de tener una lectura fallida en medio del boot.
Para esto necesito un timer que no dependa de contar ciclos de CPU a mano, así que entra pit.c.
El PIT (Programmable Interval Timer)
void pit_init(uint32_t frequency)
{
pit_hz = frequency;
uint32_t divisor = PIT_FREQUENCY / frequency;
outb(PIT_COMMAND, 0x36); /* Ch0, lo/hi, mode 3, binario */
outb(PIT_CH0, divisor & 0xFF);
outb(PIT_CH0, (divisor >> 8) & 0xFF);
}
El PIT corre a 1193182 Hz internamente (PIT_FREQUENCY). Con pit_init(100) configuro un divisor que dispara una IRQ0 cada 10ms, o sea 100 ticks por segundo. pit_ticks es un contador global incrementado desde pit_irq_handler(), y todo lo demás en el sistema (pit_wait_ms, los timeouts del FDC, floppy_wait_irq) se apoya en ese contador en vez de hacer busy-waiting a ciegas.
static bool floppy_wait_irq(void)
{
uint32_t start = pit_get_ticks();
while (!floppy_irq) {
if ((pit_get_ticks() - start) >= 200) {
terminal_writestring("FLOPPY: IRQ timeout\n");
return false;
}
__asm__ volatile ("hlt");
}
return true;
}
200 ticks a 100Hz son 2 segundos de timeout para que llegue la IRQ6 del FDC – generoso a propósito, porque no quiero que un floppy real (que puede tardar en spin-up y seek) dispare un falso timeout.
Noten el hlt en el loop: en vez de spin-waiting quemando CPU, el procesador se detiene hasta la próxima interrupción (sea la del PIT o la del FDC), que es lo que corresponde hacer en un kernel real cuando estás esperando I/O.
Sense Interrupt, Recalibrate y Seek
RECALIBRATE y SEEK generan una IRQ pero no tienen result phase propia. El status hay que pedirlo aparte con SENSE INTERRUPT:
static bool floppy_sense_interrupt(uint8_t *st0, uint8_t *cyl)
{
inb(0x80);
inb(0x80);
if (!fdc_write(FDC_CMD_SENSE_INT))
return false;
if (!floppy_read_byte(st0))
return false;
if (!floppy_read_byte(cyl))
return false;
return true;
}
RECALIBRATE manda el cabezal a la pista 0 (útil para "resincronizar" la posición física conocida) y SEEK lo mueve a un cilindro específico. En ambos casos, después de la IRQ, hago SENSE INTERRUPT y valido dos cosas: el bit Seek End (st0 & 0x20) y que el cilindro reportado (cyl) coincida con el esperado (0 para recalibrate, el cilindro pedido para seek). Si algo sale mal, lo escupo a consola.
Lectura y escritura de sectores
El comando de lectura/escritura manda 9 bytes en total:
if (!fdc_write(FDC_CMD_READ_DATA | 0xC0)) { [...] } /* MT=1, MFM=1 */
if (!fdc_write((head << 2) | FDC_DRIVE_A)) { [...] } /* HD + DR */
if (!fdc_write(cylinder)) { [...] } /* C */
if (!fdc_write(head)) { [...] } /* H */
if (!fdc_write(sector)) { [...] } /* R */
if (!fdc_write(2)) { [...] } /* N = 512 bytes */
if (!fdc_write(18)) { [...] } /* EOT: último sector de la pista */
if (!fdc_write(0x1B)) { [...] } /* GPL */
if (!fdc_write(0xFF)) { [...] } /* DTL, ignorado cuando N != 0 */
0xC0 sobre el comando activa MT (Multi-Track, el controlador puede seguir a la próxima cabeza automáticamente si cruza el EOT) y MFM (modulación estándar para floppies y storage magnético de doble densidad para arriba). N=2 le dice al FDC "cada sector son 512 bytes" usando la tabla estándar de tamaños del chip (0=128B, 1=256B, 2=512B…). EOT=18 es el número de sectores por pista en la geometría 1.44MB.
Después de la IRQ (que llega cuando termina la transferencia por DMA, no antes), viene el result phase de 7 bytes (ST0 a ST2 más C/H/R/N de vuelta), de los cuales solo chequeo los primeros tres:
if (st[0] & 0xC0) { [...] } /* error phase */
if (st[1] != 0) { [...] } /* ST1: errores de datos, CRC, etc. */
if (st[2] != 0) { [...] } /* ST2: errores específicos de la pista/sector */
Cualquier bit prendido en ST1/ST2 lo trato como fallo total de la operación – no intento distinguir "es un CRC error recuperable" de "el sector no existe", simplemente lo reintento entero.
floppy_write_sector_impl es casi un espejo de la lectura, cambiando el comando (FDC_CMD_WRITE_DATA | 0xC0, MFM sin necesidad de MT… aunque en el código dejé el mismo 0xC0) y el sentido del DMA.
CHS y la conversión desde LBA
Para no tener que pensar en cilindro/cabeza/sector en el resto del kernel, expongo floppy_read_lba / floppy_write_lba:
uint32_t cylinder = lba / (2 * 18);
uint32_t tmp = lba % (2 * 18);
uint32_t head = tmp / 18;
uint32_t sector = (tmp % 18) + 1;
Esto asume la geometría fija de un floppy 1.44MB: 2 cabezas, 18 sectores por pista, 80 cilindros (2×18×80 = 2880 sectores totales, de ahí el if (lba >= 2880) return false;). No hay lectura de geometría desde el media descriptor ni soporte para otros formatos – para tOSh, un solo tipo de floppy alcanza y sobra, y hardcodear la geometría ahorra una capa entera de abstracción que no necesito.
Retry logic
bool floppy_read_sector(uint8_t cylinder, uint8_t head, uint8_t sector, uint8_t *buffer)
{
for (int attempt = 0; attempt < 3; attempt++) {
if (floppy_read_sector_impl(cylinder, head, sector, buffer))
return true;
terminal_writestring("FLOPPY: retrying read...\n");
floppy_motor_on();
floppy_recalibrate();
floppy_motor_off();
}
return false;
}
Los floppies reales son propensos a errores transitorios: polvo, desgaste del medio, un cabezal que no asentó bien en el primer intento, poner UN IMAN DE HELADERA COMO PISAPAPELES SOBRE UNA PILA DE FLOPPIES, etc…
En vez de fallar de una ante el primer error, reintento 3 veces, y entre cada intento hago un RECALIBRATE completo para forzar al cabezal a resincronizar su posición física – si el error fue justamente por una posición de cabezal corrida, el recalibrate lo arregla antes del siguiente intento.
DMA, Chip 8237
El FDC no te entrega los datos leídos byte a byte por el FIFO cuando NDMA=0 (ver Specify más arriba) – usa el chip DMA 8237 para escribir directamente a memoria mientras la CPU hace otra cosa (o, en nuestro caso, espera en un hlt).
static void dma_transfer(uint8_t channel, uint32_t address, uint16_t count, uint8_t mode)
{
uint8_t page = (address >> 16) & 0xff;
uint16_t offset = address & 0xffff;
outb(DMA_MASK, 0x04 | channel); /* deshabilita el canal */
outb(DMA_CLEAR_FF, 0); /* resetea el flip-flop de byte lo/hi */
outb(addr_port, offset & 0xff);
outb(addr_port, (offset >> 8) & 0xff);
outb(page_port, page);
outb(DMA_CLEAR_FF, 0);
count--; /* el 8237 cuenta N-1 */
outb(count_port, count & 0xff);
outb(count_port, (count >> 8) & 0xff);
outb(DMA_MODE, mode | channel);
outb(DMA_MASK, channel); /* habilita el canal */
}
Algunos puntos que vale la pena remarcar acá:
- El flip-flop es un bit interno del 8237 que indica si el próximo byte escrito a un puerto de dirección/cuenta es el byte bajo o el alto. Como es estado compartido entre canales, hay que resetearlo explícitamente (
DMA_CLEAR_FF) antes de escribir tanto la dirección como el conteo – si no lo hacés, podés terminar escribiendo el byte alto donde iba el bajo y armar una dirección que sea cualquier cosa. - El page register (
DMA_PAGE_CH2 = 0x81) es un resabio de que el 8237 original solo maneja direcciones de 16 bits para offset. Para llegar a memoria de 24 bits (como necesita un PC real), el byte alto de la dirección se escribe aparte, en el page register del canal correspondiente. Esto es también la razón de la restricción de que el buffer no puede cruzar un límite de 64KiB: el 8237 incrementa el offset de 16 bits en cada byte transferido, pero nunca toca el page register durante la transferencia. Si el buffer arranca cerca del final de una página de 64KB y se pasa, el DMA no avanza de página – le da la vuelta al offset y empieza a pisar el principio de la misma página de 64KB. Por eso el buffer del floppy lo escribí así:
static uint8_t floppy_dma_buffer[512]
__attribute__((aligned(512), section(".dma")));
Alinearlo a 512 bytes (el tamaño de un sector) garantiza que nunca vas a tener un buffer de 512 bytes que empiece, por ejemplo, en la dirección 0xFFF00 y termine cruzando a 0x10000 en el próximo bloque de 64KB. La sección .dma del linker script (ver Parte III) mete este buffer en su propia región alineada a página de 4KB, lejos de cualquier otro dato, así que en la práctica el boundary-crossing nunca ocurre, evitando que el driver haga ningún chequeo en runtime.
-
El modo (
0x46para lectura,0x4Apara escritura) sigue la nomenclatura del 8237 mirada desde la óptica del periférico, no de la memoria:dma_read()es "el FDC lee del disco y escribe a memoria" (lo que enfloppy_read_sector_implefectivamente llena tu buffer), mientras quedma_write()es "la memoria se lee y se escribe al FDC" (para mandar datos al disco). Es un nombre fácil de confundir si pensás en términos de CPU en vez de en términos del periférico – acádma_read/dma_writeestán nombrados desde la perspectiva de "¿qué hace la FDC con el dato?", que es consistente con cómo Intel documenta el chip. -
count--porque el 8237 espera el conteo comoN-1(transferíscountbytes, pero el registro interno cuenta decount-1hasta 0).
Masomenos nos queda que floppy_read_sector con dma_read() arma el canal 2 apuntando al buffer y por 512 bytes, el comando READ DATA se manda al FDC, la transferencia ocurre en el fondo mientras la CPU espera con hlt, y cuando termina, el FDC genera la IRQ6 que atiende floppy_wait_irq.
Es un montón todo esto y quizás algunas cosas se pueden ver mejor leyendo el código fuente. Como pongo al principio de toda esta serie de artículos, si querés, podes bajarte el source desde la pagina de Resources.
El Sistema de Archivos
Probando el Filesystem en mi máquina del 97’
Hagámoslon andá'
Para tOSh no hay bloques, no hay FAT, no hay árbol de directorios, no hay journaling, no hay nombres largos ni paths.
A tOSh no le importan todas esas mierdas, ni siquiera la consistencia de tus datos. tOSh es un Sistema Operativo SOLO PARA LOCOS Y VALIENTES.
Dicho esto y hecha la aclaración, empiezo a contarles como lo armé. Hay un directorio de tamaño fijo con hasta 256 archivos, cada uno con un nombre, un sector de inicio, y un tamaño. Punto.
La razón detrás de esto es la misma que con la geometría fija del floppy: cualquier feature de un filesystem real (FAT, ext2, lo que sea) agrega una capa de indirección – bloques, listas enlazadas de clusters, bitmaps de espacio libre – que resuelve problemas que tOSh todavía no tiene, como fragmentación real de un disco que se llena y vacía muchas veces. Para lo que necesito (persistir un puñado de archivos chicos en un floppy de 1.44MB durante el desarrollo del kernel), un directorio plano alcanza.
Layout de disco
#define FS_SUPERBLOCK_SECTOR 256
#define FS_DIRECTORY_START 257
#define FS_DIRECTORY_SECTORS 32
#define FS_DATA_START 320
+--------------------------+
| 0 BOOT |
+--------------------------+
| 1-8 STAGE1 |
+--------------------------+
| 9-136 KERNEL | <- 64 KiB
+--------------------------+
| 137-255 RESERVED |
+--------------------------+
| 256 SUPERBLOCK |
| 257-288 DIRECTORY |
| 289-319 RESERVED |
+--------------------------+
| 320-2879 FILE DATA | <- ~1.28 MB
+--------------------------+
El filesystem arranca en el sector 256, bien después del kernel (que termina en el 136). El gap que hago entre 137 y 255 (119 sectores, ~59KB) es margen para que el kernel vaya creciendo mientras lo voy desarrollando y que no choque contra el filesystem – recordá que el Makefile/build.sh de la Parte III valida que el kernel no supere los 128 sectores asignados, así que ese rango reservado es el colchón real antes de tener que mover todo el layout.
El gap entre 289 y 319 (31 sectores) es lo mismo pero para el directorio: FS_DIRECTORY_SECTORS está fijado en 32, pero el tamaño real que ocupa struct fs_file[FS_MAX_FILES] puede terminar usando menos. Este margen me evita recalcular FS_DATA_START cada vez que cambio FS_MAX_FILES o el tamaño de fs_file.
El superblock
struct fs_superblock {
uint32_t magic;
uint16_t version;
uint16_t sector_size;
uint32_t total_sectors;
uint32_t data_start;
} __attribute__((packed));
FS_MAGIC es 0x48534f54, que leído como bytes ASCII little-endian da TOSH. fs_is_initialized() lee el sector 256 y compara ese magic – si no matchea, asume que el floppy nunca fue formateado con este filesystem y listo, no intenta "recuperar" ni interpretar nada más de ese sector. __attribute__((packed)) es necesario para que el struct ocupe exactamente los bytes que declaro, sin padding que el compilador metería por alineación.
El directorio
struct fs_file {
char name[FS_FILENAME_MAX];
uint32_t start_sector;
uint32_t size;
uint8_t used;
} __attribute__((packed));
static struct fs_file directory[FS_MAX_FILES];
Todo el directorio es un array de 256 entradas que vive en RAM durante la ejecución del kernel y se persiste entero al disco con fs_save_directory() cada vez que cambia algo (create, write, delete). No hay bitmap de "entradas libres" separado – una entrada está libre si used == 0, y buscar un archivo (fs_find) es simplemente recorrer el array comparando nombres.
Esto significa que cada operación que modifica el filesystem reescribe los 32 sectores completos del directorio, incluso si solo cambió una entrada. Es claramente no-óptimo en términos de I/O, pero contra un floppy que ya de por sí es lentísimo, la diferencia entre escribir 1 sector o 32 no cambia la experiencia de uso, y me ahorro tener que trackear qué sector específico del directorio corresponde a qué entrada.
No Manejando el Espacio Libre
static uint32_t fs_next_free_sector(void)
{
uint32_t sector = FS_DATA_START;
for (int i = 0; i < FS_MAX_FILES; i++) {
if (!directory[i].used)
continue;
uint32_t sectors = (directory[i].size + 511) / 512;
uint32_t end = directory[i].start_sector + sectors;
if (end > sector)
sector = end;
}
return sector;
}
La asignación de espacio es lo más primitivo que se puede hacer: recorro todos los archivos usados, encuentro el start_sector + sectores_ocupados más alto, y el próximo archivo arranca ahí. No hay bitmap de sectores libres, no hay free-list, no hay compresión.
La consecuencia directa de esto –y una decisión consciente, NO ES UN BUG, ES UN FEATURE– es que fs_delete() no libera ni recicla el espacio.
Borrar un archivo solo limpia su entrada de directorio (used = 0); los sectores que ocupaba quedan ahí, ignorados para siempre por fs_next_free_sector(), porque esa función no mira entradas used == 0, solo mira el punto más lejano ocupado por archivos actualmente vivos… pero ese punto más lejano puede haber sido dejado por un archivo que ya no existe. En otras palabras: crear y borrar archivos deja huecos que jamás se vuelven a usar hasta que reformateás el disco entero con fs_format(). A esto se le llamaba fragmentacion de disco y es lo que el "Defragmentador de Windows" solucionaba. Un poco también le pasaba, por otros motivos, a MS-DOS y Windows 9x.
Para el estado actual de tOSh esto es aceptable: el filesystem no está pensado (todavía, quien sabe) para un uso de escritura/borrado intensivo y prolongado, sino para persistir un puñado de archivos de configuración o binarios del sistema. El día que esto sea un problema real (kkjjj osea nunca, aguante tOSh), la solución más simple sin rehacer el diseño sería agregar un bitmap de sectores libres, o migrar a un esquema de bloques con free-list – pero eso es haría cambiar completamente el filesystem.
Operaciones
fs_create, fs_write, fs_read y fs_delete son bastante directas dado el diseño que expliqué arriba:
-
fs_createbusca una entrada libre, le asigna el próximo sector víafs_next_free_sector(), guarda el nombre, y persiste el directorio. El tamaño arranca en 0 – todavía no hay datos, solo la reserva de posición. -
fs_writeparte los datos en sectores de 512 bytes (rellenando el último con ceros si no es múltiplo exacto) y los escribe secuencialmente desdestart_sector. Actualizasizey vuelve a persistir el directorio entero. -
fs_readhace lo inverso: calcula cuántos sectores ocupa el archivo a partir desize, los lee todos, y copia solo los bytes válidos (no el padding de ceros) al buffer del caller. -
fs_deletelimpia la entrada, sin tocar los sectores de datos (ver justito arriba).
Ninguna de estas cuatro funciones soporta archivos que crezcan más allá del hueco libre que tenían al momento de crearse – no hay concepto de "extender" un archivo más allá de lo que había disponible cuando se escribió por primera vez de forma contigua, porque fs_write siempre escribe a partir de start_sector, asumiendo que el espacio ya fue reservado por fs_create + el orden de creación de los demás archivos.
Limitaciones conocidas
Para que me quede documentado y no se pierda con el tiempo cuando en 3 años vuelva a implementarle cosas a tOSh:
- Tamaño máximo de filesystem fijo a
FS_TOTAL_SECTORS = 2880(los 1.44MB completos del floppy, sin distinguir que una parte ya está usada por boot/kernel – la validación de que no te pisés con eso es responsabilidad de dejar bien puestoFS_DATA_START, no algo que el filesystem chequee en runtime). Igual, quizás estaria bueno si vuelvo a tocar el sistema de archivos o reescribirlos, también implementar soporte para discos rígidos, y tener eso en cuenta para hacerlo un poco más serio. Veremos. - Nombres de archivo sin jerarquía:
FS_FILENAME_MAX = 32bytes, sin separador de path, todo vive en el mismo namespace jarcodeado. - Sin fragmentación de archivos: cada archivo ocupa un rango contiguo de sectores. Esto simplifica muchísimo el código (no hay que trackear cadenas de bloques) pero significa que archivos grandes en un filesystem con muchos huecos por deletes previos eventualmente no van a entrar, aunque haya "espacio total" de sobra repartido en gaps. Eventualmente, el sistema de archivos creando y borrando archivos suficientes veces, va a crashear :)
- Sin journaling ni ningún mecanismo de recuperación ante un corte de luz a mitad de un
fs_save_directory(). Si la escritura del directorio se corta en el medio, quedás con una versión parcial y hasta inclusive te quedás sin sistema de archivos porque el superbloque se puede corromper también.
Para la próxima, corremos tOSh en varias máquinas, emuladas y reales, prestadas y/o donadas para tal fin por queridos amigos y compañeros nerds que respeto y amo fuerte.