La parte más interesante del envío mediante portales es la realización de upcalls [42]. Consisten éstas en llamadas del núcleo a las aplicaciones, o intuitivamente, la inversa de una llamada al sistema. Este mecanismo primario se emplea no sólo para enviar excepciones e interrupciones a las aplicaciones sino también para implementar los envíos de mensajes a portales tal y como se muestra en la figura 3.18. En dicha figura podemos ver a rasgos generales cómo procede el envío de interrupciones, excepciones, mensajes y PCTs emplando upcalls en algunos casos.

Figure 3.18: Uso de upcalls como base para eventos e invocaciones de portales.
En la figura 3.18-a vemos como un upcall es sencillamente
una ``llamada al sistema a la inversa''. En 3.18-b vemos que
la notificación de eventos síncronos, con respecto a la ejecución de
instrucciones de la aplicación que los sufre, es simplemente un upcall
originado por la ocurrencia de un suceso (naturalmente, éste upcall lo
causa en envío de un mensaje a un portal). En 3.18-c tenemos
un envío de mensaje asíncrono (que podría originarse dentro del
kernel
ante la ocurrencia de una interrupción--como en el paso 1 de la
figura), la entrega del mismo se efectúa de forma asíncrona mediante
un upcall, como veremos en el apartado siguiente. Por último, en
3.18-d vemos una transferencia protegida de control cuyo
funcionamiento detallaremos en el apartado 3.4.2.
Como cabe suponer, una llamada al sistema puede dar lugar a un upcall, ésta a una nueva llamada al sistema, etc.
Para implementar upcalls (ver figura 3.19) aprovechamos
el mecanismo (suministrado por el hardware) de traps. Para realizar la
primera parte de un upcall (la invocación de la rutina de usuario a la
que se desea llamar) construimos en la pila del kernel una estructura
similar a la que salva el procesador cuando se produce una llamada al
sistema. Dado que el hardware emplea esta estructura para recuperar,
en el retorno de una llamada al sistema, el estado del usuario antes
de que se produjese esta llamada, ejecutamos una instrucción
iret (como si de un retorno se tratase). El procesador toma de
la pila del kernel el ``falso'' estado que hemos dejado preparado y lo
recupera. Por supuesto, antes de invocar a la rutina salvamos el
estado del procesador para poder recuperarlo cuando se retorne del
upcall, de tal modo que el
kernel siga ejecutando la instrucción
posterior a la llamada a rutina de usuario.

Figure 3.19: Funcionamiento de un upcall: llamada.
Con este simple mecanismo conseguimos ``saltar'' a la rutina que deseamos llamar en área de usuario (denominada ``func. usr.'' en la figura). Paradójicamente, mediante una instrucción de retorno y no mediante una de llamada.
La parte más complicada es retornar de nuevo al núcleo. Para
conseguirlo (ver figura 3.20) hemos de conseguir que se
produzca una llamada al sistema justo tras el retorno de la rutina de
usuario que hemos invocado. Esto se consigue alterando la dirección de
retorno de la rutina invocada, en la pila del usuario (``ret. falso'',
en la figura), antes de invocarla. Esta dirección de retorno (que
determinará la dirección en la que se seguirá ejecutando instrucciones
cuando la rutina termine) se hace apuntar a una instrucción
(almacenada directamente por el
kernel en la pila del usuario) que
produce la llamada al sistema deseada.

Figure 3.20: Funcionamiento de un upcall: retorno.
Esta llamada al sistema (sys_prtl_ret), toma el estado del
núcleo que salvaguardamos anteriormente y lo restaura. Como efecto
lateral de la restauración, la pila del kernel vuelve a su posición
original (antes de efectuar el upcall). El upcall se ha completado en
este punto y, a partir de ese momento, se continúa con la llamada al
sistema que originó el upcall. Cuando ésta retorne encontrará el
estado de usuario correspondiente a dicha llamada al sistema y el
siguiente iret (que finaliza la llamada al sistema) restaurará
el estado del shuttle para que este continúe justo después de su
llamada al sistema.
Hay un detalle que es preciso considerar en este punto. Como es
natural, el servidor de Shuttles ofrece puntos de entrada para que sea
posible obtener (o modificar) el contenido de los registros (de
procesador) de un shuttle cualquiera
, siempre que se presenten los
correspondientes derechos de acceso. La pregunta que surge es ¿De qué
registros estamos hablando? Las llamadas al sistema pueden dar lugar a
llamadas a rutinas de usuario y éstas de nuevo a llamadas al sistema
con lo que puede haber varias llamadas al sistema en curso (anidadas)
en un mismo shuttle. ¿A qué ``registros'' nos referimos pues?, ¿A los
correspondientes a la primera llamada al sistema o a la más externa?
¿A los correspondientes a la última o más interna?. Es decir, dado que
pueden existir múltiples estados de ejecución anidados unos dentro de
otros (como sucede con los estados de la ejecución de un programa
cuando tenemos llamadas anidadas a funciones), puede existir más de un
juego de registros (tal y como los ve el usuario) para un mismo
shuttle. Naturalmente, sólo uno de estos juegos corresponderá al
estado actual del shuttle.
En realidad, dado que las llamadas anidadas no corresponden sólo a upcalls, sino también a llamadas al sistema, tenemos una sucesión de juegos de registros correspondientes al estado del usuario y otra sucesión correspondiente al estado del núcleo. Los primeros corresponden a los instantes en que se produjo una llamada al sistema (y se salvaguardó el estado del usuario) y los segundos a los instantes en que se produjo un upcall (y se salvaguardó el estado del núcleo).
En la implementación actual sólo es posible acceder al juego de registros más externo (el más reciente, o más superficial en la pila) de cada shuttle. Más concretamente, es posible acceder al juego más externo correspondiente a modo usuario y al juego más externo correspondiente a modo kernel.
Para conseguir esto, los contextos salvaguardados se enlazan entre sí en dos listas, una para contextos de usuario y otra para contextos de kernel. El mantenimiento de éstas listas se reduce a unas pocas operaciones en la entrada al núcleo y otras pocas en la salida del mismo. La existencia de éstas listas permite que con escasas modificaciones al código actual sea factible acceder a cualquier contexto de usuario o núcleo de un shuttle dado, no sólo al más externo como en la actualidad.
Una alternativa a la implementación efectuada habría sido una implementación tradicional en la que no es necesario mantener éstas listas. Consistiría en implementar un modelo similar al de los procesos UNIX, donde el núcleo, que ejecuta en el contexto de un determinado proceso, no realiza tareas sólo para el proceso en cuyo contexto ejecuta, sino que también realiza tareas en favor de otros procesos.
Nosotros en cambio hemos tratado de compartimentar la ejecución
del núcleo de modo que éste trabaje sólo en favor de aquella
aplicación en cuyo contexto ejecuta. Esto es, un shuttle que ejecuta
código de usuario, ejecuta periódicamente código del núcleo en modo
núcleo para realizar actividades (de núcleo) en favor de sí mismo. O
dicho de otro modo, cada shuttle se hace su propio trabajo esté en
área de usuario o en área de núcleo. Con el modelo implementado es
factible expulsar al núcleo de forma indefinida de tal modo que sólo
se perjudique al shuttle en que éste ejecutaba. Con el modelo
implementado, el núcleo es una entidad pasiva en la que cada
shuttle entra periódicamente mediante PCTs
.
La simplicidad de las estructuras de datos mantenidas por núcleo
(téngase en cuenta que la practica totalidad de ellas no requieren de
memoria dinámica) junto con el modelo de un núcleo pasivo en el que se
mantiene en la medida de lo posible la independencia entre shuttles
(cada ejecución en el núcleo actúa en un shuttle y en favor de dicho
shuttle) permite que el fallo de un shuttle dentro del núcleo no
afecte inmediatamente al resto del sistema. Esto ha resultado ser muy
conveniente para el desarrollo y depuración del
kernel, tanto que no ha
sido preciso emplear depurador alguno. La independencia entre shuttles
facilita así mismo la movilidad de los recursos del sistema, dado que
puede obviarse el hecho de que un shuttle esté ejecutando en modo
usuario o kernel a la hora de capturar su estado.