MIT 6.828 操作系统课程系列4 traps#

https://pdos.csail.mit.edu/6.828/2022/labs/traps.html
本次lab需要读xv6手册第4章。

riscv的calling convention文档在 riscv-non-isa/riscv-elf-psabi-doc
本来是在riscv-spec文档的,后来被挪出。
https://pdos.csail.mit.edu/6.828/2022/readings/riscv-calling.pdf
https://wiki.riscv.org/display/HOME/RISC-V+Technical+Specifications

trap相关内容在lab0和lab2里已经看了不少。

再次强烈建议先完成CS143编译原理,这些call stack的东西就很熟了。

做题1:RISC-V assembly#

  • 看看call.asm。挺难看的。

做题2:Backtrace#

  • calling convention和CS143有不同。寄存器的保存方式不同可对代码逻辑有影响。

  • c中嵌入asm的语法https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#Extended-Asm

  • 例如asm volatile("mv %0, sp" : "=r" (x) );
    AssemblerTemplate : OutputOperands的形式。%0代表右边OutputOperands的第1项。=r表示register。
    结果就是把sp的值返回到x。

  • r_fp读出fp。fp-8位置是ra。-16位置是saved fp。用saved fp往上回溯即可。

  • 限定在一页。超出退出。

做题3:Alarm#

要做一个syscall实现定时调用user函数

先看看时钟相关流程

// 时钟初始化
// 每个cpu都会执行
// 此时cpu为M模式

uint64 timer_scratch[NCPU][5];

// core local interruptor (CLINT), which contains the timer.
#define CLINT 0x2000000L
#define CLINT_MTIMECMP(hartid) (CLINT + 0x4000 + 8*(hartid))
#define CLINT_MTIME (CLINT + 0xBFF8) // cycles since boot.

timerinit()
    // each CPU has a separate source of timer interrupts.
    int id = r_mhartid();

    // ask the CLINT for a timer interrupt.
    int interval = 1000000; // cycles; about 1/10th second in qemu.
    *(uint64*)CLINT_MTIMECMP(id) = *(uint64*)CLINT_MTIME + interval;
    // 详见lab7

    // #define CLINT_MTIME (CLINT + 0xBFF8)这个地址存的是开机以来的tick数。
    // CLINT_MTIMECMP是每个cpu的timecmp地址。

    // 从CLINT + 0x4000开始每8字节存一个cpu的timecmp。
    // 当系统的tick数大于timecmp值,就触发该cpu的timer中断。这样实现定时。
    // 见riscv-privileged文档

    // 参考
    // https://github.com/riscv/riscv-aclint/blob/main/riscv-aclint.adoc#2-machine-level-timer-device-mtimer
    // https://github.com/qemu/qemu/blob/master/hw/riscv/virt.c
    // https://danielmangum.com/posts/risc-v-bytes-timer-interrupts/


    // prepare information in scratch[] for timervec.
    // scratch[0..2] : space for timervec to save registers.
    // scratch[3] : address of CLINT MTIMECMP register.
    // scratch[4] : desired interval (in cycles) between timer interrupts.
    uint64 *scratch = &timer_scratch[id][0];
    scratch[3] = CLINT_MTIMECMP(id);
    scratch[4] = interval;
    w_mscratch((uint64)scratch);

    // set the machine-mode trap handler.
    w_mtvec((uint64)timervec);
    // 配置M模式的trap处理

    // enable machine-mode interrupts.
    w_mstatus(r_mstatus() | MSTATUS_MIE);

    // enable machine-mode timer interrupts.
    w_mie(r_mie() | MIE_MTIE);
    // 使能M模式timer中断
# 时钟中断处理

# 再次设定interval的定时。并且转发到S模式。

timervec:
    # start.c has set up the memory that mscratch points to:
    # scratch[0,8,16] : register save area.
    # scratch[24] : address of CLINT's MTIMECMP register.
    # scratch[32] : desired interval between interrupts.

    csrrw a0, mscratch, a0
    sd a1, 0(a0)
    sd a2, 8(a0)
    sd a3, 16(a0)

    # schedule the next timer interrupt
    # by adding interval to mtimecmp.
    ld a1, 24(a0) # CLINT_MTIMECMP(hart)
    ld a2, 32(a0) # interval
    ld a3, 0(a1)
    add a3, a3, a2
    sd a3, 0(a1)

    # arrange for a supervisor software interrupt
    # after this handler returns.
    li a1, 2
    csrw sip, a1

    ld a3, 16(a0)
    ld a2, 8(a0)
    ld a1, 0(a0)
    csrrw a0, mscratch, a0

    mret  # start()中已设置代理。mret会跳转到kernelvec或uservec

    # 对于设备中断,kerneltrap和usertrap都会走到devintr进行检测。
    # 如果是时钟中断。会走clockintr把全局的ticks+1。

上面的流程维护好了全局的ticks
看一个相关的应用sleep

uint64
sys_sleep(void)
{
    int n;
    uint ticks0;

    argint(0, &n);                    // 等待n个tick
    acquire(&tickslock);              // 获取ticks锁
    ticks0 = ticks;
    while(ticks - ticks0 < n){        // 检查是否达到n
        if(killed(myproc())){
            release(&tickslock);
            return -1;
        }
        sleep(&ticks, &tickslock);
            struct proc *p = myproc();

            acquire(&p->lock);
            release(lk);              // 释放ticks锁

            // Go to sleep.
            p->chan = chan;           // sleep的标志
            p->state = SLEEPING;      // 设置为SLEEPING

            sched();
                swtch(&p->context, &mycpu()->context);
                // 当前进程一定是从scheduler函数切换过来的。
                // 所以这里一定是切换回scheduler函数。让kernel继续调度。
                // 此进程开始卡在此处

                    // 之后会发生时钟中断
                    clockintr()
                        acquire(&tickslock);

                        ticks++;         // ticks被更新

                        wakeup(&ticks);
                            // 让SLEEPING的进程都醒来
                            if(p->state == SLEEPING && p->chan == chan) {
                                p->state = RUNNABLE;
                            }

                        release(&tickslock);

                // 醒来后。等kernel调度,切换回此地。

            p->chan = 0;

            // Reacquire original lock.
            release(&p->lock);
            acquire(lk);


        // 到此经过了一轮sleep。ticks已更新。再次检查释放等待了n个tick。如果超过n,说明完成sleep,跳出循环返回。
    }
    release(&tickslock);
    return 0;
}

可以看到这种sleep是不准的。完全看调度的心情,如果有很多其他进程,cpu可能先跑其他进程。
等再次检查ticks时,可能早超过了n。
而且必须有个wakeup来配合,感觉不好。


  • 起两个system call。proc里起相关的数据。

  • usertrap里判断tick。如果到时,设置epc为传入的函数。此时回到user空间,能进入alarm回调函数。

  • 但是回不到timer中断时user空间主函数的位置。因为场景已经乱了。
    进中断时保存了某个场景的寄存器,而只改一个epc返回后,造成在旧的场景下执行回调函数的指令,完全混乱。

  • 课程给了解决方案,用户必须在回调里调一个sigreturn。
    那么在timer回调之前先另外保存一份原始的trapframe。sigreturn里把它恢复就行了。
    之前的本质问题就是原始的trapframe被损坏。

  • syscall默认a0会被写成返回值。针对sigreturn处理一下即可。