Sistemas de archivos de unión: Implementaciones, parte I


Este artículo ha sido elaborado por los suscriptores de LWN

Los suscriptores de LWN.net han hecho posible este artículo -y todo lo que lo rodea-. Si aprecia nuestro contenido, por favor, compre una suscripción y haga posible la siguiente serie de artículos.

25 de marzo de 2009

Este artículo ha sido aportado por Valerie Aurora (antes Henson)

En el artículo de la semana pasada, repasé los casos de uso, los conceptos básicos y los problemas comunes de diseño de los sistemas de archivos de unión. Esta semana, describiré varias implementaciones de sistemas de archivos de unión en detalle técnico. Los sistemas de archivos de unión que cubriré en este artículo son los directorios de unión de Plan 9, los montajes de unión de BSD y los montajes de unión de Linux. El próximo artículo cubrirá unionfs, aufs, y posiblemente uno o dos sistemas de archivos de unión, y concluirá la serie.

Para cada sistema de archivos, describiré su arquitectura básica, características e implementación. La discusión de la implementación se centrará en particular en la lectura de los directorios y de las hojas blancas. Terminaré con una mirada a los aspectos de ingeniería de software de cada implementación; por ejemplo, el tamaño y la complejidad del código, la invasión y la carga para los desarrolladores de sistemas de archivos.

Antes de leer este artículo, es posible que desee consultar el informe que acaba de publicar AndreasGruenbacher sobre el taller de montaje de la unión celebrado el pasado mes de noviembre. Es un buen resumen de las características de los sistemas de archivos de unión que son más urgentes para los desarrolladores de distribución. De la introducción: «Todos los casos de uso en los que estamos interesados se reducen básicamente a lo mismo: tener una imagen o sistema de archivos que se utiliza sólo para leer (ya sea porque no se puede escribir, o porque no se desea escribir en la imagen), y pretender que esta imagen o sistema de archivos se puede escribir, almacenando los cambios en otro lugar.»

Directorios de unión de Plan 9

El sistema operativo de Plan 9 (código fuente navegable aquí) implementa la unión en su propia forma especial de Plan 9. En los directorios de unión de Plan 9, sólo se fusiona el espacio de nombre del directorio de nivel superior, no los subdirectorios. Sin las restricciones de los estándares de UNIX, los directorios de unión de Plan 9 no implementan los blanqueos y ni siquiera filtran las entradas duplicadas – si el mismo nombre de archivo aparece en dos sistemas de archivos, simplemente se devuelve dos veces en los listados de directorios.

Un directorio de unión del Plan 9 se crea así:

 bind -a /home/val/bin/ /bin

Esto haría que el directorio/home/val/binse montara en unión «después» (la opción-a) de/bin; otras opciones son colocar el nuevo directorio antes del directorio existente, o reemplazar el directorio existente por completo. (Esto me parece un ordenamiento extraño, ya que me gusta que los comandos de mibin/personal tengan prioridad sobre los comandos de todo el sistema, pero ese es el ejemplo de la documentación de Plan 9). Brian Kernighanexplica uno de los usos de los directorios de unión: «Este mecanismo de directorios de unión sustituye a la ruta de búsqueda de los shells UNIX convencionales. En lo que a ti respecta, todos los programas ejecutables están en /bin». Los directorios de unión pueden reemplazar teóricamente muchos usos de los bloques fundamentales de UNIX de los enlaces simbólicos y las rutas de búsqueda.

Sin los blanqueos o la eliminación de duplicados, readdir()los directorios de unión son triviales de implementar. Los desplazamientos de la entrada del directorio desde el sistema de archivos subyacente corresponden directamente al desplazamiento en bytes de la entrada del directorio desde el principio del mismo. Un directorio unido se trata como si los contenidos de los directorios subyacentes estuvieran concatenados.

El plan 9 implementa una alternativa a readdir() que vale la pena mencionar, dirread().dirread() devuelve estructuras de tipo Dir, descritas en la página de manual de stat(). La parte importante de Dir es el miembro Qid. Un Qid es:

…una estructura que contiene los campos path y vers: path está garantizado para ser único entre todos los nombres de path actualmente en el servidor de archivos, y vers cambia cada vez que se modifica el archivo. La ruta es un long long (64 bits, vlong)y el vers es un unsigned long (32 bits, ulong).

¿Por qué es esto interesante? Una de las razones por las que readdir() es tan difícil de implementar es que devuelve el miembro d_off de struct dirent, como un único off_t (32 bits a menos que la aplicación esté compilada con soporte para archivos grandes), para marcar la entrada del directorio donde la aplicación debe continuar leyendo en la siguiente llamada a readdir(). Esto funciona bien siempre y cuando d_off sea un simple byteoffset en un archivo plano de menos de 232 bytes y las entradas de directorio existentes nunca se muevan – no es el caso de muchos sistemas de archivos modernos (XFS, btrfs, ext3 con índices htree). El marcador de lugar de 96 bits Qid es mucho más útil que el de 32 o 64 bits off_t. Para un buen resumen de las cuestiones relacionadas con la implementación de readdir(), lea el excelente post de TheodoreY. Ts’o sobre el tema en la lista de correo de btrfs.

Desde el punto de vista de la ingeniería de software, los directorios de unión del Plan 9 son un cielo. Sin blanqueos, eliminación de entradas duplicadas, complicados desplazamientos de directorios, o fusión de espacios de nombres más allá del directorio de nivel superior, la implementación es simple y fácil de mantener. Para nuestros propósitos, los directorios de unión de Plan 9 sirven principalmente como inspiración.

BSD union mounts

BSD implementa dos formas de unión: la opción"-o union"del comandomount, que produce un directorio de unión similar al de Plan 9, y el comandomount_unionfs, que implementa un sistema de archivos de unión más completo, con blanqueos y fusión de todo el espacio de nombres. Nos centraremos en este último.

Para este artículo, utilizamos dos fuentes para los detalles específicos de la implementación: la implementación original de montaje de unión de BSD tal y como se describe en el documento de USENIX de 1995Unionmounts in 4.4BSD-Lite , y la página man y el código fuente de mount_unionfs de FreeBSD7.1. OtrosBSDs pueden variar.

Un directorio puede montarse en unión tanto «por debajo» como «por encima» de un directorio existente o de un montaje en unión, siempre y cuando la rama superior de una unión escribible sea escribible. Se soportan dos modos de borrado: o bien se crea siempre un borrado cuando se elimina un directorio, o bien sólo se crea si existe otra entrada de directorio con ese nombre en una rama por debajo de la rama con capacidad de escritura. Se admiten tres modos para establecer la propiedad y el modo de los archivos copiados. El más sencillo es transparent, en el que el nuevo archivo mantiene el mismo propietario y modo del original. El modo masquerade hace que los archivos copiados sean propiedad de un usuario en particular y admite un conjunto de opciones de montaje para determinar el modo del nuevo archivo.El modo traditional establece el propietario al usuario que ejecutó el comando de montaje de la unión, y establece el modo de acuerdo con la máscara de usuario en el momento del montaje de la unión.

Cada vez que se abre un directorio, se crea un directorio del mismo nombre en la capa superior escribible si no existe ya. De la ponencia:

Al crear directorios sombra de forma agresiva durante la búsqueda, el sistema de archivos de la unión evita tener que comprobar y posiblemente crear la cadena de directorios desde la raíz del montaje hasta el punto de una copia.Dado que el espacio de disco consumido por un directorio es insignificante, la creación de directorios cuando se atraviesan por primera vez parecía una alternativa mejor.

Como resultado, un "find /union" resultará en la copia de cada directorio (pero no las entradas de directorio que apuntan a los no-directorios) a la capa de escritura. Para la mayoría de las imágenes de sistemas de archivos, esto utilizará una cantidad insignificante de espacio (menos que, por ejemplo, el espacio reservado para el usuario root, o el ocupado por los inodos no utilizados en un sistema de archivos de estilo FFS).

Un archivo se copia a la capa superior cuando se abre con permiso de escritura o se cambian los atributos del archivo. (Dado que los directorios se copian cuando se abren, se garantiza que el directorio que los contiene ya existe en la capa con permiso de escritura). Si el archivo que se va a copiar tiene varios enlaces duros, los otros enlaces se ignoran y el nuevo archivo tiene un recuento de enlaces de uno. Esto puede romper las aplicaciones que utilizan enlaces duros y esperan que las modificaciones a través de un nombre de enlace se muestren cuando se hace referencia a través de un enlace duro diferente. Tales aplicaciones son relativamente infrecuentes, pero nadie ha hecho un estudio sistemático para ver qué aplicaciones fallarán en esta situación.

Las entradas blancas se implementan con un tipo de entrada de directorio especial, DH_WHT. Las entradas de directorio Whiteout no se refieren a ningún nodo real, pero para facilitar la compatibilidad con las utilidades del sistema de archivos existentes, como fsck, cada entrada de directorio Whiteout incluye un número de nodo falso, el número de nodo Whiteout reservado WINO. El sistema de archivos subyacente debe ser modificado para soportar el tipo de entrada de directorio whiteout. Los nuevos directorios que sustituyen a una entrada de whiteout se marcan como opacos a través de un nuevo atributo de inodo «opaco» para que las búsquedas no pasen por ellos (de nuevo, se requiere un soporte mínimo del sistema de archivos subyacente).

Las entradas de directorio duplicadas y los whiteouts se gestionan en la implementación del espacio de usuarioreaddir(). En el momento de opendir(), la biblioteca C lee el directorio de una sola vez, elimina los duplicados, aplica los whiteouts y almacena en caché los resultados.

Los montajes de unión deBSD no intentan manejar los cambios en las ramas por debajo de la rama superior escribible (aunque están permitidos). No se describe la forma en que se maneja rename().

Un ejemplo de la página man de mount_unionfs:

 The commands mount -t cd9660 -o ro /dev/cd0 /usr/src mount -t unionfs -o noatime /var/obj /usr/src mount the CD-ROM drive /dev/cd0 on /usr/src and then attaches /var/obj on top. For most purposes the effect of this is to make the source tree appear writable even though it is stored on a CD-ROM. The -o noatime option is useful to avoid unnecessary copying from the lower to the upper layer.

Otro ejemplo (señalando que creo que el control de fuentes se implementa mejor fuera del sistema de archivos):

 The command mount -t unionfs -o noatime -o below /sys $HOME/sys attaches the system source tree below the sys directory in the user's home directory. This allows individual users to make private changes to the source, and build new kernels, without those changes becoming visible to other users.

Montajes de unión de Linux

Al igual que los montajes de unión de BSD, los montajes de unión de Linux implementan la unión del sistema de archivos en la capa de VFS, con algún soporte menor de los sistemas de archivos subyacentes para los whiteouts y las etiquetas de directorio opacas. Existen varias versiones de estos parches, escritas y modificadas por Jan Blunck, Bharata B. Rao y Miklos Szeredi, entre otros.

Una versión de este código fusiona sólo los directorios de nivel superior, de forma similar a los directorios de unión de Plan 9 y la opción de montaje de BSD -o union. Esta versión de los montajes de unión, a los que me refiero como directorios de unión, se describen con cierto detalle en un reciente artículo de LWN por Goldwyn Rodrigues y en el reciente post de Miklos Szeredi de un conjunto de parches actualizado. Para el resto de este artículo, nos centraremos en las versiones de montaje de unión que fusionan el espacio de nombres completo.

Los montajes de unión de Linux están actualmente en desarrollo activo. Este artículo describe la versión publicada por Jan Blunck contra Linux2.6.25-mm1, util-linux 2.13, y e2fsprogs 1.40.2. Los conjuntos de parches, de la serie asquilt, pueden descargarse del sitio ftp de Jan:

Parches del núcleo: ftp://ftp.suse.com/pub/people/jblunck/patches/

Utilidades: ftp://ftp.suse.com/pub/people/jblunck/union-mount/

He creado una página web con enlaces a las versiones git de los parches anteriores y algo de documentación tipo HOWTO en http://valerieaurora.org/union.

Una unión se crea montando un sistema de archivos con el conjunto de banderas MS_UNION. (Las banderas MS_BEFORE, MS_AFTER y MS_REPLACE están definidas en el código base mount pero no se utilizan actualmente). Si se especifica la bandera MS_UNION, entonces el sistema de archivos montado debe ser de sólo lectura o admitir los espacios en blanco. En esta versión de montajes de unión, la bandera de montaje de unión se especifica mediante la opción «-o union» de mount. Por ejemplo, para crear una unión de dos sistemas de archivos loopbackdevice, /img/ro y /img/rw, se ejecutaría:

 # mount -o loop,ro,union /img/ro /mnt/union/ # mount -o loop,union /img/rw /mnt/union/

Cada montaje de unión crea unstruct union_mount:

 struct union_mount {atomic_t u_count;/* reference count */struct mutex u_mutex;struct list_head u_unions;/* list head for d_unions */struct hlist_node u_hash;/* list head for searching */struct hlist_node u_rhash;/* list head for reverse searching */struct path u_this;/* this is me */struct path u_next;/* this is what I overlay */ };

Como se describe enDocumentation/filesystems/union-mounts.txt, «Todas las estructuras de union_mount se almacenan en caché en dos tablas hash, una para las búsquedas de la siguiente capa inferior de la pila de unión y otra para las búsquedas inversas de la siguiente capa superior de la pila de unión.»

Los blanqueos y los directorios opacos se implementan prácticamente de la misma manera que en BSD. El sistema de archivos subyacente debe soportar explícitamente los whiteouts definiendo la operación de inodo .whiteout para los directorios (actualmente, los whiteouts sólo se implementan para ext2, ext3 y tmpfs). Las implementaciones de ext2 y ext3 utilizan el tipo de entrada de directorio whiteout, DT_WHT, que se ha definido en include/linux/fs.h durante años pero que no se ha utilizado fuera del sistema de archivos Coda hasta ahora. Se ha definido un número de inodo reservado para whiteout, EXT3_WHT_INO, pero aún no se utiliza; las entradas whiteout actualmente asignan un inodo normal. Se define un nuevo inodeflag, S_OPAQUE, para marcar los directorios como opacos.Al igual que en BSD, los directorios sólo se marcan como opacos cuando reemplazan una entrada whiteout.

Los archivos se copian hacia arriba cuando el archivo se abre para escribir. Si es necesario, cada directorio en la ruta del archivo se copia en la rama superior (copia bajo demanda de directorios). Actualmente, la copia hacia arriba sólo se soporta para archivos y directorios regulares.

readdir() es uno de los puntos más débiles de la implementación actual. Se implementa de la misma manera que la unión BSD mountreaddir(), pero en el kernel. El campo d_off se establece en el desplazamiento dentro del directorio subyacente actual, menos los tamaños de los directorios anteriores. Las entradas de directorios por debajo de la capa superior deben ser comparadas con las entradas anteriores en busca de duplicados o de espacios en blanco. Tal y como se implementa actualmente, cada llamada al sistema de readdir() (técnicamente, getdents()) lee todas las entradas de directorio anteriores en una caché del núcleo, y luego compara cada entrada que se devuelve con las que ya están en la caché antes de copiarla en el búfer del usuario. El resultado final es que readdir() es complejo, lento, y potencialmente asigna una gran cantidad de memoria del núcleo.

Una solución es tomar el enfoque de BSD y hacer el almacenamiento en caché, el blanqueo y el procesamiento duplicado en el espacio de usuario. Bharata B. Raois está diseñando un soporte para el montaje de la unión readdir() en glibc (el estándar POSIX permite que readdir() se implemente a nivel de libc si la llamada al sistema del núcleo no cumple con todos los requisitos). Esto trasladaría el uso de la memoria a la aplicación y haría que la caché fuera persistente. Otra solución sería hacer que la caché en el núcleo fuera persistente de alguna manera.

Mi sugerencia es tomar una técnica de los montajes de unión BSD y extenderla: copiar proactivamente no sólo las entradas de directorio para los directorios, sino todas las entradas de directorio de los sistemas de archivos inferiores, procesar los duplicados y los borrados, hacer el directorio opaco y escribirlo en el disco. En efecto, usted está procesando las entradas de directorio en busca de borrones y duplicados en la primera apertura del directorio, y luego escribiendo el «caché» resultante de las entradas de directorio en el disco. Las entradas de directorio que apuntan a los archivos en los sistemas de archivos subyacentes deben significar de alguna manera que son entradas «fall-through» (lo contrario de un whiteout – solicita explícitamente buscar un objeto en un sistema de archivos inferior). Un efecto secundario de este enfoque es que los whiteouts ya no son necesarios.

Un problema que hay que resolver con este enfoque es cómo representar las entradas de directorio que apuntan a sistemas de archivos inferiores. Se presentan varias soluciones: la entrada podría apuntar a un número de nodo reservado, el sistema de archivos podría asignar un nodo para cada entrada pero marcarlo con un nuevo atributo de nodo S_LOOKOVERTHERE, podría crear un enlace simbólico a un objetivo reservado, etc. Este enfoque utilizaría más espacio en el sistema de archivos subyacente, pero todos los demás enfoques requieren asignar el mismo espacio en la memoria, y generalmente la memoria es más cara que el disco.

Un problema menos apremiante con la implementación actual es que los números de inodo no son estables a través del arranque (ver el artículo anterior sobre la unión de sistemas de archivos para obtener detalles sobre por qué esto es un problema).Si los directorios «fall-through» se implementan mediante la asignación de un inodo para cada entrada de directorio en los sistemas de archivos subyacentes, entonces los números de inodo estables serán un efecto secundario natural. Otra opción es almacenar un mapa de inodos persistente en algún lugar – en un archivo en el directorio de nivel superior, o en un sistema de archivos externo, tal vez.

Los enlaces duros se manejan – o, más exactamente, no se manejan – de la misma manera que los montajes de unión BSD. Una vez más, no está claro cómo muchas aplicaciones dependen de la modificación de un archivo a través de una ruta de enlace duro y ver los cambios a través de otra ruta de enlace duro (a diferencia de enlace simbólico). El único método que se me ocurre para manejar esto correctamente es mantener un caché persistente en algún lugar del disco de los inodos que hemos encontrado con múltiples enlaces duros.

Aquí hay un ejemplo de cómo funcionaría: Digamos que iniciamos una copia para el nodo 42 y encontramos que tiene un recuento de enlaces de tres. Crearíamos una entrada para la base de datos de enlaces duros que incluya el identificador del sistema de archivos, el número de nodo, el número de enlaces y el número de nodo de la nueva copia en el sistema de archivos de nivel superior. Podría almacenarse en un archivo en formato CSV, o como un enlace simbólico en un directorio reservado en el directorio raíz (por ejemplo, «/.hardlink_hack/<fs_id>/42«, que es un enlace a «<new_inode_num> 3«), o en una base de datos real. Cada vez que abrimos un inodo en un sistema de archivos subyacente, lo buscamos en nuestra base de datos de enlaces duros; si existe una entrada, disminuimos el número de enlaces y creamos un enlace duro al inodo correcto en el nuevo sistema de archivos. Cuando se encuentran todas las rutas, el recuento de enlaces desciende a uno y la entrada puede ser eliminada de la base de datos. Lo bueno de este enfoque es que la cantidad de sobrecarga está limitada y desaparecerá por completo cuando se hayan buscado todas las rutas a los inodos relevantes. Sin embargo, esto todavía introduce una cantidad significativa de complejidad posiblemente innecesaria; la implementación de BSD muestra que muchas aplicaciones se ejecutarán felizmente con un comportamiento de hardlink no del todo correcto.

Actualmente, rename() de directorios a través de ramas devuelve EXDEV, el error por tratar de renombrar un archivo a través de diferentes sistemas de archivos. El espacio de usuario normalmente maneja esto de forma transparente (ya que tiene que manejar este caso para los directorios de diferentes sistemas de archivos) y vuelve a copiar el contenido del directorio uno por uno. Implementar la recursividad rename() de los directorios a través de las ramas en el núcleo no es una idea brillante por las mismas razones que el renombramiento a través de los sistemas de archivos regulares; probablemente devolver EXDEV es la mejor solución.

Desde un punto de vista de ingeniería de software, los montajes de unión parecen ser un compromiso razonable entre las características y la facilidad de mantenimiento. La mayoría de los cambios del VFS están aislados en fs/union.c, un archivo de unas 1000 líneas. Alrededor de un tercio de este archivo es la implementación de readdir() en el núcleo, que casi seguramente será reemplazada por otra cosa antes de cualquier posible fusión.Los cambios en los sistemas de archivos subyacentes son bastante mínimos y sólo se necesitan para los sistemas de archivos montados como ramas escribibles. El principal obstáculo para fusionar este código es la implementación de readdir(). Por lo demás, los mantenedores de sistemas de archivos han sido notablemente más positivos sobre los montajes de unión que cualquier otra implementación de unión.

Un buen resumen de los montajes de unión se puede encontrar en las diapositivas de montaje de unión de Bharata B. Rao para FOSS.IN .

Próximo artículo

En el próximo artículo, revisaremos unionfs y aufs, y compararemos las diversas implementaciones de sistemas de archivos de unión para Linux. ¡Manténgase en sintonía!

Entradas en el índice de este artículo
Núcleo Sistemas de archivos/Unión
Núcleo Montajes de unión
Artículos de invitados Aurora (Henson), Valerie

Deja una respuesta

Tu dirección de correo electrónico no será publicada.