Página siguiente Página anterior Índice general

3. Sistemas de Archivos Virtuales (VFS)

3.1 Caché de Inodos e Interacción con Dcache

En orden para soportar múltiples sistemas de archivos, Linux contiene un nivel especial de interfaces del núcleo llamado VFS (Interruptor de Sistemas de Ficheros Virtuales). Esto es muy similar a la interfaz vnode/vfs encontrada en los derivados de SVR4 (originalmente venían de BSD y de las implementaciones originales de Sun).

La antememoria de inodos de Linux es implementada en un simple fichero, fs/inode.c, el cual consiste de 977 lineas de código. Es interesante notar que no se han realizado muchos cambios en él durante los últimos 5-7 años: uno todavía puede reconocer algún código comparando la última version con, digamos, 1.3.42.

La estructura de la antememoria de inodos Linux es como sigue:

  1. Una tabla global hash, inode_hashtable, donde cada inodo es ordenado por el valor del puntero del superbloque y el número de inodo de 32bit. Los inodos sin un superbloque (inode->i_sb == NULL) son añadidos a la lista doblemente enlazada encabezada por anon_hash_chain en su lugar. Ejemplos de inodos anónimos son los conectores creados por net/socket.c:sock_alloc(), llamado por fs/inode.c:get_empty_inode().
  2. Una lista global del tipo "en_uso" (inode_in_use), la cual contiene los inodos válidos con i_count>0 y i_nlink>0. Los inodos nuevamente asignados por get_empty_inode() y get_new_inode() son añadidos a la lista inode_in_use.
  3. Una lista global del tipo "sin_usar" (inode_unused), la cual contiene los inodos válidos con i_count = 0.
  4. Una lista por cada superbloque del tipo "sucia" (sb->s_dirty) que contiene los inodos válidos con i_count>0, i_nlink>0 y i_state & I_DIRTY. Cuando el inodo es marcado como sucio, es añadido a la lista sb->s_dirty si el está también ordenado. Manteniendo una lista sucia por superbloque de inodos nos permite rápidamente sincronizar los inodos.
  5. Una antememoria propia de inodos - una antememoria SLAB llamada inode_cachep. Tal como los objetos inodos son asignados como libres, ellos son tomados y devueltos a esta antememoria SLAB.

Los tipos de listas son sujetadas desde inode->i_list, la tabla hash desde inode->i_hash. Cada inodo puede estar en una tabla hash y en uno, y en sólo uno, tipo de lista (en_uso, sin_usar o sucia).

Todas estas listas están protegidas por un spinlock simple: inode_lock.

El subsistema de caché de inodos es inicializado cuando la función inode_init() es llamada desde init/main.c:start_kernel(). La función es marcada como __init, lo que significa que el código será lanzado posteriormente. Se le pasa un argumento simple - el número de páginas físicas en el sistema. Esto es por lo que la antememoria de inodos puede configurarse ella misma dependiendo de cuanta memoria está disponible, esto es, crea una tabla hash más grande si hay suficiente memoria.

Las únicas estadísticas de información sobre la antememoria de inodos es el número de inodos sin usar, almacenados en inodes_stat.nr_unused y accesibles por los programas de usuario a través de los archivos /proc/sys/fs/inode-nr y /proc/sys/fs/inode-state.

Podemos examinar una de las listas desde gdb en un núcleo en funcionamiento de esta forma:


(gdb) printf "%d\n", (unsigned long)(&((struct inode *)0)->i_list)
8
(gdb) p inode_unused
$34 = 0xdfa992a8
(gdb) p (struct list_head)inode_unused
$35 = {next = 0xdfa992a8, prev = 0xdfcdd5a8}
(gdb) p ((struct list_head)inode_unused).prev
$36 = (struct list_head *) 0xdfcdd5a8
(gdb) p (((struct list_head)inode_unused).prev)->prev
$37 = (struct list_head *) 0xdfb5a2e8
(gdb) set $i = (struct inode *)0xdfb5a2e0
(gdb) p $i->i_ino
$38 = 0x3bec7
(gdb) p $i->i_count
$39 = {counter = 0x0}

Destacar que restamos 8 de la dirección 0xdfb5a2e8 para obtener la dirección de struct inode (0xdfb5a2e0) de acuerdo a la definición de la macro list_entry() de include/linux/list.h.

Para entender cómo trabaja la antememoria de inodos, déjanos seguir un tiempo de vida de un inodo de un fichero regular en el sistema de ficheros ext2, el cómo es abierto y cómo es cerrado:


fd = open("file", O_RDONLY);
close(fd);

La llamada al sistema open(2) es implementada en la función fs/open.c:sys_open y el trabajo real es realizado por la función fs/open.c:filp_open(), la cual está dividida en dos partes:

  1. open_namei(): rellena la estructura nameidata conteniendo las estructuras dentry y vfsmount.
  2. dentry_open(): dado dentry y vfsmount, esta función asigna una nueva struct file y las enlaza a todas ellas; también llama al método específico del sistema de ficheros f_op->open() el cual fue inicializado en inode->i_fop cuando el inodo fue leído en open_namei() (el cual suministra el inodo a través de dentry->d_inode).

La función open_namei() interactúa con la antememoria dentry a través de path_walk(), el cual en el regreso llama a real_lookup(), el cual llama al método específico del sistema de ficheros inode_operations->lookup(). La misión de este método es encontrar la entrada en el directorio padre con el nombre correcto y entonces hace iget(sb, ino) para coger el correspondiente inodo - el cual nos trae la antememoria de inodos. Cuando el inodo es leido, el dentry es instanciado por medio de d_add(dentry, inode). Mientras estamos en él, nótese que en los sistemas de ficheros del estilo UNIX que tienen el concepto de número de inodos en disco, el trabajo del método lookup es mapear su bit menos significativo al actual formato de la CPU, ej. si el número de inodos en la entrada del directorio sin formato (específico del sistema de ficheros) está en el formato de 32 bits little-endian uno haría:


unsigned long ino = le32_to_cpu(de->inode);
inode = iget(sb, ino);
d_add(dentry, inode);

Por lo tanto, cuando abrimos un fichero nosotros llamamos a iget(sb, ino) el cual es realmente iget4(sb, ino, NULL, NULL), el cual hace:

  1. Intenta encontrar un inodo con el superbloque emparejado y el número de inodo en la tabla hash bajo la protección de inode_lock. Si el inodo es encontrado, su cuenta de referencia (i_count) es incrementada; si era 0 anteriormente al incremento y el inodo no estaba sucio, es quitado de cualquier tipo de lista (inode->i_list) en la que esté (tiene que estar en la lista inode_unused, por supuesto) e insertado en la lista del tipo inode_in_use; finalmente inodes_stat.nr_unused es decrementado.
  2. Si el inodo está actualmente bloqueado, esperaremos hasta que se desbloquee, por lo tanto está garantizado que iget4() devolverá un inodo desbloqueado.
  3. Si el inodo no fue encontrado en la tabla hash entonces es la primera vez que se pide este inodo, por lo tanto llamamos a get_new_inode(), pasándole el puntero al sitio de la tabla hash donde debería de ser insertado.
  4. get_new_inode() asigna un nuevo inodo desde la antememoria SLAB inode_cachep, pero esta operación puede bloquear (asignación GFP_KERNEL), por lo tanto el spinlock que guarda la tabla hash tiene que ser quitado. Desde que hemos quitado el spinlock, entonces debemos de volver a buscar el inodo en la tabla; si esta vez es encontrado, se devuelve (después de incrementar la referencia por __iget) el que se encontró en la tabla hash y se destruye el nuevamente asignado. Si aún no se ha encontrado en la tabla hash, entonces el nuevo inodo que tenemos acaba de ser asignado y es el que va a ser usado; entonces es inicializado a los valores requeridos y el método específico del sistema de ficheros sb->s_op->read_inode() es llamado para propagar el resto del inodo. Esto nos proporciona desde la antememoria de inodos la vuelta al código del sistema de archivos - recuerda que venimos de la antememoria de inodos cuando el método específico del sistema de ficheros lookup() llama a iget(). Mientras el método s_op->read_inode() está leyendo el inodo del disco, el inodo está bloqueado (i_state = I_LOCK); él es desbloqueado después de que el método read_inode() regrese y todos los que están esperando por el hayan sido despertados.

Ahora, veamos que pasa cuando cerramos este descriptor de ficheros. La llamada al sistema close(2) está implementada en la función fs/open.c:sys_close(), la cual llama a do_close(fd, 1) el cual rompe (reemplaza con NULL) el descriptor del descriptor de ficheros de la tabla del proceso y llama a la función filp_close(), la cual realiza la mayor parte del trabajo. La parte interesante sucede en fput(), la cual chequea si era la última referencia al fichero, y si es así llama a fs/file_table.c:_fput() la cual llama a __fput() en la cual es donde sucede la interacción con dcache (y entonces con la memoria intermedia de inodos - ¡recuerda que dcache es la memoria intermedia de inodos Maestra!). El fs/dcache.c:dput() hace dentry_iput() la cual nos brinda la vuelta a la memoria intermedia de inodos a través de iput(inode), por lo tanto déjanos entender fs/inode.c:iput(inode):

  1. Si el parámetro pasado a nosotros es NULL, no hacemos nada y regresamos.
  2. Si hay un método específico del sistema de archivos sb->s_op->put_inode(), es llamada inmediatamente sin mantener ningún spinlock (por lo tanto puede bloquear).
  3. El spinlock inode_lock es tomado y i_count es decrementado. Si NO era la última referencia a este inodo entonces simplemente chequeamos si hay muchas referencias a el y entonces i_count puede urdir sobre los 32 bits asignados a el si por lo tanto podemos imprimir un mensaje de peligro y regresar. Nótese que llamamos a printk() mientras mantenemos el spinlock inode_lock - esto está bien porque printk() nunca bloquea, entonces puede ser llamado absolutamente en cualquier contexto (¡incluso desde el manejador de interrupciones!).
  4. Si era la última referencia activa entonces algún trabajo necesita ser realizado.

EL trabajo realizado por iput() en la última referencia del inodo es bastante complejo, por lo tanto lo separaremos en una lista de si misma:

  1. Si i_nlink == 0 (ej. el fichero fué desenlazado mientras lo manteníamos abierto) entonces el inodo es quitado de la tabla hash y de su lista de tipos; si hay alguna página de datos mantenida en la antememoria de páginas para este inodo, son borradas por medio de truncate_all_inode_pages(&inode->i_data). Entonces el método específico del sistema de archivos s_op->delete_inode() es llamado, el cual típicamente borra la copia en disco del inodo. Si no hay un método s_op->delete_inode() registrado por el sistema de ficheros (ej. ramfs) entonces llamamos a clear_inode(inode), el cual llama s_op->clear_inode() si está registrado y si un inodo corresponde a un dispositivo de bloques, esta cuenta de referencia del dispositivo es borrada por bdput(inode->i_bdev).
  2. Si i_nlink != 0 entonces chequeamos si hay otros inodos en el mismo cubo hash y si no hay ninguno, entonces si el inodo no está sucio lo borramos desde su tipo de lista y lo añadimos a la lista inode_unused incrementando inodes_stat.nr_unused. Si hay inodos en el mismo cubo hash entonces los borramos de la lista de tipo y lo añadimos a la lista inode_unused. Si no había ningún inodo (NetApp .snapshot) entonces lo borramos de la lista de tipos y lo limpiamos/destruimos completamente.

3.2 Registro/Desregistro de sistemas de Ficheros

El núcleo Linux suministra un mecanismo para los nuevos sistemas de ficheros para ser escritos con el mínimo esfuerzo. Los motivos históricos para esto son:

  1. En el mundo donde la gente aún usa sistemas operativos no Linux para proteger sus inversiones en el software legado, Linux tiene que suministrar interoperabilidad para soportar una gran multitud de sistemas de ficheros diferentes - la mayoría no merecen existir pero sólo por compatibilidad con los existentes sistemas operativos no Linux.
  2. La interfaz para los escritores de sistemas de ficheros tiene que ser muy simple para que la gente pueda intentar hacer ingeniería inversa con los sistemas de ficheros existentes para escribir versiones de sólo lectura de ellos. Entonces el VFS de Linux hace muy fácil implementar sistemas de ficheros de sólo lectura: el 95% del trabajo está por finalizar añadiéndole un soporte total para escritura. Como un ejemplo concreto leí sistemas de ficheros BFS para Linux en modo sólo lectura en unas 10 horas, pero llevó varias semanas completarlo para tener un soporte total de escritura (e incluso hoy algunos puristas dicen que no está completo porque no tiene soporte de compactación).
  3. La interfaz VFS es exportada, y entonces todos los sistemas de ficheros Linux pueden ser implementados como módulos.

Déjanos considerar los pasos requeridos para implementar un sistema de ficheros bajo Linux. El código para implementar un sistema de ficheros puede ser un módulo dinámicamente cargado o estár estáticamente enlazado en el núcleo, el camino es realizado por Linux trasparentemente. Todo lo que se necesita es rellenar una estructura struct file_system_type y registrarla con el VFS usando la función register_filesystem() como en el siguiente ejemplo de fs/bfs/inode.c:


#include <linux/module.h>
#include <linux/init.h>

static struct super_block *bfs_read_super(struct super_block *, void *, int);

static DECLARE_FSTYPE_DEV(bfs_fs_type, "bfs", bfs_read_super);

static int __init init_bfs_fs(void)
{
        return register_filesystem(&bfs_fs_type);
}

static void __exit exit_bfs_fs(void)
{
        unregister_filesystem(&bfs_fs_type);
}

module_init(init_bfs_fs)
module_exit(exit_bfs_fs)

Las macros module_init()/module_exit() aseguran que, cuando BFS es compilado como un módulo, las funciones init_bfs_fs() y exit_bfs_fs() se convierten en init_module() y cleanup_module() respectivamente; si BFS está estáticamente enlazado en el núcleo el código exit_bfs_fs() lo hace innecesario.

La struct file_system_type es declarada en include/linux/fs.h:


struct file_system_type {
        const char *name;
        int fs_flags;
        struct super_block *(*read_super) (struct super_block *, void *, int);
        struct module *owner;
        struct vfsmount *kern_mnt; /* For kernel mount, if it's FS_SINGLE fs */
        struct file_system_type * next;
};

Los campos anteriores son explicados de esta forma:

El trabajo de la función read_super() es la de rellenar los campos del superbloque, asignando el inodo raiz e inicializando cualquier información privada del sistema de ficheros asociadas por esta instancia montada del sistema de ficheros. Por lo tanto, tipicamente el read_super() hará:

  1. Lee el superbloque desde el dispositivo especificado a través del argumento sb->s_dev, usando la función de la antememoria intermedia bread(). Si se anticipa a leer unos pocos más bloques de metadatos inmediatamente subsecuentes, entonces tiene sentido usar breada() para planificar el leer bloque extra de forma asíncrona.
  2. Verifica que el superbloque contiene el número mágico válido y todo "parece" correcto.
  3. Inicializa sb->s_op para apuntar a la estructura struct super_block_operations. Esta estructura contiene las funciones específicas del sistema de ficheros implementando las operaciones como "leer inodo", "borrar inodo", etc.
  4. Asigna el inodo y dentry raiz usando d_alloc_root().
  5. Si el sistema de ficheros no está montado como sólo lectura entonces establece sb->s_dirt a 1 y marca la antememoria conteniendo el superbloque como sucio (POR HACER: ¿porqué hacemos esto? Yo lo hice en BFS porque MINIX lo hizo ...)

3.3 Administración de Descriptores de Ficheros

Bajo Linux hay varios niveles de rodeos entre el descriptor de ficheros del usuario y la estructura de inodos del núcleo. Cuando un proceso realiza la llamada al sistema open(2), el núcleo devuelve un entero pequeño no negativo el cual puede ser usado para operaciones de E/S subsecuentes en este fichero. Cada estructura de fichero apunta a dentry a través de file->f_dentry. Y cada dentry apunta a un inodo a través de dentry->d_inode.

Cada tarea contiene un campo tsk->files el cual es un puntero a struct files_struct definida en include/linux/sched.h:


/*
 * Abre la estructura tabla del fichero
 */
struct files_struct {
        atomic_t count;
        rwlock_t file_lock;
        int max_fds;
        int max_fdset;
        int next_fd;
        struct file ** fd;      /* actualmente una matriz de descriptores de ficheros */
        fd_set *close_on_exec;
        fd_set *open_fds;
        fd_set close_on_exec_init;
        fd_set open_fds_init;
        struct file * fd_array[NR_OPEN_DEFAULT];
};

El file->count es una cuenta de referencia, incrementada por get_file() (usualmente llamada por fget()) y decrementada por fput() y por put_filp(). La diferencia entre fput() y put_filp() es que fput() hace más trabajo usualmente necesitado para ficheros regulares, como la liberación de conjuntos de bloqueos, liberación de dentry, etc, mientras que put_filp() es sólo para manipular las estructuras de tablas de ficheros, esto es, decrementa la cuenta, quita el fichero desde anon_list y lo añade a la free_list, bajo la protección del spinlock files_lock.

El tsk->files puede ser compartido entre padre e hijo si el hilo hijo fue creado usando la llamada al sistema clone() con la bandera CLONE_FILES establecida en los argumentos de las banderas de clone. Esto puede ser visto en kernel/fork.c:copy_files() (llamada por do_fork()) el cual sólo incrementa el file->count si CLONE_FILES está establecido, en vez de la copia usual de la tabla de descriptores de ficheros en la tradición respetable en el tiempo de los clásicos fork(2) de UNIX.

Cuando un fichero es abierto, la estructura del fichero asignada para él es instalada en el slot current->files->fd[fd] y un bit fd es establecido en el bitmap current->files->open_fds. Todo esto es realizado bajo la protección de escritura del spinlock read-write current->files->file_lock. Cuando el descriptor es cerrado un bit fd es limpiado en current->files->open_fds y current->files->next_fd es establecido igual a fd como una indicación para encontrar el primer descriptor sin usar la próxima vez que este proceso quiera abrir un fichero.

3.4 Administración de estructuras de ficheros

La estructura de ficheros es declarada en include/linux/fs.h:


struct fown_struct {
        int pid;                /* pid o -pgrp donde SIGIO debería de ser enviado  */
        uid_t uid, euid;        /* uid/euid del proceso estableciendo el dueño */
        int signum;             /* posix.1b rt señal para ser enviada en ES */
};

struct file {
        struct list_head        f_list;
        struct dentry           *f_dentry;
        struct vfsmount         *f_vfsmnt;
        struct file_operations  *f_op;
        atomic_t                f_count;
        unsigned int            f_flags;
        mode_t                  f_mode;
        loff_t                  f_pos;
        unsigned long           f_reada, f_ramax, f_raend, f_ralen, f_rawin;
        struct fown_struct      f_owner;
        unsigned int            f_uid, f_gid;
        int                     f_error;

        unsigned long           f_version;
  
        /* necesitado para este controlador tty, y quizás por otros */
        void                    *private_data; 
};

Déjanos mirar varios de los campos de struct file:

  1. f_list: este campo enlaza la estructura del fichero con una (y sólo una) de las listas: a) sb->s_files lista de todos los ficheros abiertos en este sistema de ficheros, si el correspondiente inodo no es anónimo, entonces dentry_open() (llamado por filp_open()) enlaza el fichero en esta lista; b) fs/file_table.c:free_list, conteniendo las estructuras de ficheros sin utilizar; c) fs/file_table.c:anon_list, cuando una nueva estructura de ficheros es creada por get_empty_filp() es colocada en esta lista. Todas estas listas son protegidas por el spinlock files_lock.
  2. f_dentry: la dentry (entrada de directorio) correspondiente a este fichero. La dentry es creada en tiempo de búsqueda de nombre y datos (nameidata) por open_namei() (o más bien path_walk() la cual lo llama a él) pero el campo actual file->f_dentry es establecido por dentry_open() para contener la dentry de esta forma encontrada.
  3. f_vfsmnt: el puntero a la estructura vfsmount del sistema de ficheros conteniendo el fichero. Esto es establecido por dentry_open(), pero es encontrado como una parte de la búsqueda de nameidata por open_namei() (o más bien path_init() la cual lo llama a él).
  4. f_op: el puntero a file_operations, el cual contiene varios métodos que pueden ser llamados desde el fichero. Esto es copiado desde inode->i_fop que es colocado aquí durante la búsqueda nameidata. Miraremos los métodos file_operations en detalle más tarde en esta sección.
  5. f_count: cuenta de referencia manipulada por get_file/put_filp/fput.
  6. f_flags: banderas O_XXX desde la llamada al sistema open(2) copiadas allí (con ligeras modificaciones de filp_open()) por dentry_open() y después de limpiar O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC - no hay sitio para almacenar estas banderas permanentemente ya que no pueden ser modificadas por las llamadas fcntl(2) F_SETFL (o consultadas por F_GETFL).
  7. f_mode: una combinación de banderas del espacio de usuario y modos, establecido por dentry_open(). El punto de conversión es almacenar los accesos de lectura y escritura en bits separados, por lo tanto uno los chequearía fácilmente como (f_mode & FMODE_WRITE) y (f_mode & FMODE_READ).
  8. f_pos: la actual posición en el fichero para la siguiente lectura o escritura. Bajo i386 es del tipo long long, esto es un valor de 64 bits.
  9. f_reada, f_ramax, f_raend, f_ralen, f_rawin: para soportar readahead - muy complejo para ser discutido por mortales ;)
  10. f_owner: dueño del archivo de E/S a recibir las modificaciones de E/S asíncronas a través del mecanismo SIGIO (ver fs/fcntl.c:kill_fasync()).
  11. f_uid, f_gid - establece el identificador del usuario y el identificador del grupo del proceso que abrió el fichero, cuando la estructura del fichero es creada por get_empty_filp(). Si el fichero es un conector, usado por netfilter ipv4.
  12. f_error: usado por el cliente NFS para devolver errores de escritura. Esto es establecido en fs/nfs/file.c y chequeado en mm/filemap.c:generic_file_write().
  13. f_version - mecanismo de versionado para la invalidación de antememorias, incrementado (usando un event global) cuando cambia f_pos.
  14. private_data: datos privados para cada fichero, los cuales pueden ser usados por los sistemas de ficheros (ej. coda almacena las credenciales aquí) o por otros controladores de dispositivos. Los controladores de dispositivos (en la presencia de devfs) pueden usar este campo para diferenciar entre múltiples instancias, en vez del clásico número menor codificado en file->f_dentry->d_inode->i_rdev.

Ahora déjanos mirar en la estructura file_operations la cual contiene los métodos que serán llamados en los archivos. Déjanos recalcar que es copiado desde inode->i_fop donde es establecido por el método s_op->read_inode(). Se declara en include/linux/fs.h:


struct file_operations {
        struct module *owner;
        loff_t (*llseek) (struct file *, loff_t, int);
        ssize_t (*read) (struct file *, char *, size_t, loff_t *);
        ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
        int (*readdir) (struct file *, void *, filldir_t);
        unsigned int (*poll) (struct file *, struct poll_table_struct *);
        int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
        int (*mmap) (struct file *, struct vm_area_struct *);
        int (*open) (struct inode *, struct file *);
        int (*flush) (struct file *);
        int (*release) (struct inode *, struct file *);
        int (*fsync) (struct file *, struct dentry *, int datasync);
        int (*fasync) (int, struct file *, int);
        int (*lock) (struct file *, int, struct file_lock *);
        ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
        ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
};

  1. owner: un puntero al módulo que es dueño del subsistema en cuestión. Sólo los controladores necesitan establecerlo a THIS_MODULE, los sistemas de ficheros puede felizmente ignorarlos porque sus cuentas de módulos son controladas en el tiempo de montaje/desmontaje, en cambio los controladores necesitan controlarlo en tiempo de apertura/liberación.
  2. llseek: implementa la llamada al sistema lseek(2). Usualmente es omitida y es usada fs/read_write.c:default_llseek(), la cual hace lo correcto (POR HACER: fuerza a todos aquellos que establecen a NULL actualmente a usar default_llseek - que es el camino por el que salvamos una if() en llseek()).
  3. read: implementa la llamada al sistema read(2). Los sistemas de ficheros pueden usar mm/filemap.c:generic_file_read() para ficheros regulares y fs/read_write.c:generic_read_dir() (la cual simplemente devuelve -EISDIR) para directorios aquí.
  4. write: implementa la llamada al sistema write(2). Los sistemas de ficheros pueden usar mm/filemap.c:generic_file_write() para ficheros regulares e ignorarlo para directorios aquí.
  5. readdir: usado por los sistema de ficheros. Ignorado por los ficheros regulares e implementa las llamadas al sistema readdir(2) y getdents(2) para directorios.
  6. poll: implementa las llamadas al sistema poll(2) y select(2).
  7. ioctl: implementa el controlador o los ioctls específicos del sistema de ficheros. Nótese que los ioctls genéricos de los ficheros como FIBMAP, FIGETBSZ, FIONREAD son implementados por niveles más altos y por lo tanto nunca leerán el método f_op->ioctl().
  8. mmap: implementa la llamada al sistema mmap(2). Los sistemas de ficheros pueden usar aquí generic_file_mmap para ficheros regulares e ignorarlo en los directorios.
  9. open: llamado en tiempo de open(2) por dentry_open(). Los sistemas de ficheros raramente usan esto, ej. coda intenta almacenar el fichero localmente en tiempo de apertura.
  10. flush: llamada en cada close(2) de este fichero, no necesariamente el último (ver el método release() a continuación). El único sistema de ficheros que lo utiliza es en un cliente NFS para pasar a disco todas las páginas sucias. Nótese que esto puede devolver un error el cual será retornado al espacio de usuario que realizó la llamada al sistema close(2).
  11. release:llamado por la última close(2) de este fichero, esto es cuando file->f_count llega a 0. Aunque definido como un entero de retorno, el valor de retorno es ignorado por VFS (ver fs/file_table.c:__fput()).
  12. fsync: mapea directamente a las llamadas al sistema fsync(2)/fdatasync(2), con el último argumento especificando cuando es fsync o fdatasync. Por lo menos no se realiza trabajo por VFS sobre esto, excepto el mapear el descriptor del fichero a una estructura de fichero (file = fget(fd)) y bajar/subir el semáforo inode->i_sem. El sistema de ficheros Ext2 ignora el último argumento y realiza lo mismo para fsync(2) y fdatasync(2).
  13. fasync: este método es llamado cuando cambia file->f_flags & FASYNC.
  14. lock: parte del mecanismo de bloqueo de la región del fcntl(2) POSIX de la porción específica del sistema de ficheros. El único fallo aquí es porque es llamado antes por una porción independiente del sistema de ficheros (posix_lock_file()), si tiene éxito pero el código de bloqueo estandart POSIX falla, entonces nunca será desbloqueado en un nivel dependiente del sistema de ficheros...
  15. readv: implementa la llamada al sistema readv(2).
  16. writev: implementa la llamada al sistema writev(2).

3.5 Administración de Puntos de Montaje y Superbloque

Bajo Linux, la información sobre los sistemas de ficheros montados es mantenida en dos estructuras separadas - super_block y vfsmount. El motivo para esto es que Linux permite montar el mismo sistema de ficheros (dispositivo de bloque) bajo múltiples puntos de montaje, lo cual significa que el mismo super_block puede corresponder a múltiples estructuras vfsmount.

Déjanos mirar primero en struct super_block, declarado en include/linux/fs.h:


struct super_block {
        struct list_head        s_list;         /* Mantiene esto primero */
        kdev_t                  s_dev;
        unsigned long           s_blocksize;
        unsigned char           s_blocksize_bits;
        unsigned char           s_lock;
        unsigned char           s_dirt;
        struct file_system_type *s_type;
        struct super_operations *s_op;
        struct dquot_operations *dq_op;
        unsigned long           s_flags;
        unsigned long           s_magic;
        struct dentry           *s_root;
        wait_queue_head_t       s_wait;

        struct list_head        s_dirty;        /* inodos sucios */
        struct list_head        s_files;

        struct block_device     *s_bdev;
        struct list_head        s_mounts;       /* vfsmount(s) de este */
        struct quota_mount_options s_dquot;     /* Opciones Específicas de Diskquota */

       union {
                struct minix_sb_info    minix_sb;
                struct ext2_sb_info     ext2_sb;
                ..... todos los sistemas de archivos necesitan sb-private ...
                void                    *generic_sbp;
        } u;
       /*
         * El siguiente campo es *sólo* para VFS. Los sistemas de ficheros
         * no tienen trabajo alguno mirando en él. Has sido avisado.
         */
        struct semaphore s_vfs_rename_sem;      /* Truco */

        /* El siguiente campo es usado por knfsd cuando convierte un
         * manejador de ficheros (basado en el número de inodo) en
         * una dentry. Tal como construye un camino en el árbol dcache
         * desde el fondo hasta arriba, quizás exista durante algún 
         * tiempo un subcamino de dentrys que no están conectados al
         * árbol principal. Este semáforo asegura que hay sólo
         * siempre un camino libre por sistema de ficheros. Nótese que
         * los ficheros no conectados (o otros no directorios) son
         * permitidos, pero no los directorios no conectados.
         */
        struct semaphore s_nfsd_free_path_sem;
};

Las diversos campos en la estructura super_block son:

  1. s_list: una lista doblemente enlazada de todos los superbloques activos; nótese que no he dicho "de todos los sistemas de ficheros montados" porque bajo Linux uno puede tener múltiples instancias de un sistema de ficheros montados correspondientes a un superbloque simple.
  2. s_dev: para sistemas de ficheros que requieren un bloque para ser montado en él. Esto es para los sistemas de ficheros FS_REQUIRES_DEV, esto es la i_dev del dispositivo de bloques. Para otros (llamados sistemas de ficheros anónimos) esto es un entero MKDEV(UNNAMED_MAJOR, i) donde i es el primer bit no establecido en la matriz unnamed_dev_in_use, entre 1 y 255 incluidos. Ver fs/super.c:get_unnamed_dev()/put_unnamed_dev(). Ha sido sugerido muchas veces que los sistemas de ficheros anónimos no deberían de usar el campo s_dev.
  3. s_blocksize, s_blocksize_bits: tamaño del bloque y log2(tamaño del bloque).
  4. s_lock: indica cuando un superbloque está actualmente bloqueado por lock_super()/unlock_super().
  5. s_dirt: establece cuando el superbloque está modificado, y limpiado cuando es vuelto a ser escrito a disco.
  6. s_type: puntero a struct file_system_type del sistema de ficheros correspondiente. El método read_super() del sistema de ficheros no necesita ser establecido como VFS fs/super.c:read_super(), lo establece para ti si el read_super() que es específico del sistema de ficheros tiene éxito, y se reinicializa a NULL si es que falla.
  7. s_op: puntero a la estructura super_operations, la cual contiene métodos específicos del sistema de ficheros para leer/escribir inodos, etc. Es el trabajo del método read_super() del sistema de ficheros inicializar s_op correctamente.
  8. dq_op: operaciones de cuota de disco.
  9. s_flags: banderas de superbloque.
  10. s_magic: número mágico del sistema de ficheros. Usado por el sistema de ficheros de minix para diferenciar entre múltiples tipos del mismo.
  11. s_root: dentry de la raiz del sistema de ficheros. Es trabajo de read_super() leer el inodo raiz desde el disco y pasárselo a d_alloc_root() para asignar la dentry e instanciarlo. Algunos sistemas de ficheros dicen "raiz" mejor que "/" y por lo tanto usamos la función más genérica d_alloc() para unir la dentry a un nombre, ej. pipefs se monta a si mismo en "pipe:" como su raiz en vez de "/".
  12. s_wait: cola de espera de los procesos esperando para que el superbloque sea desbloqueado.
  13. s_dirty: una lista de todos los inodos sucios. Recalcar que si un inodo está sucio (inode->i_state & I_DIRTY) entonces su lista sucia específica del superbloque es enlazada a través de inode->i_list.
  14. s_files: una lista de todos los ficheros abiertos en este superbloque. Util para decidir cuándo los sistemas de archivos pueden ser remontados como de sólo lectura, ver fs/file_table.c:fs_may_remount_ro() el cual va a través de la lista sb->s_files y deniega el remontar si hay ficheros abiertos para escritura (file->f_mode & FMODE_WRITE) o ficheros con desenlaces pendientes (inode->i_nlink == 0).
  15. s_bdev: para FS_REQUIRES_DEV, esto apunta a la estructura block_device describiendo el dispositivo en el que el sistema de ficheros está montado.
  16. s_mounts: una lista de todas las estructuras vfsmount, una por cada instancia montada de este superbloque.
  17. s_dquot: más miembros de diskquota.

Las operaciones de superbloque son descritas en la estructura super_operations declarada en include/linux/fs.h:


struct super_operations {
        void (*read_inode) (struct inode *);
        void (*write_inode) (struct inode *, int);
        void (*put_inode) (struct inode *);
        void (*delete_inode) (struct inode *);
        void (*put_super) (struct super_block *);
        void (*write_super) (struct super_block *);
        int (*statfs) (struct super_block *, struct statfs *);
        int (*remount_fs) (struct super_block *, int *, char *);
        void (*clear_inode) (struct inode *);
        void (*umount_begin) (struct super_block *);
};

  1. read_inode: lee el inodo desde el sistema de archivos. Es sólo llamado desde fs/inode.c:get_new_inode(), y desde iget4() (y por consiguiente iget()). Si un sistema de ficheros quiere usar iget() entonces read_inode() debe de ser implementado - en otro caso get_new_inode() fallará. Mientras el inodo está siendo leido está bloqueado (inode->i_state = I_LOCK). Cuando la función regresa, todos los que están esperando en inode->i_wait son despertados. El trabajo del método read_inode() del sistema de ficheros es localizar el bloque del disco que contiene el inodo a ser leído y usar la funcion de la antememoria intermedia bread() para leerlo e inicializar varios campos de la estructura de inodos, por ejemplo el inode->i_op y inode->i_fop para que los niveles VFS conozcan qué operaciones pueden ser efectuadas en el inodo o fichero correspondiente. Los sistemas de ficheros que no implementan read_inode() son ramfs y pipefs. Por ejemplo, ramfs tiene su propia función de generación de inodos ramfs_get_inode() con todas las operaciones de inodos llamándola cuando se necesita.
  2. write_inode: escribe el inodo de vuelta al disco. Similar a read_inode() en que necesita localizar el bloque relevante en el disco e interactuar con la antememoria intermedia llamando a mark_buffer_dirty(bh). Este método es llamado en los inodos sucios (aquellos marcados como sucios por mark_inode_dirty()) cuando el inodo necesita ser sincronizado individualmente o como parte de la actualizacion entera del sistema de ficheros.
  3. put_inode: llamado cuando la cuenta de referencia es decrementada.
  4. delete_inode: llamado cuando inode->i_count y inode->i_nlink llegan a 0. El sistema de ficheros borra la copia en disco del inodo y llama a clear_inode() en el inodo VFS para "terminar con él con el perjuicio extremo".
  5. put_super: llamado en las últimas etapas de la llamada al sistema umount(2) para notificar al sistema de ficheros que cualquier información mantenida por el sistema de ficheros sobre esa instancia tiene que ser liberada. Típicamente esto brelse() el bloque conteniendo el superbloque y kfree() cualesquiera bitmaps asignados para bloques libres, inodos, etc.
  6. write_super: llamado cuando el superbloque necesita ser vuelto a escribir en el disco. Debería de encontrar el bloque conteniendo el superbloque (usualmente mantenido en el área sb-private) y mark_buffer_dirty(bh). También debería de limpiar la bandera sb->s_dirt.
  7. statfs: implementa las llamadas al sistema fstatfs(2)/statfs(2). Nótese que el puntero a struct statfs pasado como argumento, es el puntero del núcleo, no un puntero del usuario, por lo tanto no necesitamos hacer ninguna E/S al espacio de usuario. Si no está implementada entonces statfs(2) fallará con ENOSYS.
  8. remount_fs: llamado cuando el sistema de ficheros está siendo remontado.
  9. clear_inode: llamado desde el nivel VFS clear_inode(). Los sistemas que atacan datos privados a la estructura del inodo (a través del campo generic_ip) deben liberarse aquí.
  10. umount_begin: llamado durante el desmontaje forzado para notificarlo al sistema de ficheros de antemano, por lo tanto puede ser lo mejor para asegurarse que nada mantiene al sistema de ficheros ocupado. Actualmente usado sólo por NFS. Esto no tiene nada que hacer con la idea del soporte de desmontaje forzado del nivel genérico de VFS

Por lo tanto, déjanos mirar qué pasa cuando montamos un sistema de ficheros en disco (FS_REQUIRES_DEV). La implementación de la llamada al sistema mount(2) está en fs/super.c:sys_mount() que es justo un envoltorio que copia las opciones, el tipo del sistema de ficheros y el nombre del dispositivo para la función do_mount(), la cual realiza el trabajo real:

  1. El controlador del sistema de ficheros es cargado si se necesita y la cuenta de referencia del módulo es incrementada. Nótese que durante la operación de montaje, la cuenta del sistema de ficheros es incrementada dos veces - una vez por do_mount() llamando a get_fs_type() y otra vez por get_sb_dev() llamando a get_filesystem() si read_super() tuvo éxito. El primer incremento es para prevenir la descarga del módulo mientras estamos dentro del método read_super(), y el segundo incremento es para indicar que el módulo está en uso por esta instancia montada. Obviamente, do_mount() decrementa la cuenta antes de regresar, por lo tanto, después de todo, la cuenta sólo crece en 1 después de cada montaje.
  2. Desde, que en nuestro caso, fs_type->fs_flags & FS_REQUIRES_DEV es verdad, el superbloque es inicializado por una llamada a get_sb_bdev(), la cual obtiene la referencia al dispositivo de bloques e interactúa con el método read_super() del sistema de ficheros para rellenar el superbloque. Si todo va vien, la estructura super_block es inicializada y tenemos una referencia extra al módulo del sistema de ficheros y una referencia al dispositivo de bloques subyacente.
  3. Una nueva estructura vfsmount es asignada y enlazada a la lista sb->s_mounts y a la lista global vfsmntlist. El campo vfsmount de mnt_instances nos permite encontrar todas las instancias montadas en el mismo superbloque que este. El campo mnt_list nos permite encontrar todas las instancias para todos los superbloques a lo largo del sistema. El campo mnt_sb apunta a este superbloque y mnt_root tiene una nueva referencia a la dentry sb->s_root.

3.6 Ejemplo de un Sistema de Ficheros Virtual: pipefs

Como un ejemplo simple del sistema de ficheros de Linux que no requiere un dispositivo de bloque para montar, déjanos considerar pipefs desde fs/pipe.c. El preámbulo del sistema de ficheros es bastante directo y requiere una pequeña explicación:


static DECLARE_FSTYPE(pipe_fs_type, "pipefs", pipefs_read_super,
        FS_NOMOUNT|FS_SINGLE);

static int __init init_pipe_fs(void)
{
        int err = register_filesystem(&pipe_fs_type);
        if (!err) {
                pipe_mnt = kern_mount(&pipe_fs_type);
                err = PTR_ERR(pipe_mnt);
                if (!IS_ERR(pipe_mnt))
                        err = 0;
        }
        return err;
}

static void __exit exit_pipe_fs(void)
{
        unregister_filesystem(&pipe_fs_type);
        kern_umount(pipe_mnt);
}

module_init(init_pipe_fs)
module_exit(exit_pipe_fs)

El sistema de ficheros es del tipo FS_NOMOUNT|FS_SINGLE, lo que significa que no puede ser montado desde el espacio de usuario y sólo puede haber uno en el sistema. El fichero FS_SINGLE también significa que debe de ser montado a través de kern_mount() después de que haya sido registrado con éxito a través de register_filesystem(), lo cual es exactamente lo que pasa en init_pipe_fs(). El único fallo en esta función es que si kern_mount() falla (ej. porque kmalloc() falló en add_vfsmnt()) entonces el sistema de ficheros es dejado como registrado pero la inicialización del módulo falla. Esto causará que cat /proc/filesystems falle (justamente acabo de enviar un parche a Linus mencionándole que esto no es un fallo real hoy en día porque pipefs no puede ser compilado como módulo, debería de ser escrito desde el punto de vista de que en el futuro pudiera ser modularizado).

El resultado de register_filesystem() es que pipe_fs_type es enlazado en la lista file_systems, por lo tanto uno puede leer /proc/filesystems y encontrar la entrada "pipefs" allí con la bandera "nodev" indicando que FS_REQUIRES_DEV no fue establecida. El archivo /proc/filesystems debería realmente de ser capaz de soportar todas las nuevas banderas FS_ (y yo he hecho un parche que lo hace) pero no puede ser realizado porque hará fallar a todas las aplicaciones de usuario que lo utilicen. A pesar de que los interfaces del núcleo Linux cambian cada minuto (sólo para mejor) cuando se refiere a la compatibilidad del espacio de usuario, Linux es un sistema operativo muy conservador que permite que muchas aplicaciones sean usadas durante un largo periodo de tiempo sin ser recompiladas.

El resultado de kern_mount() es que:

  1. Un nuevo número de dispositvo sin nombre (anónimo) es asignado estableciendo un bit en el bitmap unnamed_dev_in_use; si no hay más bits entonces kern_mount() fallará con EMFILE.
  2. Una nueva estructura superbloque es asignada por medio de get_empty_super(). La función get_empty_super() camina a través de las cabeceras de las lista de superbloques por super_block y busca una entrada vacía, esto es s->s_dev == 0. Si no se encuentra dicho superbloque vacío entonces uno nuevo es asignado usando kmalloc() con la prioridad GFP_USER. El número máximo de superbloques en el sistema es chequeado en get_empty_super(), por lo tanto empieza fallando, uno puede modificar el parámetro ajustable /proc/sys/fs/super-max.
  3. Un método específico del sistema de ficheros pipe_fs_type->read_super(), esto es pipefs_read_super(), es invocada, la cual asigna el inodo y la dentry raiz sb->s_root, y establece sb->s_op para ser &pipefs_ops.
  4. Entonces kern_mount() llama a add_vfsmnt(NULL, sb->s_root, "none") la cual asigna una nueva estructura vfsmount y la enlaza en vfsmntlist y sb->s_mounts.
  5. El pipe_fs_type->kern_mnt es establecido a esta nueva estructura vfsmount y es devuelta. El motivo por el que el valor de retorno de kern_mount() es una estructura vfsmount es porque incluso los sistemas de ficheros FS_SINGLE pueden ser montados múltiples veces y por lo tanto sus mnt->mnt_sb deberían apuntar a la misma cosa, que sería tonto devolverla desde múltiples llamadas a kern_mount().

Ahora que el sistema de ficheros está registrado y montado dentro del núcleo podemos usarlo. El punto de entrada en el sistema de ficheros pipefs es la llamada al sistema pipe(2), implementada por una función dependiente de la arquitectura sys_pipe(), pero el trabajo real es realizado por un función portable fs/pipe.c:do_pipe(). Déjanos mirar entonces en do_pipe(). La interacción con pipefs sucede cuando do_pipe() llama a get_pipe_inode() para asignar un nuevo inodo pipefs. Para este inodo, inode->i_sb es establecido al superbloque de pipefs pipe_mnt->mnt_sb, las operaciones del archivo i_fop son establecidas a rdwr_pipe_fops y el número de lectores y escritores (mantenidos en inode->i_pipe) es establecido a 1. El motivo por el que hay un campo de inodos separado i_pipe en vez de mantenerlo en la unión fs-private es que pipes y FIFOs comparten el mismo código y los FIFOs puede existir en otros sistemas de ficheros los cuales usan otros caminos de acceso con la misma unión, lo cual en C es muy malo y puede trabajar sólo con pura suerte. Por lo tanto, sí, los núcleos 2.2.x trabajan por pura suerte y pararán de trabajar tan pronto como tu retoques ligeramente los campos en el inodo.

Cada llamada al sistema pipe(2) incrementa una cuenta de referencia en la instancia de montaje pipe_mnt.

Bajo Linux, los pipes no son simétricos (pipes STREAM o bidireccionales) esto es, dos caras del mismo fichero tienes diferentes operaciones file->f_op - la read_pipe_fops y write_pipe_fops respectivamente. La escritura en la cara de lectura devuelve un EBADF y lo mismo si se lee en la cara de escritura.

3.7 Ejemplo de Sistema de Ficheros de Disco: BFS

Como un simple ejemplo de un sistema de ficheros Linux en disco, déjanos considerar BFS. El preámbulo del módulo de BFS está en fs/bfs/inode.c:


static DECLARE_FSTYPE_DEV(bfs_fs_type, "bfs", bfs_read_super);

static int __init init_bfs_fs(void)
{
        return register_filesystem(&bfs_fs_type);
}

static void __exit exit_bfs_fs(void)
{
        unregister_filesystem(&bfs_fs_type);
}

module_init(init_bfs_fs)
module_exit(exit_bfs_fs)

Una declaracion especial de la macro del sistema de ficheros DECLARE_FSTYPE_DEV() es usada, la cual establece el fs_type->flags a FS_REQUIRES_DEV para significar que BFS requiere un dispositivo de bloque real para ser montado.

La función de inicialización del módulo registra el sistema de ficheros con VFS y la función de limpieza (sólo presente cuando BFS está configurada para ser un módulo) lo desregistra.

Con el sistema de ficheros registrado, podemos proceder a montarlo, lo cual invocará al método fs_type->read_super() que es implementado en fs/bfs/inode.c:bfs_read_super().. El realiza lo siguiente:

  1. set_blocksize(s->s_dev, BFS_BSIZE): desde que nosotros vamos a interactuar con la capa de dispositivos de bloque a través de la antememoria intermedia, debemos inicializar unas pocas cosas, esto es, establecer el tamaño del bloque y también informar a VFS a través de los campos s->s_blocksize y s->s_blocksize_bits.
  2. bh = bread(dev, 0, BFS_BSIZE): leemos el bloque 0 del dispositivo a través de s->s_dev. Este bloque es el superbloque del sistema.
  3. El superbloque es validado contra el número BFS_MAGIC y, si es válido, es almacenado en el campo sb-private s->su_sbh (el cual es realmente s->u.bfs_sb.si_sbh).
  4. Entonces asignamos el bitmap del inodo kmalloc(GFP_KERNEL) y limpiamos todos sus bits a 0, excepto los dos primeros, los cuales estableceremos a 1 para indicar que nunca deberemos asignar los inodos 0 y 1. El inodo 2 es la raiz y el correspondiente bit será establecido a 1 unas pocas lineas después de cualquier forma - ¡el sistema de ficheros debe de tener un inodo raiz válido en tiempo de montaje!
  5. Entonces inicializamos s->s_op, lo cual significa que podemos desde este punto llamar a la memoria intermedia de inodos a través de iget(), lo cual resulta en la invocación de s_op->read_inode(). Esto encuentra el bloque que contiene el inodo especificado (por inode->i_ino y inode->i_dev) y lo lee. Si fallamos al obtener el inodo raiz entonces liberamos el bitmap de inodos y descargaremos la antememoria de superbloque a la antememoria intermedia y devolveremos NULL. Si el inodo raiz fue leido correctamente, entonces asignamos una dentry con el nombre / (como convirtiéndolo en raiz) y lo instanciamos con este inodo.
  6. Ahora vamos a través de todos los inodos del sistema de ficheros y los leemos en orden a establecer los bits correspondientes en nuestro bitmap interno de inodos y también calculamos otros parámetros internos como el desplazamiento del último inodo y el comienzo/final del último fichero. Cada inodo que leemos es devuelto atrás a la memoria intermedia de inodos a través de iput() - no mantenemos una referencia a él más tiempo del necesario.
  7. Si el sistema de ficheros no fue montado como de sólo lectura, marcamos la memoria intermedia del superbloque como sucio y establecemos la bandera s->s_dirt (POR HACER: ¿Porqué hago esto? Originalmente, lo hice porque lo hacía minix_read_super() pero ni minix ni BFS parecen modificar el superbloque en el read_super()).
  8. Todo está bién, por lo tanto regresamos atrás a este superbloque inicializado para el llamante en el nivel VFS, esto es, fs/super.c:read_super().

Después de que la función read_super() regrese con éxito, VFS obtiene la referencia al módulo del sistema de ficheros a través de la llamada a get_filesystem(fs_type) en fs/super.c:get_sb_bdev() y una referencia al dispositivo de bloques.

Ahora, déjanos examinar qué pasa cuando hacemos una E/S en el sistema de ficheros. Ya hemos examinado cómo los inodos son leidos cuando iget() es llamado y cómo son quitados en iput(). Leyendo inodos, configura entre otras cosas, inode->i_op y inode->i_fop; abriendo un fichero propagará inode->i_fop en file->f_op.

Déjanos examinar el camino de código de la llamada al sistema link(2). La implementación de la llamada al sistema está en fs/namei.c:sys_link():

  1. Los nombres del espacio de usuario son copiados en el espacio del núcleo por medio de la función getname() la cual realiza el chequeo de errores.
  2. Estos nombres son convertidos a datos usando path_init()/path_walk() interactuando con dcache. El resultado es almacenado en las estructuras old_nd y nd.
  3. Si old_nd.mnt != nd.mnt entonces "enlace a través de dispositvos" EXDEV es devuelto - uno no puede enlazar entre sistemas de ficheros, en Linux esto se traduce en - uno no puede enlazar entre múltiples instancias de un sistema de ficheros (o, en particular entre sistemas de ficheros).
  4. Una nueva dentry es creada correspondiente a nd por lookup_create() .
  5. Una función genérica vfs_link() es llamada, la cual chequea si podemos crear una nueva entrada en el directorio e invoca el método dir->i_op->link(), que nos trae atrás a la función específica del sistema de ficheros fs/bfs/dir.c:bfs_link().
  6. Dentro de bfs_link(), chequeamos si estamos intentando enlazar un directorio, y si es así, lo rechazamos con un error EPERM. Este es el mismo comportamiento que el estándar (ext2).
  7. Intentamos añadir una nueva entrada de directorio al directorio especificado por la función de ayuda bfs_add_entry() la cual va a través de todas las entradas buscando un slot sin usar (de->ino == 0) y, cuando lo encuentra, escribe en el par nombre/inodo en el bloque correspondiente y lo marca como sucio (a una prioridad no-superbloque).
  8. Si hemos añadido con éxito la entrada de directorio entonces no hay forma de fallar la operación y por lo tanto incrementamos inode->i_nlink, actualizamos inode->i_ctime y marcamos este inodo como sucio a la vez que instanciamos la nueva dentry con el inodo.

Otras operaciones de inodos relacionadas como unlink()/rename() etc. trabajan en una forma similar, por lo tanto no se gana mucho examinándolas a todas ellas en detalle.

3.8 Dominios de Ejecución y Formatos Binarios

Linux soporta la carga de aplicaciones binarias de usuario desde disco. Más interesantemente, los binarios pueden ser almacenados en formatos diferentes y la respuesta del sistema operativo a los programas a través de las llamadas al sistema pueden desviarla de la norma (la norma es el comportamiento de Linux) tal como es requerido, en orden a emular los formatos encontrados en otros tipos de UNIX (COFF, etc) y también emular el comportamiento de las llamadas al sistema de otros tipos (Solaris, UnixWare, etc). Esto es para lo que son los dominios de ejecución y los formatos binarios.

Cada tarea Linux tiene una personalidad almacenada en su task_struct (p->personality). Las personalidades actualmente existentes (en el núcleo oficial o en el parche añadido) incluyen soporte para FreeBSD, Solaris, UnixWare, OpenServer y algunos otros sistemas operativos populares. El valor de current->personality es dividido en dos partes:

  1. tres bytes altos - emulación de fallos: STICKY_TIMEOUTS, WHOLE_SECONDS, etc.
  2. byte bajo - personalidad propia, un número único.

Cambiando la personalidad, podemos cambiar la forma en la que el sistema operativo trata ciertas llamadas al sistema, por ejemplo añadiendo una STICKY_TIMEOUT a current->personality hacemos que la llamada al sistema select(2) preserve el valor del último argumento (timeout) en vez de almacenar el tiempo no dormido. Algunos programas defectuosos confían en sistemas operativos defectuosos (no Linux) y por lo tanto suministra una forma para emular fallos en casos donde el código fuente no está disponible y por lo tanto los fallos no pueden ser arreglados.

El dominio de ejecución es un rango contiguo de personalidades implementadas por un módulo simple. Usualmente un dominio de ejecución simple implementa una personalidad simple, pero a veces es posible implementar personalidades "cerradas" en un módulo simple sin muchos condicionantes.

Los dominios de ejecución son implementados en kernel/exec_domain.c y fueron completamente reescritos para el núcleo 2.4, comparado con el 2.2.x. La lista de dominios de ejecución actualmente soportada por el núcleo, a lo largo del rango de personalidades que soportan, está disponible leyendo el archivo /proc/execdomains. Los dominios de ejecución, excepto el PER_LINUX, pueden ser implementados como módulos dinámicamente cargados.

La interfaz de usuario es a través de la llamada al sistema personality(2), la cual establece la actual personalidad del proceso o devuelve el valor de current->personality si el argumento es establecido a una personalidad imposible. Obviamente, el comportamiento de esta llamada al sistema no depende de la personalidad.

La interfaz del núcleo para el registro de dominios de ejecución consiste en dos funciones:

El motivo por el que exec_domains_lock es read-write es que sólo las peticiones de registro y desregistro modifican la lista, mientras haciendo cat /proc/filesystems llama fs/exec_domain.c:get_exec_domain_list(), el cual necesita sólo acceso de lectura a la lista. Registrando un nuevo dominio de ejecución define un "manejador lcall7" y un mapa de conversión de número de señales. Actualmente, el parche ABI extiende este concepto a los dominios de ejecución para incluir información extra (como opciones de conector, tipos de conector, familia de direcciones y mapas de números de errores).

Los formatos binarios son implementados de una forma similar, esto es, una lista simplemente enlazada de formatos es definida en fs/exec.c y es protegida por un cierre read-write binfmt_lock. Tal como con exec_domains_lock, el binfmt_lock es tomado para leer en la mayoría de las ocasiones excepto para el registro/desregistro de los formatos binarios. Registrando un nuevo formato binario intensifica la llamada al sistema execve(2) con nuevas funciones load_binary()/load_shlib(). Al igual que la habilidad para core_dump(). El método load_shlib() es usado sólo por la vieja llamada al sistema uselib(2) mientras que el método load_binary() es llamada por el search_binary_handler() desde do_execve() el cual implementa la llamada al sistema execve(2).

La personalidad del proceso está determinada por el formato binario cargado por el método del correspondiente formato load_binary() usando algunas heurísticas. Por ejemplo, para determinar los binarios UnixWare7, uno primero marca el binario usando la utilidad elfmark(1), la cual establece la cabecera de ELF e_flags al valor mágico 0x314B4455, el cual es detectado en tiempo de carga del ELF y current->personality es establecido a PER_SVR4. Si esta heurística falla entonces una más genérica como el tratamiento de los caminos del intérprete ELF como /usr/lib/ld.so.1 o /usr/lib/libc.so.1 para indicar un binario SVR4, es usado y la personalidad es establecida a PER_SVR4. Uno podría escribir un pequeño programa de utilidad que usara las capacidades del ptrace(2) de Linux para, en un simple paso, códificar y forzar a un programa funcionando a cualquier personalidad.

Una vez que la personalidad (y entonces current->exec_domain) es conocida, las llamadas al sistema son manejadas como sigue. Déjanos asumir que un proceso realiza una llamada al sistema por medio de la instrucción puerta lcall7. Esto transfiere el control a ENTRY(lcall7) de arch/i386/kernel/entry.S porque fue preparado en arch/i386/kernel/traps.c:trap_init(). Después de la apropiada conversión de la pila, entry.S:lcall7 obtiene el puntero a exec_domain desde current y entonces un desplazamiento del manejador lcall7 con el exec_domain (el cual es codificado fuertemente como 4 en código ensamblador, por lo tanto no puedes desplazar el campo handler a través de la declaración en C de struct exec_domain) y salta a él. Por lo tanto, en C, se parecería a esto:


static void UW7_lcall7(int segment, struct pt_regs * regs)
{
       abi_dispatch(regs, &uw7_funcs[regs->eax & 0xff], 1);
}

donde abi_dispatch() es un envoltorio sobre la tabla de punteros de función que implementa las llamadas al sistema de esta personalidad uw7_funcs.


Página siguiente Página anterior Índice general