在实现了第一个系统调用myHelloWorld、虚存管理后,为了实现能够做到分时系统的进程管理,我们需要启用定时器中断。
寄存器
为了实现定时器中断,你需要知道(牢记)如下寄存器,这些寄存器是你在处理定时器中断时特别关心的。
scause
里面存放了中断/异常的原因。

scause存放的内容与对应的含义如下表

例如当发生S mode软件中断时,scause=0x8000000000000001 ;发生S mode定时器中断时,scause=0x8000000000000005 。
sip
用于表示S mode 当前挂起的是什么中断。sip的p表示挂起(pending)

SEIE STIE SSIE表示外部设备中断、定时器中断、软件中断是否被挂起。
sie
用于控制S mode是否启用对应中断

SEIE STIE SSIE表示是否启用(enable)外部设备中断、定时器中断、软件中断
mip
mip寄存器各标志位如下图。我们会注意到一件事情,这里面有sip的SEIP、STIP、SSIP位。原因是sip与mip在物理上是同一个寄存器,但是由于它们的视图view不同,一个是s mode的视角,一个是m mode的视角,所以sip并没有mip的MTIP等位,sip是看不到也访问不到的。

mtime
用于计时,mtime 不是一个 CSR(控制状态寄存器),而是一个 内存映射 I/O(MMIO) 寄存器。
因此在xv6中查看这个寄存器中的时间不是通过csrw指令,而是访存。
// memlayout.h 相关宏定义
// 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.
// start.c->timerinit()中初始化定时器
// 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;
...
不过我们还有另一个方式来查看定时器中的时间,就是rdtime,这是一条非特权级指令,所以可以在用户态使用。rdtime对于使用opensbi实现的方法十分关键,因为opensbi方式下,我们总是没有m mode权限的,也就访问不了mtime。
定时器中断使能、配置、处理
虽然按照顺序应当启用→配置→处理,但由于有不同的方式,所以我们先来看看现有的处理方式。
xv6的定时器中断处理方式与sbi的方式有差异,先说xv6。不同xv6的代码有些许区别,这里使用的是2022年的xv6方案。
xv6定时器中断处理方案
xv6的时间中断处理是这样的。用户态发生定时器中断时,直接进入m mode,跳转到mtvec寄存器存放的入口,即timervec(位于kernelvec.S中),它会重置timer时间并写sip寄存器的SSIP位,将其转变成一个S mode的软件中断。
# timervec
...
li a1, 2
csrw sip, a1
...
mret
在timervec最后执行mret后,按照我的理解,应当会回到sepc中存放的入口,如果是在用户态发生的定时器中断,那这个入口应该是usertrap函数。在usertrap函数里面通过scause并结合devintr函数分析。可以转到对应的处理位置,如在这里,我们希望能进行yield(),让当前进程状态变成RUNNABLE,并切换到另一个RUNNABLE进行,从而实现我们的分时,time slice时间片。
opensbi定时器中断处理方案
用户态发生定时器中断时,同样会到m mode对应的handler中去,下面展示的是将_trap_handler入口写入MTVEC的代码
/* handler的配置代码 ,位于固件中,需要通过open_sbi的仓库查看 */
/* Setup trap handler */
lla a4, _trap_handler
csrr a5, CSR_MISA
srli a5, a5, ('H' - 'A')
andi a5, a5, 0x1
beq a5, zero, _skip_trap_handler_hyp
lla a4, _trap_handler_hyp
_skip_trap_handler_hyp:
csrw CSR_MTVEC, a4
之后会来到
/* sbi_trap.c */
static int sbi_trap_nonaia_irq(unsigned long irq)
{
switch (irq) {
case IRQ_M_TIMER:
sbi_timer_process();
break;
case IRQ_M_SOFT:
sbi_ipi_process();
break;
case IRQ_M_EXT:
return sbi_irqchip_process();
default:
if (irq == sbi_pmu_irq_bit()) {
sbi_pmu_ovf_irq();
return 0;
}
return SBI_ENOENT;
}
return 0;
}
其中sbi_timer_process()为
void sbi_timer_process(void)
{
csr_clear(CSR_MIE, MIP_MTIP);
/*
* If sstc extension is available, supervisor can receive the timer
* directly without M-mode come in between. This function should
* only invoked if M-mode programs the timer for its own purpose.
*/
if (!sbi_hart_has_extension(sbi_scratch_thishart_ptr(),
SBI_HART_EXT_SSTC))
csr_set(CSR_MIP, MIP_STIP);
}
我们注意到会将MIP_STIP位写入MIP寄存器的,根据上面有关MIP寄存器的介绍,我们知道也即写入了SIP.STIP位。所以变成了一个S mode的时间中断,而这便是与xv6中的不同,xv6中是视为S mode的软件中断,而这里是视为S mode的时间中断。
由于sbi实现方式下mtvec存放的是sbi的内容,而不像是xv6中我们自己定义的timervec。所以后续我们还需要在S mode识别出这是一个时间中断后,重置定时器。
定时器中断使能
以sbi方案为例,根据描述我们知道我们需要启用m mode的时间中断,s mode的时间中断。m mode我们是不用管的,所以我们只用配置sie寄存器就好了。由于之后可能还有其它中断,例如磁盘的外部设备中断等,所以我们sie寄存器还是启用了三个标志位。
w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);
定时器中断配置
我们需要配置在sbi的_trap_handler执行mret后的跳转地址,按照我的理解应当是kernelvec、uservec。不过这一部分理论上应该在我们实现系统调用的中断处理时就已经完成了。
除了这部分配置,我们还要记得重置定时器。
首先是timerinit()中,第一次重置定时器,我是调用了自己实现的reset_timer
void reset_timer()
{
// ask the CLINT for a timer interrupt.
int interval = 1000000; // cycles; about 1/10th second in qemu.
uint64 n;
asm volatile("rdtime %0" : "=r"(n));
set_timer(n + interval);
}
这里面的set_timer是我的自定义函数,本质使用的是sbi_call long sbi_set_timer(uint64_t stime_value ) 想要实现这个sbi_call,我们需要知道它的EID以及FID
参考 RISC-V Supervisor Binary Interface Specification这份pdf

然后按照sbi call规范进行调用就好了
long set_timer(uint64 next_time)
{
return sbi_call_wrapper(
SBI_EXT_TIME, // EID:
0, // FID: 0
next_time, // arg0 (a0): 绝对时间值
0, // arg1 (a1): 忽略
0 // arg2 (a2): 忽略
);
}
static inline int sbi_call_wrapper(
uint64 ext_id,
uint64 func_id,
uint64 arg0,
uint64 arg1,
uint64 arg2)
{
register uint64 a0 asm("a0") = arg0;
register uint64 a1 asm("a1") = arg1;
register uint64 a2 asm("a2") = arg2;
register uint64 a6 asm("a6") = func_id; // FID 放入 a6
register uint64 a7 asm("a7") = ext_id;
asm volatile("ecall" : "=r"(a0) // 输出:a0 寄存器的值作为返回值
: "r"(a0),
"r"(a1), "r"(a2), "r"(a6), "r"(a7) // 输入:a0, a1, a2, a6, a7
: "memory"); // 告诉编译器内存可能被修改
// 返回值 a0 通常包含错误码(0 表示成功)
return a0;
}
见下图,然后是之后每次发生定时器中断的时候,都要重置定时器。我是放在devintr里实现的。因为不管是u mode 还是 s mode,发生定时器中断的时候都会使用这个函数,
之所以if中为scause == 0x8000000000000001L || scause == 0x8000000000000005L,是因为原来xv6中的实现方案仅仅为0x8000000000000001L,但我的为0x8000000000000005L。为了兼容旧版本(其实时不敢轻易动这部分代码),所以我并没有删掉0x8000000000000001L。
int devintr()
{
uint64 scause = r_scause();
// 定时器中断、软件中断
if (scause == 0x8000000000000001L || scause == 0x8000000000000005L)
{
// software interrupt from a machine-mode timer interrupt,
// forwarded by timervec in kernelvec.S.
reset_timer();
if (cpuid() == 0)
{
clockintr();
}
// acknowledge the software interrupt by clearing
// the SSIP bit in sip.
w_sip(r_sip() & ~2);
return 2;
}
else
{
// 其他外设中断全部忽略
return 0;
}
}
阶段性总结
如是,我们便应该能实现分时系统。但是怎么测试呢,感觉最让人有把握的方式应该是能成功运行fork系统调用。
1873

被折叠的 条评论
为什么被折叠?



