En este capítulo describimos la memoria intermedia de páginas de Linux 2.4. La memoria intermedia de páginas es - como sugiere el nombre - una memoria intermedia de páginas físicas. En el mundo UNIX el concepto de memoria intermedia de páginas se convirtió popular con la introdución de SVR4 UNIX, donde reemplazó a la antememoria intermedia para las operaciones de E/S.
Mientras la memoria intermedia de páginas de SVR4 es sólamente usada como
memoria intermedia de datos del sistema de ficheros y estos usan la estructura
vnode y un desplazamiento dentro del fichero como parámetros hash, la memoria
intermedia de páginas de Linux está diseñada para ser más genérica, y entonces
usa una estructura address_space (explicada posteriormente) como primer
parámetro. Porque la memoria intermedia de páginas Linux está cerradamente
emparejada a la notación del espacio de direcciones, necesitarás como mínimo un
conocimiento previo del adress_spaces para entender la forma en la que trabaja
la memoria intermedia de páginas.
Un address_space es algún tipo de software MMU que mapea todas las
páginas de un objeto (ej. inodo) a otro concurrentemente (tipicamente
bloques físicos de disco). La estructura address_space está definida en
include/linux/fs.h
como:
struct address_space {
struct list_head clean_pages;
struct list_head dirty_pages;
struct list_head locked_pages;
unsigned long nrpages;
struct address_space_operations *a_ops;
struct inode *host;
struct vm_area_struct *i_mmap;
struct vm_area_struct *i_mmap_shared;
spinlock_t i_shared_lock;
};
Para entender la forma en la que address_spaces trabaja, sólo
necesitamos mirar unos pocos de estos campos:
clean_pages
, dirty_pages
y locked_pages
son
listas doblemente enlazadas de páginas limpias, sucias y bloqueadas
pertenecientes a este address_space, nrpages
es el número total
de páginas en este address_space. a_ops
define los métodos de
este objeto y host
es un puntero perteneciente a este inodo
address_space - quizás sea NULL, ej. en el caso del swapper (intercambiador)
address_space (mm/swap_state.c,
).
El uso de clean_pages
, dirty_pages
,
locked_pages
y nrpages
es obvio, por lo tanto
hecharemos un severo vistazo a la estructura
address_space_operations
, definida en la misma cabecera:
struct address_space_operations {
int (*writepage)(struct page *);
int (*readpage)(struct file *, struct page *);
int (*sync_page)(struct page *);
int (*prepare_write)(struct file *, struct page *, unsigned, unsigned);
int (*commit_write)(struct file *, struct page *, unsigned, unsigned);
int (*bmap)(struct address_space *, long);
};
Para una vista básica del principio de address_spaces (y de la memoria intermedia
de páginas) necesitamos hechar una mirada a ->writepage
y
->readpage
, pero en la práctica necesitamos también mirar en
->prepare_write
y ->commit_write
.
Probablemente supongas que los métodos address_space_operations lo hacen en virtud de sus nombres solamente; no obstante, requieren hacer alguna explicación. Su uso en el camino de las E/S de los datos de los sistemas de ficheros, por lo lejos del más común camino a través de la memoria intermedia de páginas, suministra una buena forma para entenderlas. Como la mayoría de otros sistemas operativos del estilo UNIX, Linux tiene unas operaciones genéricas de ficheros (un subconjunto de las operaciones vnode SYSVish) para los datos E/S a través de la memoria intermedia de páginas. Esto significa que los datos no interactúan directamente con el sistema de ficheros en read/write/mmap, pero serán leidos/escritos desde/a la memoria intermedia de páginas cuando sea posible. La memoria intermedia de páginas tiene que obtener datos desde el sistema de ficheros actual de bajo nivel en el caso de que el usuario quiera leer desde una página que todavía no está en memoria, o escribir datos al disco en el caso de que la memoria sea insuficiente.
En el camino de lectura, los métodos genéricos primero intentarán encontrar una página que corresponda con la pareja buscada de inodo/índice.
hash = page_hash(inode->i_mapping, index);
Entonces testeamos cuando la página actualmente existe.
hash = page_hash(inode->i_mapping, index);
page = __find_page_nolock(inode->i_mapping, index, *hash);
Cuando no existe, asignamos una nueva página, y la añadimos al hash de la memoria intermedia de páginas.
page = page_cache_alloc();
__add_to_page_cache(page, mapping, index, hash);
Después de que la página haya sido hashed (ordenada) utilizamos la operación
->readpage
address_space para en este instante rellenar la página
con datos (el fichero es una instancia abierta del inodo).
error = mapping->a_ops->readpage(file, page);
Finalmente podemos copiar los datos al espacio de usuario.
Para escribir en el sistema de archivos existen dos formas: una para mapeos escribibles (mmap) y otra para la familia de llamadas al sistema write(2). El caso mmap es muy simple, por lo tanto será el que primero discutamos. Cuando un usuario modifica los mapas, el subsistema VM marca la página como sucia.
SetPageDirty(page);
El hilo del núcleo bdflush que está intentando liberar páginas, como
actividad en segundo plano o porque no hay suficiente memoria, intentará
llamar a ->writepage
en las páginas que están explicitamente
marcadas como sucias. El método ->writepage
tiene ahora que
escribir el contenido de las páginas de vuelta al disco y liberar la
página.
El segundo camino de escritura es _mucho_ más complicado. Para cada
página que el usuario escribe, tenemos básicamente que hacer lo
siguiente: (para el código completo ver
mm/filemap.c:generic_file_write()
).
page = __grab_cache_page(mapping, index, &cached_page);
mapping->a_ops->prepare_write(file, page, offset, offset+bytes);
copy_from_user(kaddr+offset, buf, bytes);
mapping->a_ops->commit_write(file, page, offset, offset+bytes);
Por lo tanto intentamos encontrar la página ordenada o asignar una
nueva, entonces llamamos al método ->prepare_write
address_space, copiamos la antememoria del usuario a la memoria del núcleo y
finalmente llamamos al método ->commit_write
. Tal como
probablemente has visto ->prepare_write y ->commit_write
son
fundamentalmente diferentes de ->readpage
y
->writepage
, porque ellas no sólo son llamadas cuando la E/S
física se quiere actualizar sino que son llamadas cada vez que el
usuario modifica el fichero.
Hay dos (¿o más?) formas para manejar esto, la primero es usar la
antememoria intermedia de Linux para retrasar la E/S física, rellenando un puntero
page->buffers
con buffer_heads, que será usado en
try_to_free_buffers (fs/buffers.c
) para pedir E/S una vez que
no haya suficientemente memoria, y es usada de forma muy difundida en el
actual núcleo. La otra forma justamente establece la página como
sucia y confía en que ->writepage
realice todo el trabajo.
Debido a la carencia de un bitmap de validez en la página de estructuras,
esto no realiza todo el trabajo que tiene una granularidad más pequeña que
PAGE_SIZE
.