1.分时多任务系统

分时多任务系统:任务的切换不是通过用户程序来自行放弃cpu的使用权作为前提的,而是内核自己来决定何时切换任务,这个切换的原则就是每个任务一次只能运行一段时间,时间一到就会被操作系统强制切换到下一个任务执行

因此需要定时器来实现时钟中断

riscv的时钟中断

中断可以分为三类:

  • 软件中断:由软件控制放出的中断
  • 时钟中断:由时钟电路发出的中断
  • 外部中断:由外设发出的中断

scause的最高位为1时代表此次触发的异常为中断类型:

Interrupt Exception Code Description
1 1 Supervisor software interrupt (S模式下软件中断)
1 3 Machine software interrupt(M模式下软件中断)
1 5 Supervisor timer interrupt(S模式下时钟中断)
1 7 Machine timer interrupt(M模式下时钟中断)
1 9 Supervisor external interrupt (S模式下外部中断)
1 11 Machine external interrupt(M模式下外部中断)

可以看到这三种中断每一个都有 M/S 特权级两个版本。中断的特权级可以决定该中断是否会被屏蔽,以及需要 TrapCPU 的哪个特权级进行处理。我们的目标是在S态使用时钟中断,这涉及到两个个在S态控制中断的寄存器sstatus,sie

sstatusbit[2]用来使能S态模式下的所有中断

image-20240619151405654

siebit[5]用来专门使能S态的时钟中断

  • STIEbit[5]用来专门使能S态的时钟中断
  • 它的三个字段 ssie/stie/seie 分别控制 S 特权级的软件中断、时钟中断和外部中断的中断使能

image-20240619151533223

比如对于 S 态时钟中断来说,如果 CPU 不高于 S 特权级,需要 sstatus.siesie.stie 均为 1 该中断才不会被屏蔽;如果 CPU 当前特权级高于 S 特权级,则该中断一定会被屏蔽。 —- 例如当前CPU特权级为M模式

计时器:

  • RISC-V 64 架构上,该计数器保存在一个 64 位的 CSR mtime 中,我们无需担心它的溢出问题,在内核运行全程可以认为它是一直递增的。这个计数器一般我们叫做RTC
  • 另外一个 64 位的 CSR mtimecmp 的作用是:一旦计数器 mtime 的值超过了 mtimecmp,就会触发一次时钟中断。

OS/timer.c

timer_init()函数中,分别将sstatus.sie 置 1 和sie.stie ,操作sie寄存器的代码放在riscv.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include "os.h"
#define CLOCK_FREQ 10000000
#define TICKS_PER_SEC 500

/* 设置下次时钟中断的cnt值 */
void set_next_trigger()
{
sbi_set_timer(r_mtime() + CLOCK_FREQ / TICKS_PER_SEC); // 计算下一次中断的时间(当前时间 + 每秒的时钟频率除以每秒的时钟滴答数)
}

/* 开启S模式下的时钟中断 */
void timer_init()
{
reg_t sstatus =r_sstatus();
sstatus |= (1L << 1) ; // 设置sie 为1
w_sstatus(sstatus);
reg_t sie = r_sie();
sie |= SIE_STIE; // 设置stie为1
w_sie(sie);
set_next_trigger();
}
/* 以us为单位返回时间 */
/* 以us为单位返回时间 */
uint64_t get_time_us()
{
reg_t time = r_mtime() / (CLOCK_FREQ / TICKS_PER_SEC); // 除以每秒的时钟频率除以每秒的时钟滴答数,以微秒为单位返回当前时间。
return time;
}

OS/riscv.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* Supervisor Interrupt Enable*/
#define SIE_SEIE (1L << 9) // external
#define SIE_STIE (1L << 5) // timer
#define SIE_SSIE (1L << 1) // software

static inline reg_t r_sie()
{
reg_t x;
asm volatile("csrr %0, sie" : "=r" (x) );
return x;
}

static inline void w_sie(reg_t x)
{
asm volatile("csrw sie, %0" : : "r" (x));
}

/* 获取mtime*/
static inline reg_t r_mtime()
{
reg_t x;
asm volatile("rdtime %0" : "=r"(x));
// asm volatime("csrr %0, 0x0C01" : "=r" (x) )
return x;
}

获取mtime的值:

  • 为了设置时钟中断的频率我们需要先读到mtime的值,然后设置mtimecmp ,这两个寄存器都是M模式下的,在S模式下不能直接访问
  • 第一种是使用rdtime这个伪指令,这里是在哪里找的呢,在opensbi的源码中,在lib/sbi/sbi_timer.c
  • 第二种方式是asm volatime("csrr %0, 0x0C01" : "=r" (x) )来读取,mtime这个寄存器通过MMIO映射到了一个确定的地址,这个地址和平台有关,在opensbi源码的sbi_emulate_csr.c中,opensbimtime的值映射到了0xc01的地方,这是opensbi做了二次映射,用于S态的程序来读取,实际mtime的映射地址应该由qemu来做的

OS/sbi.c

mtimecmp的值可以通过opensbi提供的接口来设置

1
2
3
4
5
6
7
8
9
10
11
/**
* sbi_set_timer() - Program the timer for next timer event.
* @stime_value: The value after which next timer event should fire.
*
* Return: None
*/
void sbi_set_timer(uint64_t stime_value)
{
sbi_ecall(SBI_EXT_TIME, SBI_FID_SET_TIMER, stime_value,
0, 0, 0, 0, 0);
}

OS/sbi.h

1
2
3
4
5
enum sbi_ext_time_fid {
SBI_EXT_TIME_SET_TIMER = 0,
};

#define SBI_FID_SET_TIMER SBI_EXT_TIME_SET_TIMER
  • qemurtc的频率为10mhz,即10^7
  • 在上面的代码中,我将1s分成了1000个时间片,即每隔1us触发一次时钟中断,因此每次触发时钟中断设置的mtimecmp值为:r_mtime() + CLOCK_FREQ / TICKS_PER_SEC

2.实现

OS/trap.c

修改:只需要在时钟中断到来时,设置下一次时钟中断的mtimecmp的值,并切换一次任务

  • scause最高位为1时代表为中断则进入中断的判断分支,否则进入异常的处理分支。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
trap_Context* trap_handler(trap_Context* cx)
{
reg_t scause = r_scause();

reg_t cause_code = scause & 0xfff;

// 1 << 63 = 0x8000000000000000
if (scause & 0x8000000000000000){
// 中断模式
switch (cause_code)
{
/* rtc 中断*/
case 5:
set_next_trigger();
schedule();
break;
default:
printk("undfined interrrupt scause:%x\n", scause);
break;
}
} else {
// 异常模式
switch (cause_code)
{
/* U模式下的syscall */
case 8:
cx->a0 = __SYSCALL(cx->a7,cx->a0,cx->a1,cx->a2);
cx->sepc += 8;
break;
default:
printk("undfined exception scause:%x\n",scause);
break;
}

}


return cx;
}

3.测试

OS/app.c

  • 注释掉yield 然后让任务自主根据定时器切换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
uint64_t sys_gettime() {
return syscall(__NR_gettimeofday,0,0,0);
}

void task1()
{
const char *message = "task1 is running!\n";
int len = strlen(message);
while (1)
{
sys_wirte(1,message, len);

}
}


void task2()
{
const char *message = "task2 is running!\n";
int len = strlen(message);
while (1)
{
sys_wirte(1,message, len);

}



}

void task3()
{
const char *message = "task3 is running!\n";

int len = strlen(message);
while (1)
{
sys_wirte(1,message, len);
//task_delay(10000);
//sys_yield();
}

}


void task_init(void)
{
task_create(task1);
task_create(task2);
task_create(task3);
}

OS/main.c

添加时钟初始化

1
2
3
4
5
6
7
8
9
10
11
#include "os.h"

void os_main()
{
printk("hello!!!\n");
trap_init();
task_init();
timer_init(); // 时钟初始化
run_first_task();

}

总结:

其余的修改见commit,主要就是添加了timer.c文件 已经修改 中断与异常分发,区分中断与异常 为1则为中断,以及配置相应的时钟,获取RTL时间。

  • RISC-V 64 架构上,该计数器保存在一个 64 位的 CSR mtime 中,我们无需担心它的溢出问题,在内核运行全程可以认为它是一直递增的。这个计数器一般我们叫做RTC
  • 另外一个 64 位的 CSR mtimecmp 的作用是:一旦计数器 mtime 的值超过了 mtimecmp,就会触发一次时钟中断。
1
2
3
4
5
/* 关键代码*/
case 5:
set_next_trigger();
schedule();
break;

sudo ./build.sh

sudo ./run.sh

image-20240619170728006