Extensiones de Ext2, Ext3 y Ext4
En el post anterior describía con más o menos detalle el sistema de archivos Ext2, y comentaba que la únicas diferencias entre Ext2, Ext3 y Ext4 residen en las extensiones, o features, disponibles. Puedes ver qué features utiliza un sistema de archivos con el comando dumpe2fs, y puedes activar y desactivar algunas de ellas con tune2fs; otras solamente se pueden especificar cuando se crea el sistema de archivos, con mke2fs (los comandos mkfs.ext2, mkfs.ext3 y mkfs.ext4 son alias del anterior que asumen ciertas opciones por defecto).
En este post describiré algunas de las features más utilizadas.
filetype
Esta es una de las features más antiguas, ya presente en Ext2. La idea es ampliar la estructura de las entradas de directorio para incluyan un campo indicando el tipo de archivo al que se refiere.
En los sistemas Unix tradicionales las entradas de directorio solamente contienen el nombre y el número de inodo, tal y como se refleja en la llamada al sistema readdir y la estructura dirent. Si se necesita más información sobre el archivo se debe leer el inodo en sí (con la llamada al sistema stat, por ejemplo). El caso es que en muchos casos se lee el inodo solamente para descubrir que el inodo no es del tipo buscado (archivo ordinario, directorio, link simbólico, etc.), por lo que es una optimización razonable guardar el tipo de archivo en la propia entrada de directorio.
En Ext2, la estructura de una entrada de directorio es:
struct DirEntry { uint32_t inode; uint16_t entry_len; uint16_t name_len; char name[]; };
Pero resulta que, debido a otras limitaciones del sistema, la longitud del nombre no puede ser mayor de 255 bytes, por lo que el byte alto de name_len es siempre cero. Esta feature redefine la estructura así:
struct DirEntry { uint32_t inode; uint16_t entry_len; uint8_t name_len; uint8_t type; char name[]; };
Y los posibles valores de type son:
const uint8_t TYPE_UNKNOWN = 0; const uint8_t TYPE_NORMAL = 1; const uint8_t TYPE_DIR = 2; const uint8_t TYPE_CHAR = 3; const uint8_t TYPE_BLOCK = 4; const uint8_t TYPE_FIFO = 5; const uint8_t TYPE_SOCK = 6; const uint8_t TYPE_LINK = 7;
Nótese que el valor 0 indica un tipo de archivo desconocido, con lo que se garantiza la compatibilidad binaria hacia atrás. Si una aplicación (o driver) se encuentra este valor en este campo debe leer el inodo para descubrir de qué tipo de archivo se trata.
En la API del sistema, este valor se encuentra en el campo d_type de la estructura dirent de glibc (aunque las constantes son distintas).
Debe considerarse siempre un error de programación grave asumir que este campo no pueda ser unknown.
Esta feature es incompatible, es decir, no es compatible hacia adelante. La razón es que si un driver que no sepa de esta extensión se encuentra un valor distinto de 0 en este campo lo interpretaría como un nombre de archivo gigante.
dir_index
Los directorios se guardan como una lista de estructuras DirEntry, descritas más arriba. Los sucesivos valores de entry_len construyen en la práctica una lista enlazada simple. Para borrar una entrada, por ejemplo, simplemente se incrementa el valor de entry_len de la entrada anterior. Y cuando se quiere añadir una entrada se busca un hueco en el que quepa, o si no hay ninguno, se inserta al final del directorio.
¿Y para borrar la primera entrada del directorio? Todos los directorios contienen las entradas . y .. que ¡no pueden borrarse!
Esta solución está bien para enumerar el contenido de un directorio, pero es tremendamente ineficiente para buscar un nombre particular en directorios muy grandes (miles o cientos de miles de entradas). Esta extensión (disponible desde Ext3) crea una tabla hash en cada directorio para acelerar la búsqueda de nombres. Hay que tener en cuenta que el índice no se mantiene necesariamente completo o sincronizado, pero el driver conoce su estado. Es decir, es posible que el índice no sea concluyente sobre si el nombre existe o no en el directorio, en cuyo caso habría que recorrer el directorio linealmente.
La descripción del índice se guarda al principio del directorio, camuflado en las entradas . y ..:
struct DirIndex { //DirEntry '.' uint32_t inode_dot; //el inodo del directorio uint16_t entry_len_dot; //12 uint8_t name_len_dot; //1 uint8_t type_dot; //TYPE_DIR si se usa "filetype" char dot[4]; //"." //DirEntry '..' uint32_t inode_dot2; //el inodo del padre uint16_t entry_len_dot2;//12 + tamaño del índice uint8_t name_len_dot2; //2 uint8_t type_dot; //TYPE_DIR si se usa "filetype" char dot2[4]; //".." //Índice //... //DirEntries reales //... };
De esta manera se consigue que sea una feature compatible.
sparse_super
En el Ext2 original se guarda una copia del superbloque y de los descriptores de bloques al principio de cada grupo de bloques. De esta manera, aunque se corrompa una parte significativa del sistema de archivos, siempre quedará alguna copia disponible para rescatar los datos que se puedan. Esto está bien cuando se tienen decenas de grupos de bloques, pero los discos modernos, que alcanzan los TB pueden tener muchos miles de grupos de bloques, y mantener tantísimas copias redundantes es un desperdicio de espacio y de tiempo, sin una ventaja real.
Con esta feature se guardan copias del superbloque y de los descriptores de bloques solamente en los bloques 0, 1 y en las potencias de 3, 5 y 7. Es decir, en los bloques 0, 1, 3, 5, 7, 9, 25, 27, 49, 81, 125, 243, 343, 625…
Es una feature de solo-lectura, es decir, se puede montar en solo-lectura sin interpretar esta extensión, porque solo necesita leerse la copia del grupo 0, pero no puede montarse como lectura-escritura porque cualquier cambio en el sistema de archivos implicaría modificar todas las copias, y para eso hay que saber dónde se guardan.
has_journal
Esta es la feature distintiva de Ext3 respecto a Ext2. El journal (o diario) es una estructura de datos en la que se guardan todos los cambios de la estructura del sistema de archivos antes de grabarlos. De esta manera, en el caso de que se cuelgue el sistema o se corte la energía, se puede reproducir el diario y llevar el sistema a un estado consistente. Normalmente en el diario solo se guardan los cambios de estructura, no de datos, pues guardarlo todo sería un coste excesivo, por lo que la existencia de esta funcionalidad no previene la pérdida de datos en caso de un cuelque. Solamente evita la corrupción de la estructura, es decir, que el sistema quede en un estado inconsistente.
Curiosamente, esta feature es compatible hacia adelante. Esto se consigue porque el journal se guarda en un inodo reservado (el núermo 8), por lo que versiones antiguas del driver, no necesitan tratarlo de forma especial.
Otra feature relacionada es needs_recovery, aunque en realidad no es una extensión, sino un flag. Cuando se monta un sistema de archivos con has_journal se activa la opción needs_recovery, y cuando se desmonta el sistema de archivos se desactiva.
La idea es que si al montar el sistema de archivos se encuentra needs_recovery, se supone que se ha colgado y hay que reproducir el diario para
mantener la integridad de la estructura. Por eso, needs_recovery es una feature incompatible.
flex_bg
Esta feature hizo su aparcición en Ext4. Su definición es en principio muy sencilla: se elimina la restricción de que el bitmap de bloques, el bitmap de inodos y la tabla de inodos de un grupo de bloques deban estar dentro del propio grupo. Es, lógicamente, una feature incompatible, aunque muy fácil de implementar.
La decisión de dónde se colocan los bitmaps y la tabla de inodos la toma el programa que crea el sistema de archivos (mke2fs), y luego obviamente no puede cambiarse.
La idea detrás de esta extensión es que se definen “grupos de grupos de bloques”, llamados flex_bg, de manera que los bitmaps y las tablas de inodos se guardan todos juntos al principio de dicho flex_bg. Gestionando el espacio disponible inteligentemente, unas veces a nivel de flex_bg y otras veces a nivel de grupo se consigue una mejora en el rendimiento.
extents
Esta es la feature más importante introducida en Ext4. Se basa en la observación de que la mayoría de los bloques de datos de un fichero se asignan contiguos en disco. Esto es bueno, pues los discos son más eficientes cuando leen o escriben bloques consecutivos.
Ocurre que si miramos, por ejemplo, el contenido de un bloque indirecto veremos que en muchos casos contiene un array de números consecutivos, a veces “todos” los números del bloque indirecto forman una secuencia. Ciertamente tiene que haber una forma más compacta de representar esto. Y aquí es donde se inventan los extents, que son estructuras en forma de árbol que definen rangos de bloques de datos utilizados por el inodo.
Cuando un sistema con la feature extents activada crea un inodo nuevo, lo marca con un flag que indica que va a utilizar extents. Si el flag no está activado se considerará que el inodo utiliza bloques indirectos de forma clásica.
Recordemos que un inodo clásico contiene 12 números de bloque, un número de bloque indirecto, otro de doble indirecto y otro de triple indirecto. En total son 15 números de bloque o 60 bytes.
En un inodo con extents estos 60 bytes no contienen números de bloque sino la siguiente estructura:
struct ExtentHeader { uint16_t magic; uint16_t entries; uint16_t max_entries; uint16_t depth; uint32_t generation; };
Los campos son los siguientes:
- magic: debe ser 0xF30A, y se usa para comprobar la integridad del los datos.
- entries: el número de estructuras presentes después de esta (véase más abajo).
- max_entries: el número máximo de estructuras que caben después de esta. En un inodo es 4.
- depth: la profundidad del árbol de extents. Todas las ramas del árbol deben tener siempre la misma longitud.
- generation: que yo sepa, este campo no sirve para nada.
Después de la estructura ExtentHeader se encuentra un array de una de las dos siguientes estructuras, una de ellas es para las hojas y la otra para todos los nodos intermedios del árbol. Se distingue un caso del otro observando el valor de depth. El número de elementos del array viene dado por entries.
struct Extent { uint32_t block; uint16_t len; uint16_t data_hi; uint32_t data_lo; }; struct ExtentNodo { uint32_t block; uint32_t next_lo; uint16_t next_hi; uint16_t dummy; }
Los campos de Extent son:
- block: El número del primer bloque del extent dentro del inodo, de 0 al tamaño del fichero, en bloques, menos uno.
- len: La longitud del extent, en bloques.
- data_lo: Los 32 bits menos significativos del número de bloque en el que empieza el extent.
- data_hi: Los 16 bits más significativos del valor anterior.
- dummy: Relleno, para alinear la estructura.
Y los campos de ExtentNodo son:
- block: Lo mismo que en la estructura Extent, pero refiriéndose a toda la rama del arbol que cuelga desde aquí..
- next_lo: Los 32 bits menos significativos de un número de bloque, en el que se guardan los hijos de este nodo.
- next_hi: Los 16 bits más significativos del valor anterior.
El campo next_lo/next_hi de ExtentNodo apunta a un bloque de datos en el que se guarda un ExtentHeader al principio (con uno menos en depth) seguido de tantos Extent o ExtentNodo como sean necesarios o quepan en el bloque.
Por ejemplo, un fichero poco fragmentado, con 4 extents o menos, tendrá todo el árbol dentro del inodo (depth==0).
Supongamos que el tamaño del bloque es de 2048 bytes, entonces en cada bloque caben 2048 / 12 = 170 estructuras, menos 1 por el ExtentHeader, por lo que un fichero con depth==1 puede tener hasta 4 * 170 = 680 extents. Si depth==2 los extents aumentan hasta 4 * 170 * 170 = 115600, etc.
En realidad debería haber muy pocos ficheros con depth > 2, salvo en casos patológicos, porque además cada extent puede tener hasta 65535 bloques.
Artículos relacionados:
- Analizando Ext2, Ext3 y Ext4 Resulta paradójico que el sistema de archivos más utilizado de...
- Utilidad para leer particiones Ext2/3/4 En los dos últimos posts describí el formato del sistema...
- Jugando con device-mapper y loop Hace unos días tuve que mover un archivo grande de...
RSS
Deja un comentario