一:进程的表示:
在jos系统里面,进程用一个struct结构体Env来存储相应的信息,这个ENV的结构体有点类似于进程描述符一类的东西。
struct Env {
struct Trapframe env_tf; // Saved registers
struct Env *env_link; // Next free Env
envid_t env_id; // Unique environment identifier
envid_t env_parent_id; // env_id of this env's parent
enum EnvType env_type; // Indicates special system environments
unsigned env_status; // Status of the environment
uint32_t env_runs; // Number of times environment has run
// Address space
pde_t *env_pgdir; // Kernel virtual address of page dir
}
env_tf:表示的是进程的寄存器信息。
env_link: 指向下一个空闲的进程项。
env_id: 进程号。
env_parent_id:父进程pid
EnvType env_type: 进程类型,应该就是用来指明是用户进程还是系统进程。
env_status: 进程的状态,主要是四个:阻塞态,就绪态,运行态,结束。还有一个表明是空闲进程,即进程没有被申请。
env_runs: 进程运行的时间,后面的进程调度应该会用到,在lab3中,暂时没有什么用,只是记录一下运行的次数。
env_pgdir: 进程的地址空间,在调换进程之后, 用命令lcr3(env_pgdir)可以很方便的切换进程的地址空间。
env_tf:进程保存寄存器信息的结构体,
struct PushRegs {
uint32_t reg_edi;
uint32_t reg_esi;
uint32_t reg_ebp;
uint32_t reg_oesp; /* Useless */
uint32_t reg_ebx;
uint32_t reg_edx;
uint32_t reg_ecx;
uint32_t reg_eax;
}
struct Trapframe {
struct PushRegs tf_regs;
uint16_t tf_es;
uint16_t tf_padding1;
uint16_t tf_ds;
uint16_t tf_padding2;
uint32_t tf_trapno;
/* below here defined by x86 hardware */
uint32_t tf_err;
uintptr_t tf_eip;
uint16_t tf_cs;
uint16_t tf_padding3;
uint32_t tf_eflags;
/* below here only when crossing rings, such as from user to kernel */
uintptr_t tf_esp;
uint16_t tf_ss;
uint16_t tf_padding4;
}
其中的padding表示的是填充的数据,考虑到数据对其的关系,每一个寄存器都用32位数据存储,而es,ds等寄存器是16位的,所以用数据填充的方式来填剩下的16位数据,以保证4字节对其的要求。
二:进程管理
在JOS系统里面,采用和管理页相同的方式来管理进程,即用一个进程表来管理所有的进程,空闲的进程通过env_link相连接,用env_free_list指向空闲进程表的头节点。
进程表的初始化:
env_free_list = 0;
int i;
for( i = NENV -1; i>=0; i--){
envs[i].env_id = 0;
envs[i].env_link = env_free_list;
env_free_list = &envs[i];
}
进程的管理方法和页面的管理方法是相同的,都是用一组结构体的数组来管理,其具体的结构参照页面的结构,两个结构是完全一样的。
在这种管理的方式下,对于空闲进程的申请和添加,只需要用env_free_list这个参数就可以了。
通过动态的调整env_free_list,可以非常方便的判断进程表是否为空,添加删除操作也非常的简单,和页面的添加删除是一样的。
三:第一个用户进程的建立
在lab3里面,内核只建立一个用户进程,所以可以简化为以下几个步骤:
1. 向进程表申请空闲的进程,如果失败,返回一个负数。
2.将用户进程的elf文件载入用户进程的地址空间。
void
env_create(uint8_t *binary, enum EnvType type)
{
struct Env* env=0;
int r = env_alloc(&env, 0); //申请进程
if(r < 0)
panic("env_create fault\n");
load_icode(env, binary);
env->env_type = type;
}
3.1向进程表申请空闲的进程,相关代码如下:
int
env_alloc(struct Env **newenv_store, envid_t parent_id)
{
int32_t generation;
int r;
struct Env *e;
if (!(e = env_free_list))
return -E_NO_FREE_ENV;
// Allocate and set up the page directory for this environment.
if ((r = env_setup_vm(e)) < 0)
return r;
// Generate an env_id for this environment.
generation = (e->env_id + (1 << ENVGENSHIFT)) & ~(NENV - 1);
if (generation <= 0) // Don't create a negative env_id.
generation = 1 << ENVGENSHIFT;
e->env_id = generation | (e - envs);
// Set the basic status variables.
e->env_parent_id = parent_id;
e->env_type = ENV_TYPE_USER;
e->env_status = ENV_RUNNABLE;
e->env_runs = 0;
// Clear out all the saved register state,
memset(&e->env_tf, 0, sizeof(e->env_tf));
// Set up appropriate initial values for the segment registers.
e->env_tf.tf_ds = GD_UD | 3;
e->env_tf.tf_es = GD_UD | 3;
e->env_tf.tf_ss = GD_UD | 3;
e->env_tf.tf_esp = USTACKTOP;
e->env_tf.tf_cs = GD_UT | 3;
// You will set e->env_tf.tf_eip later.
// commit the allocation
env_free_list = e->env_link;
*newenv_store = e;
cprintf("[%08x] new env %08x\n", curenv ? curenv->env_id : 0, e->env_id);
return 0;
}
在env_alloc的函数中,系统主要做了两件事:1.向进程表申请空闲的进程。2.对新申请的进程的struct Env(进程描述符)进行初始化,主要是对段寄存器的初始化。
内核要开始第一个用户进程,而第一个用户进程不是通过中断等方法来进入到内核的,而是由内核直接载入,创建的。所以需要人为的制造假象,让系统以为,第一个进程是原来就有的,它通过中断,进入了内核。在内核处理完了相应的操作之后,才返回用户态的。所以,我们需要认为的模拟这个过程,要对进程的进程描述符进行初始化,特别是一个寄存器,要模仿int x 指令的作用。
向进程表申请进程比较简单,下面主要来看对进程的Env进行初始化的过程。
1.初始化pgdir的过程。
if ((r = env_setup_vm(e)) < 0)
return r;
函数env_setup就是初始化e的页表目录的过程,具体的函数如下:
static int
env_setup_vm(struct Env *e)
{
int i;
struct PageInfo *p = NULL;
// Allocate a page for the page directory
if (!(p = page_alloc(ALLOC_ZERO)))
return -E_NO_MEM;
e->env_pgdir =page2kva(p);
p->pp_ref++;
//map UTOP以上的部分
memcpy(e->env_pgdir, kern_pgdir, PGSIZE);
memset(e->env_pgdir, 0, UTOP>>PTSHIFT);
// UVPT maps the env's own page table read-only.
// Permissions: kernel R, user R
e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_P | PTE_U;
return 0;
}
pgdir的初始化,只要注意一点。由于每个用户进程都需要共享内核空间,所以对于用户进程而言,在UTOP以上的部分,和系统内核的空间是完全一样的。因此在pgdir开始设置的时候,只需要在一级页表目录上,把共享部分的一级页表目录部分复制进用户进程的地址空间就可以了,这样,就实现了页面的共享。因为一级页目录里面存储的是二级页表目录的物理地址,其直接映射到物理内存部分,而共享的内核部分的二级页目录在前期的内核操作中,已经完成了映射,所以二级页目录是不需要初始化的。简单来说,不需要映射二级页表的原因是,用户进程可以和内核共用这些二级页表。
接下来,就是初始化进程描述符的各个变量,包括id, 父进程id,寄存器清零等。最后,就是讲各个段寄存器都初始化位用户段,初始化esp地址。
注意,上述对寄存器的初始化都是在进程描述符里面的变量初始化,实际的各个寄存器是不变的。
3.2 将用户进程的文件载入内存
要看明白这部分内容,先要对elf文件格式有一定的了解,传送门:http://blog.csdn.net/fang92/article/details/48092165
load_icode(env, binary)函数:
即把用户进程文件载入内存,在lab3中,由于还没有实现文件系统,所以用户进程实际的存放的位置实际上是在内存中的,文件载入内存,实际上是内存之间的数据的复制而已。
这里需要注意一点,在讲程序载入内存的时候,需要把pgdir设置为用户进程的页目录,这样,这些程序才会载入用户进程所属的地址空间。
而且在载入的过程中,根据elf文件中程序头表中载入内存的va和memsz,还需要实时的为用户空间申请新的地址映射,在这个过程中,会建立新的页表。
从这个过程中,可以看出,为什么二级页表相对于一级页表,会有很大的物理内存的节约。
在进程刚开始建立的时候,系统只会为进程建立一个一级页表,二级页表是暂时不建立的。在系统把进程文件读入内存的时候,整个文件需要载入内存的部分都在程序头表中,在程序头表中,指出了需要载入的内存内容,程序头表中会给出载入内存文件的起始虚拟地址va和所占内存大小的memsz。有了这两个参数,对于程序,只需要建立载入内存中的文件所占据的虚拟地址段的相应的二级页表就可以了。而一级页表,由于每个页表之间要求是连续的,所以需要开辟一整块空间来给所有的页表,而其中,其实对于大部分的用户进程来说,有很多的页表所对应的地址都是用不到的,所以会造成巨大的浪费。
load_icode(env, binary)函数:用户文件载入内存(注意开始和最后的pgdir的变换)
static void
load_icode(struct Env *e, uint8_t *binary)
{
struct Elf* elf = (struct Elf*) binary;
if (elf->e_magic != ELF_MAGIC)
panic("e_magic is not right\n");
//首先要更改私有地址的pgdir
lcr3( PADDR(e->env_pgdir));
struct Proghdr *ph =0;
struct Proghdr *phEnd =0;
int phNum=0;
pte_t* va=0;
ph = (struct Proghdr*) ( binary + elf->e_phoff );
int num = elf->e_phnum;
int i=0;
for(; i<num; i++){
ph++;
cprintf("ph%d = %x\n", i, (unsigned int)ph);
//可载入段
if(ph->p_type == ELF_PROG_LOAD){
region_alloc(e, (void *)ph->p_va, ph->p_memsz);
memmove((void*)ph->p_va, (void*)(binary + ph->p_offset), ph->p_filesz);
memset((void*) (ph->p_va + ph->p_filesz), 0, ph->p_memsz - ph->p_filesz);
}
}
phEnd = ph + elf->e_phnum;
e->env_tf.tf_eip = elf->e_entry;
// Now map one page for the program's initial stack
// at virtual address USTACKTOP - PGSIZE.
region_alloc(e,(void*)USTACKTOP - PGSIZE,PGSIZE);
lcr3(PADDR(kern_pgdir));
}
region_alloc(env,va, memsz): 为va申请地址,并且完成映射
static void
region_alloc(struct Env *e, void *va, size_t len)
{
pde_t* pgdir = e->env_pgdir;
int i=0;
//page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
//struct PageInfo *page_alloc(int alloc_flags)
int npages = (ROUNDUP((pte_t)va + len, PGSIZE) - ROUNDDOWN((pte_t)va, PGSIZE)) / PGSIZE;
for(;i<npages;i++){
struct PageInfo* newPage = page_alloc(0);
if(newPage == 0)
panic("there is no more page to region_alloc for env\n");
int ret = page_insert(pgdir, newPage, va+i*PGSIZE, PTE_U|PTE_W );
if(ret)
panic("page_insert fail\n");
}
return ;
}
4. 进程运行
通过上面的准备,进程已经可以运行了。
进程的运行部分分为两个部分:
lcr3( PADDR(curenv->env_pgdir) );
env_pop_tf(& (curenv->env_tf) );
1.切换用户进程的地址空间,通过cr3寄存器来实现。
2.载入寄存器的值。
在上面的向进程表中申请进程的过程中,已经对进程描述符里面的寄存器进行了初始化,所以这里需要调用env_pop_tf()函数来给寄存器真正的赋值。
env_pop_tf函数
由于要给寄存器赋值,所以要用内嵌汇编来实现。
void
env_pop_tf(struct Trapframe *tf)
{
__asm __volatile("movl %0,%%esp\n"
"\tpopal\n"
"\tpopl %%es\n"
"\tpopl %%ds\n"
"\taddl $0x8,%%esp\n" /* skip tf_trapno and tf_errcode */
"\tiret"
: : "g" (tf) : "memory");
panic("iret failed"); /* mostly to placate the compiler */
}
这个内联汇编,首先把tf的值赋给esp,即tf结构体的首位地址被傅给了栈指针,注意出栈,是从低地址向高地址走的。所以这里的内嵌汇编,就是从头到尾,一个一个的弹出*tf里面的变量。
结合Trapframe这个结构体,就可以很清楚的看到,整个寄存器赋值的过程是怎么进行的了。
首先用popa操作来弹出所有的通用寄存器,其中esp寄存器的值不压入esp,会被直接舍弃。
之后,弹出es,ds,然后通过给esp+8的指令,跳过trapno和err两个变量,所以现在的esp寄存器指向的其实是tf_eip的值,而tf_eip在前面载入“文件”的时候,已经被赋值为用户程序的入口地址了。