OS Lab3笔记

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!!!

上一篇:maven依赖范围


下一篇:Mybatis plus 报错Invalid bound statement (not found) 终极解决办法