清华大学操作系统_LAB4

内核线程管理

实验执行流程 概述

lab2和lab3完成了对内存的虚拟化 ,但整个控制留还是一条线串行执行。lab4将在此基础上进行CPU的虚拟化,即让ucore实现分时共享CPU,实现多条控制流能够并发执行。
**从某种程度看,我们可以把控制流看做是一个内核线程。**内核线程是一种特殊的进程,内核线程和用户进程的区别有两个:

  • 内核线程只运行在内核态,而用户进程会在用户态和内核态交替运行
  • 所有内核线程直接使用共同的ucore内核的内存空间,不需要为每个内核线程维护单独的内存空间,而用户进程需要维护各自的用户内存空间

因此,从内存空间占用情况这个角度上看,我们可以把线程看做是一种共享内存空间的轻量级进程
为了实现内核线程,需要设计管理线程的数据结构,包括进程控制块(在这里也可以叫做线程控制块)、进程控制块链表调度器(scheduler)等。
在kern_init函数中,当完成虚拟内存的初始化工作后,就可以调用proc_init函数,这个函数完成了idleproc内核线程initproc内核线程的复制或创建工作。
idleproc内核线程的工作就是不停的查询,看是否有其他内核线程可以执行了。如果有,马上让调度器选择那个内核线程执行。所有idleproc内核线程是在ucore操作系统没有其他内核线程可执行的情况下才会被调用。接着就是调用kernel_thread函数来创建initproc内核线程。initproc内核线程的工作就是显示“Hello World”,表明自己存在且能正常工作了。

设计关键数据结构 – 进程控制块

在lab4中,进程管理信息用struct proc_struct来表示。在kern/process/proc.h中定义如下:

struct proc_struct {
   
    enum proc_state state;                      // Process state
    int pid;                                    // Process ID
    int runs;                                   // the running times of Proces
    uintptr_t kstack;                           // Process kernel stack
    volatile bool need_resched;                 // bool value: need to be rescheduled to release CPU?
    struct proc_struct *parent;                 // the parent process
    struct mm_struct *mm;                       // Process's memory management field
    struct context context;                     // Switch here to run process
    struct trapframe *tf;                       // Trap frame for current interrupt
    uintptr_t cr3;                              // CR3 register: the base addr of Page Directroy Table(PDT)
    uint32_t flags;                             // Process flag
    char name[PROC_NAME_LEN + 1];               // Process name
    list_entry_t list_link;                     // Process link list 
    list_entry_t hash_link;                     // Process hash list
};

其中的几个比较重要的成员变量如下:

  • mm:内存管理的信息,包括内存映射列表、页表指针等。mm成员变量在lab3中用于虚存管理。但在实际OS中,内核线程常驻内存,因此不需要考虑页面换入换出的swap page问题,在lab5中涉及到了用户进程,才考虑进程用户内存空间的swap page问题,mm才会发挥作用。所有在lab4中mm对于内核线程就没有用了,在内核线程的proc_struct的成员变量mm = NULL。mm里有个很重要的项pgdir,记录的是该进程使用的一级页表的物理地址。由于mm = NULL,所以在proc_struct数据结构中需要有一个代替pgdir项来记录页表起始地址,这就是proc_struct数据结构中的cr3成员变量
// the control struct for a set of vma using the same PDT
struct mm_struct {
   
    list_entry_t mmap_list;        // linear list link which sorted by start addr of vma
    struct vma_struct *mmap_cache; // current accessed vma, used for speed purpose
    pde_t *pgdir;                  // the PDT of these vma
    int map_count;                 // the count of these vma
    void *sm_priv;                 // the private data for swap manager
};
  • state:该进程所处的状态。
// process's state in his life cycle
enum proc_state {
   
    PROC_UNINIT = 0,  // uninitialized
    PROC_SLEEPING,    // sleeping
    PROC_RUNNABLE,    // runnable(maybe running)
    PROC_ZOMBIE,      // almost dead, and wait parent proc to reclaim his resource
};
  • parent:指向用户进程的父进程(创建它的进程)的指针。在所有进程中,只有一个进程没有父进程,就是内核创建的第一个内核线程idleproc。内核根据这个父子关系建立一个树形结构,用于维护一些特殊操作,例如确定某个进程是否可以对另外一个进程进行某种操作等等。
  • context:进程的上下文,用于进程切换(参见switch.S)。在ucore中,所有的进程在内核中也是相对独立的(例如独立的内核堆栈以及上下文等)。使用context保存寄存器的目的就在于在内核态中能够进行上下文之间的切换。实际利用context进行上下文切换的函数是在kern/process/switch.S中定义的switch_to。
// Saved registers for kernel context switches.
// Don't need to save all the %fs etc. segment registers,
// because they are constant across kernel contexts.
// Save all the regular registers so we don't need to care
// which are caller save, but not the return register %eax.
// (Not saving %eax just simplifies the switching code.)
// The layout of context must match code in switch.S.
struct context {
   
    uint32_t eip;
    uint32_t esp;
    uint32_t ebx;
    uint32_t ecx;
    uint32_t edx;
    uint32_t esi;
    uint32_t edi;
    uint32_t ebp;
};
  • tf:中断帧的指针,总是指向内核栈的某个位置:当进程从用户控件跳到内核空间时,中断帧记录了进程在被中断前的状态。当内核需要跳回用户空间时,需要调整中断帧以恢复让进程继续执行的各寄存器的值。此外,ucore内核允许嵌套中断。因此为了保证嵌套中断发生时tf总是能指向当前的trapframe,ucore在内核栈上维护了tf的链,可以参考trap.c::trap函数做进一步的了解。
struct trapframe {
   
    struct pushregs tf_regs;
    uint16_t tf_gs;
    uint16_t tf_padding0;
    uint16_t tf_fs;
    uint16_t tf_padding1;
    uint16_t tf_es;
    uint16_t tf_padding2;
    uint16_t tf_ds;
    uint16_t tf_padding3;
    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_padding4;
    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_padding5;
} __attribute__((packed));
  • cr3:cr3保存页表的物理地址,目的就是进程切换的时候方便直接使用lcr3实现页表切换,避免每次都根据mm来计算cr3。mm数据结构是用来实现用户空间的虚存管理的,但是内核线程没有用户空间,它执行的只是内核中的一小段代码(通常是一小段函数),所以它没有mm结构。当某个进程是一个普通用户态进程的时候,PCB中的cr3就是mm中页表(pgdir)的物理地址;而当它是内核线程的时候,cr3等于boot_cr3。而boot_cr3指向了ucore启动时建立好的内核虚拟空间的页目录表首地址。
  • kstack:每个线程都有一个内核栈,并且位于内核地址空间的不同位置。对于内核线程,该栈就是运行时的程序使用的栈;而对于普通进程,该栈是发生了特权级改变的时候,用来保存被打断的硬件信息用的栈。ucore在创建进程时分配了2个连续的物理页(在memlayout.h中KSTACKSIZE的定义)作为内核栈的空间。这个栈很小,所以在内核中的代码应该尽可能的紧凑,并且避免在栈上分配大的数据结构,以免栈溢出,导致系统崩溃。kstack记录了分配给该进程/线程的内核栈的位置。主要作用有以下几点:1、当内核准备从一个进程切换到另一个的时候,需要根据kstack的值正确的设置好tss,以便在进程切换以后再发生中断时能够使用正确的栈;2、内核栈位于内核地址空间,并且是不共享的(每个线程都有自己的栈),因此不受到mm的管理,当进程退出的时候,内核能够根据kstack的值决定快速定位栈的位置并进行回收。
#define KSTACKPAGE          2                           // # of pages in kernel stack
#define KSTACKSIZE          (KSTACKPAGE * PGSIZE)       // sizeof kernel stack

另一方面,为了管理系统中所有的进程控制块,ucore维护了如下全局变量(位于kern/process/proc.c):

  • static struct proc *current:当前占用CPU且处于“运行”状态进程控制块指针。通常这个变量是只读的,只有在进程切换的时候才能进行修改,并且整个切换和修改过程需要保证操作的原子性,目前至少需要屏蔽中断。可以参考switch_to的实现。
  • static struct proc *initproc:本实验中,指向一个内核线程。本实验以后,指向第一个用户态进程。
  • static list_entry_t hash_list[HASH_LIST_SIZE]:所有进程控制块的哈希表,proc_struct中的成员变量hash_link将基于pid链接如这个哈希表中。
  • static list_entry_t proc_list:所有进程控制块的双向线性列表,proc_struct中的成员变量list_link将链接到这个链表中。

创建并执行内核线程

建立进程控制块(proc.c中的alloc_proc函数)后,就可以通过进程控制块来创建具体的进程/线程了。

// alloc_proc - alloc a proc_struct and init all fields of proc_struct
static struct proc_struct *
alloc_proc(void) {
   
    struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
    if (proc != NULL) {
   
    //LAB4:EXERCISE1 YOUR CODE
    /*
     * below fields in proc_struct need to be initialized
     *       enum proc_state state;                      // Process state
     *       int pid;                                    // Process ID
     *       int runs;                                   // the running times of Proces
     *       uintptr_t kstack;                           // Process kernel st
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
清华大学是中国著名的高等学府,提供了丰富多样的学习资源和工具支持。其中,清华大学提供了一个Ubuntu操作系统的镜像,方便学生和教职员工使用。 Ubuntu是一种基于Linux的操作系统,它具有开源、稳定和安全的特点。清华大学提供的Ubuntu镜像是一个经过定制和优化的版本,旨在满足清华大学用户的需求。 首先,清华大学提供的Ubuntu操作系统镜像具有高度的兼容性。它支持各种硬件设备和软件应用程序,使用户可以灵活地进行开发、研究和学习。 其次,清华大学的Ubuntu镜像也是一个社区支持的操作系统。用户可以通过清华大学提供的官方网站和社区论坛获取相关的技术支持和解决方案。这为用户提供了一个良好的学习和交流平台。 此外,清华大学的Ubuntu操作系统镜像还提供了丰富的应用程序和工具。这些应用程序包括文档处理、图形设计、代码编辑和编译等,可以满足用户各种不同的需求。 最后,清华大学的Ubuntu操作系统镜像还拥有强大的安全性。它支持安全的网络连接和数据交换,可以有效地保护用户的隐私和信息安全。 总之,清华大学提供的Ubuntu操作系统镜像是一个功能强大、稳定可靠的操作系统,为用户提供了便捷的学习和研究环境。同时,它也是一个社区支持的操作系统,可以获得丰富的技术支持和应用程序。通过使用清华大学的Ubuntu镜像,用户能够更好地利用操作系统的各种功能,提高学习和工作效率。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值