Este documento analiza el mecanismo completo de una llamada al sistema (syscall) en el sistema operativo xv6, tomando como referencia la función write(). Se desrcibe el recorrido desde el espacio de usuario hasta el núcleo del sistema y el retorno.
Fase 1: Invocación desde el espacio de usuario
Cuando un programa llama a write(), se ejecuta una rutina ensamblador que prepara los registros y dispara el mecanismo de transición al modo supervisor.
#include "kernel/syscall.h"
write:
li a7, SYS_write
ecall
ret
La constante SYS_write está definida en kernel/syscall.h con el valor 16. Este identificador numérico se carga en el registro a7 para indicar al kernel qué operación se solicita.
La instrucción ecall genera una excepción síncrona que provoca los siguientes efectos:
- El procesador cambia del modo usuario al modo supervisor.
- La dirección de retorno (valor del contador de programa) se almacena en el registro especail
sepc. - La ejecución salta a la dirección almacenada en el registro
stvec(Supervisor Trap Vector).
En xv6, el registro stvec apunta al punto de entrada uservec dentro de trampoline.S. La instrucción ecall por sí sola es insuficiente: es necesario preservar los 31 registros de propósito general del proceso, conmutar la tabla de páginas del usuario a la del kernel, obtener una pila del kernel y transferir el control a la lógica de tratamiento de traps.
# Al ejecutar ecall, el tipo de syscall se indica en a7
# y los parámetros se colocan en los registros a0 a a5
Fase 2: Rutina uservec en trampoline.S
Esta rutina se encarga de salvar el estado completo del contexto del usuario antes de proseguir con la lógica del kernel.
uservec:
# Se intercambia a0 con sscratch para poder usar a0 como puntero
csrw sscratch, a0
# Cargar dirección base del trapframe en a0
li a0, TRAPFRAME
# Persistir registros de propósito general en el trapframe
sd ra, 40(a0)
sd sp, 48(a0)
sd gp, 56(a0)
sd tp, 64(a0)
...
# Recuperar el valor original de a0 desde sscratch y guardarlo
csrr t0, sscratch
sd t0, 112(a0)
# Cargar la pila del kernel desde el trapframe
ld sp, 8(a0)
# Cargar el identificador del hart (CPU) actual
ld tp, 32(a0)
# Cargar dirección de la función usertrap()
ld t0, 16(a0)
# Cargar la tabla de páginas del kernel
ld t1, 0(a0)
# Barrera de memoria antes de cambiar la tabla de páginas
sfence.vma zero, zero
# Conmutar a la tabla de páginas del kernel escribiendo en satp
csrw satp, t1
sfence.vma zero, zero
# Transferir control a usertrap()
jr t0
Fase 3: Función usertrap() en trap.c
Una vez en el kernel, usertrap() determina la causa del trap y ejecuta el tratamiento adecuado.
void usertrap(void) {
int dispositivo = 0;
// Verificar que el trap provino del modo usuario
if((r_sstatus() & SSTATUS_SPP) != 0)
panic("usertrap: trap no originado en modo usuario");
// Redirigir stvec al manejador de traps del kernel
w_stvec((uint64)kernelvec);
struct proc *p = myproc();
// Conservar el valor de sepc en el trapframe del proceso
p->trapframe->epc = r_sepc();
uint64 causa = r_scause();
if(causa == 8){
// Es una llamada al sistema
if(killed(p))
exit(-1);
// Avanzar el PC a la instrucción siguiente a ecall
p->trapframe->epc += 4;
// Habilitar interrupciones durante el procesamiento
intr_on();
// Despachar la syscall según el código en a7
syscall();
} else if((dispositivo = devintr()) != 0){
// Interrupción de dispositivo manejada
} else {
printf("usertrap(): scause inesperado %p pid=%d\n", causa, p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
setkilled(p);
}
if(killed(p))
exit(-1);
// Ceder la CPU si se trató de una interrupción de temporizador
if(dispositivo == 2)
yield();
usertrapret();
}
¿Por qué guardar el valor de sepc en el trapframe? Durante la ejecución en el kernel, el planificador puede cambiar de proceso. Si otro proceso ejecuta su propia syscall, el registro sepc se sobrescribiría. Almacenar el valor en el trapframe garantiza que cada proceso pueda restaurar su propio punto de reanudación.
El campo scause es establecido automáticamente por el hardware cuando ocurre una excepción síncrona como ecall.
Fase 4: Preparación del retorno con usertrapret()
Antes de devolver el control al usuario, se restauran los parámetros necesarios para el próximo trap.
void usertrapret(void)
{
struct proc *p = myproc();
// Desactivar interrupciones mientras se reconfigura stvec
intr_off();
// Restaurar stvec para que apunte a uservec
uint64 destino_uservec = TRAMPOLINE + (uservec - trampoline);
w_stvec(destino_uservec);
// Guardar información del kernel en el trapframe
p->trapframe->kernel_satp = r_satp();
p->trapframe->kernel_sp = p->kstack + PGSIZE;
p->trapframe->kernel_trap = (uint64)usertrap;
p->trapframe->kernel_hartid = r_tp();
unsigned long sreg = r_sstatus();
// Limpiar bit SPP para retornar a modo usuario con sret
sreg &= ~SSTATUS_SPP;
// Activar bit SPIE para que las interrupciones se habiliten tras sret
sreg |= SSTATUS_SPIE;
w_sstatus(sreg);
// Restaurar la dirección de reanudación del usuario
w_sepc(p->trapframe->epc);
// Construir el valor de satp para la tabla de páginas del usuario
uint64 satp_usuario = MAKE_SATP(p->pagetable);
// Calcular la dirección de userret dentro del trampolín
uint64 destino_userret = TRAMPOLINE + (userret - trampoline);
// Invocar userret pasando la tabla de páginas como argumento
((void (*)(uint64))destino_userret)(satp_usuario);
}
Fase 5: Restauración final con userret()
La rutina ensamblador userret conmuta a la tabla de páginas del usuario, restaura todos los registros y ejecuta sret para volver al modo usuario.
userret:
# a0 contiene la dirección de la tabla de páginas del usuario
# Conmutar la tabla de páginas
sfence.vma zero, zero
csrw satp, a0
sfence.vma zero, zero
# Establecer a0 como puntero al trapframe
li a0, TRAPFRAME
# Restaurar registros de propósito general desde el trapframe
ld ra, 40(a0)
ld sp, 48(a0)
ld gp, 56(a0)
ld tp, 64(a0)
...
# Restaurar el registro a0 original
ld a0, 112(a0)
# sret realiza tres acciones:
# 1. Cambia el procesador a modo usuario
# 2. Copia sepc al contador de programa (pc)
# 3. Habilita las interrupciones si SPIE estaba activo
sret
En este punto, la llamada al sistema ha concluido y el programa continúa su ejecución en el espacio de usuario justo después de la instrucción ecall.