手写简易操作系统(十八)--实现用户进程

一、TSS

TSS是Task State Segment的缩写,即任务状态段,早在简述特权级的时候我们就讲过了一点 手写简易操作系统(八),现在我们讲一下这些保存的寄存器是干嘛的。

这一部分需要讲点历史,硬件与软件的关系是相互促进的,最初的CPU只支持单任务,但是后面随着多任务的需求越来越急迫,在软件上面实现多任务不仅存在效率不高的问题,而且容易出bug,于是CPU的厂商想办法来解决这个问题。

单核CPU要想实现多任务,唯一的方案就是多个任务共享同一个CPU,也就是只能让CPU在多个任务间轮转,让所有任务轮流使用CPU,除此之外还真别无他法。

CPU执行任务时,需要把任务运行所需要的数据加载到寄存器、栈和内存中。CPU只能直接处理这些资源中的数据,这是CPU在设计之初时工程师们决定的。于是问题来了,任务的数据和指令是CPU的处理对象,任务的执行要占用一套存储资源,如寄存器和内存,这些存储资源中装的是任务的数据和指令,但是CPU不愿意直接使用内存中的数据,他更喜欢寄存器这个高速的缓存,因此内存中的数据往往被加载到高速的寄存器后再处理,处理完成后,再将结果回写到低速的内存中,所以,任何时候,寄存器中的内容才是任务的最新状态。采取轮流使用CPU的方式运行多任务,当前任务在被换下CPU时,任务的最新状态,也就是寄存器中的内容应该找个地方保存起来,以便下次重新将此任务调度到CPU上时可以恢复此任务的最新状态,这样任务才能继续执行,否则就出错了。

CPU的厂商也是这么想的,于是硬件厂商的想法是给每个任务一个任务状态段TSS。这里有一套完整的寄存器映像,恢复这些寄存器映像就能恢复任务状态。TSS是由程序员提供的,其实就是提供了一个内存地址,告诉了CPU和操作系统去哪里找相应任务的TSS。但是确实CPU在维护的,CPU自动的就可以使用这段内存保存任务的上下文以及寄存器。

CPU又如何知晓该切换TSS了呢,在CPU中有一个专门存储TSS信息的寄存器,这就是TR寄存器,它始终指向当前正在运行的任务,任务切换的实质就是TR寄存器指向不同的TSS。但是在Linux中并未这样做,因为切换TSS的开销是很大的,不如TR寄存器只指向一个TSS,我们直接替换TSS中的寄存器映像。这样也间接的实现了切换任务,且开销较小。

1.1、TSS描述符

image-20240325190557599

TSS和其他段一样,本质上是一片存储数据的内存区域,Intel打算用这片内存区域保存任务的最新状态(也就是任务运行时占用的寄存器组等),因此它也像其他段那样,需要用某个描述符结构来“描述”它,这就是TSS描述符,TSS描述符也要在GDT中注册,

TSS描述符属于系统段描述符,因此S为0,在S为0的情况下,TYPE的值为10B1。我们这里关注一下B位,B表示busy位,B位为0时,表示任务不繁忙,B位为1时,表示任务繁忙。

任务繁忙有两方面的含义,一方面就是指此任务是否为当前正在CPU上运行的任务。另一方面是指此任务嵌套调用了新的任务,CPU正在执行新任务,此任务暂时挂起,等新任务执行完成后CPU会回到此任务继续执行,所以此任务马上就会被调度执行了。这种有嵌套调用关系的任务数不只两个,可以很多,比如任务A调用了任务A.1,任务A.1又调用了任务A.1.1等,为维护这种嵌套调用的关联,CPU把新任务TSS中的B位置为1,并且在新任务的TSS中保存了上一级旧任务的TSS指针(还要把新任务标志寄存器eflags中NT位的值置为1),新老任务的调用关系形成了调用关系链。

当任务刚被创建时,此时尚未上CPU执行,因此,此时的B位为0,TYPE的值为1001。当任务开始上CPU执行时,处理器自动地把B位置为1,此时TYPE的值为1011。当任务被换下CPU时,处理器把B位置0。注意,B位是由CPU来维护的,不需要干预。

B位存在的意义可不是单纯为了表示任务忙不忙,而是为了给当前任务打个标记,目的是避免当前任务调用自己,也就是说任务是不可重入的。

1.2、TSS结构

TSS中的字段基本上全是寄存器名称,这些寄存器就是任务运行中的最新状态。可见TSS的主要作用就是保存任务的快照。

image-20240325190927221

除了一般的寄存器外,TSS中还有“I/O位图”和“上一个任务的TSS指针”,分别位于TSS结构图的左上角和右下角。I/O位图可以在单个端口的粒度上进行IO特权控制,

另外要说的就是TSS中有三组栈:SS0和esp0,SS1和esp1,SS2和esp2。之前已经介绍过,除了从中断和调用门返回外,CPU不允许从高特权级转向低特权级。没有特权级会跳到特权级3,所以不会有特权级3的栈。

Linux只用到了0特权级和3特权级,用户进程处于3特权级,内核位于0特权级,因此对于Linux来说只需要在TSS中设置SS0和esp0,咱们也效仿它,只设置SS0和esp0的值就够了。

TSS是CPU原生支持的数据结构,因此CPU能够直接、正确识别其中的所有字段。当任务被换下CPU时,CPU会自动将当前寄存器中的值存储到TSS中的对应位置,当有新任务上CPU运行时,CPU会自动从新任务的TSS中找到相应的寄存器值加载到对应的寄存器中。

和LDT一样,CPU对TSS的处理也采取了类似的方式,它提供了一个寄存器来存储TSS的起始地址及偏移大小。但也许让人有点意外,这个寄存器不叫TSSR,而是称为TR(Task Register),也许是称为 TR,

image-20240325191141962

将tss加载到寄存器TR的指令是ltr,其指令格式为:

ltr “16位通用寄存器”或“16位内存单元”

有了TSS后,任务在被换下CPU时,由CPU自动地把当前任务的资源状态(所有寄存器、必要的内存结构,如栈等)保存到该任务对应的TSS中(由寄存器TR指定)。CPU通过新任务的TSS选择子加载新任务时,会把该TSS中的数据载入到CPU的寄存器中,同时用此TSS描述符更新寄存器TR。注意啦,以上动作是CPU自动完成的,不需要干预。这就是硬件级别的原生支持。

1.3、现代操作系统支持的任务切换方式

Linux为每个CPU创建一个TSS,在各个CPU上的所有任务共享同一个TSS,各CPU的TR寄存器保存各CPU上的TSS,在用ltr指令加载TSS后,该TR寄存器永远指向同一个TSS,之后再也不会重新加载TSS。在进程切换时,只需要把TSS中的SS0及esp0更新为新任务的内核栈的段地址及栈指针。

实际上Linux对TSS的操作是一次性加载TSS到TR,之后不断修改同一个TSS的内容,不再进行重复加载操作。Linux在TSS中只初始化了SS0、esp0和I/O位图字段,除此之外TSS便没用了,就是个空架子,不再做保存任务状态之用。

那任务的状态信息保存在哪里呢?当CPU由低特权级进入高特权级时,CPU会“自动”从TSS中获取对应高特权级的栈指针(TSS是CPU内部框架原生支持的嘛,当然是自动从中获取新的栈指针)。Linux只用到了特权3级和特权0级,因此CPU从3特权级的用户态进入0特权级的内核态时(比如从用户进程进入中断),CPU自动从当前任务的TSS中获取SS0和esp0字段的值作为0特权级的栈,然后Linux“手动”执行一系列的push指令将任务的状态的保存在0特权级栈中,也就是TSS中SS0和esp0所指向的栈。

Intel当初是打算让TR寄存器指向不同任务的TSS以实现任务切换的,Linux这里只换了TSS中的部分内容,而TR本身没换,还是指向同一个TSS,这种“自欺欺人”的好处是任务切换的开销更小了,因为和修改TSS中的内容所带来的开销相比,在TR中加载TSS的开销要大得多。

每次切换任务都要用ltr指令重新加载新任务的TSS到寄存器TR,TSS是位于内存中的,随着任务数量一多,这种频繁重复加载的开销就更为“可观”。另外,Linux中任务切换不使用call和jmp指令,这也避免了任务切换的低效。

二、定义并实现TSS

首先看一下tss的结构,和上图的顺序是一样的

/* 任务状态段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;
};

其初始化和更新程序为

/* 更新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;
    tss.io_base = tss_size;

    /* 在gdt中添加dpl为0的TSS描述符,以及特权级为3的用户段 */
    *((struct gdt_desc*)(0xc0000603 + 8*4)) = make_gdt_desc((uint32_t*)&tss, tss_size - 1, TSS_ATTR_LOW, TSS_ATTR_HIGH);
    *((struct gdt_desc*)(0xc0000603 + 8*5)) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_CODE_ATTR_LOW_DPL3, GDT_ATTR_HIGH);
    *((struct gdt_desc*)(0xc0000603 + 8*6)) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_DATA_ATTR_LOW_DPL3, GDT_ATTR_HIGH);
    /* gdt段基址为0x603,把tss放到第4个位置,也就是0x602+0x20的位置 */
    uint64_t gdt_operand = ((8 * 7 - 1) | ((uint64_t)(uint32_t)0xc0000603 << 16));
    asm volatile ("lgdt %0" : : "m" (gdt_operand));
    asm volatile ("ltr %w0" : : "r" (SELECTOR_TSS));
    put_str("tss_init and ltr done\n");
}

我们先仿真一下

image-20240325195033307

可以看到所有的gdt,已经设置正确了。

三、实现用户进程

我们的目标是在现有线程的基础上实现进程,先回忆下,我们创建线程是通过thread_start进行的,其内部实现的流程如下图所示

image-20240325200342444

如果要基于线程实现进程,我们把function替换为创建进程的新函数就可以啦,先把控制权拿到手再说,进程相关的具体工作再由新函数完成。

3.2、用户进程的虚拟地址

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

每个进程都拥有4GB的虚拟地址空间,虚拟地址连续而物理地址可以不连续,这就是保护模式下分页机制的优势。为此我们需要为每个进程维护一个虚拟地址池,用此地址池来记录该进程的虚拟中,哪些已被分配,哪些可以分配。

为此task_struct 结构杯修改以下

/* 进程或线程的pcb,程序控制块 */
struct task_struct {
    uint32_t* self_kstack;	 // 线程或者进程内核栈的栈顶,就是pcb的高位
    enum task_status status; // 线程的运行状态
    char name[16];           // 线程名,最多16个字母
    uint8_t priority;		 // 线程优先级
    uint8_t ticks;	         // 每次在处理器上的执行时间的滴答数
    uint32_t elapsed_ticks;  // 这个任务总的滴答数
    struct list_elem general_tag;       // 线程在一段队列中的节点
    struct list_elem all_tag;// 线程在所有任务队列中的节点
    uint32_t* pgdir;         // 进程自己页表的虚拟地址
    struct virtual_addr userprog_vaddr; // 用户进程的虚拟地址池
    uint32_t stack_magic;	 // 用这串数字做栈的边界标记,用于检测栈的溢出
};

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

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

进程与线程的区别是进程拥有独立的地址空间,不同的地址空间就是不同的页表,因此我们在创建进程的过程中需要为每个进程单独创建一个页表。我们这里所说的页表是“页目录表+页表”,页目录表用来存放页目录项PDE,每个PDE又指向不同的PTE。

为此我们需要修改以下memory.c,增加当用户申请内存时的程序。

首先为内存池结构添加锁

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

vaddr_get 函数中添加对用户进程的支持

/* 在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) {
            put_str("vaddr_get error: kernel vaddr error!\n");
            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();
        // 获得pg_cnt个连续的虚拟地址
        bit_idx_start = bitmap_scan(&cur->userprog_vaddr.vaddr_bitmap, pg_cnt);
        // 如果没有这么大空间返回空 
        if (bit_idx_start == -1) {
            put_str("vaddr_get error: User vaddr error!\n");
            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;
    }
    return (void*)vaddr_start;
}

添加申请用户物理内存池的函数

/* 从用户物理内存池中申请pg_cnt页内存,成功则返回其虚拟地址,失败则返回NULL */
void* get_user_pages(uint32_t pg_cnt) {
    void* vaddr = malloc_page(PF_USER, pg_cnt);
    if (vaddr != NULL) {
        memset(vaddr, 0, pg_cnt * PG_SIZE);
    }
    else {
        put_str("get_user_pages error: vaddr error!\n");
    }
    return vaddr;
}

添加将地址vaddr与pf池中的物理地址关联的函数

/* 将地址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 = (vaddr - cur->userprog_vaddr.vaddr_start) / PG_SIZE;
        bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx, 1);
    }
    // 如果是内核线程申请内核内存,就修改内核虚拟地址位图
    else if (cur->pgdir == NULL && pf == PF_KERNEL) {
        bit_idx = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;
        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) {
    uint32_t* pte = pte_ptr(vaddr);
    // (*pte)的值是页表所在的物理页框地址, 去掉其低12位的页表项属性+虚拟地址vaddr的低12位 
    return ((*pte & 0xfffff000) + (vaddr & 0x00000fff));
}

3.4、进入特权级3

用户进程工作在特权级3,但是目前为止,我们都在特权级0下工作,除了中断,特权级之间不能随意跳转,高特权级更不能跳转到低特权级,除了中断返回,但是现在还没有中断啊,怎么中断返回呢,还是之前的思路,反正不管是中断还是中断返回,都是从栈中调用数据,那么我们直接从栈中把要返回的地址和参数给系统准备好,系统直接返回不就行了。

从中断返回肯定要用到iretd指令,iretd指令会用到栈中的数据作为返回地址,还会加载栈中eflags的值到eflags寄存器,如果栈中cs.rpl若为更低的特权级,处理器的特权级检查通过后,会将栈中cs载入到CS寄存器,栈中ss载入SS寄存器,随后处理器进入低特权级。

在中断发生时,我们在中断入口函数“intr%1entry”中通过一系列的push操作来保存任务的上下文,因此在intr_exit中恢复任务上下文要通过一系列的pop操作,这属于“intr%1entry”的逆过程。任务的上下文信息被保存在任务pcb中的struct intr_stack中,注意啦,struct intr_stack并不要求有固定的位置,它只是一种保存任务上下文的格式结构。

在汇编函数intr_exit里面的一系列的pop操作是为了恢复任务的上下文,我们也可以利用它,借一系列pop出栈的机会,将用户进程的上下文信息载入CPU的寄存器,为用户进程的运行准备好环境。

当执行完intr_exit中的iretd指令后,CPU便恢复了任务中断前的状态:中断前是哪个特权级就进入哪个特权级。

CPU是如何知道从中断退出后要进入哪个特权级呢?这是由栈中保存的CS选择子中的RPL决定的,我们知道,CS.RPL就是CPU的CPL,当执行iretd时,在栈中保存的CS选择子要被加载到代码段寄存器CS中,因此栈中CS选择子中的RPL便是从中断返回后CPU的新的CPL。

既然用户进程的特权级为3,操作系统不能辜负CPU的委托,它有责任把用户进程所有段选择子的RPL都置为3,因此,在RPL=CPL=3的情况下,用户进程只能访问DPL为3的内存段,即代码段、数据段、栈段。我们前面的工作中已经准备好了DPL为3的代码段及数据段,由此我们得出关键点4,栈中段寄存器的选择子必须指向DPL为3的内存段。

对于可屏蔽中断来说,任务之所以能进入中断,是因为标志寄存器eflags中的IF位为1,退出中断后,还得保持IF位为1,继续响应新的中断。

用户进程属于最低的特权级,对于IO操作,不允许用户进程直接访问硬件,只允许操作系统有直接的硬件控制。这是由标志寄存器eflags中IOPL位决定的,必须使其值为0。

所以我们得到六个关键点

1、从中断返回,必须要经过intr_exit
2、必须提前准备好用户进程所用的栈结构,在里面填装好用户进程的上下文信息,借一系列pop出栈的机会,将用户进程的上下文信息载入CPU的寄存器,为用户进程的运行准备好环境。
3、在栈中存储的CS选择子,其RPL必须为3。
4、栈中段寄存器的选择子必须指向DPL为3的内存段。
5、必须使栈中eflags的IF位为1。
6、必须使栈中eflags的IOPL位为0。

3.5、创建用户进程的流程

我们看一下如何创建的进程

/* 创建用户进程 */
void process_execute(void* filename, char* name) { 
    // pcb是操作系统的数据,由操作系统来维护
    struct task_struct* thread = get_kernel_pages(1);
    // 初始化线程
    init_thread(thread, name);
    // 完善pcb中的用户虚拟地址位图,这也是线程是内核线程还是用户进程的判别
    create_user_vaddr_bitmap(thread);
    // 创建线程
    thread_create(thread, start_process, filename);
    // 创建用户进程的页目录表,用户进程有自己的页目录表,这样就实现了进程的隔离
    thread->pgdir = create_page_dir();
    // 将当前线程加入多级反馈优先队列
    mlfq_new(thread);
}

1、申请一页内核地址作为用户进程的pcb

2、初始用户进程的pcb

3、用户进程中有用户进程的虚拟地址位图,初始化这个位图

4、创建进程,其中要执行的函数是 start_process, 传入的参数是 filename

5、 创建用户进程的页目录表,用户进程有自己的页目录表,这样才能实现了进程的隔离,用户进程的页目录表的高3G的地址是复制的内核线程的,这样也实现了不同进程高1GB地址的通用

6、将当前进程加入多级反馈有限队列等待调用。

发现了嘛,进程和内核线程几乎是一样的,只是用户进程不能访问硬件资源,用户进程有自己的页目录表,页目录表的高1G地址还是复制的内核页表。我们看一下 start_process 这个函数,毕竟创建进程后就等待调用了,一旦 schedule() 函数调用了这个进程,那么就开始执行 start_process(filename) 这个函数

/* 构建用户进程初始上下文信息 */
void start_process(void* filename_) {
    // 后面我们是要从硬盘中读取用户程序的,所以这里是一个filename,不是function
    void* function = filename_;
    struct task_struct* cur = running_thread();
    cur->self_kstack += sizeof(struct thread_stack);  //跨过thread_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;		     // 不允许用户态直接访问硬件显存
    proc_stack->ds = SELECTOR_U_DATA;
    proc_stack->es = SELECTOR_U_DATA;
    proc_stack->fs = SELECTOR_U_DATA;
    proc_stack->eip = function;	 // 待执行的用户程序地址
    proc_stack->cs = SELECTOR_U_CODE;
    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) ;
    proc_stack->ss = SELECTOR_U_DATA; 
    asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (proc_stack) : "memory");
}

首先就是获取当前的线程,当前的线程其实就是我们的用户进程了,为什么这么讲,我们在 schedule() 中执行 switch_to 函数以后当前线程就已经了新创建的用户进程了,直到通过栈执行到 start_process

看一下上面的C代码,可以发现只是填写中断栈,填写中断栈的作用只有一个中断返回,没有进入中断不要紧,我们构造一个中断栈,中断栈内压入要执行的用户程序地址作为中断返回地址,然后执行中断返回,从此就实现了内核程序向用户程序的跳转。

四、仿真

可以看到现在的打印还有问题

image-20240327212410835

这个问题就好分析了,虽然我们对控制台上了锁,但是我们的打印是分三次进行的,在打印出 0x 后,进入中断,任务调换了,再次进入任务a时才打印后面的数字以及回车符。

结束语

本节我们创建了用户程序,但是用户程序连打印都做不到,需要内核级线程的帮助才能打印

正常的Linux系统是通过 0x80 中断来实现低特权级向高特权级跳转的,也就是通过 0x80 中断调用系统服务。下节我们实现 0x80 中断。

老规矩,本节的代码地址:https://github.com/lyajpunov/os

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/502346.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于SpringBoot + Vue实现的中国陕西民俗网设计与实现+毕业论文

介绍 本系统包含管理员、用户两个角色。 管理员角色&#xff1a;登录、用户管理功能、民俗介绍管理功能(发布和管理民俗文化的介绍文章)、公告信息管理功能(发布网站的重要通知和活动信息)、商品管理功能(对商家发布的商品进行监管)、商品评价管理功能(监管商品评价内容&#…

乐理通识

2023 年搞了台雅马哈 61 键的电子琴&#xff0c;顺手看了下啊 B 的上的课程 《零基础自学音乐学乐理合集-第一季》&#xff0c;这里是部分笔记&#xff08;给博客加点不一样的东西&#x1f440;&#xff09;。 简谱各部分一览 C 表示音名竖线为小节线 音名 完整钢琴键盘 88 键…

leetcode:392. 判断子序列

题目&#xff1a; class Solution { public:bool isSubsequence(string s, string t) {} }; 题解&#xff1a; 很巧妙的题解&#xff1a;循环遍历两个字符串&#xff0c;两个字符串都没遍完就继续遍历&#xff0c;字符串s先遍历完结果为true&#xff0c;字符串t先遍历完结果为…

项目管理【环境】概述

系列文章目录 【引论一】项目管理的意义 【引论二】项目管理的逻辑 【环境】概述 一、组织运行环境 1.1 事业环境因素EEFs 1.2 组织过程资产OPA 1.3 二者差异 二、组织结构类型 2.1 组织架构 2.2 职能型组织 2.3 项目型组织 2.4 矩阵型组织 2.5 项目管理者在不同组织中的特…

NSSCTF Round#20 Basic 真亦假,假亦真 CSDN_To_PDF V1.2 出题笔记 (附wp+源码)

真亦假&#xff0c;假亦真 简介&#xff1a;java伪造php一句话马。实则信息泄露一扫就出&#xff0c;flag在/flag里面。 题目描述&#xff1a;开开心心签个到吧&#xff0c;祝各位师傅们好运~ 静态flag&#xff1a;NSS{Checkin_h4v3_4_g00D_tINNe!} /路由显示 <?php e…

数据库安全(redis、couchdb、h2database)CVE复现

redis服务默认端口&#xff1a;6379&#xff1b;我们可以通过端口扫描来判断是否存在该服务。 Redis 是一套开源的使用ANSI C 编写、支持网络、可基于内存亦可持久化的日志型、键值存储数据库&#xff0c;并提供多种语言的API。 Redis 如果在没有开启认证的情况下&#xff0c;…

论文笔记:TALK LIKE A GRAPH: ENCODING GRAPHS FORLARGE LANGUAGE MODELS

ICLR 2024&#xff0c;reviewer评分 6666 1 intro 1.1 背景 当下LLM的限制 限制1&#xff1a;对非结构化文本的依赖 ——>模型有时会错过明显的逻辑推理或产生错误的结论限制2&#xff1a;LLMs本质上受到它们训练时间的限制&#xff0c;将“最新”信息纳入到不断变化的世…

MySQL优化之外连接消除----空值拒绝

什么叫空值拒绝呢&#xff1f;请看下面用例 可以看到&#xff0c;外连接中&#xff0c;从表的表连接列&#xff0c;tb2.id出现了null&#xff0c;因为tb2中并没有id5的情况 点击(此处)折叠或打开 select * from tb1 left join tb2 on tb1.idtb2.id where tb2.id>1; sel…

商品说明书的制作工具来啦,用这几个就够了!

商品说明书是用户了解产品特性、性能、使用方法的重要途径。一个明确、易懂的商品说明书&#xff0c;可以显著提升用户体验&#xff0c;进而提升产品的销量。但我们都知道&#xff0c;制作一份高质量的说明书并不容易&#xff0c;需要仔细设计、计划和撰写。幸运的是&#xff0…

解决 linux 服务器 java 命令不生效问题

在Linux系统中&#xff0c;当你安装Java并设置了JAVA_HOME环境变量后&#xff0c;你可能需要使用source /etc/profile命令来使Java命令生效。这是因为/etc/profile是一个系统级的配置文件&#xff0c;它包含了系统的全局环境变量设置。 但是需要注意的是&#xff0c;source /e…

数据运营分析-详解

一、指标与指标体系 指标体系就是业务逻辑的框架,也是思考业务逻辑的第一步 案例: 老板,我负责的用户活跃,主要考察每天启动产品的注册用户数量,整体来看,每月活跃保持7.3%的增长,是因为渠道团队的拉新活动带来很多新增注册用户,占每月活跃用户的40%,新一年会继续沿…

手写简易操作系统(十七)--编写键盘驱动

前情提要 上一节我们实现了锁与信号量&#xff0c;这一节我们就可以实现键盘驱动了&#xff0c;访问键盘输入的数据也属于临界区资源&#xff0c;所以需要锁的存在。 一、键盘简介 之前的 ps/2 键盘使用的是中断驱动的&#xff0c;在当时&#xff0c;按下键盘就会触发中断&a…

图的基础和图的遍历(--蓝桥云)

图的基础概念 度数&#xff1a;出边入边的条数 有向边&#xff1a;有箭头 图的存储方式 //邻接表 List<int []> list[N] list<x>//存放x的所有出点的信息 list[i][j]{first,second}//其中first表示从i出发的某个出点的编号&#xff08;这个出点是i的第j个出点&…

Lecture 1 - Introduction

Lecture 1 - Introduction MIT 6.824 Distributed Systems 1、概念预览 分布式系统需要考虑的因素&#xff1a; Parallelism &#xff1a;并行性Fault tolerence &#xff1a;容错性Physicial &#xff1a;不同系统之间物理距离引起的通信问题**Security ** &#xff1a;不…

eps32官方ESP-IDF开发工具IDE的使用

1、创建工程 创建的工程目录如下&#xff1a; .c文件中很多报错&#xff0c;需要编译下&#xff0c;点击左上角的"锤子"按钮 2、烧录 3、打开终端查看信息 如上信息表明运行正常 附 问题&#xff1a;如果烧录的过程中报如下错误&#xff1a; UnicodeDecodeError: gb…

黑马鸿蒙笔记2

1.图片设置&#xff1a; 1 加载网络图片&#xff0c;申请权限。 申请权限&#xff1a;entry - src - resources - module.json5 2 加载本地图片 ,两种加载方式 API 鼠标悬停在Image&#xff0c; 点击show in API Reference interpolation&#xff1a;看起来更加清晰 resou…

测底解决msvcp140.dll丢失难题,总结5种靠谱的解决方法

在尝试运行特定程序或执行一段代码时&#xff0c;系统反馈了一条明确且关键的错误信息&#xff1a;“找不到msvcp140.dll”。这意味着&#xff0c;由于缺失了名为“msvcp140.dll”的动态链接库文件&#xff08;Dynamic Link Library&#xff09;&#xff0c;当前的操作无法按照…

22 多态

目录 多态的概念多态的定义及实现抽象类多态的原理单继承和多继承关系中的虚函数表继承和多态常见的面试问题 前言 需要声明的&#xff0c;下面的代码和解释的哦朴实vs2013x86环境&#xff0c;涉及指针是4bytes&#xff0c;如果要其他平台下&#xff0c;部分代码需要改动。比…

.NET CORE 分布式事务(二) DTM实现TCC

目录 引言&#xff1a; 1. TCC事务模式 2. TCC组成 3. TCC执行流程 3.1 TCC正常执行流程 3.2 TCC失败回滚 4. Confirm/Cancel操作异常 5. TCC 设计原则 5.1 TCC如何做到更好的一致性 5.2 为什么只适合短事务 6. 嵌套的TCC 7. .NET CORE结合DTM实现TCC分布式事务 …

03_制作U盘启动盘

一、准备工作 系统镜像.ISO UltraISO 二、制作U盘启动盘 打开UltraISO 文件-打开&#xff0c;选择已下载的镜像文件。 插入U盘&#xff0c;启动-写入硬盘映像。