Manual del estudiante de Ingeniería en Sistemas de UTN/Diseño e Implementación de Sistemas Operativos/La gestión de memoria en MINIX

De Wikilibros, la colección de libros de texto de contenido libre.

La gestión de la memoria es de tipo segmentación. Los procesos tienen tres segmentos (texto, datos y pila) que deben cargarse en la memoria física para que el proceso pueda ejecutarse. La gestión del espacio será por lo tanto análoga al mecanismo de particiones múltiples de tamaño variable y de hecho veremos la existencia de listas de huecos libres y ocupados. Para los procesos servidores (kernel, fs, mm, inet) y tareas la gestión de la memoria smx es la propuesta en MINIX:

  • Un hueco para el texto.
  • Un hueco para los datos y la pila, junto con el espacio reservado para el crecimiento de dicha pila y de la zona de datos.

Es decir, hay dos huecos para cada proceso, a pesar de que la tabla que lo describe tenga tres entradas ya que estas entradas describen segmentos:

  • Segmento de texto: hueco de texto.
  • Segmentos de datos y pila: un único hueco.

Las direcciones virtuales 0 del segmento de texto y del de datos se corresponden con la dirección inicial en la memoria física. Sin embargo la dirección 0 del segmento de pila es la menor de las direcciones posibles para evitar subdesbordamientos que supondrían la escritura en la zona de datos (recordad la existencia de un único hueco para datos+pila). Por ello la dirección virtual inicial del segmento de pila coincide con el tamaño del hueco entre el segmento de datos y el de pila. Los procesos de usuario en smx disfrutan de huecos separados para el texto, los datos y la pila. Estos huecos pueden crecer según se vaya necesitando. El espacio físico de direcciones de smx es en realidad el espacio virtual del proceso Solaris. El servidor MM se encuentra en la capa número 3 de MINIX. Los procesos de usuario solicitan servicios al servidor MM mediante el envío de mensajes que codifican el tipo de llamada al sistema. Pueden utilizar la función /usr/local/smx/src/lib/other/syscall.c:_syscall() o bien construir su propia invocación. A su vez el servidor MM hace uso de las tareas de nivel 2 mediante el envío de mensajes. Para ello hace uso de las funciones de biblioteca que comienzan con sys_. Se encuentran en /usr/local/smx/src/lib/syslib.

Archivos de cabecera y estructuras de datos[editar]

src/mm/mm.h
Es el principal fichero de cabecera que contiene todos los archivos de cabecera que deben ser compilados con el sistema de gestión de memoria.
proto.h
Prototipos de funciones.
glo.h
Variables globales del administrador de memoria. Para este fichero la constante EXTERN se sustituye por una cadena nula.
  • mp: puntero a la entrada de la tabla de procesos (de tipo struct mproc) que se corresponde con el proceso cuya llamada al sistema está siendo procesada. Como ya se explicó, la tabla de procesos está dividida entre el kérnel, el sistema de gestión de memoria y el de ficheros. Todas ellas contienen el mismo número de entradas pero con campos diferentes.
  • who: índice del proceso en curso (mp = &mproc[who] siendo mproc[] la tabla de procesos).
  • procs_in_use: número de entradas de la tabla de procesos que están siendo utilizadas, es decir, número de procesos (y tareas en ejecución). Se utiliza para saber si se pueden crear nuevos procesos.
  • core_name: nombre del fichero core que será creado si se presenta algún problema.
  • core_ssets: mapa de bits que describen los problemas (las señales) que provocan la creación del fichero core. Este fichero contiene una copia de la imagen del proceso en memoria en el momento de ocurrir un error.
mproc.h
tabla de procesos del sistema de gestión de memoria (struct mproc { ...}).

El campo más interesante es el que describe el mapa de memoria del proceso:

struct mem_map mp_seg[NR_SEGS];         /* points to text, data, stack */

Hay tres entradas, una para el segmento de texto, otra para el de datos y otra para la pila. El tipo mem_map se encuentra declarado en /usr/local/smx/include/minix/type.h:

struct mem_map {
  vir_clicks mem_vir;           /* virtual address */
  phys_clicks mem_phys;         /* physical address */
  vir_clicks mem_len;           /* length */
};

Cada entrada tiene un campo de dirección virtual, otro de dirección física y un tercero de tamaño. Todos ellos vienen en clicks; en smx un click son 8192 Bytes. Se trata de una especie de tabla de segmentos utilizada para convertir direcciones virtuales en físicas. mp_flags: indica el estado de una entrada de la tabla de procesos y por consiguiente del proceso asociado. Valores utilizados:

/* Flag values */
#define IN_USE           001    /* set when 'mproc' slot in use */
#define WAITING          002    /* set by WAIT system call */
#define HANGING          004    /* set by EXIT system call */
#define PAUSED           010    /* set by PAUSE system call */
#define ALARM_ON         020    /* set when SIGALRM timer started */
#define SEPARATE         040    /* set if file is separate I & D space */
#define TRACED          0100    /* set if process is to be traced */
#define STOPPED         0200    /* set if process stopped for tracing */
#define SIGSUSPENDED    0400    /* set by SIGSUSPEND system call */

mp_procargs: puntero al principio del array argv[] del proceso. Otros campos interesantes:

 char mp_exitstatus;           /* storage for status when process exits */
 pid_t mp_pid;                 /* process id */
 int mp_parent;                /* index of parent process */
 uid_t mp_realuid;             /* process' real uid */
 uid_t mp_effuid;              /* process' effective uid */
table.c
la variable más importante es el array de punteros a funciones call_vec. Sirve para invocar a la función de tratamiento de cada mensaje en función de su identificación. En la posición indexada por dicha identificación se almacena el puntero a la función:
(int (*call_vec[NCALLS]), (void) ) = {
       no_sys,         /*  0 = unused  */
       do_mm_exit,     /*  1 = exit    */
       do_fork,        /*  2 = fork    */
etc...

El servidor mm[editar]

Se trata de un proceso MINIX encargado de recibir mensajes de solicitud procedentes de los procesos e implementarlas. Está compilado y enlazado de forma independiente al kérnel y, por supuesto, al resto de procesos. Se encuentra en main.c:

PUBLIC void main()
{
/* Main routine of the memory manager. */
 int error;
 mm_init();                    /* initialize memory manager tables */
 /* This is MM's main loop-  get work and do it, forever and forever. */
 while (TRUE) {
       /* Wait for message. */
       get_work();             /* wait for an MM system call */
       mp = &mproc[who];
       /* Set some flags. */
       error = OK;
       dont_reply = FALSE;
       err_code = -999;

       /* If the call number is valid, perform the call. */
       if (mm_call < 0 || mm_call >= NCALLS)
               error = EBADCALL;
       else
               error = (*call_vec[mm_call])();
 
       /* Send the results back to the user to indicate completion. */
       if (dont_reply) continue;       /* no reply for EXIT and WAIT */
       if (mm_call == EXEC && error == OK) continue;
       reply(who, error, result2, res_ptr);
 }
}

Como se puede ver, después de una inicialización (mm_init()) se invoca a la función get_work() que bloquea al proceso en espera de un mensaje. Una vez recibido se trata dicho mensaje (error = (*call_vec[mm_call])()) y, en su caso, se envía un mensaje de respuesta. main.c:mm_init(): rutina de inicialización del sistema de gestión de memoria. Invoca a la función sys_getmap() que obtiene información acerca del uso de la memoria por parte del kérnel y por lo tanto de la memoria libre que queda:

 /* Get the memory map of the kernel to see how much memory it uses,
  * including the gap between address 0 and the start of the kernel.
  */
 sys_getmap(SYSTASK, kernel_map);
 minix_clicks = kernel_map[S].mem_phys + kernel_map[S].mem_len;
#if MACHINE == SUN
 /*
  * Don't include the area from addr 0 to the start of the kernel text
  * segment in the minix memory total, as it is not used for smx
  * "physical" memory!
  */
 minix_clicks -= kernel_map[T].mem_phys;
 set_stack_high(kernel_map[T].mem_phs);
#endif

La variable minix_clicks almacena el tamaño (en clicks) del núcleo. Esta información es necesaria para disponer del resto como memoria libre. En smx la dirección física de comienzo del kernel no contabiliza como memoria ocupada (en otras palabras, se la hace equivaler a la dirección cero). A continuación se inicializa la tabla de procesos:

 /* Initialize MM's tables. */
 for (proc_nr = 0; proc_nr <= INIT_PROC_NR; proc_nr++) {
       rmp = &mproc[proc_nr];
       rmp->mp_flags |= IN_USE;
       sys_getmap(proc_nr, rmp->mp_seg);
       if (rmp->mp_seg[T].mem_len != 0) rmp->mp_flags |= SEPARATE;
       minix_clicks += (rmp->mp_seg[S].mem_phys + rmp->mp_seg[S].mem_len)
                               - rmp->mp_seg[T].mem_phys;
 }
 

Notar que si el campo mem_len del segmento de texto tiene un valor diferente de cero eso implica que el segmento de texto y el de datos se tratan separadamente lo cuál se indica también en el campo mp_flags. Además la variable minix_clicks se incrementa en la medida en que las diferentes tareas y servidores vayan haciendo uso de la memoria. La operación de la última línea sirve para calcular el espacio total utilizado por la tarea: véase que se suma la dirección física inicial de la pila más su tamaño con lo que se obtiene la última dirección de dicha tarea. A ese valor se le resta la dirección inicial del segmento de texto que es la primera de la tarea. Esta diferencia es el tamaño de la mencionada tarea. Se espera a continuación que el sistema de ficheros (fs) informe sobre el tamaño del disco RAM:

 /* Wait for FS to send a message telling the RAM disk size then go "on-line".
  */
 if (receive(FS_PROC_NR, &mess) != OK)
       panic("MM can't obtain RAM disk size from FS", NO_NUM);
 ram_clicks = mess.m1_i1;

Se invoca a la función alloc.c:mem_init() encargada de inicializar la lista de agujeros de la memoria. alloc.c:mem_init(). Inicializa las listas de agujeros de memoria. La estructura básica es la tabla hole que mantiene una lista de agujeros en la memoria física. Está ordenada en orden creciente de dirección física. Contiene direcciones físicas partiendo de la 0x0. Esta estructura es necesaria para evitar asignar memoria que está utilizando el núcleo, los vectores de interrupción y el servidor de memoria. hole está declarada en alloc.c. Como se puede observar se ha declarado como estática y esto nos puede resultar extraño. La razón para hacer esto es que se simplifica su gestión:

PRIVATE struct hole {
 phys_clicks h_base;           /* where does the hole begin? */
 phys_clicks h_len;            /* how big is the hole? */
 struct hole *h_next;          /* pointer to next entry on the list */
} hole[NR_HOLES];

Su inicialización se realiza en mem_init(). En primer lugar se inicializa el campo h_next:

 /* Put all holes on the free list. */
 for (hp = &hole[0]; hp < &hole[NR_HOLES]; hp++) hp->h_next = hp + 1;
 hole[NR_HOLES-1].h_next = NIL_HOLE;
 

A continuación se inicializa la lista hole_head que apunta al principio de la lista de agujeros libres en memoria. En un principio está vacía pero inmediatamente le será asignada la memoria:

hole_head = NIL_HOLE;

Se hace lo mismo con la lista free_slots que apunta a entradas libres de la tabla hole. Inicialmente la tabla hole se considera vacía completamente y se irá utilizando según se vayan añadiendo los agujeros que el núcleo no utiliza:

free_slots = &hole[0];

Y, finalmente, se entra en un bucle en el que se va preguntando (mediante mensajes) al núcleo por los trozos de memoria que no está utilizando para así inicializar las tablas utilizando la función free_mem():

 *free = 0;
 for (;;) {
       mess.m_type = SYS_MEM;
       if (sendrec(SYSTASK, &mess) != OK) panic("bad SYS_MEM?", NO_NUM);
       base = mess.m1_i1;
       size = mess.m1_i2;
       if (size == 0) break;           /* no more? */

       free_mem(base, size);
       *total = mess.m1_i3;
       *free += size;
 }
 

Las variables total y free se corresponden con las variables externas total_clicks y free_clicks. alloc.c:free_mem(base, clicks): recibe dos argumentos, la dirección base a partir de donde se quiere liberar y el número de clicks a liberar. Añade dicha memoria a la tabla hole. Veamos cómo lo hace: Comprobaciones e inicialización de la variable local new_ptr que se utilizará para la asignación del nuevo hueco libre. Además free_slots apunta al siguiente elemento de la lista hole y se comienza a comprobar la lista de huecos libres hole_head:

if (clicks == 0) return;
 if ( (new_ptr = free_slots) == NIL_HOLE) panic("Hole table full", NO_NUM);
 new_ptr->h_base = base;
 new_ptr->h_len = clicks;
 free_slots = new_ptr->h_next;
 hp = hole_head;

Si la lista de huecos libres (ahora apuntada por hp) está vacía o bien la dirección del hueco a liberar es menor que la del primer hueco libre, se coloca el nuevo hueco al principio de la lista hole_head. merge() unirá huecos libres consecutivos:

 if (hp == NIL_HOLE || base <= hp->h_base) {
       /* Block to be freed goes on front of the hole list. */
       new_ptr->h_next = hp;
       hole_head = new_ptr;
       merge(new_ptr);
       return;
 }

Si no va al principio de la lista, se busca su lugar y se inserta:

 /* Block to be returned does not go on front of hole list. */
 while (hp != NIL_HOLE && base > hp->h_base) {
       prev_ptr = hp;
       hp = hp->h_next;
 }

 /* We found where it goes.  Insert block after 'prev_ptr'. */
 new_ptr->h_next = prev_ptr->h_next;
 prev_ptr->h_next = new_ptr;
 merge(prev_ptr);              /* sequence is 'prev_ptr', 'new_ptr', 'hp' */
 

Inicialización[editar]

El mapa de memoria luego del arranque del sistema queda así:


Espacio para programas de usuario
Disco RAM
INIT
Sistema de Ficheros
Manejador de Memoria
Kernel
Tarea de la terminal
Tarea del disco
Tarea del reloj
Tarea hardware
Manejo de los procesos
Espacio sin utilizar
Vectores de interrupción