手写操作系统(三十八)-实现进程大致流程

代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。

1.进程虚拟进程空间

进程与内核线程最大的区别是进程有单独的4GB空间,这指的是虚拟地址,物理地址空间可未必有那么大,看似无限的虚拟地址经过分页机制之后,最终要落到有限的物理页中。

每个进程都拥有4GB的虚拟地址空间,虚拟地址连续而物理地址可以不连续。我们需要单独为每个进程维护一个虚拟地址池,用此地址池来记录该进程的虚拟中,哪些已被分配,哪些可以分配。

进程是基于线程实现的,因此它和线程一样使用相同的pcb结构,即struct task_struct,我们要做的就是在此结构中增加一个成员,用它来跟踪用户空间虚拟地址的分配情况。

/thread/thread.h

......
#include "stdint.h"
#include "list.h"
#include "memory.h"

......
/*进程或线程的pcb,程序控制块*/
struct task_struct{
    uint32_t* self_kstack;      //各内核线程都用自己的内核栈
    enum task_status status;
    uint8_t priority;           //线程优先级
    char name[16];
    uint8_t ticks;              //每次在处理器上的嘀咕数
    uint32_t elapsed_ticks;     //此任务自上CPU运行至今用了多少CPU嘀咕数,也就是任务运行了多久
    struct list_elem general_tag;       //用于线程在一般队列中的节点
    struct list_elem all_list_tag;      //用于线程队列thread_all_list中的结点
    uint32_t* pgdir;            //进程自己页表的虚拟地址,如果是线程则为NULL
    struct virtual_addr userprog_vaddr; //用户进程的虚拟地址
    uint32_t stack_magic;       //栈的边界标记,用于检测栈的溢出
};

......

代码更改了task_struct结构体

加入struct virtual_addr userprog_vaddr,这是每个用户进程的虚拟地址池。

加入pgdir用于存放进程页目录表的虚拟地址,这将在为进程创建页表时为其赋值。页表寄存器 cr3 中的应该是页目录表的物理地址,但成员 pgdir 是虚拟地址,这是因为页目录表本身也要占用内存来存储,我们在为进程创建页目录表时,必然要为其申请内存,但内存管理系统返回的地址肯定都是虚拟地址,不可能返回物理地址,因为返回物理地址也没用,在分页机制下,引用的任何地址都被当作虚拟地址,该“物理地址”也要再次被转换成别的物理地址,这就错了。因此在往寄存器cr3中加载页目录地址时,我们会将pgdir转换成物理地址。

 

2.进程页表和3特权级栈

进程与线程的区别是进程拥有独立的地址空间,不同的地址空间就是不同的页表,因此我们在创建进程的过程中需要为每个进程单独创建一个页表。页目录表用来存放页目录项PDE,每个PDE又指向不同的页表。 页表虽然用于管理内存,但它本身也要用内存来存储,所以要为每个进程单独申请存储页目录项及页表项的虚拟内存页。

之前创建的线程属于内核的线程,它们运行在特权级0。用户进程还会在特权级3下工作,我们还要为用户进程创建在3特权级的栈。 栈也是内存区域,所以,还得为进程分配内存(虚拟内存)作为3级栈空间。

修改/kernel/memory.c

#include "memory.h"
#include "bitmap.h"
#include "stdint.h"
#include "global.h"
#include "debug.h"
#include "print.h"
#include "string.h"
#include "sync.h"
#include "thread.h"

#define PG_SIZE 4096
//4KB
#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22)
#define PTE_IDX(addr) ((addr & 0x003ff000) >> 12)

/*位图地址
因为0xc009_f000是内核主线程栈顶,0xc009_e000是内核主线程的pcb。
一个页框大小的位图可表示128MB内存,位图位置安排在地址0xc009_a000,
这样本系统最大支持4个页框的位图,即512MB
*/
#define MEM_BITMAP_BASE 0xc009a000
/****************/

/*0xc000_0000是内核从虚拟地址3G起
0x10_0000是指跨过低端1MB内存,使虚拟地址在逻辑上连续*/
#define K_HEAP_START 0xc0100000

//内存池结构,生成两个实例用于管理内核内存池和用户内存池
struct pool{
    struct bitmap pool_bitmap;  //本内存池用到的位图结构,用于管理物理内存
    uint32_t phy_addr_start;    //本内存池所管理物理内存的起始地址
    uint32_t pool_size;         //本内存池的字节容量
    struct lock lock;           //申请内存时互斥
};

struct pool kernel_pool,user_pool;  //生成内核内存池和用于内存池
struct virtual_addr kernel_vaddr;   //用来给内核分配虚拟地址

/*在pf表示的虚拟内存池中申请pg_cnt个虚拟页,成功则返回虚拟页起始地址,失败则返回NULL*/
static void* vaddr_get(enum pool_flags pf,uint32_t pg_cnt){
    int vaddr_start = 0,bit_idx_start = -1;
    uint32_t cnt = 0;
    if(pf == PF_KERNEL){        //内核内存池
        //在位图中申请pg_cnt位
        bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap,pg_cnt);
        if(bit_idx_start == -1)
            return NULL;
        //申请成功则分配页
        while (cnt < pg_cnt)
            bitmap_set(&kernel_vaddr.vaddr_bitmap,bit_idx_start + cnt++,1);
        vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start*PG_SIZE;
    }else{          //用户内存池
        struct task_struct* cur = running_thread();
        bit_idx_start = bitmap_scan(&cur->userprog_vaddr.vaddr_bitmap,pg_cnt);
        if(bit_idx_start == -1)
            return NULL;
        //申请成功则分配页
        while(cnt < pg_cnt)
            bitmap_set(&cur->userprog_vaddr.vaddr_bitmap,bit_idx_start + cnt++,1);
        vaddr_start = cur->userprog_vaddr.vaddr_start + bit_idx_start*PG_SIZE;
        /*(0xc0000000 - PG_SIZE)作为用户3级栈已经在start_process被分配*/
        ASSERT((uint32_t)vaddr_start < (0xc0000000 - PG_SIZE));
    }
    return (void*)vaddr_start;
}

/*得到虚拟地址vaddr对应的pte指针*/
uint32_t* pte_ptr(uint32_t vaddr){
    uint32_t* pte = (uint32_t*)(0xffc00000 + ((vaddr & 0xffc00000) >> 10) + PTE_IDX(vaddr)*4);
    return pte;
}

/*得到虚拟地址vaddr对应的pde指针*/
uint32_t* pde_ptr(uint32_t vaddr){
    uint32_t* pde = (uint32_t*)((0xfffff000) + PDE_IDX(vaddr)*4);
    return pde;
}

/*在m_pool指向的物理内存池中分配 一个物理页,成功则返回页框物理地址,失败则返回NULL*/
static void* palloc(struct pool* m_pool){
    //检测有无空间
    int bit_idx = bitmap_scan(&m_pool->pool_bitmap,1);
    //分配空间
    if(bit_idx == -1)
        return NULL;
    bitmap_set(&m_pool->pool_bitmap,bit_idx,1);
    uint32_t page_phyaddr = m_pool->phy_addr_start + (bit_idx*PG_SIZE);
    return (void*)page_phyaddr;
}

// /*页表中添加虚拟地址_vaddr与物理地址_page_phyadddr的映射*/
static void page_table_add(void* _vaddr,void* _page_phyaddr){
    uint32_t vaddr = (uint32_t)_vaddr;
    uint32_t page_phyaddr = (uint32_t)_page_phyaddr;
    uint32_t* pte = pte_ptr(vaddr);
    uint32_t* pde = pde_ptr(vaddr);

    /*要确保pde创建完成后才能执行pte*/
    if(*pde & 0x00000001){
        //如果pde存在,则将pte指向物理地址
        ASSERT(!(*pte & 0x00000001));   //pte理应不存在
        if(!(*pte & 0x00000001)){
            *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
        }else{
            PANIC("pte repeat!");
            *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
        }
    }else{
        //页目录项不存在,创建页目录项
        uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool);  //页表中用到的页框一律从内核空间分配
        *pde = (pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
        //pte清零
        memset((void*)((int)pte & 0xfffff000),0,PG_SIZE);   //pte & 0xfffff000表示新页表的起始地址,将整页清零
        ASSERT(!(*pte & 0x00000001));
        *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
    }
}

// /*分配pg_cnt个页空间,成功则返回起始虚拟地址,失败则返回NULL*/
void* malloc_page(enum pool_flags pf,uint32_t pg_cnt){
    ASSERT(pg_cnt > 0 && pg_cnt < 3840);
    /*malloc_page原理的三个动作:
    1. 通过vaddr_get在虚拟内存池中申请虚拟地址
    2. 通过palloc在物理内存池中申请物理地址
    3. 通过page_table_add将两个地址进行映射*/

    //1.在虚拟内存池中申请
    void* vaddr_start = vaddr_get(pf,pg_cnt);
    if(vaddr_start == NULL)
        return NULL;
    uint32_t vaddr = (uint32_t)vaddr_start;
    uint32_t cnt = (uint32_t)pg_cnt;
    struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;

    //2.虚拟内存是连续的,物理内存不是,需要逐个映射
    while (cnt--){
        void* page_phyaddr = palloc(mem_pool);  //申请一个物理地址
        if(page_phyaddr == NULL)
            return NULL;
        //3.在页表中做映射
        page_table_add((void*)vaddr,page_phyaddr);
        vaddr += PG_SIZE;   //下一个虚拟页
    }
    return vaddr_start;
}

// /*在内核物理池中申请一页内存,成功则返回其虚拟地址,失败返回NULL*/
void* get_kernel_pages(uint32_t pg_cnt) {
    lock_acquire(&kernel_pool.lock);
    void* vaddr = malloc_page(PF_KERNEL, pg_cnt);
    if (vaddr != NULL) { // 若分配的地址不为空,将页框清0后返回
        memset(vaddr, 0, pg_cnt * PG_SIZE);
    }
    lock_release(&kernel_pool.lock);
    return vaddr;
}

/*在用户空间中申请4K内存,并返回虚拟地址*/
void* get_user_pages(uint32_t pg_cnt){
    lock_acquire(&user_pool.lock);
    void* vaddr = malloc_page(PF_USER,pg_cnt);      //在用户内存池中以整页为单位分配内存,并返回分配的虚拟地址
    if (vaddr != NULL) { // 若分配的地址不为空,将页框清0后返回
        memset(vaddr, 0, pg_cnt * PG_SIZE);
    }
    lock_release(&user_pool.lock);
    return vaddr;
}

/*将地址vaddr与pf池中的物理地址关联,仅支持一页空间分配;申请一页内存,并用vaddr映射到该页(可以指定虚拟地址)*/
void* get_a_page(enum pool_flags pf,uint32_t vaddr){
    struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
    lock_acquire(&mem_pool->lock);
    /*先将虚拟地址对应的位图置1*/
    struct task_struct* cur = running_thread();
    int32_t bit_idx = -1;

    /*若当前是用户进程申请用户内存,就修改用户进程自己的虚拟地址位图*/
    if(cur->pgdir != NULL && pf == PF_USER){
        bit_idx = (vaddr - cur->userprog_vaddr.vaddr_start) / PG_SIZE;
        ASSERT(bit_idx >= 0);
        bitmap_set(&cur->userprog_vaddr.vaddr_bitmap,bit_idx,1);
    }else if(cur->pgdir == NULL && pf == PF_KERNEL){
        /*如果是内核线程申请内核内存,就修改kernel_vaddr*/
        bit_idx = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;
        ASSERT(bit_idx > 0);
        bitmap_set(&kernel_vaddr.vaddr_bitmap,bit_idx,1);
    }else{
        PANIC("get_a_page:not allow kernel alloc userspace or user alloc kernel space by get_a_page");
    }

    void* page_phyaddr = palloc(mem_pool);
    if(page_phyaddr == NULL){
        lock_release(&mem_pool->lock);
        return NULL;
    }
    page_table_add((void*)vaddr,page_phyaddr);
    lock_release(&mem_pool->lock);
    return (void*)vaddr;
}

/*得到虚拟地址映射到的物理地址*/
uint32_t addr_v2p(uint32_t vaddr){
    uint32_t* pte = pte_ptr(vaddr);
    /*(pte)的值是页表所在的物理页框地址,去掉其低12位的页表项属性 + 虚拟地址vaddr的低12位*/
    return ((*pte & 0xfffff000) + (vaddr & 0x00000fff));    //原理:PDE + PTE + 12位偏移地址 = 物理地址;这里直接用pte得到页表项,然后页表项 + 12位偏移地址 = 物理地址
}

// //初始化内存池
static void mem_pool_init(uint32_t all_mem){
    put_str("   mem_pool_init start\n");
    uint32_t page_table_size = PG_SIZE * 256;
    //页表大小=1页的页目录表 + 第0和第768个页目录项指向同一个页表 + (769~1022)个页目录项共指向254个页表 = 256
    uint32_t used_mem = page_table_size + 0x100000;     //0x10_0000是指跨过低端1MB内存 + 页表,使虚拟地址在逻辑上连续
    uint32_t free_mem = all_mem - used_mem;
    uint16_t all_free_pages = free_mem/PG_SIZE;
    uint16_t kernel_free_pages = all_free_pages / 2;
    uint16_t user_free_pages = all_free_pages - kernel_free_pages;

    //为简化位图操作,余数不处理
    uint32_t kbm_length = kernel_free_pages / 8;
    uint32_t ubm_length = user_free_pages / 8;
    uint32_t kp_start = used_mem;   //内核内存池起始地址
    uint32_t up_start = kp_start + kernel_free_pages*PG_SIZE;   //用户内存池起始地址

    kernel_pool.phy_addr_start = kp_start;
    user_pool.phy_addr_start = up_start;

    kernel_pool.pool_size = kernel_free_pages*PG_SIZE;
    user_pool.pool_size = user_free_pages*PG_SIZE;

    kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length;
    user_pool.pool_bitmap.btmp_bytes_len = ubm_length;

    /*内核内存池和用户内存池位图
    位图是全局数据,长度不固定
    全局/静态数组需要在编译时知道长度
    我们需要根据总内存大小算出需要多少字节,所以改为指定一块内存来生成位图
    */
    kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE;
    user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length);

    /*输出内存池信息*/
    put_str("   kernel_pool_bitmap_start:");
    put_int((int)kernel_pool.pool_bitmap.bits);
    put_str("   kernel_pool_phy_addr_start:");
    put_int(kernel_pool.phy_addr_start);
    put_str("\n");
    put_str("user_pool_bitmap_start:");
    put_int((int)user_pool.pool_bitmap.bits);
    put_str("   user_pool_phy_addr_start:");
    put_int((int)user_pool.phy_addr_start);
    put_str("\n");

    /*将位图置0*/
    bitmap_init(&kernel_pool.pool_bitmap);
    bitmap_init(&user_pool.pool_bitmap);
    /*初始化锁*/
    lock_init(&kernel_pool.lock);
    lock_init(&user_pool.lock);

    /*初始化内核虚拟地址的位图,按照实际物理内存大小生成数组*/
    kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_length;
    //用于维护内核堆的虚拟地址,所以要和内核内存池大小一致
    /*位图的数组指向一块未使用的内存,目前定位在内核内存池和用户内存池之外*/
    kernel_vaddr.vaddr_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length + ubm_length);
    kernel_vaddr.vaddr_start = K_HEAP_START;
    bitmap_init(&kernel_vaddr.vaddr_bitmap);
    put_str("   mem_pool_init done\n");
}

// /*内存管理部分初始化入口*/
void mem_init(){
    put_str("mem_init start\n");
    uint32_t mem_bytes_total = (*(uint32_t*)(0xb00));
    mem_pool_init(mem_bytes_total);
    put_str("mem_init done\n");
}

上面修改了很多,接下来一一列举并解释。

1.在内存池 struct pool 中新增了锁 struct lock lock,用它来在内存申请时做互斥,避免公共资源的竞争。

struct pool{
    struct bitmap pool_bitmap;  //本内存池用到的位图结构,用于管理物理内存
    uint32_t phy_addr_start;    //本内存池所管理物理内存的起始地址
    uint32_t pool_size;         //本内存池的字节容量
    struct lock lock;           //申请内存时互斥
};

 

2.vaddr_get函数中新增了在用户内存池分配内存的功能,即else部分。

/*在pf表示的虚拟内存池中申请pg_cnt个虚拟页,成功则返回虚拟页起始地址,失败则返回NULL*/
static void* vaddr_get(enum pool_flags pf,uint32_t pg_cnt){
    int vaddr_start = 0,bit_idx_start = -1;
    uint32_t cnt = 0;
    if(pf == PF_KERNEL){        //内核内存池
        //在位图中申请pg_cnt位
        bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap,pg_cnt);
        if(bit_idx_start == -1)
            return NULL;
        //申请成功则分配页
        while (cnt < pg_cnt)
            bitmap_set(&kernel_vaddr.vaddr_bitmap,bit_idx_start + cnt++,1);
        vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start*PG_SIZE;
    }else{          //用户内存池
        struct task_struct* cur = running_thread();
        bit_idx_start = bitmap_scan(&cur->userprog_vaddr.vaddr_bitmap,pg_cnt);
        if(bit_idx_start == -1)
            return NULL;
        //申请成功则分配页
        while(cnt < pg_cnt)
            bitmap_set(&cur->userprog_vaddr.vaddr_bitmap,bit_idx_start + cnt++,1);
        vaddr_start = cur->userprog_vaddr.vaddr_start + bit_idx_start*PG_SIZE;
        /*(0xc0000000 - PG_SIZE)作为用户3级栈已经在start_process被分配*/
        ASSERT((uint32_t)vaddr_start < (0xc0000000 - PG_SIZE));
    }
    return (void*)vaddr_start;
}

此部分的处理逻辑同在内核内存池中分配内存一样

 

3.函数get_user_pages用来在用户内存池中以整页为单位分配内存,返回分配的虚拟地址,实现用之前介绍过的函数

/*在用户空间中申请4K内存,并返回虚拟地址*/
void* get_user_pages(uint32_t pg_cnt){
    lock_acquire(&user_pool.lock);
    void* vaddr = malloc_page(PF_USER,pg_cnt);      //在用户内存池中以整页为单位分配内存,并返回分配的虚拟地址
    if (vaddr != NULL) { // 若分配的地址不为空,将页框清0后返回
        memset(vaddr, 0, pg_cnt * PG_SIZE);
    }
    lock_release(&user_pool.lock);
    return vaddr;
}

这里get_user_pages函数加上锁,同样在该函数上面的get_kernel_pages函数也加上锁

 

4.函数get_a_page用来在某个内存池中获取一个页。

/*将地址vaddr与pf池中的物理地址关联,仅支持一页空间分配;申请一页内存,并用vaddr映射到该页(可以指定虚拟地址)*/
void* get_a_page(enum pool_flags pf,uint32_t vaddr){
    struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
    lock_acquire(&mem_pool->lock);
    /*先将虚拟地址对应的位图置1*/
    struct task_struct* cur = running_thread();
    int32_t bit_idx = -1;

    /*若当前是用户进程申请用户内存,就修改用户进程自己的虚拟地址位图*/
    if(cur->pgdir != NULL && pf == PF_USER){
        bit_idx = (vaddr - cur->userprog_vaddr.vaddr_start) / PG_SIZE;
        ASSERT(bit_idx >= 0);
        bitmap_set(&cur->userprog_vaddr.vaddr_bitmap,bit_idx,1);
    }else if(cur->pgdir == NULL && pf == PF_KERNEL){
        /*如果是内核线程申请内核内存,就修改kernel_vaddr*/
        bit_idx = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;
        ASSERT(bit_idx > 0);
        bitmap_set(&kernel_vaddr.vaddr_bitmap,bit_idx,1);
    }else{
        PANIC("get_a_page:not allow kernel alloc userspace or user alloc kernel space by get_a_page");
    }

    void* page_phyaddr = palloc(mem_pool);
    if(page_phyaddr == NULL){
        lock_release(&mem_pool->lock);
        return NULL;
    }
    page_table_add((void*)vaddr,page_phyaddr);
    lock_release(&mem_pool->lock);
    return (void*)vaddr;
}

参数vaddr用来指定绑定的虚拟地址,所以此函数的功能是申请一页内存,并用vaddrd映射到该页,也就是说我们可以指定虚拟地址。此函数内部实现就是把之前介绍过的方法重新拼合了。

 

5.函数addr_v2p,此函数返回虚拟地址vaddr所映射的物理地址。原理是根据页表映射原理,先得到虚拟地址vaddr最终所映射到的物理页框起始地址,也就是在页表中vaddr所在的pte中记录的那个物理页地址,然后再将vaddr的低12位与此值相加,所得的地址和便是vaddr映射的物理地址。

/*得到虚拟地址映射到的物理地址*/
uint32_t addr_v2p(uint32_t vaddr){
    uint32_t* pte = pte_ptr(vaddr);
    /*(pte)的值是页表所在的物理页框地址,去掉其低12位的页表项属性 + 虚拟地址vaddr的低12位*/
    return ((*pte & 0xfffff000) + (vaddr & 0x00000fff));    //原理:PDE + PTE + 12位偏移地址 = 物理地址;这里直接用pte得到页表项,然后页表项 + 12位偏移地址 = 物理地址
}

“uint32_t* pte = pte_ptr(vaddr)”,在指针变量pte中得到vaddr的所在pte的地址,此时*pte的内容是vaddr所在pte的内容,也就是vaddr最终所映射到的物理页框的32位地址中的高20位和12位的页表项属性,因为页框都是自然页,低12位地址是0,所以页表项pte (和页目录项pde)中只需要记录页框的高20位地址即可。为了获取pte中的地址部分,在此要把低12位的属性值去掉,也就是return语句中的代码”(*pte & 0xfffff000)”,另外的代码”(vaddr& 0x00000m)”就是获取原虚拟地址vaddr的低12位。

 

6.我们在内存池 struct pool 中增加了锁,在内存池初始化函数mem_pool_init 中,我们需要增加锁的初始化: “lock_init(&kernel_pool.lock)”和“lock_init(&user_pool.lock)”。

/*初始化锁*/
lock_init(&kernel_pool.lock);
lock_init(&user_pool.lock);

 

对应/kernel/memory.h修改如下:

#ifndef __KERNEL_MEMORY_H
#define __KERNEL_MEMORY_H
#include "stdint.h"
#include "bitmap.h"
//虚拟地址池,用于虚拟地址管理
struct virtual_addr
{
    struct bitmap vaddr_bitmap; //虚拟地址用到的位图结构
    uint32_t vaddr_start;       //虚拟地址起始地址
};

enum pool_flags{
    PF_KERNEL = 1,  //内核内存池
    PF_USER = 2     //用户内存池
};

#define PG_P_1 1    //页表项或页目录项存在属性位:此页在内存中存在
#define PG_P_0 0    //页表项或页目录项存在属性位:此页在内存中不存在
#define PG_RW_R 0   //R/W属性位值,读/写操作:此页运行读、写、执行
#define PG_RW_W 2   //R/W属性位值,读/写操作:此页允许读、执行
#define PG_US_S 0   //U/S属性位值,系统级:只允许特权级0、1、2的程序访问
#define PG_US_U 4   //U/S属性位值,用户级:允许所有特权级的进程访问

extern struct pool kernel_pool, user_pool;

// static void* vaddr_get(enum pool_flags pf,uint32_t pg_cnt);

/*得到虚拟地址vaddr对应的pte指针*/
uint32_t* pte_ptr(uint32_t vaddr);

/*得到虚拟地址vaddr对应的pde指针*/
uint32_t* pde_ptr(uint32_t vaddr);

/*在m_pool指向的物理内存池中分配 一个物理页,成功则返回页框物理地址,失败则返回NULL*/
// static void* palloc(struct pool* m_pool);

/*页表中添加虚拟地址_vaddr与物理地址_page_phyadddr的映射*/
// static void page_table_add(void* _vaddr,void* _page_phyaddr);

/*分配pg_cnt个页空间,成功则返回起始虚拟地址,失败则返回NULL*/
void* malloc_page(enum pool_flags pf,uint32_t pg_cnt);

/*将地址vaddr与pf池中的物理地址关联,仅支持一页空间分配;申请一页内存,并用vaddr映射到该页(可以指定虚拟地址)*/
void* get_a_page(enum pool_flags pf,uint32_t vaddr);

/*在内核物理池中申请一页内存,成功则返回其虚拟地址,失败返回NULL*/
void* get_kernel_pages(uint32_t pg_cnt);

/*得到虚拟地址映射到的物理地址*/
uint32_t addr_v2p(uint32_t vaddr);

/*内存管理部分初始化入口*/
void mem_init(void);

#endif

 

3.创建进程大致流程

特权级0到3

CPU不允许从高特权级转向低特权级,除非是从中断和调用门返回的情况下。我们系统进入特权级3使用从中断返回的方式,但用户进程还没有运行,不能被中断,但是可以骗过CPU。

首先得在特权级0的环境中,其次是执行iretd指令。从中断返回肯定要用到iretd指令,iretd指令会用到栈中的数据作为返回地址,还会加载栈中eflags的值到eflags寄存器,如果栈中cs.rpl若为更低的特权级,处理器的特权级检查通过后,会将栈中cs载入到CS寄存器,栈中ss载入SS寄存器,随后处理器进入低特权级。因此我们必然要在栈中提前准备好数据供iretd指令使用。我们这里直接将进程的上下文都存到栈中,通过一系列的pop操作把用户进程的数据装载到寄存器,最后再通过iretd指令退出中断。

从中断返回必须要经过intr_exit,即使是假装,退出中断的出口是汇编语言函数intr_exit,这是我们定义在kernel.S中的,此函数用来恢复中断发生时、被中断的任务的上下文状态,并且退出中断在中断发生时,我们在中断入口函数”intr%lentry”中通过一系列的push操作来保存任务的上下文,因此在intr_exit中恢复任务上下文要通过一系列的pop操作,这属于”intr%lentry”的逆过程。任务的上下文信息被保存在任务pcb中的struct intr_stack中,struct intr_stack并不要求有固 定的位置,它只是一种保存任务上下文的格式结构。

当执行完intr_exit中的iretd指令后,CPU便恢复了任务中断前的状态,中断前是哪个特权级就进入哪个特权级。CPU从中断退出后要进入哪个特权级是由栈中保存的CS选择子中的RPL决定的,CS.RPL就是CPU的CPL,当执行iretd时,在栈中保存的CS选择子要被加载到代码段寄存器CS中,因此栈中CS选择子中的RPL便是从中断返回后CPU的新的CPL,我们进入(假装返回)到 3 特权级,由此我们要在栈中存储的 CS 选择子,其 RPL必须为 3。

用户进程的特权级为3,CPU会把用户进程所有段选择子的RPL都置为3,在RPL=CPL=3的情况下,用户进程只能访问DPL为3的内存段,即代码段、数据段、栈段。我们前面的工作中已经准备好了DPL为3的代码段及数据段,所以栈中段寄存器的选择子必须指向DPL为3的内存段。对于可屏蔽中断来说,任务之所以能进入中断,是因为标志寄存器eflags中的IF位为1,退出中断后,还得保持IF位为1,继续响应新的中断。所以必须使栈中eflags的IF位为1,用户进程属于最低的特权级,对于IO操作,不允许用户进程直接访问硬件,只允许操作系统有直接的硬件控制。这是由标志寄存器eflags中IOPL位决定的,必须使其值为0,所以必须使栈中eflags的IOPL位为0。

所以我们要完成的工作是:

  • 从中断返回必须要经过intr_exit,即使是假装。
  • 必须提前准备好用户进程所用的栈结构,在里面填装好用户进程的上下文信息。
  • 要在栈中存储的 CS 选择子,其 RPL必须为 3。
  • 栈中段寄存器的选择子必须指向DPL为3的内存段。
  • 必须使栈中eflags的IOPL位为0。
  • 必须使栈中eflags的IOPL位为0。

 

用户进程创建的流程

进程从创建到运行在总体上分为两步,进程创建的工作是由函数process_execute完成的,进程的执行是由时钟中断调用schedule,schedule从就绪队列中调度进程完成的。

process_execute的参数是 user_prog,这是待执行的用户进程,由于进程的实现基于线程,故进程创建过程中我们用到了很多创建线程的函数。 在process_execute 中,先调用函数get_kernel_pages申请1页内存创建进程的 pcb,这里的 pcb 就是 thread,接下来调用函数 init_thread 对 thread 进行初始化。 随后调用函数 create user_vaddr_bitmap为用户进程创建管理虚拟地址空 间的位图。 接着调用thread_create创建线程,此函数的作用是将函数start_process和用户进程user_prog作为kernel_thread的参数,以使kernel_thread能够调用start_proces(user_prog)。 接下来是调用函数create_page_dir 为进程创建页表,随后通过函数list_append将进程pcb,也就是thread加入就绪队列和全部队列,至此用 户进程的创建部分完成。

进程的运行是由时钟中断调用schedule,由调用器schedule调度实现的。当schedule从就绪队列中获取的pcb恰好是新创建的进程pcb_thread时,该进程马上就要被执 行了。在schedule中,调用了process_activate来激活进程或线程的相关资源(页表等),随后通过switch_to 函数调度进程,根据先前进程创建时函数thread_create的工作,已经将kernel_thread作为函数switch_to 的返回地址,即在switch_to中退出后,处理器会执行kernel_thread函数,”相当于” switch_to调用 kernel_thread。同样在之前的thread_create中,已经将 start_process和user_prog作为了kernel_thread的参 数,故在kernel_thread中可以以此形式调用start_process(user_prog)。函数start_process主要用来构建用户 进程的上下文,它会将user_prog作为进程从中断返回的地址,这里的从中断返回是假装的,目的是让用户进程顺利进入3特权级。由于是从0特权级的中断返回,故返回地址user_prog被iretd指令使用,为了复用中断退出的代码,现在需要跳转到中断出口intr_exit (kernel.S中汇编代码完成的函数) 处,利用那里的iretd指令使返回地址user_prog作为EIP寄存器的值以使user_prog得到执行,故相当于 start process调用intr_exit,intr_exit 调用user_prog,最终用户进程user_prog在3特权级下执行。

 

构造用户进程的上下文环境,需要定义标志寄存器eflags的属性位。

这里我们在global.h中定义它。加上一下代码即可。

...
#define EFLAGS_MBS      (1 << 1)    //此项必须设置
#define EFLAGS_IF_1     (1 << 9)    //if为1,开中断
#define EFLAGS_IF_0     0           //if为0,关中断
#define EFLAGS_IOPL_3   (3 << 12)   //IOPL3,用于测试用户程序在非系统调用下进行IO
#define EFLAGS_IOPL_0   (0 << 12)   //IOPL0

#define NULL ((void*)0)
#define DIV_ROUND_UP(X,STEP) ((X + STEP - 1) / (STEP))  //实现除法向上取整
...

 

完整代码

现在创建文件process.c来实现用户进程。

/userprog/process.c

#include "thread.h"
#include "string.h"
#include "process.h"
#include "tss.h"

extern void intr_exit(void);

/*构建用户进程filename_的初始上下文信息,即struct intr_stack*/
void start_process(void* filename_){    //filename_表示用户进程的名称,因为用户进程是从文件系统中加载到内存的
    void* function = filename_;
    struct task_struct* cur = running_thread();
    cur->self_kstack += sizeof(struct thread_stack);
    struct intr_stack* proc_stack = (struct intr_stack*)cur->self_kstack;
    proc_stack->edi = proc_stack->esi = proc_stack->ebp = proc_stack->esp_dummy = 0;
    proc_stack->ebx = proc_stack->edx = proc_stack->ecx = proc_stack->eax = 0;
    proc_stack->gs = 0;
    proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA;
    proc_stack->eip = function;         //cs:eip是程序入口地址
    proc_stack->cs = SELECTOR_U_CODE;   //cs寄存器赋值为用户级代码段
    proc_stack->eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1);
    proc_stack->esp = (void*)((uint32_t)get_a_page(PF_USER,USER_STACK3_VADDR) + PG_SIZE);   //3特权级下的栈
    proc_stack->ss = SELECTOR_U_DATA;
    asm volatile("movl %0,%%esp;jmp intr_exit" : : "g"(proc_stack) : "memory");     //通过假装从中断返回的方式,使filename_运行
}

/*激活页表*/
void page_dir_activate(struct task_struct* p_thread){
    /*执行此函数时,当前任务可能是线程。
    *因为线程是内核级,所以线程用的是内核的页表。每个用户进程用的是独立的页表。
    *之所以对线程也要重新安装页表,原因是上一次被调度的可能是进程,否则不恢复页表的话,线程就会使用进程的页表了
    */
    uint32_t pagedir_phy_addr = 0x100000;   /*若为内核线程,需要重新填充页表为内核页表:00x10_0000*/
    //默认为内核的页目录物理地址,也就是内核线程所用的页目录表
    if(p_thread->pgdir != NULL){
        pagedir_phy_addr = addr_v2p((uint32_t)p_thread->pgdir);     //判断是线程/进程的方法:进程的pcb中pgdir指向页表的虚拟地址,而线程中无页表所以pgdir = NULL
    }
    /*更新页目录表寄存器cr3,使新页表生效*/
    asm volatile("movl %0,%%cr3" : : "r"(pagedir_phy_addr):"memory");   //将进程/线程的页表/内核页表加载到cr3寄存器中
    //每个进程都有独立的虚拟地址空间,本质上是各个进程都有自己单独的页表。
    //页表是存储在页表寄存器cr3中的,cr3只有一个。在不同进程执行前,我们要在cr3寄存器中为其换上配套的页表,从而实现虚拟地址空间的隔离。
}

/*激活线程/进程的页表,更新tss中的esp0为进程的特权级0的栈*/
void process_activate(struct task_struct* p_thread){
    ASSERT(p_thread != NULL);
    /*激活该进程/线程的页表*/
    page_dir_activate(p_thread);
    /*内核线程特权级本身是0,CPU进入中断时并不会从tss中获取0特权级栈地址,故不需要更新esp0*/
    if(p_thread->pgdir){
        /*更新该进程的esp0,用于此进程被中断时保留上下文*/
        update_tss_esp(p_thread);   //更新tss中的esp0为进程的特权级0栈
        //时钟中断进行进程调度,中断需要用到0特权级的栈
    }
}

下面对上面的代码进行解释。

开头声明了外部函数intr_exit,这是用户进程进入 3 特权级的关键。

extern void intr_exit(void);

 

start_process函数

函数start_process接收一个参数filename_,此参数表示用户程序的名称,用户程序肯定是从文件系统 上加载到内存的,因此进程名是进程的文件名。 此函数用来创建用户进程filename_的上下文,也就是填充用户进程的struct intr_stack,通过假装从中断返回的方式,间接使filename_运行。我们说过用户进程是基于线程来实现的,因此在线程创建流程中,函数start_process相当于最下面的function,也就是说,我们创建进程的第一步是在线程中运行函数start_process。

/*构建用户进程filename_的初始上下文信息,即struct intr_stack*/
void start_process(void* filename_){    //filename_表示用户进程的名称,因为用户进程是从文件系统中加载到内存的
    void* function = filename_;
    struct task_struct* cur = running_thread();
    cur->self_kstack += sizeof(struct thread_stack);
    struct intr_stack* proc_stack = (struct intr_stack*)cur->self_kstack;
    proc_stack->edi = proc_stack->esi = proc_stack->ebp = proc_stack->esp_dummy = 0;
    proc_stack->ebx = proc_stack->edx = proc_stack->ecx = proc_stack->eax = 0;
    proc_stack->gs = 0;
    proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA;
    proc_stack->eip = function;         //cs:eip是程序入口地址
    proc_stack->cs = SELECTOR_U_CODE;   //cs寄存器赋值为用户级代码段
    proc_stack->eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1);
    proc_stack->esp = (void*)((uint32_t)get_a_page(PF_USER,USER_STACK3_VADDR) + PG_SIZE);   //3特权级下的栈
    proc_stack->ss = SELECTOR_U_DATA;
    asm volatile("movl %0,%%esp;jmp intr_exit" : : "g"(proc_stack) : "memory");     //通过假装从中断返回的方式,使filename_运行
}

“void* function=filename_;”,CPU只能直接执行位于内存中的指令。 用户进程在执行前,是由操作系统的程序加载器将用户程序从文件系统读到内存,再根据程序文件的格式解析其内容,将程序中的段展开到相应的内存地址。 程序格式中会记录程序的入口地址, CPU把CS:[E]IP指向它,该程序就被执行了。 C语言中虽然不能直接控制这两个寄存器,但函数调用其实就是改变这两个寄存器的指向,故C语言编写的操作系统可以像调用函数那样调用执行用户程序。 因此,用户进程被加载到内存中后同函数一样,仅仅是个指令区域。 由于目前我们尚未实现文件系统,前期我们用普通函数代替用户程序,所以用function代替了filename_。

用户进程上下文保存在struct intr_stack栈中,虽然此栈的位置不固定,但我们还得为它安排个合适的位置。

在函数 init_thread 中有代码: “pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE);”,目的是初始化线程所用的栈的基址,后面的两个栈”struct intr_stack”和”struct thread_stack”的布局及所占的空间以此基地址往下顺延,这个布局操作是在函数 thread_create中完成的,相关代码是:

/*先预留中断使用栈的空间*/
pthread->self_kstack -= sizeof(struct intr_stack);

/*再预留出线程栈空间*/
pthread->self_kstack -= sizeof(struct thread_stack);

struct intr_stack 栈用来存储进入中断时任务的上下文, struct thread_stack 用来存储在中断处理程序中、 任务切换(switch_to)前后的上下文。

这两个栈的布局情况如图所示:

左边黑色背景的结构体是实际定义的栈代码,值得注意的是结构体中成员的地址是越往下越高。 而右边白色横格是栈,其中的内容是结构体中的成员,地址是越往下越低。 self_kstack在 init_thread中被赋予指向PCB上顶端,经过上面引号中的两行代码后, self_kstack 指 PCB 中struct thread_stack栈的最底端,如图中横向箭头的位置所示。

在线程创建过程中,我们把线程的上下文保存在了 struct thread_stack 栈中,实际操作 struct thread_stack 栈的代码如图所示。

struct thread_stack栈是由函数kernel_thread使用的,之后kernel_thread调用function (最终在线程中 执行的函数),function 因此得以执行。

目前栈struct intr_stack还是空的,此栈有两个作用,一方面是任务被中断时,用来保存任务的上下文,另一方面这是为了给进程预留的,用来填充用户进程的上下文,也就是寄存器环境。

process.c的start_process函数,为了引用 struct intr_stack 栈,通过代码“cur->self_kstack += sizeof(struct thread_stack);”使指针self_kstack跨过struct thread_stack栈,最终指向struct intr_stack 栈的最低处。

此时PCB中栈的情况如图所示:

接下来的声明 struct intr_stack* 指针 proc_stack,使其指向 self_kstack,也就是 struct intr_stack 栈的最低处,如上图中横向箭头指向的位置所示。这么做的原因是结构体指针proc_stack指向结构体的最低处,对结构体成员的访问是由低向高处做偏移,这样符合结构体成员访问的方式。

然后是对栈中8个通用寄存器初始化,把它们初始化为0即可。接下来proc_stack->gs = 0;是对栈中显存段寄存器gs初始化,操作系统不允许用户进程访问显存,所以将其初始化为0,说明一下,此处不允许用户进程直接控制显存是操作系统的管理方法。其实CPU是允许低特权级(用户进程)的任务直接访问显存的,因为显存毕竟是块内存区域,访问内存区域就要通过描述符,因此只要在描述符中把它的DPL设置成低特权就好,也就是显存段的DPL数值上大于等于用户特权级即可。即使此处的gs不置为0,CPU也会将其置0,原因是执行iretd从中断返回时,CPU会进行特权级检查,如果发现未来的CPL (也就是内核栈中CS.RPL)权限低于(数值上大于) CPU中段寄存器(如DS、ES、FS、GS)中选择子指向的内存段的DPL,CPU会自动将相应段寄存器的选择子置为0,这样一来,如果低特权级程序用此0值选择子访问GDT,必然会导致访问GDT中第0个不可访问的哑描述符,导致CPU抛异常,从而阻止了越权访问。因此,在特权为3的用户环境下gs选择子用不上,即使赋值成其他值,由于cpl为3,,特权检查时CPU就将gs置0了,干脆这里直接置为 0。

代码proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA是将栈中段寄存器ds,es和fs的值设置为选择子SELECTOR_U_DATA,此选择子在global.h 中定义。

程序能上CPU运行,原因就是CS:[E]IP指向了程序入口地址。”proc_stack->eip = function;”先对栈中eip赋值为function,这是start_process的参数 filename_的值。然后通过”proc_stack->cs = SELECTOR_U_CODE”将栈中代码段寄存器cs赋值为先前我们已在GDT中安装好的用户级代码段。

接下来对栈中eflags赋值,EFLAGS_IOPL_0表示IOPL位为0,EFLAGS_IF_1表示IF位为1,EFLAGS_MBS 固定为 1,它们在 eflags 中的位置如图所示:

proc_stack->esp = (void*)((uint32_t)get_a_page(PF_USER,USER_STACK3_VADDR) + PG_SIZE)为用户进程分配3特权级下的栈,也就是栈中proc_stack->esp需要指向从用户内存池中分配的地址。

继续之前先介绍下C程序的内存分布:

用户程序内存空间的最顶端用来存储命令行参数及环境变量,这些内容是由某操作系统下的C运行库写进去的,将来实现从文件系统加载用户进程并为其传递参数时会介绍这部分。紧接着是栈空间和堆空间,栈向下扩展,堆向上扩展,栈与堆在空间上是相接的,这两个空间由操作系统管理分配,由于栈与堆是相向扩展的,操作系统需要检测栈与堆的碰撞。

最下面的未初始化数据段 bss、初始化数据段 data及代码段 text由链接器和编译器负责。

在4GB 的虚拟地址空间中,(0xc0000000-1)是用户空间的最高地址,0xc0000000~0xffffffff是内核空间。

我们也效仿这种内存结构布局,把用户空间的最高处即0xc0000000-1,及以下的部分内存空间用于存储用户进程的命令行参数,之下的空间再作为用户的栈和堆。命令行参数也是被压入用户栈的(在后面章节介绍加载用户进程时会了解),因此虽然命令行参数位于用户空间的最高处,但它们相当于位于栈的最高地址处,所以用户栈的栈底地址为0xc0000000,由于在申请内存时,内存管理模块返回的地址是内存空间的下边界,所以我们为栈申请的地址应该是(0xc0000000-0x1000),此地址是用户栈空间栈顶的下边界。 这里我们用宏来定义此地址,即USER_STACK3_VADDR,它定义在 process.h 中。

对应/userprog/process.h

#ifndef __USERPROG_PROCESS_H
#define __USERPROG_PROCESS_H
#include "stdint.h"
#define USER_STACK3_VADDR (0xc0000000 - 0x1000)
/*构建用户进程filename_的初始上下文信息,即struct intr_stack*/
void start_process(void* filename_);

/*激活页表*/
void page_dir_activate(struct task_struct* p_thread);

/*激活线程/进程的页表,更新tss中的esp0为进程的特权级0的栈*/
void process_activate(struct task_struct* p_thread);
#endif

 

再回到process.c的start_process函数,”get_a_page(PF_USER, USER_STACK3_VADDR)”先获取特权级3的栈的下边界地址,将此地址再加上PG_SIZE,所得的和就是栈的上边界,即栈底,将此栈底赋值给proc_stack->esp,用户进程使用的3级栈必然要建立在用户进程自己的页表中。

在进程创建部分,有一项工作是create_page_dir,这是提前为用户进入创建了页目录表,在进程执行部分,有一项工作是process_activate,这是使任务(无论任务是否为新创建的进程或线程,或是老进程、老线程)自己的页表生效。 我们是在函数start_process中为用户进程创建了3特权级栈,start_process是在执行任务页表激活之后执行的,也就是在process_activate之后运行,那时已经把页表更新为用户进程的页表了,所以3特权级栈是安装在用户进程自己的页表中的。

栈段可以用普通的数据段,proc_stack->ss = SELECTOR_U_DATA为栈中SS赋值为用户数据段选择子SELECTOR_U_DATA。

最后通过内联汇编,将栈esp替换为我们刚刚填充好的proc_stack,然后通过jmp intr_exit使程序流程跳转到中断出口地址intr_exit,通过那里的一系列pop指令和iretd指令,将proc_stack中的数据载入 CPU的寄存器,从而使程序“假装”退出中断,进入特权级3。

proc_stack的数据类型是struct intr_stack,虽然咱们把它定义在PCB中,位于PCB中最顶端,但它完全可以用局部变量代替,因为它只用这一次,故可以在函数 start_process 中这样声明 proc_stack: “struct intr_stack proc_stack”,不过这里直接把它定义在PCB中。

还要注意的是:

每个进程都拥有独立的虚拟地址空间,本质上就是各个进程都有单独的页表,页表是存储在页表寄存器CR3中的,CR3寄存器只有1个,因此,不同的进程在执行,我们要在CR3寄存器中为其换上与之配套的页表,从而实现了虚拟地址空间的隔离。

 

page_dir_activate函数

page_dir_activate函数接受一个参数p_thread,用来激活p_thread的页表,p_thread 可能是进程,也可能是线程。

进程才有独立的地址空间,才有自己的页表,但是激活页表这类工作针对的不止是进程,线程也需要呢。

目前我们的线程并不是为用户进程服务的,它是为内核服务的,因此与内核共享同一地址空间,也就是和内核用的是同一套页表。当进程A切换到进程B时,页表也要随之切换到进程B所用的页表,这样才保证了地址空间的独立性。当进程B又切换到线程C时,由于目前在页表寄存器CR3中的还是进程B的页表,因此,必须要将页表更换为内核所使用的页表。所以,无论是针对进程,还是线程,都要考虑页表切换。

uint32_t pagedir_phy_addr = 0x100000将pagedir_phy_addr初始化为0x100000,这是内核所使用的页表的物理地址,也是所有内核线程的页表。判断 pcb中的pgdir是否等于NULL可以知道当前任务是线程,还是进程,pcb->pgdir用来指向页表的虚拟地址。线程pcb中是没有页表的,所以pgdir等于NULL,如果是进程,其pgdir不为 NULL。若为进程,则将进程的页表地址加载到 CR3 寄存器。注意,pgdir 中的是页表的虚拟地址,因为页表需要单独的内存空间,创建页表(函数create_page_dir完成)时必然要为页表申请内存,内存管理模块返回的地址是虚拟地址,因此页表地址也是虚拟地址,所以在把页表加载到CR3之前,要将其转换成物理地址。

“pagedir_phy_addr=addr_v2p((uint32_t)p_thread->pgdir)”通过函数 addr_v2p将虚拟地址p_thread->pgdir转换为物理地址,重新为变量pagedir_phy_addr赋值。

接着通过内联汇编将pagedir_phy_addr的值通过mov指令写入到寄存器CR3中,由此 实现了页表切换。

 

process_activate函数

process_activate函数的功能有两个,一是激活线程或进程的页表,二是更新tss中的esp0为进程的特权级0的栈。进程与线程都是独立的执行流,它们有各自的栈和页表,只不过线程的页表是和其他线程共用的,而进程的页表是单独的。 进程或线程在被中断信号打断时,处理器会进入0特权级,并会在0特权级栈中保存进程或线程的上下文环境。 如果当前被中断的是3特权级的用户进程,处理器会自动到tss中获取esp0的值作为用户进程在内核态(0特权级)的栈地址,如果被中断的是0特权级的内核线程,由于内核线程已经是0特权级,进入中断后不涉及特权级的改变,所以处理器并不会到tss中获取esp0,所以,代码“if(p_thread->pgdir)”来判断:如果是用户进程的话才去更新tss中的esp0。 这两个功能的实现调用tss.c中定义的update_tss_esp和上面刚介绍的page_dir_activate完成的。 只有在任务调度时才会切换页表及更新0级栈,因此process_activate是被schedule调用的。

 

4.参考

郑钢著操作系统真象还原

田宇著一个64位操作系统的设计与实现

丁渊著ORANGE’S:一个操作系统的实现

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇