Lab3
进程控制块
PCB记录进程的外部特征,描述进程的运动变化过程。PCB是系统感知进程存在的唯一标志。
struct Env {
struct Trapframe env_tf; // Saved registers
LIST_ENTRY(Env) env_link; // Free LIST_ENTRY
u_int env_id; // Unique environment identifier
u_int env_parent_id; // env_id of this env's parent
u_int env_status; // Status of the environment
Pde *env_pgdir; // Kernel virtual address of page dir
u_int env_cr3;
LIST_ENTRY(Env) env_sched_link;
u_int env_pri;
};
- LIST_ENTRY(Env):双向链表的指针域,空闲进程链表
struct { \
struct type *le_next; /* next element */ \
struct type **le_prev; /* address of previous next element */ \
}
- env_status:
ENV_FREE:该进程不活动,处于空闲状态,空闲链表中
ENV_NOT_RUNNABLE:阻塞状态,需要等待条件。(suspend)
ENV_RUNNABLE:就绪状态,等待调度,或者正在运行(running||ready)
- env_cr3:保存该进程页目录的物理地址
PS:进程的管理和物理内存的管理极度相似,都是使用一个结构体进行管理,此结构体只是一个信息标记,并不是我们要管理的对象本身
free_list:空闲对象标记组成的链表
array:标识整个系统中现有的所有的对象,无论状态
进程的标识
每个进程独一无二的标识符,env_id
在env.c文件中找到一个叫做mkenvid的函数,生成一个新的进程id
mkenvid函数
mkenvid函数:传入一个Env指针e,返回进程独一无二的env_id
envid的组成
低位:e指针在envs数组中的下标(0-9位)
高位:调用次数计数器(从1开始)
envid2env函数
传入envid,修改的是指针*penv
- envid==0 置为curenv
- 否则置为envs[ENVX(envid)](ENVX:传入id获取index)
返回值:
- 成功:0
- 失败:-E_BAD_ENV,置penv为NULL
流程:
- 根据传入的envid将目标env赋给e(非0的找法是找到envid的0-9位即为index)
- 根据目标env的状态判断操作是否是成功的
- 检查env操作权限
- checkperm为1,代表访问的权限只有curevn或者其直接子进程,如果越界访问,操作失败
- checkperm为0,随意访问
- 置最终的*penv,返回成功的0
设置进程控制块
类比于申请一页的物理内存空间。(page_alloc)
- 申请一个空闲的PCB,空白
- 初始化申请的进程的信息
- 分配资源(alloc出来就是一个实实在在的进程了,不再是一个Env结构,因此需要资源)
- 维护free_list
env_setup_vm
初始化进程的地址空间
对于不同的进程而言,ULIM以上,虚拟地址到物理地址的映射关系都是一样的。
不涉及进程管理,由OS内核管理。
我们的内核态和用户态实际是在同一个4G地址空间中的。
进程从内核态提升到了内核态,只是提升了内核的权限!!!,这种权限变化是动态的。
OS中,每一个完整的进程都有成为临时内核的资格,所有的进程都可以发出请求变成临时的内核态。
因此,每一个进程我们都需要拷贝只有内核才能使用的虚页表,在2G/2G模式下,每一个进程都有可能请求后变成内核态来访问上面2G空间,而用户态下,只能访问自己那2G用户态空间。
- 建立这个新进程对应的页目录,在对应的Env中更新信息,使用page_alloc申请一页的内存存放页目录(page_alloc传入的是一个struct Page**类型,还需要转成page2kva才可以直接使用,而且申请后需要手动让pp_ref++)
- UTOP-ULIM是属于此进程自己的,可以由用户访问,但是用户不能随意修改,只能由操作系统修改,配合OS管理资源的内存区域。
- 因此,将页目录的UTOP前清0
- 页目录的UTOP以上部分子进程和父进程都一样,直接照抄。
- 将页目录对应的项赋值成cr3
env_alloc
- e->env_tf.cp0_status = 0x10001004的解释:
CP0_status就是MIPSR3000里面的SR寄存器
28bit设置为1,处于用户模式下。
12bit设置为1,4号中断可以被响应。
SR寄存器的低6位是一个二重栈的结构,KUo和IEo是一组,每次当中断发生的时候,硬件将KUp和IEp的数值拷贝到这里。KUp和IEp是一组,当中断发生的时候,硬件会把KUc和IEc的数值拷贝到这里。
KU:标识是否在内核模式下,1:内核模式;0:用户模式
IE:1:中断开启;0:不开启
每当rfe指令调用的时候,就会进行逆操作。
KUo IEo KUp IEp KUc IEc
当rfe指令调用的时候,向右赋值
因此:将status后六位设置为000100
运行到第一个进程之前,也就是ref之后,变为000001
- 申请一个空闲的进程控制块(找到的是freelist的头节点 LIST_FIRST),没有free的就返回错误。
- 为这个新的进程控制块设置虚拟内存的结构
- 设置进程控制块的状态
- 维护链表,返回操作码0
加载二进制镜像
为进程的程序分配空间
加载一个ELF到内存,只需要将ELF文件中所有需要加载的segment加载到对应的虚拟地址上就可以了。
int load_elf(u_char *binary, int size, u_long *entry_point, void *user_data,
int (*map)(u_long va, u_int32_t sgsize,
u_char *bin, u_int32_t bin_size, void *user_data))
binary - 待加载的ELF文件,size为该ELF文件的大小
entry_point存放的是解析出的入口地址。
接受一个自定义的函数以及你想传递给自定义函数的额外参数 user_data
load_elf()解析到一个需要加载的segment的时候,会将ELF文件里与加载有关的信息,作为参数传递个map函数,并在map函数中完成加载单个segment的任务
load_icode_mapper函数
加载单个segment到内存
va:该段需要被加载到的内存虚拟地址
sgsize:该段在内存中的大小
bin:该段在ELF文件中的内容
bin_size:该段在文件中的大小。
bin_size可以大于sgsize,其余可以使用0天充
map的本质,按页的大小申请物理空间,并填充对应的页目录项(pgdir_walk),页表项,申请物理空间需要insert
因此对于一个va,按页申请内存,申请到bin_size,这部分是将bin文件的内容填入的
注意va不是4K对齐的,需要先转换到4K对齐,再page_insert建立映射
拷贝关系是(因为有offset)
b-b+4KB-offset pa+offset
b+4KB-offset pa
b+8KB-offset pa
如果segsize大于binsize,继续申请,这部分清0
load_icode
设置用户栈指针(读/写)
加载elf文件
调整PC到entry
load_elf
根据program header table找到phdr项,解析出segment的信息。
加载到各个segment上
创建一个进程(env_create_priority)
申请一个新的Env结构体,
设置进程控制控制块(env_alloc)
设置进程的优先级
将binary加载(load_icode)
插入已经进程调度链表中
PS:创建一个进程不仅需要建立内存管理,还要加载ELF到指定位置,申请堆栈
#define ENV_CREATE_PRIORITY(x, y) \
{ \
extern u_char binary_##x##_start[];\
extern u_int binary_##x##_size; \
env_create_priority(binary_##x##_start, \
(u_int)binary_##x##_size, y); \
}
x代表user_A或者user_B,不同的用户输入不同的binary
y代表priority
进程的运行和切换
创建结束了,就该运行了
env_run()
-
保存当前进程的上下文
保存CPU寄存器,保存在TIMESTACK区域
将CPU寄存器逐个copy到env_tf里面去
pc的值设为原来的epc
-
恢复要启动进程的上下文,加载对应进程的页目录
回复上下文使用的是env_pop_tf
-
运行这个进程
异常的分发
每当发生异常的时候,一般来说,处理器会进入一个用于分发异常的程序
作用是检测发生了哪一种异常,并调用相应的异常处理程序
分发程序被要求放在固定的某个物理地址上
mfc0 k1,CP0_CAUSE
la k0,exception_handlers
andi k1,0x7c
addu k0,k1
lw k0,(k0)
nop
jr k0
nop
取异常码到k1寄存器
也即取出bit2-bit6 ExcCode
以取得道德异常码作为索引去exception_handlers数组中找到对应的中断处理函数
跳转到对应的中断处理函数中,响应异常,并将异常交给对应的异常处理函数去处理。
.text.exec_vec3这段程序需要放在特定的位置0x80000080处
异常向量组
exception_handlers数组就是所谓的中断向量组
这个数组里面存放的是对应的处理程序的首地址
0 号异常的处理函数为handle_int
,
1 号异常的处理函数为handle_mod
,
2 号异常的处理函数为handle_tlb
,
3 号异常的处理函数为handle_tlb
,
8 号异常的处理函数为handle_sys
,
时钟中断
时间片轮转调度:每个进程被分配一个时间段,称为时间片,即该进程允许运行的时间,如果在时间片结束时进程还在运行,那么就挂起,切换到另外一个程序运行。
CPU知道一个进程时间片结束的方法就是通过定时器产生时钟中断,当时一个时钟中断产生的时候,就表明到点了,直接挂起。
初始化时钟主要是在kclock_init函数中,这个函数调用set_timer函数
- 向0xb5000100写入1,其中0xb5000000这个位置是模拟器映射实时钟的位置。offset为0x100来设置时钟中断的频率,写入1表示1秒中断1次。如果写入0,关闭时钟。触发的是4号中断。
- 一旦实时钟中断产生,就会触发MIPS中断,从而MIPS将PC指向0X80000080,跳转到.text.exc_vec3执行。对于实时钟引起的中断,通过分发,最终调用handle_int来处理实时钟中断。
- 在handle_int判断CP0_CAUSE寄存器是不是对应的4号中断位引发的中断,如果是,执行中断服务函数timer_irq。
- 在timer_irq中跳转到sched_ yield 中,也就是调度函数,完成进程的调度。
进程调度
因此,中断的时候最终是调用了sched_yield函数来进行进程的调度
优先级设置为时间片的大小
用两个链表来存储所有就绪状态进程
用一个指针指向当前调度队列
每当一个进程状态变为ENV_RUNNABLE,要将其插入第一个就绪状态进程的链表,调用sched_yield时,先判断当前时间片是否用完,如果用完,就将其插入另外一个就绪进程链表,判断当前就绪状态进程链表是否为空,如果为空,就切换到另外一个就绪状态进程的链表。
指针指向的是当前运行的进程
注意理解一下env_run()的含义,env_run()的意思是关闭当前进程curenv,执行传入的进程next_env,一定要确保传入的进程和当前进程不一样,这个函数由两个动作,先切换,再运行,不是直接运行的
讨论cur是否为NULL!!!