next up previous contents
Next: Implementación de procesos Up: Administración de Procesos Previous: Jerarquías de Scheduling

Administración de procesos en nSystem

El nSystem es un sistema de procesos livianos o nano tareas que corren en un solo proceso pesado de Unix. Las tareas de nSystem se implementan multiplexando el tiempo de CPU del proceso Unix en tajadas. Esto se puede hacer gracias a que un proceso Unix tiene asociado su propio reloj regresivo, un mecanismo de interrupciones (sen ales), E/S no bloqueante y todas las características del hardware que se necesitan para implementar multiprogramación en un solo procesador, con la salvedad de que este hardware es emulado en software por Unix.

La figura gif muestra la organización de nSystem en módulos. nSystem es una biblioteca de procedimientos que se sitúa entre la aplicación (el código del programador) y la API de Unix.

 

 


: Organización de nSystem

Los procedimientos superiores corresponden a la API de nSystem y son por lo tanto invocados por la aplicación. Los procedimientos de más abajo son procedimientos internos que se requieren en la implementación de la API. El nombre que aparece verticalmente es el archivo en donde se implementa un módulo en particular. Las flechas indican que el procedimiento desde donde se origina la flecha necesita invocar el procedimiento de destino de la flecha.

La pila

Cada nano tarea tiene su propia pila. Los registros de activación de los procedimientos que se invocan en una tarea se apilan y desapilan en esta estructura de datos (ver figura gif). Un registro del procesador, denominado sp, apunta hacia el tope de la pila.

  
Figure: Llamado y retorno de procedimientos usando una pila

La pila es de taman o fijo, por lo que una recursión demasiado profunda puede desbordarla. El nSystem chequea el posible desborde de la pila en cada cambio de contexto. De ocurrir un desborde el nSystem termina con un mensaje explicativo. Sin embargo si no hay cambios de contexto no puede detectar el desborde. En este caso, el nSystem podría terminar en un segmentation fault o bus error, sin mayor explicación de la causa de la caída.

El cambio de contexto

En nSystem nunca hay más de una nano tarea corriendo realmente. El registro sp del procesador apunta al tope de la pila de la tarea que está corriendo.

El cambio de contexto en nSystem lo realiza el scheduler llamando al procedimiento:

void ChangeContext(nTask out_task, nTask in_task);

Este procedimiento realiza las siguientes actividades (ver figura gif):

  
Figure: Cambio de pila durante un cambio de contexto.

  1. Resguarda los registros del procesador en el tope de la pila de la tarea saliente ( out_task).

  2. Guarda sp en el descriptor de esta tarea saliente (out_task->sp).

  3. Rescata el puntero al tope de la pila de la tarea entrante a partir de su descriptor de proceso (in_task->sp).

  4. Restaura los registros de la tarea entrante que se encuentran en el tope de la pila.

  5. Retoma la ejecución a partir del llamado a ChangeContext de la tarea entrante.

Es decir que el llamado a ChangeContext de la tarea saliente no retorna hasta que se haga un nuevo cambio hacia aquella tarea (ver figura gif).

  
Figure: Cambio de contexto en nSystem.

Observe que las labores que realiza ChangeContext no pueden llevarse a cabo en C y por lo tanto parte de este procedimiento está escrito en assembler y es completamente dependiente de la arquitectura del procesador. Sin embargo, este cambio de contexto no requiere una llamada al sistema, el cambio de contexto se realiza sólo con instrucciones del modo usuario en el proceso Unix en donde corre el nSystem.

Scheduling en nSystem

La estrategia de scheduling de nSystem podría denominarse como preemptive last come first served o PLCFS. Las ráfagas son atendidas en orden LIFO, es decir que las tareas que llegan al scheduler toman el control de procesador de inmediato y la tarea que se ejecutaba en ese instante se coloca en primer lugar en la cola del scheduler. Para evitar que una tarea acapare indefinidamente el procesador, cada t milisegundos el scheduler le quita el procesador a la tarea en ejecución, cualquiera sea esta tarea, y la coloca al final de una cola.

Esta estrategia se implementa usando una cola dual, es decir en donde se pueden agregar tareas ya sea al final como al comienzo. Cuando una tarea que estaba en estado de espera (por ejemplo a la espera de una operación de E/S) pasa al modo lista para ejecutarse (producto de una interrupción de E/S) el scheduler coloca la tarea en ejecución al comienzo de la cola dual y retoma de inmediato la tarea que llega (ver figura gif).

  
Figure: Scheduling en nSystem

El cambio de tarea cada t milisegundos se implementa programando una sen al que se invoca cada t milisegundos de tiempo virtual del proceso Unix. En esta interrupción el scheduler coloca la tarea en ejecución al final de la cola dual, extrae la tarea que se encuentra en primer lugar y la retoma.

Para esta sen al se usa el tiempo virtual del proceso Unix, es decir el tiempo de uso del procesador real por parte del proceso Unix sin contar el tiempo en que el procesador estaba en poseción de otro proceso Unix. En efecto, cambiar de tarea cada t milisegundos de tiempo real no tendría sentido, pues durante ese intervalo podría haber sucedido que el proceso Unix en donde corre nSystem no hubiese tenido el procesador real y por lo tanto la tarea en ejecución no pudo avanzar tampoco.

Scheduling simplificado

La siguiente es una versión simplificada del código del scheduler. En ella no se consideran ciertas condiciones de borde que obscurecen innecesariamente el código. Estas condiciones son el caso en que no hay tareas listas para correr y el caso en que el scheduling es non-preemptive.

El scheduler mantiene las siguientes variables:

El scheduler retoma una tarea con el siguiente procedimiento:

ResumeNextReadyTask()
{
  nTask this_task= current_task;
  nTask next_task= GetTask(ready_queue);
  ChangeContext(this_task, next_task);
  current_task= this_task; /* (1) */
}

Observe que si T era la tarea que corría cuando se invocó ResumeNextReadyTask, el llamado a ChangeContext no retornará hasta que se retome T. Es decir cuando se invoque nuevamente ResumeNextReadyTask y T aparezca en primer lugar en la cola ready_queue. Mientras tanto, el nSystem ejecutó otras tareas y realizó eventualmente otros cambios de contexto. Por esta razón en (1) se asigna T a la tarea actual y no next_task.

En cada sen al periódica se invoca el siguiente procedimiento:

VtimerHandler()
{
  /* Programa la siguiente interrupcion */
  SetAlarm(VIRTUALTIMER, current_slice,
           VtimerHandler);
  /* Coloca la tarea actual al final
     de la cola */
  PutTask(ready_queue, current_task);
  /* Retoma la primera tarea en la cola */
  ResumeNextReadyTask();
}

Para implementar SetAlarm se usaron las llamadas al sistema setitimer y sigaction.

Note que la tarea que recibe el procesador estaba bloqueada en el llamado a ChangeContext de ResumeNextReadyTask y por lo tanto es ella la que actualizará la variable current_task con su propio identificador. En nSystem todos los cambios de contexto se realizan a través de ResumeNextReadyTask. La única excepción es el cambio de contexto que se realiza al crear una tarea.

Entrada/Salida no bloqueante

En Unix se puede colocar un descriptor de archivo ( fd) en un modo no bloqueante y hacer que se invoque la sen al SIGIO cuando haya algo para leer en ese descriptor. Esto significa que read(fd,...) no se bloquea cuando no hay nada que leer en fd, sino que retorna -1. Esto se hace en Unix con la llamada al sistema fcntl.

Esto es importante porque de no programar los descriptores en modo bloqueante, cuando una tarea intente leer un descriptor se bloquearán todas las tareas hasta que haya algo para leer en ese descriptor. Esto es inadmisible en un sistema multiprogramado.

Una primera implementación de nRead es:

int nRead(int fd, char *buf, int nbyte)
{ /* Intentamos leer */
  int rc= read(fd, buf, nbyte);
  while (rc<0 && errno==EAGAIN)
  { /* No hay nada disponible */
    AddWaitingTask(fd, current_task);
    current_task->status= WAIT_READ;
    /* Pasamos a la proxima tarea ready */
    ResumeNextReadyTask();
    /* Ahora si que deberia funcionar */
    rc= read(fd, buf, nbyte);
  }
  return rc;
}

El valor EAGAIN en errno indica que no hay nada para leer y que read debería ser reinvocado más tarde. Unix informa al proceso de nSystem que hay algo para leer en algún descriptor invocando la sen al SIGIO. El procedimiento que atiende esta sen al es:

SigioHandler()
{
  PushTask(current_task, ready_queue);
  ... preparar argumentos para select ...
  select(...);
  ... por cada tarea task que puede
      avanzar:
      PushTask(task, ready_queue);
  ...
  ResumeNextReadyTask();
}

El llamado al sistema select determina qué descriptores de archivo tienen datos para leer o en qué descriptores se puede escribir sin llenar los buffers internos de Unix que obligarían a bloquear el proceso Unix. En el código que sigue al select se busca si hay tareas bloqueadas a la espera de alguno de los descriptores cuya lectura/escritura no causará un bloqueo temporal de nSystem. Estas tareas quedarán antes en la cola que la tarea en ejecución cuando se invocó SigioHandler, porque el scheduling es preemptive LCFS.

Implementación de secciones críticas en nSystem

El código de nRead puede funcionar mal esporádicamente cuando se reciba alguna sen al en medio del llamado a AddWaitingTask. Esto se debe a que el código de nRead es una sección crítica y por lo tanto se debe evitar que otras tareas invoquen este procedimiento concurrentemente.

nSystem implementa las secciones críticas de la forma más simple posible en un mono-procesador: inhibe las interrupciones. Por lo tanto la versión final de nRead es:

int nRead(int fd, ...)
{
  START_CRITICAL();
    ... como antes ...
  END_CRITICAL();
}

El procedimiento START_CRITICAL inhibe las sen ales de Unix usando la llamada al sistema sigprocmask. La misma llamada se usa en END_CRITICAL para reactivar las sen ales.

No es necesario inhibir las sen ales en VtimerHandler o SigioHandler porque estos procedimientos son para atender sen ales de Unix, y en este caso Unix los invoca desde ya con las sen ales inhibidas. De igual forma cuando estos procedimientos retornan, se reactivan automáticamente las sen ales.

Implementación de semáforos en nSystem

Un semáforo se representa mediante una estructura de datos que tiene la siguiente información:

El procedimiento nWaitSem debe bloquear la tarea que lo invoca cuando count es 0:

void nWaitSem(nSem sem)
{
  START_CRITICAL();
  if (sem->count>0)
    sem->count--;
  else
  {
    current_task->status= WAIT_SEM;
    PutTask(sem->queue, current_task);
    /* Retoma otra tarea */
    ResumeNextReadyTask();
  }
  END_CRITICAL();
}

El procedimiento nSignalSem debe desbloquear una tarea cuando count es 0 y hay tareas en espera. La tarea desbloqueada toma de inmediato el control del procesador (ya que el scheduling es LCFS).

void nSignalSem(nSem sem)
{
  START_CRITICAL();
  if (EmptyQueue(sem->queue))
    sem->count++;
  else
  {
     nTask wait_task= GetTask(sem->queue);
     wait_task->status= READY;
     PushTask(ready_queue, current_task);
     PushTask(ready_queue, wait_task);
     /* wait_task es la primera en la cola! */
     ResumeNextReadyTask();
  }
  END_CRITICAL();
}

Es importante destacar que cuando en nSignalSem se inhiben las sen ales de Unix y se retoma otra tarea llamando a ResumeNextReadyTask, es esta otra tarea la que reactivará las sen ales en el END_CRITICAL del final de nWaitSem.

El END_CRITICAL que se encuentra al final de nSignalSem sólo se invocará cuando se retome nuevamente la tarea que invocó este procedimiento. Y entonces se reactivarán las sen ales que fueron inhibidas por otra tarea, aquella que perdió el procesador.



next up previous contents
Next: Implementación de procesos Up: Administración de Procesos Previous: Jerarquías de Scheduling



José M. Piquer
Fri Apr 9 15:57:37 CLT 1999