Open utterances-bot opened 2 years ago
原文: 而通过软件设置这些中断代理 CSR 之后,就可以到低特权级处理,“但是 Trap 到的特权级不能低于中断的特权级”。
引号中的内容,是想表达“中断处理程序的特权级不能低于引起中断的程序的特权”吗?
@lindyang 中断并非由程序引起,而是来自 I/O 外设,在 RV 架构下中断也有着特权级,如 S 态/M态时钟中断特权级不同。这里要表达的是中断处理程序特权级不低于中断自身特权级。RV 中中断和异常都属于 Trap,另一条规则是 Trap 之后特权级不能变得更低。有关中断/异常/Trap的名词解释参考这里。
pub fn set_next_trigger() { set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC); }
上文中的“RISC-V 架构要求处理器要有一个内置时钟,其频率一般低于 CPU 主频”,这个也让我困惑。
@lindyang CLOCK_FREQ
是时钟频率,而计数器也是对该时钟的时钟周期进行计数,因此CLOCK_FREQ
也是一秒之内计数器的增量。而我们将一秒钟分为TICKS_PER_SEC
,也即100个时间片,每个时间片10ms,那么每个时间片内计数器的增量就应该是一秒内的总体增量除以这个整体被分为的份数,所以是CLOCK_FREQ/100
。
对于这句话“RISC-V 架构要求处理器要有一个内置时钟,其频率一般低于 CPU 主频”,实际上在 CPU 的微架构内通常有多个不同频率的时钟源(比如各种晶振等),然后他们进行一些组合电路的处理又会得到更多不同频率的信号,不同的电路模块可能使用不同频率的时钟信号以满足同步需求。CPU 的主体流水线所采用的时钟信号的频率是 CPU 的主频,但同时还有另一个用来计时的时钟模块(也就是上面提到的时钟)运行在另一个不同的频率。他们两个的另一个区别是,CPU 的时钟周期在mcycle
寄存器中计数,而时钟的时钟周期在mtime
寄存器中计数,因此这是两个独立且不同的频率。
我有一个疑问,ch3
分支中的os/src/syscall/mod.rs
文件中
pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize {
match syscall_id {
SYSCALL_WRITE => sys_write(args[0], args[1] as *const u8, args[2]),
SYSCALL_EXIT => sys_exit(args[0] as i32),
SYSCALL_YIELD => sys_yield(),
SYSCALL_GET_TIME => sys_get_time(),
_ => panic!("Unsupported syscall_id: {}", syscall_id),
}
}
其中sys_get_time()是定义在user/src/syscall.rs
中的,然后通过user/src/lib.rs
导出的,这里是如何找到sys_get_time这个函数而不报错的呢?
问题已经解决,我是用的云端的vscode,我终端git checkout之后,vscode 显示的os/src/syscall/mod.rs内容与实际的os/src/syscall/mod.rs的内容不一致。所以编译是没有问题的,我读源码反而找不到os/src/syscall/mod.rs中对于sys_get_time()的实现。重新打开vscode之后就可以显示checkout之后的os/src/syscall/mod.rs了
处理陷入时,是不是应该改成只要切换了任务,就重设定时器?现在不是因为定时器中断得到执行权的任务会因为定时器中断提前换出。
@YdrMaster 这样做能使得分时更加精细,但我们目前着重于保底机制:即任务运行了超过一个时间片一定会被切换,而不保证任务单次运行时间的下限。
请问“这是因为当 CPU 在 U 态接收到一个 S 态时钟中断时会被抢占,这时无论 SIE 位是否被设置都会进入 Trap 处理流程”该怎么理解?中断是否被屏蔽是硬件的工作吧?为什么SIE为0也会进入trap_handler呢?
请问“这是因为当 CPU 在 U 态接收到一个 S 态时钟中断时会被抢占,这时无论 SIE 位是否被设置都会进入 Trap 处理流程”该怎么理解?中断是否被屏蔽是硬件的工作吧?为什么SIE为0也会进入trap_handler呢?
sstatus 下的 SIE 位只控制着在 S 模式下的中断使能,如果 sstatus.SIE 标记为 0,则在 S 模式下不会响应中断;但如果控制流在 U 模式下时,sstatus.SIE 位是不会影响中断响应判断的,此时任何 S 特权级的中断都会被响应。
RISC-V 架构的 U (用户态)特权级中断
目前,RISC-V 用户态中断作为代号 N 的一个指令集拓展而存在。有兴趣的同学可以阅读最新版的 RISC-V 特权级架构规范一探究竟。
N extension在最新版(20211203)的privileged spec里被移除了……
Preface
...
Additionally, the following compatible changes have been made since version 1.11:
- Removed the N extension
@whfuyn 谢谢提醒! 虽然N extension在最新版(20211203)的privileged spec里被移除了,但我们觉得用户态中断是一个有趣和有用的功能。
在判断中断是否会被屏蔽的时候,有以下规则:
- 如果中断的特权级低于 CPU 当前的特权级,则该中断会被屏蔽,不会被处理;
想请教一下,这里说的“中断特权级低于CPU当前特权级就会被屏蔽”,是因为低于x
特权级的中断在xie
里都是屏蔽的吗,还是因为都会在xideleg
里被设置成delegate到下一特权级处理才被屏蔽呢?
我在privileged spec里看到了下面这几句,所以有点疑惑:
By default, all traps at any privilege level are handled in machine mode..
An interrupt i will trap to M-mode (causing the privilege mode to change to M-mode) if all of the following are true:
- (a) either the current privilege mode is M and the MIE bit in the mstatus register is set, or the current privilege mode has less privilege than M-mode;
- (b) bit i is set in both mip and mie;
- and (c) if register mideleg exists, bit i is not set in mideleg.
@whfuyn 看了一下文档,我发现自己之前的理解似乎有些问题。我把最新的理解整理一下:
当执行指令的时候触发了一个Trap(可能是中断Interrupt或异常Exception),首先根据中断/异常代理寄存器(ideleg/edeleg)判断这个Trap需要到哪个特权级处理。这个应该是从M特权级逐级向下,比如一个中断,如果在mideleg对应位是1的话还需要看sideleg看是否代理到U,否则就是在M特权级处理。这里有一条优先级更高的规则:如果这个Trap是个异常(对于中断应该没有这条规则),处理这个异常的特权级不能低于异常触发之前CPU所在的特权级,比如如果异常触发之前CPU特权级为M,这个异常被设置为代理到S处理,那么这种情况下这个异常还是会在M处理。参考原文:
Traps never transition from a more-privileged mode to a less-privileged mode. For example, if Mmode has delegated illegal instruction exceptions to S-mode, and M-mode software later executes an illegal instruction, the trap is taken in M-mode, rather than being delegated to S-mode. By contrast, traps may be taken horizontally. Using the same example, if M-mode has delegated illegal instruction exceptions to S-mode, and S-mode software later executes an illegal instruction, the trap is taken in S-mode.
然后是考虑这个Trap是否会被屏蔽。如果这个Trap属于异常,那么绝对不会被屏蔽(注意刚刚提到的那条规则);如果是中断,设中断前CPU特权级为A,中断需要在特权级B处理,有如下几种情况:
如果还有疑问的话欢迎指出。
提问: 我们是如何确定 在QEMU环境下 CLOCK_FREQ
的值?我简单搜索了一圈也没有找到源
@hongjil 在 qemu 提供的设备树里有,可以看我存出来的,也可以用:
qemu-system-riscv64 -machine virt,dumpdtb=dump.dtb
得到 dump.dtb 文件,然后:
dtc -o dump.dts dump.dtb
得到你的环境里的 dts 文件,进去查一下,不同版本不一定一样
@YdrMaster 感谢你的解答
虽然但是,看上去似乎数值上有些偏差 timebase-frequency = <0x989680>
=> 10000000
!= 12500000
另外,这个似乎是cpu主屏,而我们找的CLOCK_FREQ
更像是对应这个?
是我太naive了吗 QAQ
@hongjil 不是,其实是教程有问题,老版本 qemu 是 12.5 MHz,新的改了。CLOCK_FREQ
就是 CPU 主频。不过说是 CPU 主频,但实际就是 time 寄存器自增的频率而已,这个必须是一个稳定的值,真正 CPU 运行的频率不一定。
“只要 CPU 一直在做实际的工作就好 ” --> "要是CPU一直在做有效的工作就好",读起来通顺些
实现这一章的代码时遇到了一个奇怪的 bug: 每当第一个程序退出时, 内核总是会陷入死循环, 无法继续运行.
经过调试后, 发现原因是我把 MAX_APP_NUM
设置成了 512
, 改成 4
后即可正常运行. 试了一下把 MAX_APP_NUM
调到 330 左右就会触发这个 bug:
// os/src/config.rs
pub const MAX_APP_NUM: usize = 330;
猜测原因可能是内核为每个程序都创建了内核栈与用户栈, 当程序太多的时候会导致内存溢出.
更新: 再次测试了一下, 似乎是当 MAX_APP_NUM
足够大时, 会创建很多 TaskControlBlock
, 导致栈溢出.
// os/src/timer.rs
const MICRO_PER_SEC: usize = 1_000_000;
pub fn get_time_us() -> usize { time::read() / (CLOCK_FREQ / MICRO_PER_SEC) } timer 子模块的 get_time_us 以微秒为单位返回当前计数器的值,这让我们终于能对时间有一个具体概念了。实现原理就不再赘述。
仔细思考了一下,这个 get_time_us 返回的应该是当前处理器从通电开始的计时,而非计数器的值。
@linyn-zero 计数器的值就是指你说的那个啊,前面应该讲过。
实现这一章的代码时遇到了一个奇怪的 bug: 每当第一个程序退出时, 内核总是会陷入死循环, 无法继续运行. 经过调试后, 发现原因是我把
MAX_APP_NUM
设置成了512
, 改成4
后即可正常运行. 试了一下把MAX_APP_NUM
调到 330 左右就会触发这个 bug:// os/src/config.rs pub const MAX_APP_NUM: usize = 330;
~猜测原因可能是内核为每个程序都创建了内核栈与用户栈, 当程序太多的时候会导致内存溢出.~
更新: 再次测试了一下, 似乎是当
MAX_APP_NUM
足够大时, 会创建很多TaskControlBlock
, 导致栈溢出.
这个原因可以展开说下不?我自己也试了一下,调大MAX_APP_NUM之后 __all_traps 处的内存变成了一些垃圾数据,这个地方本来应该是一些trap入口指令才对~~
调大 MAX_APP_NUM 是不是会导致 boot_stack 溢出
set_timer 函数是对 mtimecmp 的更新,是对 M 态的 CSR 进行操作,假如进程正处于内核态,此时发生的时钟中断应该是来自M态的,处于内核态的进程没法屏蔽这个时钟中断吧?
set_timer 函数是对 mtimecmp 的更新,是对 M 态的 CSR 进行操作,假如进程正处于内核态,此时发生的时钟中断应该是来自M态的,处于内核态的进程没法屏蔽这个时钟中断吧?
不过,根据 rustsbi 的文档,set_timer 是设置 S 态的时钟.....
fn set_timer(&self, stime_value: u64) Programs the clock for next event after stime_value time. stime_value is in absolute time. This function must clear the pending timer interrupt bit as well. If the supervisor wishes to clear the timer interrupt without scheduling the next timer event, it can either request a timer interrupt infinitely far into the future (i.e., (uint64_t)-1), or it can instead mask the timer interrupt by clearing sie.STIE CSR bit.
@CelestialMelody mtimecmp是一个M态的寄存器,但是也可以把它看成一个per-hart的设备寄存器,事实上不论当前特权级如何,通过一个CLINT约定的MMIO地址都是可以读写的。此外,时钟中断最初应该是一个M态中断,进入rustsbi之后会以软件方式转发给kernel对应的S态中断的handler,如果此时kernel屏蔽了此中断的话,那么会等到kernel取消屏蔽之后再触发S态中断的handler。
比如对于 S 态时钟中断来说,如果 CPU 不高于 S 特权级,需要 sstatus.sie 和 sie.stie 均为 1 该中断才不会被屏蔽;如果 CPU 当前特权级高于 S 特权级,则该中断一定会被屏蔽。
这里说法有问题,RISC-V手册原文:
The SIE bit enables or disables all interrupts in supervisor mode. When SIE is clear, interrupts are not taken while in supervisor mode. When the hart is running in user-mode, the value in SIE isignored, and supervisor-level interrupts are enabled. The supervisor can disable indivdual interrupt sources using the sie register.
注意sstatus.sie和sie寄存器的区别。sstatus.sie位控制所有中断是否有效,sstatus.sie为0时,S态下中断被屏蔽。但是当cpu运行在U态时,sstatus.sie的值被忽略,S态中断始终有效。S态下可以使用sie寄存器来单独设置某个中断源。
// os/src/sbi.rs
const SBI_SET_TIMER: usize = 0;
pub fn set_timer(timer: usize) { sbi_call(SBI_SET_TIMER, timer, 0, 0); }
文档中的代码,在最新的ch3分支中已经改为了 /// use sbi call to set timer pub fn set_timer(timer: usize) { sbi_rt::settimer(timer as ); }
pub fn init_app_cx(app_id: usize) -> usize {
let user_stack_ptr = (&USER_STACK) as *const [UserStack; MAX_APP_NUM];
let kernel_stack_ptr = (&KERNEL_STACK) as *const [KernelStack; MAX_APP_NUM];
debug!(
"UserStack begin {:p}, UserStack end {:p}",
user_stack_ptr,
user_stack_ptr.wrapping_add(MAX_APP_NUM)
);
debug!(
"KernelStack begin {:p}, KernelStack end {:p}",
kernel_stack_ptr,
kernel_stack_ptr.wrapping_add(MAX_APP_NUM)
);
KERNEL_STACK[app_id].push_context(TrapContext::app_init_context(
get_base_i(app_id),
USER_STACK[app_id].get_sp(),
))
}
[DEBUG] [kernel] .text [0x80200000, 0x80208000) [DEBUG] [kernel] .rodata [0x80208000, 0x8023c000) [DEBUG] [kernel] .data [0x8023c000, 0x80272000) [DEBUG] [kernel] boot_stack top=bottom=0x80282000, lower_bound=0x80272000 [DEBUG] [kernel] .bss [0x80282000, 0x8028b000) [DEBUG] UserStack begin 0x80229000, UserStack end 0x80329000 [DEBUG] KernelStack begin 0x80209000, KernelStack end 0x80409000
我想通过打印地址来知道 用户栈和内核栈空间分配在哪里, 但是发现地址范围跨越了多个数据段。请教一下,为什么这里 UserStack 的地址会跨越 .rodata .data .bss
上面的已找到答案:
let user_stack_ptr = (&USER_STACK) as *const [UserStack; MAX_APP_NUM] as *const UserStack;
let kernel_stack_ptr = (&KERNEL_STACK) as *const [KernelStack; MAX_APP_NUM] as *const KernelStack;
debug!(
"UserStack begin {:p}, UserStack end {:p}",
user_stack_ptr,
user_stack_ptr.wrapping_add(MAX_APP_NUM)
);
debug!(
"KernelStack begin {:p}, KernelStack end {:p}",
kernel_stack_ptr,
kernel_stack_ptr.wrapping_add(MAX_APP_NUM)
);
user_stack_ptr.wrapping_add(MAX_APP_NUM) 相当于 user_stack_ptr + sizeof(UserStack) * MAX_APP_NUM
或者:
let user_stack_ptr1: usize = USER_STACK.as_ptr() as usize;
let kernel_stack_ptr1: usize = KERNEL_STACK.as_ptr() as usize;
debug!(
"Userstack begin {:#x}, Userstack end {:#x}",
user_stack_ptr1,
user_stack_ptr1 + USER_STACK_SIZE * MAX_APP_NUM
);
debug!(
"Kernelstack begin {:#x}, Kernelstack end {:#x}",
kernel_stack_ptr1,
kernel_stack_ptr1 + KERNEL_STACK_SIZE * MAX_APP_NUM
);
"timer 子模块的 get_time 函数可以取得当前 mtime 计数器的值;"放在了代码段的最后边,而非代码段的前边,给人一种是对下一段代码段的注解.希望能够纠正一下.
分时多任务系统与抢占式调度 — rCore-Tutorial-Book-v3 3.6.0-alpha.1 文档
https://rcore-os.github.io/rCore-Tutorial-Book-v3/chapter3/4time-sharing-system.html