《操作系统真象还原》第十一章

《操作系统真象还原》第十一章

本篇对应书籍第十一章的内容

本篇内容介绍了用户进程的实现和切换,这里的用户进程就像是高级版的线程,用户进程拥有自己的空间,执行特权级为3。

为什么要有任务状态段 TSS

硬件厂商提供的多任务硬件解决方案主要是 LDT 和 TSS。

TSS 的作用

Intel 的建议是给每个任务关联一个任务状态段,这就是 TSS,用它来表示任务,任务的切换通过TSS的切换实现

TSS 是由程序员“提供”的,由 CPU 来维护。

CPU 中有一个专门存储 TSS 信息的寄存器,这就是 TR 寄存器,它始终指向当前正在运行的任务。

TSS 和其它段一样,本质上是一片存储数据的内存区域,需要注册到 GDT,就像是 TSS 段

Intel 打算用这片内存区域保存任务的最新状态。使用 TSS 描述符来“描述” TSS。

TSS 描述符格式:

在这里插入图片描述

TSS 结构:

在这里插入图片描述

TSS 的主要作用就是保存任务的快照,也就是 CPU 执行该任务时,寄存器当时的瞬时值。

除了从中断和调用门返回外,CPU 不允许从高特权级转向低特权级。另外,CPU 在不同特权级下用不同的栈。

Linux 只用到了 0 特权级和 3 特权级,因此只设置 SS0 和 esp0 的值就够了。

TR 寄存器:

在这里插入图片描述

TSS通过选择子来访问:

ltr <16位通用寄存器/16位内存单元>

CPU 原生支持的任务切换方式

原生支持的任务切换方式效率不高,所以现在操作系统都不使用。

CPU 厂商提供了 LDT 和 TSS两种原生支持,他们要求每个任务都要分配一个LDT和TSS,通过切换这两个结构来进行切换任务,TSS用于保存和恢复任务的状态,LDT则是任务的实体资源(可以直接采用平坦模型,使用那个4GB大小的GDT),实际上LDT可有可无,实际起作用的是TSS

进行任务切换的方法有:

  1. 通过中断+任务门进行切换(步骤贼多)
  2. call或jmp+任务门(call和jmp消耗的时钟周期很长)
  3. iretd

这几种方法怪复杂的,也用不上,就不记录了,以后有闲心了再来了解吧

现代操作系统采用的任务切换方式

TSS 是 x86 结构 CPU 的特定结构,被用来定义任务

使用 TSS 的唯一理由是为 0 级的任务提供栈,CPU 向更高特权级转移时所使用的栈地址,需要提前在 TSS 中写入。

CPU 要求用 TSS 硬指标,所以需要应付这个指标

Linux 为每一个 CPU 都创建 1 个 TSS,每个 CPU 的 ltr 也指向这个 TSS,不再发生改变,任务切换时,只需要将 ss0 和 esp0 更新为新任务的内核栈和栈指针即可

Linux 在 TSS 中只初始化了 SS0、esp0、I/O位图字段,不用于保存任务状态

任务状态会在CPU从低级进入高级时(CPU自动获得新的栈指针),经过一系列“手动”push,存到0级栈中。

定义并初始化 TSS

这里的工作是初始化TSS结构,创建 GDT 描述符,并注册到 GDT 中去

kernel/global.h

增改如下内容:

/*-------------- GDT描述符属性 ------------*/
#define	DESC_G_4K    1
#define	DESC_D_32    1
#define DESC_L	     0	// 64位代码标记,此处标记为0便可。
#define DESC_AVL     0	// cpu不用此位,暂置为0  
#define DESC_P	     1
#define DESC_DPL_0   0
#define DESC_DPL_1   1
#define DESC_DPL_2   2
#define DESC_DPL_3   3
/* 
   代码段和数据段属于存储段,tss和各种门描述符属于系统段
   s为1时表示存储段,为0时表示系统段.
*/
#define DESC_S_CODE	1
#define DESC_S_DATA	DESC_S_CODE
#define DESC_S_SYS	0
#define DESC_TYPE_CODE	8	// x=1,c=0,r=0,a=0 代码段是可执行的,非依从的,不可读的,已访问位a清0.  
#define DESC_TYPE_DATA  2	// x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0.
#define DESC_TYPE_TSS   9	// B位为0,不忙

#define RPL0 0  
#define RPLl 1  
#define RPL2 2
#define RPL3 3

#define TI_GDT 0
#define TI_LDT 1

#define SELECTOR_K_CODE 	((1 << 3) + (TI_GDT << 2) + RPL0) 
#define SELECTOR_K_DATA 	((2 << 3) + (TI_GDT << 2) + RPL0) 
#define SELECTOR_K_STACK 	SELECTOR_K_DATA
#define SELECTOR_K_GS 		((3 << 3) + (TI_GDT << 2) + RPL0)
/* 第3个段描述符是显存,第4个是tss */
#define SELECTOR_U_CODE	   ((5 << 3) + (TI_GDT << 2) + RPL3)
#define SELECTOR_U_DATA	   ((6 << 3) + (TI_GDT << 2) + RPL3)
#define SELECTOR_U_STACK   SELECTOR_U_DATA

#define GDT_ATTR_HIGH		     ((DESC_G_4K << 7) + (DESC_D_32  << 6) + (DESC_L << 5)      + (DESC_AVL << 4))
#define GDT_CODE_ATTR_LOW_DPL3	 ((DESC_P << 7)    + (DESC_DPL_3 << 5) + (DESC_S_CODE << 4) + DESC_TYPE_CODE)
#define GDT_DATA_ATTR_LOW_DPL3	 ((DESC_P << 7)    + (DESC_DPL_3 << 5) + (DESC_S_DATA << 4) + DESC_TYPE_DATA)

/*-------------- TSS描述符属性 ------------*/
#define TSS_DESC_D  0 

#define TSS_ATTR_HIGH ((DESC_G_4K << 7) + (TSS_DESC_D << 6) + (DESC_L << 5) + (DESC_AVL << 4) + 0x0)
#define TSS_ATTR_LOW ((DESC_P << 7) + (DESC_DPL_0 << 5) + (DESC_S_SYS << 4) + DESC_TYPE_TSS)
#define SELECTOR_TSS ((4 << 3) + (TI_GDT << 2) + RPL0)

struct gdt_desc {
   uint16_t limit_low_word;
   uint16_t base_low_word;
   uint8_t  base_mid_byte;
   uint8_t  attr_low_byte;
   uint8_t  limit_high_attr_high;
   uint8_t  base_high_byte;
}; 

这里新增了 GDT 的属性和新的描述符(用户代码/数据/栈段描述符)

在这里插入图片描述

userprog/tss.h

#ifndef __USERPROG_TSS_H
#define __USERPROG_TSS_H

#include "thread.h"

void update_tss_esp(struct task_struct* pthread);

void tss_init(void);

#endif

userprog/tss.c

#include "tss.h"
#include "stdint.h"
#include "global.h"
#include "string.h"
#include "print.h"
#include "../thread/thread.h"

/* 任务状态段 tss 结构 */
struct tss {
    uint32_t  backlink;
    uint32_t* esp0;
    uint32_t  ss0;
    uint32_t* esp1;
    uint32_t  ss1;
    uint32_t* esp2;
    uint32_t ss2;
    uint32_t cr3;
    uint32_t (*eip) (void);
    uint32_t eflags;
    uint32_t eax;
    uint32_t ecx;
    uint32_t edx;
    uint32_t ebx;
    uint32_t esp;
    uint32_t ebp;
    uint32_t esi;
    uint32_t edi;
    uint32_t es;
    uint32_t cs;
    uint32_t ss;
    uint32_t ds;
    uint32_t fs;
    uint32_t gs;
    uint32_t ldt;
    uint32_t trace;
    uint32_t io_base;
}; 

static struct tss tss;


/* 更新 tss 中 esp0 字段的值为 pthread 的 0 级线 */
void update_tss_esp(struct task_struct* pthread) {
    tss.esp0 = (uint32_t*) ((uint32_t) pthread + PG_SIZE);
}


/* 创建 gdt 描述符 */
static struct gdt_desc make_gdt_desc(uint32_t* desc_addr, 
                                     uint32_t limit, 
                                     uint8_t attr_low, 
                                     uint8_t attr_high) {

    uint32_t desc_base = (uint32_t) desc_addr;
    struct gdt_desc desc;
    desc.limit_low_word = limit & 0x0000ffff;
    desc.base_low_word = desc_base & 0x0000ffff;
    desc.base_mid_byte = ((desc_base & 0x00ff0000) >> 16);
    desc.attr_low_byte = (uint8_t) (attr_low);
    desc.limit_high_attr_high = (((limit & 0x000f0000) >> 16) + (uint8_t) (attr_high));
    desc.base_high_byte = desc_base >> 24;
    return desc;                                    
}


/* 在 gdt 中创建 tss 并重新加载 gdt */
void tss_init() {
    put_str("tss_init start\n");
    uint32_t tss_size = sizeof(tss);
    memset(&tss, 0, tss_size);
    tss.ss0 = SELECTOR_K_STACK;
    // 当 io位图偏移地址 大于等于 tss大小 - 1 时表示没有 io位图
    tss.io_base = tss_size;

    // gdt 段基址为 0x900, 把 tss 放到第 4 个位置, 也就是 0x900 + 0x20 的位置
    // 在 gdt 中添加 DPL 为 0 的 TSS 描述符
    *((struct gdt_desc*) 0xc0000920) = make_gdt_desc((uint32_t*) &tss, tss_size - 1, TSS_ATTR_LOW, TSS_ATTR_HIGH);
    // 在 gdt 中添加 DPL 为 3 的数据段和代码段描述符
    *((struct gdt_desc*)0xc0000928) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_CODE_ATTR_LOW_DPL3, GDT_ATTR_HIGH);
    *((struct gdt_desc*)0xc0000930) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_DATA_ATTR_LOW_DPL3, GDT_ATTR_HIGH);

    // gdt 16 位的 limit + 32 位的段基址 = 48 位(0~15 = limit, 16~47 = GDT起始地址)
    // limit: 7个描述符(包括第0个描述符), 所以 limit = 8 * 7 - 1
    // 将 0xc0000900 先转位32位再转为64位(不可一步转为64位)
    uint64_t gdt_operand = ((8 * 7 - 1) | ((uint64_t) (uint32_t) 0xc0000900 << 16));
    asm volatile("lgdt %0" : : "m"(gdt_operand));
    asm volatile("ltr %w0" : : "r"(SELECTOR_TSS));
    put_str("tss_init and ltr done\n");
}

前面先定义了TSS的结构

  • update_tss_esp函数把tss中的esp0字段更新位当前线程的0级栈
  • make_gdt_desc函数用来拼接创建GDT描述符
  • tss_init函数用来初始化TSS,同时也创建TSS、3级代码段和3级数据段

这两段代码还算比较好懂,初始化TSS的操作也就是给TSS结构的SS0和IO位图赋值,至于esp0,则任务切换的时候会通过update_tss_esp函数进行赋值

init.c

#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "../device/timer.h"
#include "memory.h"
#include "../thread/thread.h"
#include "../device/console.h"
#include "../device/keyboard.h"
#include "../userprog/tss.h"

/* 负责初始化所有模块 */
void init_all() {
    put_str("init_all\n");
    idt_init();         // 初始化中断
    mem_init();         // 初始化内存管理系统
    thread_init();      // 初始化线程相关结构
    timer_init();       // 初始化 PIT
    console_init();     // 初始化终端
    keyboard_init();    // 键盘初始化
    tss_init();         // tss 初始化
}

在这里插入图片描述

运行 Bochs

编译,运行:

在这里插入图片描述

TSS段成功注册到 GDT 中

实现用户进程

实现用户进程的原理

这里基于线程来实现进程,先来回顾一下线程创建的流程:

在这里插入图片描述

线程通过thread_start函数进行创建,通过init_thread函数初始化线程PCB信息,通过thread_create函数设置线程栈内容这个线程栈就是线程运行的时候恢复到线程上的栈,这里的eip指向线程函数,由kernel_thread函数执行我们的线程函数

初始化PCB信息和线程栈后,将该PCB加入线程就绪队列,等时间片来了就可以执行了

要基于线程实现进程,只要把function替换成创建进程的函数就行了。

至于为啥,先往后看

用户进程的虚拟空间

进程与内核线程最大的区别就是进程有单独4GB的空间

为了演示需要,我们单独为每个进程维护一个虚拟地址池,用来记录该进程的虚拟地址中的分配情况

因为是基于线程的进程,所以它和线程一样用PCB来存储信息,这里我们要为PCB添加一个新的成员来跟踪用户空间虚拟地址分配情况

thread/thread.h
#include "bitmap.h"
#include "../kernel/memory.h"

/* 进程或线程的 pcb, 程序控制块 */
struct task_struct {
    uint32_t* self_kstack;                  // 各内核线程都用自己的内核栈, 此处为栈顶指针
    enum task_status status;                // 线程状态
    char name[16];
    uint8_t priority;                       // 线程优先级

    uint8_t ticks;                          // 每次在处理器上执行的时间嘀嗒数
    uint32_t elapsed_ticks;                 // 此任务上 cpu 运行后至今占用了多少嘀嗒数, 也就是此任务执行了多久

    struct list_elem general_tag;           // 用于线程在一般队列中的结点
    struct list_elem all_list_tag;          // 用于线程在 thread_all_list 中的结点

    uint32_t* pgdir;                        // 进程自己页表的虚拟地址
    struct virtual_addr userprog_vaddr;     // 用户进程的虚拟地址
    uint32_t stack_magic;                   // 栈的边界标记, 用于检测栈的溢出
};

这里的struct virtual_addr userprog_vaddr;就是我们新增的成员

为进程创建页表和 3 特权级栈

进程和线程的区别就是不同的空间,不同的空间意味着不同的页表,所以要为每个进程单独申请存储页目录项和页表项的虚拟内存页

用户进程工作在3特权级,所以需要创建个3特权级的栈

这里涉及的是内存管理相关的操作,本节添加内存功能:申请用户内存地址、互斥申请操作

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

/* 将地址 vaddr 与 pf 池中的物理地址关联, 仅支持一页空间分配 */
void* get_a_page(enum pool_flags pf, uint32_t vaddr);

/* 在用户空间中申请 4k 内存, 并返回其虚拟地址 */
void* get_user_pages(uint32_t pg_cnt);

增改如下内容:

kernel/memory.c

增改如下内容:

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

增加成员lock,用于申请内存时互斥,避免公共资源的竞争

/* 
 * 在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) {
        bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap, pg_cnt);    // 获取申请的虚拟页的位起始值
        if (bit_idx_start == -1) {
            return NULL;
        }

        // 将位起始值开始连续置1, 直到设置完需要的页位置
        while (cnt < pg_cnt) {
            bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt, 1);
            cnt++;
        }
        // 获取起始页的虚拟地址
        vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;

    } else {
        // 用户内存池
        struct task_struct* cur = running_thread();
        // 在用户进程的虚拟地址位图申请 pg_cnt 个页
        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_get函数中新增else条件中的内容:用户内存池,处理逻辑与内核内存池相同,只是要先获取一下当前线程的PCB(内核线程则不用)

/* 在用户空间中申请 4k 内存, 并返回其虚拟地址 */
void* get_user_pages(uint32_t pg_cnt) {
    lock_acquire(&user_pool.lock);
    // 从用户物理内存池申请内存, 从当前线程的虚拟内存池申请虚拟内存, 
    // 将申请的虚拟内存与物理内存做映射, 返回起始虚拟地址
    void* vaddr = malloc_page(PF_USER, pg_cnt);
    memset(vaddr, 0, pg_cnt * PG_SIZE);
    lock_release(&user_pool.lock);
    return vaddr;
}


/* 将地址 vaddr 与 pf 池中的物理地址关联, 仅支持一页空间分配 */
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 = (虚拟地址 - 虚拟地址起始地址) / 每位大小
        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 kernelspace by get_a_page");
    }

    // 在物理内存池中申请一个物理页
    void* page_phyaddr = palloc(mem_pool);
    if(page_phyaddr == NULL) {
        return NULL;
    }
    // 将虚拟地址与物理地址做映射
    page_table_add((void*) vaddr, page_phyaddr);
    lock_release(&mem_pool->lock);
    return (void*) vaddr;
}


/* 得到虚拟地址映射到的物理地址 */
uint32_t addr_v2p(uint32_t vaddr) {
    // 得到虚拟地址对应的 pte, pte中记录了物理页框地址
    uint32_t* pte = pte_ptr(vaddr);
    // (*pte)的值是页表所在的物理页框地址,
    // 去掉其低12位的页表项属性 + 虚拟地址 vaddr 的低12位(偏移地址)
    return ((*pte & 0xfffff000) + (vaddr & 0x00000fff));
}

新增3个函数:3个函数都比较容易看懂

  • get_user_pages:在用户内存中以整页为单位分配内存
  • get_a_page:在某个内存池中获取一个页,与get_user_pages和get_kernel_pages不同的是,可以指定虚拟内存的地址
  • addr_v2p:返回虚拟地址所映射的物理地址
// 初始化内核物理池和用户物理地址池的锁
lock_init(&kernel_pool.lock);
lock_init(&user_pool.lock);

以上两行添加到mem_pool_init函数中去

在这里插入图片描述

我们的内存分配是 内核有一个专门的虚拟内存管理池 而进程是每个进程都有一个专门的虚拟内存管理池
物理内存的话 还是只有两个 内核物理内存池 和 用户物理内存池

怎么保证每个进程中操作系统都是可见的 因为每个进程都有一个单独的页表
单独的页表里面共有的部分是内核部分的页目录项 操作系统创建好后的内存地址就是固定的 这样的话 操作系统对于每个进程都是可见的

怎么保证进程的独立空间呢 大家都用相同的虚拟地址怎么才能不冲突呢 之前说过的的每个进程独享一个页表 那么相同的虚拟页目录项相对应的物理地址是可以不同的 分配是由操作系统来做的
但是为什么进程每个都还要一个独用的虚拟内存管理池呢 因为对于一个进程里面的资源 是不可以用相同的虚拟地址 里面的地址都是唯一的

进入特权级 3

当前我们在CPU特权级0下,要进入用户的特权级3只有两种方法,中断门和调用门、或者iretd,这里使用iretd指令来假装从中断返回从而进入特权级3,需要具备以下条件:

  1. 从中断返回,必须要经过intr_exit
  2. 必须提前准备好用户进程所需要的栈结构,填好用户进程的上下文
  3. 在栈中存储的CS选择子的RPL必须为3
  4. 栈中段寄存器的选择子必须指向DPL为3的内存段
  5. 栈中eflags的IF位为1(继续响应新的中断)
  6. 栈中eflags的IOPL位为0(不允许用户进程直接访问硬件)

用户进程创建的流程

进程从创建到运行总体上分为两步,进程的创建是通过process_execute函数完成的,进程的执行由时钟中断调用函数schedule完成,步骤大致如图所示:

创建进程大概的流程是:

  • 先在内核中分配一页内存 为线程thread分配一页pcb
  • 初始化线程后 为我们的进程的虚拟内存位图分配内存
  • 创造线程 此线程为了调用start_process初始化函数(备注 start_process中把进程的环境给初始化好 即可调用intr_exit iretd跑路 当然这是后话)
  • 紧接着把进程的页表空间分配好 并且初始化好
  • 把含有start_process的线程放到就绪列表中 就等着被调用了
  • 线程schedule调用后 发现pgdir非空 换页表重新装载 并调用start_process
  • start_process做了一大堆事情 并把分配了一页内存给栈空间 之后就jmp intr_exit切换去了
  • 各种pop之后 cs ip ds各种寄存器已经被切换了 进入用户进程
  • 切换进程时 只需要把TSS中的esp0切换即可 即内核线程栈

看不明白就先往后看

在这里插入图片描述


在这里插入图片描述

实现用户进程(上)

构造进程的上下文环境免不了要设置eflags

kernel/global.h

添加如下内容:

//---------------    eflags属性    ---------------- 

/********************************************************
--------------------------------------------------------------
		  Intel 8086 Eflags Register
--------------------------------------------------------------
*
*     15|14|13|12|11|10|F|E|D C|B|A|9|8|7|6|5|4|3|2|1|0|
*      |  |  |  |  |  | | |  |  | | | | | | | | | | | '---  CF……Carry Flag
*      |  |  |  |  |  | | |  |  | | | | | | | | | | '---  1 MBS
*      |  |  |  |  |  | | |  |  | | | | | | | | | '---  PF……Parity Flag
*      |  |  |  |  |  | | |  |  | | | | | | | | '---  0
*      |  |  |  |  |  | | |  |  | | | | | | | '---  AF……Auxiliary Flag
*      |  |  |  |  |  | | |  |  | | | | | | '---  0
*      |  |  |  |  |  | | |  |  | | | | | '---  ZF……Zero Flag
*      |  |  |  |  |  | | |  |  | | | | '---  SF……Sign Flag
*      |  |  |  |  |  | | |  |  | | | '---  TF……Trap Flag
*      |  |  |  |  |  | | |  |  | | '---  IF……Interrupt Flag
*      |  |  |  |  |  | | |  |  | '---  DF……Direction Flag
*      |  |  |  |  |  | | |  |  '---  OF……Overflow flag
*      |  |  |  |  |  | | |  '----  IOPL……I/O Privilege Level
*      |  |  |  |  |  | | '-----  NT……Nested Task Flag
*      |  |  |  |  |  | '-----  0
*      |  |  |  |  |  '-----  RF……Resume Flag
*      |  |  |  |  '------  VM……Virtual Mode Flag
*      |  |  |  '-----  AC……Alignment Check
*      |  |  '-----  VIF……Virtual Interrupt Flag  
*      |  '-----  VIP……Virtual Interrupt Pending
*      '-----  ID……ID Flag
*
*
**********************************************************/
#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))
#define bool int
#define true 1
#define false 0

在这里插入图片描述

userprog/process.h
#ifndef __USERPROG_PROCESS_H
#define __USERPROG_PROCESS_H

#include "../thread/thread.h"
#include "stdint.h"

// 默认优先级
#define default_prio 31

// (0xc0000000 - 1)为用户空间最高地址, 往上为内核地址
// (0xc0000000 - 0x1000) 为栈顶的下边界, 0x1000 = 4096 = 4k
#define USER_STACK3_VADDR  (0xc0000000 - 0x1000)

// 用户进程起始地址, 这里采用 Linux 的入口地址
#define USER_VADDR_START   0x8048000

/* 创建用户进程 */
void process_execute(void* filename, char* name);

/* 构建用户进程初始上下文信息(填充用户进程的 intr_stack) */
void start_process(void* filename_);

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

/* 激活页表(更新 cr3 指向的页目录表, 每个进程有自己的页目录表) */
void page_dir_activate(struct task_struct* p_thread);

/* 创建页目录表, 将当前页表的表示内核空间的 pde 复制,
 * 成功则返回页目录的虚拟地址, 否则返回 -1 */
uint32_t* create_page_dir(void);

/* 创建用户进程虚拟地址位图 */
void create_user_vaddr_bitmap(struct task_struct* user_prog);

#endif
userprog/process.c

这里就把函数单独都拿出来介绍了:

extern void intr_exit(void);        // 通过中断返回指令进入3特权级


/* 构建用户进程初始上下文信息(填充用户进程的 intr_stack) */
void start_process(void* filename_) {
    void* function = filename_;
    struct task_struct* cur = running_thread();         // 获取当前线程
    cur->self_kstack += sizeof(struct thread_stack);    // 指向 intr_stack
    // 用新变量保存 intr_stack 地址
    struct intr_stack* proc_stack = (struct intr_stack*) cur->self_kstack;
    proc_stack->edi = 0;                                // 初始化通用寄存器
    proc_stack->esi = 0;
    proc_stack->ebp = 0;
    proc_stack->esp_dummy = 0;

    proc_stack->ebx = 0;
    proc_stack->edx = 0;
    proc_stack->ecx = 0;
    proc_stack->eax = 0;

    proc_stack->gs = 0;                                 // 用户态用不上, 直接初始为 0
    proc_stack->ds = SELECTOR_U_DATA;                   // 选择子设置为3特权的区域
    proc_stack->es = SELECTOR_U_DATA;
    proc_stack->fs = SELECTOR_U_DATA;
    proc_stack->eip = function;                         // 待执行的用户程序地址
    proc_stack->cs = SELECTOR_U_CODE;                   // 修改 CS 段特权级
    // 修改 eflags 的 IF/IOPL/MBS
    proc_stack->eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1);

    // 申请用户栈空间, 申请函数返回的是申请内存的下边界, 
    // 所以这里的地址(USER_STACK3_VADDR)应该是用户栈的下边界, 所以 + PG_SIZE得到栈底地址
    proc_stack->esp = (void*) ((uint32_t) get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE);
    proc_stack->ss = SELECTOR_U_DATA;                   // 修改 SS 段特权级

    // 修改 esp 指针, 执行 intr_exit, 将 proc_stack 中的数据载入CPU寄存器, 进入特权级3
    asm volatile("movl %0, %%esp; jmp intr_exit" : : "g"(proc_stack) : "memory");
}

start_process函数,用来构建用户进程初始化上下文信息,修改intr_stack栈的信息如段选择子,cs段,ss段,esp位置,eflags,初始化通用寄存器等,这个函数在创建完成用户页表后执行,所以会在自己的用户空间内进行初始化

intr_stack栈的作用有两个:

  1. 任务被中断用来保存上下文
  2. 给进程预留,用来填充进程的上下文

C 程序在内存空间的分布:

在这里插入图片描述

最高地址存放环境变量和命令行参数,然后是栈和堆,栈和堆是相向扩展的,所以操作系统需要检测是否冲突,再之下是bss,data,text这些东西由编译器和链接器负责

这里我们也进行效仿,用户空间最高位用来存命令行参数,也就是 0xc0000000 - 1,然后栈就申请在 0xc0000000 - 0x1000的位置(申请时候用的是低地址)然后再加上0x1000赋值给esp作为栈底

下一个函数:

/* 激活页表(更新 cr3 指向的页目录表, 每个进程有自己的页目录表) */
void page_dir_activate(struct task_struct* p_thread) {
    /********************************************************
    * 执行此函数时, 当前任务可能是线程。
    * 之所以对线程也要重新安装页表, 原因是上一次被调度的可能是进程,
    * 否则不恢复页表的话, 线程就会使用进程的页表了。
    ********************************************************/

    // 若为内核线程, 需要重新填充页表为 0x100000
    // 默认为内核的页目录物理地址, 也就是内核线程所用的页目录表
    uint32_t pagedir_phy_addr = 0x100000;
    
    if(p_thread->pgdir != NULL) {
        // 用户态进程有自己的页目录表(每个页目录表代表4GB = 1024 * 4MB), 根据页表虚拟地址获得物理地址
        pagedir_phy_addr = addr_v2p((uint32_t) p_thread->pgdir);
    }

    // 更新页目录寄存器cr3, 使新页表生效
    asm volatile("movl %0, %%cr3" : : "r"(pagedir_phy_addr) : "memory");
}


/* 激活线程或进程的页表, 更新 tss 中的 esp0 为进程的特权级0的栈 */
void process_activate(struct task_struct* p_thread) {
    ASSERT(p_thread != NULL);
    // 激活该进程或线程的页表(更新 cr3 指向的页目录表)
    page_dir_activate(p_thread);

    // 内核线程特权级本身就是 0, 处理器进入中断时并不会从
    // tss 中获取 0 特权级栈地址, 故不需要更新 esp0
    if(p_thread->pgdir) {
        // 更新该进程的 esp0, 用于此进程被中断时保留上下文
        update_tss_esp(p_thread);
    }
}

判断当前任务是进程还是线程,是根据PCB中的pgdir进行的,如果是NULL,则表示是线程,反之是进程

只有在任务调度时才会切换页表以及更新0级栈

process_activate函数是被schedule调用的

bss 简介

在程序加载之初,系统需要为堆和栈指定起始地址,C语言程序上大体上分为预处理、编译、汇编、链接四个阶段,在链接阶段将目标文件内属性相同的节合并成一个段(一方面为了安全检查、一方面为了方便操作系统加载)

从C程序内存布局中可以看到,堆的起始位置应该在bss段之上

bss段是用来存储运行过程中的未初始化的全局变量和静态局部变量的,bss仅存在于内存中,不存在于文件中(仅在elf文件头中记录了bss的虚拟地址,大小等),bss段存在的目的就是为了这些未初始化的数据预留空间

但是bss段和数据段的属性是一样的,会被自动归结到数据段当中(bss尺寸会被合并到数据段尺寸, 所以数据段预留bss的了内存空间),所以只要知道数据段的起始地址和大小,就可以确定堆的起始地址了

将来实现用户进程的堆,只要堆在用户进程地址上最高的段之上就可以了

实现用户进程(下)

userprog/process.c
/* 创建页目录表, 将当前页表的表示内核空间的 pde 复制,
 * 成功则返回页目录的虚拟地址, 否则返回 -1 */
uint32_t* create_page_dir(void) {
    // 用户进程的页表不能让用户直接访问到, 所以在内核空间来申请
    // 申请作为用户页目录表的基地址
    uint32_t* page_dir_vaddr = get_kernel_pages(1);     
    if(page_dir_vaddr == NULL) {
        console_put_str("create_page_dir: get_kernel_pages failed!");
        return NULL;
    }

    // 1. 先复制页表(将当前页表的表示内核空间的 pde 复制到新页表对应映射内核部分)
    // page_dir_vaddr + 0x300 * 4 是内核页目录的第 768 项
    // 0xfffff000 是内核页目录表的基地址, 会访问到当前页目录表的最后一个目录项, 也就是当前页目录表本身
    memcpy((uint32_t*) ((uint32_t) page_dir_vaddr + 0x300 * 4), (uint32_t*) (0xfffff000 + 0x300 * 4), 1024);

    // 2. 更新页目录地址
    // 得到新页目录的物理地址
    uint32_t new_page_dir_phy_addr = addr_v2p((uint32_t) page_dir_vaddr);
    // 页目录地址是存入在页目录的最后一项
    // 更新页目录地址为新页目录的物理地址
    page_dir_vaddr[1023] = new_page_dir_phy_addr | PG_US_U | PG_RW_W | PG_P_1;

    return page_dir_vaddr;
}


/* 创建用户进程虚拟地址位图 */
void create_user_vaddr_bitmap(struct task_struct* user_prog) {
    user_prog->userprog_vaddr.vaddr_start = USER_VADDR_START;   // 提前定好的起始位置

    // 除法向上取整, 计算位图需要的内存页数
    // (内核起始地址 - 用户进程起始地址) / PG_SIZE / 8 = 位图需要的 byte
    // 再 / PG_SIZE 向上取整得到位图需要的内存页数
    uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8, PG_SIZE);  

    // 给位图分配空间
    user_prog->userprog_vaddr.vaddr_bitmap.bits = get_kernel_pages(bitmap_pg_cnt);
    
    //计算位图长度, 内存大小 / 页大小(1位=1页)/ 8位(1字节=8位)
    user_prog->userprog_vaddr.vaddr_bitmap.btmp_bytes_len = (0xc0000000 - USER_VADDR_START) / PG_SIZE / 8;

    // 将位图初始化
    bitmap_init(&user_prog->userprog_vaddr.vaddr_bitmap);
}


/* 创建用户进程 */
void process_execute(void* filename, char* name) {
    // pcb 内核的数据结构, 由内核来维护进程信息, 因此要在内核内存池中申请
    struct task_struct* thread = get_kernel_pages(1);
    init_thread(thread, name, default_prio);        // 初始化进程基本信息
    create_user_vaddr_bitmap(thread);               // 创建用户进程虚拟地址位图
    // 初始化线程栈 thread_stack, start_process为进程待执行函数, filename为其参数
    // start_process: 填充用户进程的 intr_stack
    thread_create(thread, start_process, filename);
    thread->pgdir = create_page_dir();              // 创建进程的页目录表

    enum intr_status old_status = intr_disable();   // 关中断
    ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));
    // 将进程加入就绪队列
    list_append(&thread_ready_list, &thread->general_tag);  

    ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));
    // 将进程加入所有任务队列
    list_append(&thread_all_list, &thread->all_list_tag);
    intr_set_status(old_status);
}

create_page_dir 函数申请了一页新的内核内存空间,用来存放用户进程的页目录表,为了让共享内核,所以将内核所占有的1GB虚拟空间的页目录表的映射,复制到用户页目录表中,也就是768~1023个页目录项,然后把第1023个页目录项改成自己的页目录表的地址,返回页目录表虚拟地址

create_user_vaddr_bitmap 函数为用户进程创建一个虚拟地址位图并清零初始化,位图信息在用户进程PCB的userprog_vaddr.vaddr_bitap

process_execute 函数的目的是创建用户进程并加入就绪队列,申请1页空间存储PCB,初始化PCB,向PCB中添加位图信息,向PCB中添加栈上下文信息,给PCB中的pgdir赋值(通过调用create_page_dir 函数),到这里PCB的内容已经填充完毕了,接下来关中断,把PCB加入就绪队列和全部队列中,开中断,实现原子操作

进程的创建和线程的创建基本上是一样的流程,不同点在于进程要填充的PCB信息比线程多一点,多了一个虚拟位图信息和pgdir页目录信息,其他流程是一样的

PCB不仅能表示线程,还能表示一个进程,通过pgdir成员来区分

用户进程调度

当前的调度器调度线程一律按内核线程处理,0级特权,内核页表,而进程是3级特权,用户页表,所以需要改进一下:

thread/thread.c
/* 实现任务调度 */
void schedule() {
    ASSERT(intr_get_status() == INTR_OFF);

    struct task_struct* cur = running_thread();
    if(cur->status == TASK_RUNNING) {
        // 若此线程只是 CPU 时间片到了, 将其加入到就绪队尾
        ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
        list_append(&thread_ready_list, &cur->general_tag);
        cur->ticks = cur->priority;
        cur->status = TASK_READY;
    } else {
        // 若此线程阻塞, 不需要将其加入队列
    }

    ASSERT(!list_empty(&thread_ready_list));
    thread_tag = NULL;      // thread_tag清空
    // 将 thread_ready_list 队列中的第一个就绪线程弹出, 准备将其调度上cpu
    thread_tag = list_pop(&thread_ready_list);
    // 获取 thread_tag 对应的 PCB
    struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag);
    next->status = TASK_RUNNING;

    // 切换前将页表进行切换,根据任务是否是进程,修改 tss 中的 esp0
    process_activate(next);	//新增内容

    switch_to(cur, next);
}

这里的新增仅1行,就是在切换前将页表进行切换,根据任务是否是进程,修改tss中的esp0

process.h:

在这里插入图片描述

process.c:

在这里插入图片描述

测试用户进程

kernel/main.c
#include "print.h"
#include "init.h"
#include "debug.h"
#include "memory.h"
#include "../thread/thread.h"
#include "interrupt.h"
#include "../device/console.h"
#include "../userprog/process.h"

void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);

int test_var_a = 0, test_var_b = 0;

int main() {
	put_str("I am kernel\n");
	init_all();

	thread_start("k_thread_a", 31, k_thread_a, "argA ");
	thread_start("k_thread_b", 31, k_thread_a, "argB ");
	process_execute(u_prog_a, "user_prog_a");
	process_execute(u_prog_b, "user_prog_b");

	intr_enable();             // 打开中断, 使时钟中断起作用
	while (1);
	return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {     
   char* para = arg;
   while(1) {
      console_put_str(" v_a:0x");
      console_put_int(test_var_a);
   }
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {     
   char* para = arg;
   while(1) {
      console_put_str(" v_b:0x");
      console_put_int(test_var_b);
   }
}

/* 测试用户进程 */
void u_prog_a(void) {
   while(1) {
      test_var_a++;
   }
}

/* 测试用户进程 */
void u_prog_b(void) {
   while(1) {
      test_var_b++;
   }
}
// 只打开时钟中断, 其它全部关闭
outb(PIC_M_DATA, 0xfe);
outb(PIC_S_DATA, 0xff);

运行 Bochs

BUILD_DIR = ./build
ENTRY_POINT = 0xc0001500
AS = nasm
CC = gcc
LD = ld
LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/
ASFLAGS = -f elf
CFLAGS = -Wall -m32 -fno-stack-protector $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes
LDFLAGS = -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \
       $(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o \
       $(BUILD_DIR)/debug.o $(BUILD_DIR)/memory.o $(BUILD_DIR)/string.o \
	   $(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o $(BUILD_DIR)/list.o  \
	   $(BUILD_DIR)/switch.o $(BUILD_DIR)/sync.o $(BUILD_DIR)/console.o \
	   $(BUILD_DIR)/keyboard.o $(BUILD_DIR)/ioqueue.o $(BUILD_DIR)/tss.o \
	   $(BUILD_DIR)/process.o


############ C 代码编译 ##############
$(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h	\
					 lib/stdint.h kernel/init.h lib/string.h
	$(CC) $(CFLAGS) $< -o $@


$(BUILD_DIR)/init.o: kernel/init.c kernel/init.h lib/kernel/print.h \
        			 lib/stdint.h kernel/interrupt.h device/timer.h
	$(CC) $(CFLAGS) $< -o $@


$(BUILD_DIR)/interrupt.o: kernel/interrupt.c kernel/interrupt.h \
        				  lib/stdint.h kernel/global.h lib/kernel/io.h lib/kernel/print.h
	$(CC) $(CFLAGS) $< -o $@


$(BUILD_DIR)/timer.o: device/timer.c device/timer.h lib/stdint.h \
        			  lib/kernel/io.h lib/kernel/print.h
	$(CC) $(CFLAGS) $< -o $@


$(BUILD_DIR)/debug.o: kernel/debug.c kernel/debug.h \
        			  lib/kernel/print.h lib/stdint.h kernel/interrupt.h
	$(CC) $(CFLAGS) $< -o $@


$(BUILD_DIR)/string.o: lib/string.c lib/string.h \
					   kernel/debug.h kernel/global.h
	$(CC) $(CFLAGS) $< -o $@


$(BUILD_DIR)/memory.o: kernel/memory.c kernel/memory.h \
					   lib/stdint.h lib/kernel/bitmap.h kernel/debug.h lib/string.h
	$(CC) $(CFLAGS) $< -o $@
	

$(BUILD_DIR)/bitmap.o: lib/kernel/bitmap.c lib/kernel/bitmap.h \
					   lib/string.h kernel/interrupt.h lib/kernel/print.h kernel/debug.h
	$(CC) $(CFLAGS) $< -o $@


$(BUILD_DIR)/thread.o: thread/thread.c thread/thread.h \
					   lib/stdint.h lib/string.h kernel/global.h kernel/memory.h \
					   kernel/debug.h kernel/interrupt.h lib/kernel/print.h
	$(CC) $(CFLAGS) $< -o $@


$(BUILD_DIR)/list.o: lib/kernel/list.c lib/kernel/list.h \
					 kernel/interrupt.h lib/stdint.h kernel/debug.h
	$(CC) $(CFLAGS) $< -o $@


$(BUILD_DIR)/sync.o: thread/sync.c thread/sync.h \
					 lib/stdint.h thread/thread.h kernel/debug.h kernel/interrupt.h
	$(CC) $(CFLAGS) $< -o $@
	
$(BUILD_DIR)/console.o: device/console.c device/console.h \
						lib/kernel/print.h thread/sync.h
	$(CC) $(CFLAGS) $< -o $@


$(BUILD_DIR)/keyboard.o: device/keyboard.c device/keyboard.h \
						 lib/kernel/print.h lib/kernel/io.h kernel/interrupt.h \
						 kernel/global.h lib/stdint.h device/ioqueue.h
	$(CC) $(CFLAGS) $< -o $@


$(BUILD_DIR)/ioqueue.o: device/ioqueue.c device/ioqueue.h \
						kernel/interrupt.h kernel/global.h kernel/debug.h
	$(CC) $(CFLAGS) $< -o $@


$(BUILD_DIR)/tss.o: userprog/tss.c userprog/tss.h \
					kernel/global.h thread/thread.h lib/kernel/print.h
	$(CC) $(CFLAGS) $< -o $@


$(BUILD_DIR)/process.o: userprog/process.c userprog/process.h \
						lib/string.h kernel/global.h kernel/memory.h lib/kernel/print.h \
						thread/thread.h kernel/interrupt.h kernel/debug.h device/console.h
	$(CC) $(CFLAGS) $< -o $@


##############    汇编代码编译    ###############
$(BUILD_DIR)/kernel.o: kernel/kernel.asm
	$(AS) $(ASFLAGS) $< -o $@


$(BUILD_DIR)/print.o: lib/kernel/print.asm
	$(AS) $(ASFLAGS) $< -o $@


$(BUILD_DIR)/switch.o: thread/switch.asm
	$(AS) $(ASFLAGS) $< -o $@


##############    链接所有目标文件    #############
$(BUILD_DIR)/kernel.bin: $(OBJS)
	$(LD) $(LDFLAGS) $^ -o $@


.PHONY: mk_dir hd clean all

mk_dir:
	if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fi

hd:
	sudo dd if=$(BUILD_DIR)/kernel.bin \
            of=/home/steven/source/os/bochs/hd60M.img \
            bs=512 count=200 seek=9 conv=notrunc

clean:
	cd $(BUILD_DIR) && rm -f ./*

build: $(BUILD_DIR)/kernel.bin

all: mk_dir build hd

编译,运行:

在这里插入图片描述

流程

创建线程

在这里插入图片描述

创建进程

在这里插入图片描述

本篇总结

本章节的内容是实现用户进程,为了让用户程序和系统程序有所区分,就有了用户进程

进程和线程的区别是,进程有独立的内存空间,进程之间的空间是隔离开的

但本书是基于线程来实现进程的,也就是说,进程也会有一个自己的PCB,进程的PCB会比线程的PCB多一些东西:页表地址(虚拟地址)、虚拟空间位图

进程也是由PCB来标识的,在这里的进程和线程一样,是一个执行流,可以理解成,单线程的进程就是一个线程,进程的作用得等到到时候做进程内多线程才能知道

或者可以认为,进程就是一个主线程运行在了一个独立的空间

因为 CPU 硬件级要求使用 TSS 来进行任务切换,但是 TSS 并不好用,但是又不能不用,所以就用其中一个tss.esp0来进行0级栈的切换了

实现用户进程,要考虑解决的问题如下:

  • 用户进程要有自己的内存空间:给进程创建一个页表,将页表地址(vaddr)写到PCB中,切换进程的时候,判断是不是进程,是进程就切换cr3的值
  • 用户进程要运行在3特权级下:在进程第一次执行的时候通过伪造中断现场,使用iretd指令从0级返回3级段空间,执行3级指令

经过类似线程的PCB构建流程之后,将进程PCB添加到PCB队列和PCB就绪队列中,等待时间片就可以开始执行了

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值