前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ucore-lab4

ucore-lab4

作者头像
Heeler-Deer
发布2023-03-10 14:17:10
5050
发布2023-03-10 14:17:10
举报
文章被收录于专栏:HD-学习笔记

知识点

由于本人在操作系统精髓与设计原理中学习过这一部分,故略

练习解答

实验目的

  • 了解内核线程创建/执行的管理过程
  • 了解内核线程的切换和基本调度过程

内核线程是一种特殊的进程,内核线程与用户进程的区别有两个:

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

练习0

填写已有实验

meld合并即可。

kern/mm/vmm.c

练习1

分配并初始化一个进程控制块(需要编码)

alloc_proc函数(位于kern/process/proc.c中)负责分配并返回一个新的struct proc_struct结构,用于存储新建立的内核线程的管理信息。ucore需要对这个结构进行最基本的初始化,你需要完成这个初始化过程。

【提示】在alloc_proc函数的实现中,需要初始化的proc_struct结构中的成员变量至少包括:state/pid/runs/kstack/need_resched/parent/mm/context/tf/cr3/flags/name。

请在实验报告中简要说明你的设计实现过程。请回答如下问题:

  • 请说明proc_struct中struct context contextstruct trapframe *tf成员变量含义和在本实验中的作用是啥?(提示通过看代码和编程调试可以判断出来)

这东西就照着注释写就行,没意思。

代码语言:javascript
复制
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 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
     */
    proc->state = PROC_UNINIT;
        proc->pid = -1;
        proc->runs = 0;
        proc->kstack = 0;
        proc->need_resched = 0;
        proc->parent = NULL;
        proc->mm = NULL;
        memset(&(proc->context), 0, sizeof(struct context));
        proc->tf = NULL;
        proc->cr3 = boot_cr3;
        proc->flags = 0;
        memset(proc->name, 0, PROC_NAME_LEN);
    }
    return proc;
}

不难想到context是上下文的那个context,trapframe应该是陷阱相关代码。

我们看一下proc.c的butterfly图。

在看一下proc.c的内部调用图。

在proc.h中不难找到:

代码语言:javascript
复制
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;
};

容易发现,context中存储的是进程/线程切换上下文所需要的'寄存器',不管对于内核态还是用户态都要用到这几个寄存器.

学习过csapp的人应该对这几个名字都很熟悉了,不再介绍了。

在trap.h中不难找到:

代码语言:javascript
复制
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));

显然,trap是用户态转为内核态时所作的操作,上述代码按照名字猜测,cs/ss/ds以及另外三个都是段寄存器的名称,该结构体应该是定义了从内核态切换到用户态所需要的数据。

我们着重看一下pro.c中的copy_thread函数来理解两者的作用。

代码语言:javascript
复制
static void
copy_thread(struct proc_struct *proc, uintptr_t esp, struct trapframe *tf) {
    proc->tf = (struct trapframe *)(proc->kstack + KSTACKSIZE) - 1;
    *(proc->tf) = *tf;
    proc->tf->tf_regs.reg_eax = 0;
    proc->tf->tf_esp = esp;
    proc->tf->tf_eflags |= FL_IF;

    proc->context.eip = (uintptr_t)forkret;
    proc->context.esp = (uintptr_t)(proc->tf);
}

在内核态切换到用户态的过程中,该函数不仅使用了context切换上下文,还使用了tf去切换到用户态。这两者似乎有重复。

然而实际上,通过context代码可以看出来,所谓的上下文切换,只是单纯的切换了寄存器数据,并没有更改内核态为用户态,仅用context切换数据的进程还处于原先的状态,context里面的eip寄存器,用来存储CPU要读取指令的地址,而esp只有设置为tf,才能执行用户代码。

练习2

练习2:为新创建的内核线程分配资源(需要编码)

创建一个内核线程需要分配和设置好很多资源。kernel_thread函数通过调用do_fork函数完成具体内核线程的创建工作。do_kernel函数会调用alloc_proc函数来分配并初始化一个进程控制块,但alloc_proc只是找到了一小块内存用以记录进程的必要信息,并没有实际分配这些资源。ucore一般通过do_fork实际创建新的内核线程。do_fork的作用是,创建当前内核线程的一个副本,它们的执行上下文、代码、数据都一样,但是存储位置不同。在这个过程中,需要给新内核线程分配资源,并且复制原进程的状态。你需要完成在kern/process/proc.c中的do_fork函数中的处理过程。它的大致执行步骤包括:

  • 调用alloc_proc,首先获得一块用户信息块。
  • 为进程分配一个内核栈。
  • 复制原进程的内存管理信息到新进程(但内核线程不必做此事)
  • 复制原进程上下文到新进程
  • 将新进程添加到进程列表
  • 唤醒新进程
  • 返回新进程号

请在实验报告中简要说明你的设计实现过程。请回答如下问题:

  • 请说明ucore是否做到给每个新fork的线程一个唯一的id?请说明你的分析和理由。

注意该句:

do_fork的作用是,创建当前内核线程的一个副本,它们的执行上下文、代码、数据都一样,但是存储位置不同。

显然,我们需要注意上下文、代码、数据

do_fork:

代码语言:javascript
复制
// 分配一个PCB
if ((proc = alloc_proc()) == NULL)
    goto fork_out;
// 设置子进程的父进程
proc->parent = current;
// 分配内核栈
if (setup_kstack(proc) != 0)
    goto bad_fork_cleanup_proc;
// 复制内存数据--数据
if (copy_mm(clone_flags, proc) != 0)
    goto bad_fork_cleanup_kstack;
// 用刚刚讲过的copy_thread--上下文
copy_thread(proc, stack, tf);
// 将子进程的PCB添加进hash表,让内核认识pcb
bool intr_flag;
local_intr_save(intr_flag);
{
    proc->pid = get_pid();
    hash_proc(proc);
    list_add(&proc_list, &(proc->list_link));
    nr_process ++;
}
local_intr_restore(intr_flag);
// 设置新的子进程可执行
wakeup_proc(proc);
// 返回子进程的pid
ret = proc->pid;

ucore是否做到给每个新fork的线程一个唯一的id?显然,既然需要加入hash表,那么一定可以唯一的标识一个线程,查看get_pid函数可知,

代码语言:javascript
复制
static int
get_pid(void) {
    //确认pid大于进程数量
    static_assert(MAX_PID > MAX_PROCESS);
    struct proc_struct *proc;
    list_entry_t *list = &proc_list, *le;
    //注意是static,储存在静态存储空间而非栈
    static int next_safe = MAX_PID, last_pid = MAX_PID;
    //last_pid大于max_pid的话,设置last_pid=1
    //即跳转到inside,重新设置next_safe
    if (++ last_pid >= MAX_PID) {
        last_pid = 1;
        goto inside;
    }
    //last_pid>next_safe,
    if (last_pid >= next_safe) {
    inside:
        next_safe = MAX_PID;
    repeat:
        le = list;
        //分配唯一的pid
        while ((le = list_next(le)) != list) {
            //在proc_list构建结构体
            proc = le2proc(le, list_link);
            //两者pid相等的话
            if (proc->pid == last_pid) {
                //last_pid>=next_safe|max_pid就重置
                if (++ last_pid >= next_safe) {
                    if (last_pid >= MAX_PID) {
                        last_pid = 1;
                    }
                    next_safe = MAX_PID;
                    goto repeat;
                }
            }
            //不等且
            else if (proc->pid > last_pid && next_safe > proc->pid) {
                //以proc的pid构建next_safe
                next_safe = proc->pid;
            }
        }
    }
    return last_pid;
}

上述代码实际上是构建了一个next_safe,last_pid的区间。通过确认没有进程的id在则这个区间内部,来分配一个合适的pid。

通过移动last_pid(+1,重置时置1),移动proc->pid(已有进程的pid),以及next_safe(等于max_pid,分配成功时置为proc->pid)

结果:

需要注意的是,这个grade.sh有点问题,需要修改kern/mm/kmalloc.c的代码为:

代码语言:javascript
复制
void check_slab(void) {
  cprintf("check_slab() succeeded!\n");
}

才可以100分。应该是设计评分系统时与之前输出语句不同,因此会造成上图的error

练习3

阅读代码,理解 proc_run 函数和它调用的函数如何完成进程切换的。(无编码工作)

请在实验报告中简要说明你对proc_run函数的分析。并回答如下问题:

  • 在本实验的执行过程中,创建且运行了几个内核线程?
  • 语句local_intr_save(intr_flag);....local_intr_restore(intr_flag);在这里有何作用?请说明理由

proc_run:

代码语言:javascript
复制
void proc_run(struct proc_struct *proc) {
    if (proc != current) {
        bool intr_flag;
        struct proc_struct *prev = current, *next = proc;
        local_intr_save(intr_flag);
        {
            // 设置当前执行的进程
            current = proc;
            // 设置ring0的内核栈地址
            load_esp0(next->kstack + KSTACKSIZE);
            // 加载页目录表
            lcr3(next->cr3);
            // 切换上下文
            switch_to(&(prev->context), &(next->context));
        }
        local_intr_restore(intr_flag);
    }
}

butterfly图:

涉及到的函数及注释:

代码语言:javascript
复制
static inline bool
__intr_save(void) {
    //读取flag,关闭中断
    if (read_eflags() & FL_IF) {
        intr_disable();
        return 1;
    }
    return 0;
}

/* *
 * load_esp0 - change the ESP0 in default task state segment,
 * so that we can use different kernel stack when we trap frame
 * user to kernel.
 * */
//加载内核栈基地址
void
load_esp0(uintptr_t esp0) {
    ts.ts_esp0 = esp0;
}

//cr3页寄存器改为页目录表
static inline void
lcr3(uintptr_t cr3) {
    asm volatile ("mov %0, %%cr3" :: "r" (cr3) : "memory");
}

//允许中断
static inline void
__intr_restore(bool flag) {
    if (flag) {
        intr_enable();
    }
}



//kern/process/switch.S
.text
.globl switch_to
switch_to:                      # switch_to(from, to)

    # save from's registers
    movl 4(%esp), %eax          # eax points to from
    popl 0(%eax)                # save eip !popl
    #从这里开始保存寄存器到context
    movl %esp, 4(%eax)
    movl %ebx, 8(%eax)
    movl %ecx, 12(%eax)
    movl %edx, 16(%eax)
    movl %esi, 20(%eax)
    movl %edi, 24(%eax)
    movl %ebp, 28(%eax)

    # restore to's registers
    #从这里开始恢复context
    movl 4(%esp), %eax          # not 8(%esp): popped return address already
                                # eax now points to to
    movl 28(%eax), %ebp
    movl 24(%eax), %edi
    movl 20(%eax), %esi
    movl 16(%eax), %edx
    movl 12(%eax), %ecx
    movl 8(%eax), %ebx
    movl 4(%eax), %esp

    pushl 0(%eax)               # push eip

    ret

  • 在本实验的执行过程中,创建且运行了几个内核线程?

两个内核线程,分别是idleprocinitproc

首先是idleproc内核线程,该线程是最初的内核线程,完成内核中各个子线程的创建以及初始化。之后循环执行调度,执行其他进程。还有一个是initproc内核线程,该线程主要是为了显示实验的完成而打印出字符串"hello world"的内核线程。

语句local_intr_save(intr_flag);....local_intr_restore(intr_flag);在这里有何作用?请说明理由

见上文

challenge

实现支持任意大小的内存分配算法

这不是本实验的内容,其实是上一次实验内存的扩展,但考虑到现在的slab算法比较复杂,有必要实现一个比较简单的任意大小内存分配算法。可参考本实验中的slab如何调用基于页的内存分配算法(注意,不是要你关注slab的具体实现)来实现first-fit/best-fit/worst-fit/buddy等支持任意大小的内存分配算法。

由first-fit更改为best-fit代码参考github

核心代码为:

代码语言:javascript
复制
static void *best_fit_alloc(size_t size, gfp_t gfp, int align)
{
	assert( (size + SLOB_UNIT) < PAGE_SIZE );
	// This best fit allocator does not consider situations where align != 0
    //确认align==0
	assert(align == 0);
    //slob大小
	int units = SLOB_UNITS(size);

	unsigned long flags;
	spin_lock_irqsave(&slob_lock, flags);

	slob_t *prev = slobfree, *cur = slobfree->next;
	int find_available = 0;
	int best_frag_units = 100000;
	slob_t *best_slob = NULL;
	slob_t *best_slob_prev = NULL;
   //循环找到符合条件的单元
	for (; ; prev = cur, cur = cur->next) {
		if (cur->units >= units) {
			// Find available one.
			if (cur->units == units) {
				// If found a perfect one...
				prev->next = cur->next;
				slobfree = prev;
				spin_unlock_irqrestore(&slob_lock, flags);
				// That's it!
				return cur;
			}
			else {
				// This is not a prefect one.
                //如果不能完美的放进去,就更改你的best大小
				if (cur->units - units < best_frag_units) {
					// This seems to be better than previous one.
					best_frag_units = cur->units - units;
					best_slob = cur;
					best_slob_prev = prev;
					find_available = 1;
				}
			}

		}

		// Get to the end of iteration.
        //符合条件就分配
		if (cur == slobfree) {
			if (find_available) {
				// use the found best fit.
				best_slob_prev->next = best_slob + units;
				best_slob_prev->next->units = best_frag_units;
				best_slob_prev->next->next = best_slob->next;
				best_slob->units = units;
				slobfree = best_slob_prev;
				spin_unlock_irqrestore(&slob_lock, flags);
				// That's it!
				return best_slob;
			}
			// Initially, there's no available arena. So get some.
			spin_unlock_irqrestore(&slob_lock, flags);
			if (size == PAGE_SIZE) return 0;

			cur = (slob_t *)__slob_get_free_page(gfp);
			if (!cur) return 0;

			slob_free(cur, PAGE_SIZE);
			spin_lock_irqsave(&slob_lock, flags);
			cur = slobfree;
		}
	}
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022年5月7日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 知识点
  • 练习解答
    • 实验目的
      • 练习0
        • 练习1
          • 练习2
            • 练习3
              • challenge
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档