Estoy empezando una serie de posts sobre ZFS. Intentaré proporcionar en la medida de lo posible diagramas de arquitectura y ejemplos de código. La serie es para personas que quieran aprender más sobre la arquitectura general del sistema de archivos, especialmente sobre ZFS (OpenZFS).
Me limitaré por ahora a la aplicación y la arquitectura en Illumos, la bifurcación de código abierto de OpenSolaris.
Empecemos con la abstracción de más alto nivel, el Sistema de Archivos Virtual. Como la mayoría de los sistemas Unix, Illumos utiliza un marco llamado Sistema de Archivos Virtual (VFS) que representa una capa de abstracción bajo la cual se pueden implementar múltiples sistemas de archivos concretos. Una de las primeras versiones de VFS se introdujo en SunOS 2.0 con la introducción de NFS (Network file system). Es bueno saber que la implementación en Illumos fue en realidad la primera que se hizo en cuanto a sistemas de archivos virtuales. La implementación en SunOS fue la que se utilizó en la primera versión comercial de un sistema operativo Unix Sistema Unix V versión 4.
VFS permite abstraer casi cualquier objeto como archivos y sistemas de archivos. Mencionaré algunas de las categorías de sistemas de archivos que se utilizan hoy en día:
Dentro de VFS hay 2 conceptos clave. El primero es el archivo virtual que se abstrae como un objeto vnode y el segundo son los objetos del sistema de archivos virtual u objetos vfs. Un vnode proporciona funciones relacionadas con el archivo y vfs proporciona funciones relacionadas con el sistema de archivos. En Illumos las operaciones vnode están definidas en usr/src/uts/common/sys/vnode.h y contenidas dentro de la estructura vnodeops_t.
/*
* Operaciones sobre vnodos. Nota: Los sistemas de ficheros nunca deben operar directamente
* sobre una estructura 'vnodeops' -- ¡cambiará en futuras versiones! Ellos
* deben usar vn_make_ops() para crear la estructura.
*/
typedef struct vnodeops {
const char *nombre_vnop;
VNODE_OPS; /* Firmas de todas las operaciones vnode (vops) */
} vnodeops_t;
Por mencionar algunas de las operaciones de vnode disponibles: abrir, cerrar, leer, escribir, buscar, sincronizar. Todas las firmas de las funciones están en VNODE_OPS.
Las funciones vnode y vfs delegan en las implementaciones concretas apropiadas del sistema de archivos. Todas las funciones relacionadas con archivos llegan a la capa vfs/vnode a través de una llamada al sistema y desde allí se dirigen a la implementación apropiada del sistema de archivos. El siguiente diagrama muestra la arquitectura de alto nivel del VFS.
Los ficheros se referencian en el espacio de proceso mediante descriptores de fichero. Los descriptores de fichero son números enteros, concretamente un int de tipo C. Por ejemplo, los 3 descriptores de fichero POSIX estándar correspondientes a los 3 flujos: STDIN - 0, STDOUT - 1 y STDERR - 2. Los descriptores de fichero se asignan cuando se abre el fichero y se liberan cuando se cierra.
Cada proceso tiene su propia lista de archivos. La información de ficheros por proceso en Illumos se guarda en una estructura llamada uf_info_t. Se puede encontrar en usr/src/uts/common/sys/user.h.
/*
* Información de archivo por proceso.
*/
typedef struct uf_info {
kmutex_t fi_lock; /* ver más abajo */
int fi_badfd; /* mal descriptor de fichero # */
int fi_action; /* acción a tomar en caso de mal uso del fd */
int fi_nfiles; /* número de entradas en fi_list[] */
uf_entry_t *volatile fi_list; /* lista de archivos actual */
uf_rlist_t *fi_rlist; /* listas de archivos retirados */
} uf_info_t;
Las listas de archivos están indexadas por el descriptor de archivo entero. Veamos ahora cómo podemos acceder normalmente a esta información, la lista de ficheros actual. En usr/src/uts/common/sys/user.h tenemos definida la estructura user_t que contiene todos los datos por proceso relacionados con un usuario. A través de user_t, accediendo a su campo u_finfo (este es de tipo uf_info_t) accedemos a la información de ficheros por proceso y a la lista de ficheros actual. Un buen ejemplo está en el código de Dtrace (dtrace.c): uf_info_t *finfo = &curthread->t_procp->p_user.u_finfo. Hay herramientas que te permitirán ver la fi_list basándose en el ID del proceso, como pfiles por ejemplo.
La lista de archivos contiene elementos de tipo uf_entry_t. La definición de uf_entry_t se encuentra de nuevo en el mismo archivo usr/src/uts/common/sys/user.h.
/*
* Entrada en la lista de ficheros abiertos por proceso.
* Nota: sólo ciertos campos se copian en flist_grow() y flist_fork().
* Esto se indica entre paréntesis en los comentarios de los miembros de la estructura.
*/
typedef struct uf_entry {
kmutex_t uf_lock; /* bloqueo por fd [nunca se copia] */
struct file *uf_file; /* puntero de archivo [crecer, bifurcarse] */
struct fpollinfo *uf_fpollinfo; /* estado de sondeo [grow] */
int uf_refcnt; /* LWPs accediendo a este fichero [grow] */
int uf_alloc; /* asignaciones del subárbol derecho [grow, fork] */
short uf_flag; /* banderas fcntl F_GETFD [grow, fork] */
short uf_busy; /* archivo asignado [grow, fork] */
kcondvar_t uf_wanted_cv; /* esperando setf() [nunca copiado] */
kcondvar_t uf_closing_cv; /* esperando close() [nunca copiado] */
struct portfd *uf_portfd; /* asociado a puerto [crecer] */
/* Evitar falsa compartición - pad a granularidad de coherencia (64 bytes) */
char uf_pad[64 - sizeof (kmutex_t) - 2 * sizeof (void*) -
2 * sizeof (int) - 2 * sizeof (short) -
2 * sizeof (kcondvar_t) - sizeof (struct portfd *)];
} uf_entry_t;
Como podemos ver la uf_entry_t contiene el puntero al fichero en su campo: uf_file. Echemos un vistazo más adelante, para ver la referencia entre el puntero al fichero y su vnode adjunto. En Illumos la definición de la estructura fichero reside en usr/src/uts/common/sys/file.h.
/*
* Se asigna una estructura de fichero para cada llamada open/creat/pipe.
* El uso principal es mantener el puntero de lectura/escritura asociado con
* cada archivo abierto.
*/
typedef struct fichero {
kmutex_t f_tlock; /* bloqueo a corto plazo */
ushort_t f_flag;
ushort_t f_flag2; /* banderas extra (FSEARCH, FEXEC) */
struct vnode *f_vnode; /* puntero a estructura vnode */
offset_t f_offset; /* puntero a carácter de lectura/escritura */
struct cred *f_cred; /* credenciales del usuario que lo abrió */
struct f_audit_data *f_audit_data; /* datos de auditoría del fichero */
int f_count; /* recuento de referencias */
} file_t;
Desde el puntero del fichero podemos alcanzar el vnode adjunto a todo el sistema a través del campo f_vnode. Múltiples procesos pueden tener referencias al mismo vnode. Estamos casi al final del nivel del sistema de archivos virtual aquí, mientras nos movemos hacia la implementación concreta del sistema de archivos. La última barrera es básicamente el vnode. La definición del vnode reside en usr/src/uts/common/sys/vnode.h.
typedef struct vnode {
kmutex_t v_lock; /* protege los campos del nodo */
uint_t v_flag; /* banderas del nodo (véase más abajo) */
uint_t v_count; /* recuento de referencias */
void *v_data; /* datos privados para fs */
struct vfs *v_vfsp; /* ptr to containing VFS */
struct stdata *v_stream; /* flujo asociado */
enum vtype v_type; /* tipo de nodo */
dev_t v_rdev; /* dispositivo (VCHR, VBLK) */
...
} vnode_t;
El punto de entrada a la implementación específica del sistema de ficheros es el campo v_data dentro de la estructura vnode_t. Teniendo ZFS debajo, v_data apuntará a un znode. En ZFS el znode es el equivalente al inode de UFS. El v_data se casted a una estructura znode_t a través de la macro: #define VTOZ(VP) ((znode_t *)(VP)->v_data). Más información sobre el znode y la implementación de operaciones ZFS posix en los siguientes posts.
Hagamos una breve recapitulación a través de la ruta del código para llegar desde los datos de usuario a nivel de proceso hasta la implementación de la Capa Posix de ZFS.
proc_t proceso;
user_t p_user = process->p_user; // la estructura de usuario
uf_info_t file_info = p_user->u_finfo; // información del fichero por proceso
uf_entry_t file_entry = file_info->fi_list[0]; // Esto es en caso de que tengamos fd 0
file_t p_file = file_entry->uf_file; // puntero a estructura de fichero
vnode_t file_vnode = p_file->f_vnode; // el vnode
(znode_t *)file_vnode->v_data // alcanzado el znode ZFS
El próximo post se sumergirá en la capa posix de ZFS y en la ruta del código de ciertas llamadas sys.
Feliz codificación.