- 当按下系统电源按键后,做一些硬件层面的配置和初始化:
上电复位:在开机时,计算机进行硬件复位,确保寄存器和其他硬件组件处于初始状态。
检查和测试硬件:计算机进行一系列硬件检查和自检操作,以确保硬件组件功能正常。这可能包括内存检测、CPU测试等。
初始化硬件组件:初始化和配置计算机上的各种硬件组件,例如内存控制器、输入输出设备和其他外设。这包括为这些组件提供电源、设置时钟信号等。
读取配置数据:从非易失性存储器(例如CMOS)读取系统配置数据,如BIOS设置、引导设备顺序等。
这些操作通常由计算机的固件(例如BIOS或UEFI)完成。完成硬件和固件的初始化后,
- 计算机将运行引导加载程序(boot loader),该程序存储在只读存储器(ROM)中。引导加载程序负责将xv6内核加载到内存,并将执行权交给内核。
在M模式中,cpu执行内核的起点是 _entry,这个时候虚拟内存还没有工作,所以此时虚拟地址是和物理地址直接映射的
# qemu -kernel loads the kernel at 0x80000000
# and causes each CPU to jump there.
# kernel.ld causes the following code to
# be placed at 0x80000000.
.section .text
.global _entry
_entry:
# set up a stack for C.
# stack0 is declared in start.c,
# with a 4096-byte stack per CPU.
# sp = stack0 + (hartid * 4096)
la sp, stack0
li a0, 1024*4
csrr a1, mhartid # 将当前CPU的硬件线程ID(hartid)读取到寄存器a1中。这个ID是唯一的,用于区分不同的CPU。
addi a1, a1, 1
mul a0, a0, a1
add sp, sp, a0
# jump to start() in start.c
call start
spin:
j spin
这段汇编代码中,设置了stack0这个栈,这样就可以调用C代码了。sp栈指针指向stack0 + 4096这个地址(栈顶)。然后调用start(call start
)
- start函数
// entry.S jumps here in machine mode on stack0.
void
start()
{
// set M Previous Privilege mode to Supervisor, for mret.
unsigned long x = r_mstatus();
x &= ~MSTATUS_MPP_MASK;
x |= MSTATUS_MPP_S;
w_mstatus(x);
// set M Exception Program Counter to main, for mret.
// requires gcc -mcmodel=medany
w_mepc((uint64)main);
// disable paging for now.
w_satp(0);
// delegate all interrupts and exceptions to supervisor mode.
w_medeleg(0xffff);
w_mideleg(0xffff);
w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);
// configure Physical Memory Protection to give supervisor mode
// access to all of physical memory.
w_pmpaddr0(0x3fffffffffffffull);
w_pmpcfg0(0xf);
// ask for clock interrupts.
timerinit();
// keep each CPU's hartid in its tp register, for cpuid().
int id = r_mhartid();
w_tp(id);
// switch to supervisor mode and jump to main().
asm volatile("mret");
}
先做一些只能在M mode做的配置,然后进入S mode.
riscv提供了mret这个指令来实现这个操作。
mret指令的功能是:从S mode进入M mode时,在M mode调用这个mret指令,将会返回到S mode。
此时,虽然在M mode,但是并不是从S mode进来的。
所以,将mstatus
设置为S mode,表示此前的特权模式是 supervisor。
然后,把返回地址设置为main
,通过把main的地址写到mepc
中
然后,禁止虚拟地址翻译,通过把0这个值写入satp
(页表寄存器)
然后,把所有的中断和异常交给S mode
然后,对时钟芯片进行编程,以便产生定时器中断。
对时钟芯片编程意味着配置时钟芯片的相关寄存器,以便在特定时间间隔后产生定时器中断。定时器中断在操作系统中非常重要,因为它们使操作系统能够实现多任务调度、计时等功能。操作系统可以根据需要设置定时器中断的间隔,从而控制时间片长度或其他相关参数。
在xv6操作系统中,对时钟芯片编程主要涉及以下几个步骤:
选择一个合适的时间间隔,用于触发定时器中断。这通常基于处理器的时钟频率和期望的中断频率来计算。
将计算出的时间间隔值写入时钟芯片的相关寄存器。在RISC-V平台上,这通常涉及编程mtimecmp寄存器。mtimecmp寄存器存储了下一个定时器中断的时间值。每当mtime寄存器的值达到mtimecmp寄存器的值时,时钟芯片就会产生一个定时器中断。
配置处理器以接收时钟芯片产生的定时器中断。这可能涉及设置中断控制器的相关寄存器,例如启用定时器中断并配置优先级等。
完成上述操作后,时钟芯片将定期产生定时器中断。操作系统会在每个中断发生时执行特定的中断处理程序,例如更新系统计时器、调度进程等。这使得操作系统能够有效地进行多任务处理、资源管理和时间跟踪。
设置时钟芯片的代码(暂时不重要)
// set up to receive timer interrupts in machine mode,
// which arrive at timervec in kernelvec.S,
// which turns them into software interrupts for
// devintr() in trap.c.
void
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;
// 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);
// enable machine-mode interrupts.
w_mstatus(r_mstatus() | MSTATUS_MIE);
// enable machine-mode timer interrupts.
w_mie(r_mie() | MIE_MTIE);
}
最后,返回到main
- main
main函数初始化设备和子系统;main调用userinit创建第一个进程;第一个进程执行initcode.S中的RISC-V汇编代码。
// Set up first user process.
void
userinit(void)
{
struct proc *p;
p = allocproc();
initproc = p;
// allocate one user page and copy init's instructions
// and data into it.
uvminit(p->pagetable, initcode, sizeof(initcode));
p->sz = PGSIZE;
// prepare for the very first "return" from kernel to user.
p->trapframe->epc = 0; // user program counter
p->trapframe->sp = PGSIZE; // user stack pointer
safestrcpy(p->name, "initcode", sizeof(p->name));
p->cwd = namei("/");
p->state = RUNNABLE;
release(&p->lock);
}
initcode.S加载SYS_EXEC到寄存器a7。
initcode.S调用ecall指令,使CPU重新进入内核。
# Initial process that execs /init.
# This code runs in user space.
#include "syscall.h"
# exec(init, argv)
.globl start
start:
la a0, init
la a1, argv
li a7, SYS_exec
ecall
# for(;;) exit();
exit:
li a7, SYS_exit
ecall
jal exit
# char init[] = "/init\0";
init:
.string "/init\0"
# char *argv[] = { init, 0 };
.p2align 2
argv:
.long init
.long 0
当ecall指令被执行时,CPU将重新进入内核,处理发起的exec系统调用。exec系统调用用于加载和运行一个用户程序,替换当前进程的内存映像。在这种情况下,第一个进程将加载和运行xv6操作系统中定义的初始用户程序。
在这段代码中,exec读取到的file是init,这就是初始化程序,创建了一个新的console,和一些fd。打开fd 0,1,2,打开shell。
标签:定时器,操作系统,启动,中断,xv6,start,init,mode,寄存器 From: https://www.cnblogs.com/ijpq/p/17337986.html