第一章:系统操作接口
每个正在运行的程序,称为进程,都有包含指令、数据和堆栈的内存。指令实现了程序的运算,数据是计算所依赖的变量,堆栈组织程序的过程调用。一台给定的计算机通常有许多进程,但只有一个内核。
一个进程在用户空间和内核空间之间交替执行。
内核使用CPU提供的硬件保护机制来确保每个在用户空间执行的进程只能访问它自己的内存。内核程序的执行拥有操控硬件的权限,它需要实现这些保护;而用户程序执行时没有这些特权。当用户程序调用系统调用时,硬件会提升权限级别,并开始执行内核中预先安排好的函数。
1.1:进程和内存
Xv6进程由用户空间内存(指令、数据和堆栈)和对内核私有的每个进程状态组成。
1.2:I/O和文件描述符
/*2024.4.2*/
#include "kernel/types.h"
#include "user/user.h"
#define RD 0
#define WR 1
const uint INT_LEN = sizeof(int);
//获取左边的第一个数据,其一定为素数
int lpipe_frist_data(int lpipe[2], int *dst){
if(read(lpipe[RD], dst, INT_LEN) == sizeof(int)){
printf("prime %d\n",*dst);
return 0;
}
return -1;
}
//将不能被素数整除的数,传递到下一个进程
void transmit_data(int lpipe[2], int rpipe[2], int frist){
int data;
if(read(lpipe[RD], &data, sizeof(int)) == sizeof(int)){
//进行整除
if(data % frist){
write(rpipe[WR], &data, INT_LEN);
}
close()
}
}
void primes(int lpipe[2]){
close(lpipe[WR]);
int frist;
if(lpipe_frist_data(lpipe, &frist) == 0){
int p[2];
pipe(p);
transmit_data(lpipe, p, frist);
if(fork() == 0){
primes(p);
}
else{
close(lpipe[RD]);
wait(0);
}
}
exit(0);
}
int main(int argc,char const * argv[]){
int p[2];
pipe(p);
for(int i = 2; i <= 35; i++)
write(p[WR], &i, INT_LEN);
//子进程处理过程
if(fork() == 0){
primes(p);
}
else{
close(p[RD]);
close(p(WR));
wait(0);
}
exit(0);
}
在计算机体系结构和汇编语言中,"callee" 和 "caller" 寄存器通常用于描述函数调用中的寄存器使用约定。
-
Caller 寄存器:
- Caller 寄存器是指在函数调用过程中,被调用函数(也称为子函数)调用之前保存的寄存器值。
- 在函数调用前,caller 函数(也称为父函数)需要保存它需要在调用子函数后恢复的寄存器值,以确保子函数调用不会破坏 caller 函数的状态。
- Caller 寄存器保存的内容可能包括函数参数、返回地址以及其他需要在函数调用后恢复的寄存器状态。
-
Callee 寄存器:
- Callee 寄存器是指在函数调用过程中,被调用函数(子函数)可以自由使用而不需要保存和恢复的寄存器值。
- 被调用函数(子函数)可以在调用过程中覆盖 callee 寄存器的内容,因为它们不需要将这些寄存器的值保留到调用结束后。
- Callee 寄存器通常用于保存子函数的局部变量、临时变量以及其他与函数执行相关的状态。
总的来说,caller 寄存器是 caller 函数需要保存和恢复的寄存器状态,而 callee 寄存器是 callee 函数可以自由使用而无需保存和恢复的寄存器状态。这两种寄存器的使用约定对于函数调用的正确执行非常重要。
中断的处理:
首先是位于start.c的start函数,这里将所有的中断都设置在Supervisor mode,然后设置SIE寄存器来接收External,软件和定时器中断,之后初始化定时器。
void
start()
{
// set M Previous Privilege mode to Supervisor, for mret.
// mstatus寄存器保存着cpu的模式,将其从机器模式设置为管理模式
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
// 将main的地址看作机器模式下发生异常时指令的地址,并将其保存至mepc寄存器中,当执行mret时,程序从发生异常的指令处恢复执行。
w_mepc((uint64)main);
// disable paging for now.
// 将0写入页表寄存器satp来禁用虚拟地址转换。
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().
// 执行mret指令,系统由机器模式切换至监督者模式,并从main()开始处运行。
asm volatile("mret");
}
执行完start.c后,转入mian函数执行,第一个函数就是console.c函数。
其中console.c对调用uartinit函数对uart进行初始化,先关闭中断,之后设置波特率,设置字符长度为8bit,重置FIFO,最后再重新打开中断。
以上就是uartinit函数,运行完这个函数之后,原则上UART就可以生成中断了。但是因为我们还没有对PLIC编程,所以中断不能被CPU感知。
最终,在main函数中,需要调用plicinit函数。
PLIC与外设一样,也占用了一个I/O地址(0xC000_0000)。代码的第一行使能了UART的中断,这里实际上就是设置PLIC会接收哪些中断,进而将中断路由到CPU。类似的,代码的第二行设置PLIC接收来自IO磁盘的中断。
到目前为止,我们有了生成中断的外部设备,我们有了PLIC可以传递中断到单个的CPU。但是CPU自己还没有设置好接收中断,因为我们还没有设置好SSTATUS寄存器。在main函数的最后,程序调用了scheduler函数。