Lab-3实验报告
20373915-朱文涛
实验思考题
Thinking 3.1
思考envid2env函数:
为什么envid2env中需要判断e->env_id != envid的情况?如果没有这步判断会发生什么情况?
我们先观察一下envid
的具体结构:
事实上只有ASID
可以标志一个env
的唯一性,我们观察一下envid2env
中关于通过envid
获取对应的env
的做法:
// #define ENVX(envid) ((envid) & (NENV - 1))
// ENVX获取envid的低10位,e即为获取的envs中的envid对应的那一项
e = envs + ENVX(envid);
可以看出它仅仅是通过envid
的后十位在envs
去寻找,但实际上我们需要需要确定找到的就是envid指向的进程,且该进程仍然存活,而不是某个正好占据相同位置的新进程。如果不做此步检查,当一个老进程结束后一个新进程正好占据老进程的位置,此时再用老进程的envid
就会找到新进程的env
块,就会返回一个错误的进程,导致发生严重的错误。
Thinking 3.2
结合include/mmu.h 中的地址空间布局,思考env_setup_vm 函数:
• UTOP 和ULIM 的含义分别是什么,UTOP 和ULIM 之间的区域与UTOP以下的区域相比有什么区别?
• 请结合系统自映射机制解释代码中pgdir[PDX(UVPT)]=env_cr3的含义。
• 谈谈自己对进程中物理地址和虚拟地址的理解。
-
UTOP
是用户进程可以自由使用的地址空间的最高点,ULIM
是kuseg
和kseg0
的分界,即用户空间和内核空间的分界线。用户进程对在UTOP
到ULIM
之间区域的内存一般没有写权限。 -
pgdir[PDX(UVPT)]
实际上是满足页目录自映射的页目录项,将这一项设置为env_cr3
(页目录物理地址),这样,用户进程可以直接通过UVPT
这一虚拟地址访问页目录,不仅省下一页的内存占用,也加速了访问速度。 -
进程中,直接用来访问内存的地址都是虚拟地址。真实地址只有在
TLB
重填时才会被用到。可以说,对用户态进程来说,物理地址是透明的,用户进程无需关心数据的物理地址,只要有虚拟地址即可正常访问。
Thinking 3.3 找到 user_data 这一参数的来源,思考它的作用。没有这个参数可不可以?为什么?(可以尝试说明实际的应用场景,举一个实际的库中的例子)
user_data
这个参数允许我们更好的定制load_elf
的行为,没有这个参数会影响系统的灵活性。我们在
load
时,可能会使用多种不同的mapper,这些mapper可能会需要不同的额外数据来辅助进行映射,
void *
类型的user_data
是一个最好的传递额外数据的方式,因为向void *
型指针强制转换可以自动完
成,同时void *
可读性也更好。
在真实库中,如果某个函数需要使用到用户提供的函数,且希望具有类似泛型的,可处理多种数据的能力,就会用到这种设计。案例:qsort()
函数的width
参数说明了数组每一个元素的大小,方便向比较函数传参。(比较函数的参数都是void *
,需要一个元素的大小,确定是什么样的指针)
// qsort()函数
void qsort(
void* base,
size_t num,
size_t width,
int (*compare)(const void* e1,const void* e2)
);
Thinking 3.4 结合load_icode_mapper 的参数以及二进制镜像的大小,考虑该函数可能会面临哪几种复制的情况?你是否都考虑到了?
-
va
-
va
与页面大小对齐 -
va
与页面大小不对齐
-
-
bin_size
-
bin_size
<=BYP2G
-
bin_size
>BYP2G
-
-
va
+bin_size
-
va
+bin_size
后还在va所在页内 -
va
+bin_size
后超出va所在页内 -
va
+bin_size
页面对齐 -
va
+bin_size
页面不对齐
-
-
sgsize
-
sgsize
>bin_size
:需要填充 -
sgsize
=bin_size
:不需要填充 -
va
+sgsize
页面对齐 -
va
+sgsize
页面不对齐
-
Thinking 3.5 思考上面这一段话,并根据自己在lab2 中的理解,回答:
• 你认为这里的 env_tf.pc 存储的是物理地址还是虚拟地址?
• 你觉得entry_point其值对于每个进程是否一样?该如何理解这种统一或不同?
-
“指令位置”针对的是虚拟空间,因为我们取指时用的地址是虚拟地址。
-
不一定一样。
e_entry
在ELF
文件头是有定义的:
大部分ELF
格式可执行文件的entry_point
都是相同的,但是ELF格式文件中也允许设定程序的entry_point
,这正体现了虚拟内存的优势:进程可以自主的决定自己的布局。此外,操作系统可以支持多种类型的可执行文件,这些可执行文件的entry_point
也不一定相同。
Thinking 3.6 请查阅相关资料解释,上面提到的epc是什么?为什么要将env_tf.pc设置为epc呢?
-
epc
是指即将执行的下一条指令的位置。 -
在我们的
OS
里,如果要进行进程切换,一定是因为发生了中断或者异常发。进入env_run
时如果当前curenv
不是null
,则当前进程进入中断时的寄存器状态必定在TIMESTACK
处存放(中断处理时会先调用.\include\stackframe.h
中的saveall
,而saveall
依赖的sp
指针值在时钟中断(目前唯一的中断)时正是TIMESTACK
)。由于是通过中断进入的,EPC
指向的值就是受害指令,如果我们以后要恢复这个进程的运行,当然是从受害指令开始重新执行,因此应设为env_tf.cp0_epc
。
Thinking 3.7 关于 TIMESTACK,请思考以下问题:
• 操作系统在何时将什么内容存到了 TIMESTACK 区域
• TIMESTACK 和 env_asm.S 中所定义的 KERNEL_SP 的含义有何不同
-
OS
在系统发生中断或者异常是将当前进程的上下文、现场信息保存在TIMESTACK
,以便恢复异常后进程可以正确运行。 -
我们可以观察在
.\lib\genex.S
中关于异常处理的内容,如下:.macro BUILD_HANDLER exception handler clear
.align 5
NESTED(handle_\exception, TF_SIZE, sp)
.set noat
nop
SAVE_ALL
__build_clear_\clear
.set at
move a0, sp
jal \handler
nop
j ret_from_exception
nop
END(handle_\exception)
.endm这个宏表现出了异常处理的一般形式:先保存上下文(
SAVE_ALL
),在跳转至特定的异常处理函数,最后从异常返回。其中,保存上下文(SAVE_ALL
)这个过程定义在.\include\stackframe.h
里SAVE_ALL
先调用get_sp
来获得栈指针,并把寄存器等上下文信息存入栈中,get_sp
内容如下:.macro get_sp
mfc0 k1, CP0_CAUSE
andi k1, 0x107C
xori k1, 0x1000
bnez k1, 1f
nop
li sp, 0x82000000
j 2f
nop
1:
bltz sp, 2f
nop
lw sp, KERNEL_SP
nop
2:
nop
.endmget_sp
所做的事情其实是:如果CP0_CAUSE
中,exccode
的值为0且IRQ4
值为1,则使用0x82000000
作为栈。否则,如果sp>
0x80000000
,则直接使用sp,否则使用KERNEL_SP
作为栈地址。而0x82000000
就是我们的TIME_STACK
,同时,IRQ4
正是时钟中断的中断请求。KERNEL_SP
是内核处理各种异常中断时的通用的栈,而TIME_STACK
专用于处理时钟中断和与之紧密联系的进程切换等任务。
Thinking 3.8 试找出上述 5 个异常处理函数的具体实现位置。
-
handle_int、handle_tlb、handle_tlb、handle_mod
定义在`lib/genex.S中。 -
handle_sys
定义在lib/syscall.S
中。
Thinking 3.9 阅读 kclock_asm.S 和 genex.S 两个文件,并尝试说出set_timer 和timer_irq 函数中每行汇编代码的作用
-
先观察
lib/kclock_asm.S
中的set_timer
函数:
LEAF(set_timer)
#向0xb5000100中写入0xc8,其中0xb5000000 是模拟器(gxemul) 映射实时钟的位置。
#偏移量为0x100 表示来设置实时钟中断的频率。
#0xc8 则表示1 秒钟中断200次,如果写入0,表示关闭实时钟。
li t0, 0xc8
sb t0, 0xb5000100
#设置KERNEl_SP,内核异常处理栈的值
sw sp, KERNEL_SP
#设置CP0的Status寄存器
#STATUS_CU0为仅开启CU0,表示CP0存在的状态;
#0x1001,最低位1开启终端,第13位1使能IRQ4即时钟中断。
setup_c0_status STATUS_CU0|0x1001 0
#返回
jr ra
nop
END(set_timer)
-
在观察处于
lib/genex.S
中的timer_irq
函数:
timer_irq:
#先关闭时钟中断
sb zero, 0xb5000110
#跳转到调度函数中毒对进程进行调度
1: j sched_yield
nop
#跳转到恢复异常函数中
j ret_from_exception
nop
Thinking 3.10 阅读相关代码,思考操作系统是怎么根据时钟周期切换进程的。
-
时钟中断发生时,系统在保存上下文之后跳转到
sched_yield
函数,进行进程的调度。 -
sched_yield
函数首先判断当前进程时间片是否用完,若未用完继续执行当前进程,否则根据调度算法选择一个新进程继续执行,原进程上下文被保存并再次进入就绪队列。 -
最终,新的进程通过调用
env_run
函数被执行。下次时钟中断发生时,重复上述步骤。
实验难点
位图法管理进程
//管理64个进程
static u_int asid_bitmap[2] = {0}; //64位管理64个ASID
static u_int asid_alloc() {
int i, index, inner;
for (i = 0; i < 64; ++i) {
index = i >> 5;
inner = i & 31;
//如果对应第i位ASID没有被申请,则申请
if ((asid_bitmap[index] & (1 << inner)) == 0) {
asid_bitmap[index] |= 1 << inner;
return i;
}
}
//无空闲ASID后报错
panic("too many processes!");
}
static void asid_free(u_int i) {
int index, inner;
index = i >> 5;
inner = i & 31;
//当一个进程被释放时,其对应的ASID也被释放
asid_bitmap[index] &= ~(1 << inner);
}
u_int mkenvid(struct Env *e) {
u_int idx = e - envs;
u_int asid = asid_alloc();
//观察envid的组成,其实只有asid可以保证一个进程的唯一性
return (asid << (1 + LOG2NENV)) | (1 << LOG2NENV) | idx;
}
加载段信息
load_icode_mapper,加载一节的内容到内存中
最坏的情况:
static int load_icode_mapper(u_long va, u_int32_t sgsize,
u_char *bin, u_int32_t bin_size, void *user_data)
{
struct Env *env = (struct Env *)user_data;
struct Page *p = NULL;
u_long i;
int r;
//offset为起va相对va所在页首地址的偏移
u_long offset = va - ROUNDDOWN(va, BY2PG);
//size是va到va所在页的尾地址的大小,即该节的在第一页中的大小
u_long size = BY2PG - offset;
**************先拷贝第一页中不对齐的那部分即[va,(va<<12 + 1) >> 12 -1]*****************
if (offset >= 0) {
//判断2段业内共享
if ((p = page_lookup(env->env_pgdir, va, NULL)) == 0) {
if ((r = page_alloc(&p)) < 0) return r;
//注意这里映射是虚拟地址为va所在页的起始地址
if ((r = page_insert(env->env_pgdir,p,va - offset,PTE_R)) < 0) return r;
}
//使用min旨在考虑va + bin_size仍然在va所在页面内
bcopy((void *)bin,(void *)(page2kva(p) + offset),MIN(bin_size,size));
}
***************拷贝bin_size中剩余的部分,即[(va<<12 + 1) >> 12,va + i -1]******************
//从i=size开始拷贝,因为[0,size-1]已经被拷贝了
for (i = size; i < bin_size; i += BY2PG) {
if ((r = page_alloc(&p)) < 0) return r;
if ((r = page_insert(env->env_pgdir,p,va + i,PTE_R)) < 0) return r;
//这里的min旨在考虑bin_size结束时不足一页的情况
//多余部分不许手动拷贝0,因为page_alloc已经做了这件事
bcopy((void *)(bin + i),(void *)page2kva(p),MIN(bin_size - i,BY2PG));
}