\documentclass[a4paper]{ctexart}
\input{mypreamble}
\renewcommand{\mychapternum}{4}
\renewcommand{\mylabname}{内核线程管理}
\renewcommand{\mydate}{2024年5月17日}

\begin{document}
    \mytitle
    \begin{enumerate}
        \item{\textbf{研读理解相关代码后，回答以下问题：}}
        \begin{enumerate}
            \questionandanswer[]{
                请简单说明\mintinline{C}{proc_struct}中各个成员变量含义以及作用。它与理论课中讲述的PCB结构有何不同之处？
            }{}
            {\kaishu
            \setlength{\parskip}{2em}
                \begin{minted}[]{C}
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
};
                \end{minted}

                \mintinline{C}{state}是进程的状态，\mintinline{C}{pid}是进程的ID，\mintinline{C}{runs}是进程的被调度到的次数，\mintinline{C}{kstack}是进程的堆栈，\mintinline{C}{need_resched}用于非抢占式调度，表示进程是否运行结束可以把CPU重新调度给其他进程，\mintinline{C}{parent}是进程的父进程，\mintinline{C}{mm}是进程的内存管理的部分的指针，\mintinline{C}{context}是进程的上下文，即
                \begin{minted}{C}
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;
};
                \end{minted}
                \mintinline{C}{tf}是进程的中断帧，记录了中断的相关信息，\mintinline{C}{cr3}是进程的页目录表的指针，\mintinline{C}{flags}是进程的各种标志位，\mintinline{C}{name}是进程的名称，\mintinline{C}{list_link}是所有进程的链表表项，\mintinline{C}{hash_link}是具有同一个哈希值的进程的链表表项。

                它与理论课中讲述的PCB结构的不同之处在于没有优先级、进程组、进程运行时间、进程使用的CPU时间、进程的子进程的CPU时间、文件管理（因为还没实现文件系统？）。
            }
            \questionandanswer[]{
                试简单分析 uCore中 内核线程的创建过程。（要求说明处理流程，相关的主要函数、它们的功能以及调用关系。）
            }{}
            {\kaishu
            \setlength{\parskip}{2em}
                \begin{center}
                    \includegraphics[width=1\linewidth]{imgs/2024-05-17-19-18-00.png}
                \end{center}
                \begin{minted}{C}
// proc_init - set up the first kernel thread idleproc "idle" by itself and 
//           - create the second kernel thread init_main
void
proc_init(void) {
    int i;

    list_init(&proc_list);
    for (i = 0; i < HASH_LIST_SIZE; i ++) {
        list_init(hash_list + i);
    }

    if ((idleproc = alloc_proc()) == NULL) {
        panic("cannot alloc idleproc.\n");
    }

    idleproc->pid = 0;
    idleproc->state = PROC_RUNNABLE;
    idleproc->kstack = (uintptr_t)bootstack;
    idleproc->need_resched = 1;
    set_proc_name(idleproc, "idle");
    nr_process ++;

    current = idleproc;

    int pid = kernel_thread(init_main, "Hello world!!", 0);
    if (pid <= 0) {
        panic("create init_main failed.\n");
    }

    initproc = find_proc(pid);
    set_proc_name(initproc, "init");

    assert(idleproc != NULL && idleproc->pid == 0);
    assert(initproc != NULL && initproc->pid == 1);
}
                \end{minted}

                主要的内核线程创建过程在\mintinline{C}{proc_init}中，首先调用\mintinline{C}{list_init}初始化全部进程的链表和相同哈希的进程列表，然后调用 \mintinline{C}{alloc_proc}给当前进程分配一个进程控制块，并且设置相关属性，其中调用\mintinline{C}{set_proc_name}设置当前进程的名称为\mintinline{C}{"idle"}，之后调用\mintinline{C}{kernel_thread}创建一个\mintinline{C}{init_main}进程，并调用\mintinline{C}{find_proc}，再调用\mintinline{C}{set_proc_name}将其名称设置为\mintinline{C}{init}。

                \begin{minted}{C}
// kernel_thread - create a kernel thread using "fn" function
// NOTE: the contents of temp trapframe tf will be copied to 
//       proc->tf in do_fork-->copy_thread function
int
kernel_thread(int (*fn)(void *), void *arg, uint32_t clone_flags) {
    struct trapframe tf;
    memset(&tf, 0, sizeof(struct trapframe));
    tf.tf_cs = KERNEL_CS;
    tf.tf_ds = tf.tf_es = tf.tf_ss = KERNEL_DS;
    tf.tf_regs.reg_ebx = (uint32_t)fn;
    tf.tf_regs.reg_edx = (uint32_t)arg;
    tf.tf_eip = (uint32_t)kernel_thread_entry;
    return do_fork(clone_flags | CLONE_VM, 0, &tf);
}
                \end{minted}

                \mintinline{C}{kernel_thread}的功能是通过调用一个函数创建一个新的线程，由于新的线程是由当前线程创建的，这里就调用了\mintinline{C}{do_fork}函数。

                \begin{minted}{C}
/* do_fork -     parent process for a new child process
 * @clone_flags: used to guide how to clone the child process
 * @stack:       the parent's user stack pointer. if stack==0, It means to fork a kernel thread.
 * @tf:          the trapframe info, which will be copied to child process's proc->tf
 */
int
do_fork(uint32_t clone_flags, uintptr_t stack, struct trapframe *tf) {
    int ret = -E_NO_FREE_PROC;
    struct proc_struct *proc;
    if (nr_process >= MAX_PROCESS) {
        goto fork_out;
    }
    ret = -E_NO_MEM;
    //LAB4:EXERCISE2 YOUR CODE
    /*
     * Some Useful MACROs, Functions and DEFINEs, you can use them in below implementation.
     * MACROs or Functions:
     *   alloc_proc:   create a proc struct and init fields (lab4:exercise1)
     *   setup_kstack: alloc pages with size KSTACKPAGE as process kernel stack
     *   copy_mm:      process "proc" duplicate OR share process "current"'s mm according clone_flags
     *                 if clone_flags & CLONE_VM, then "share" ; else "duplicate"
     *   copy_thread:  setup the trapframe on the  process's kernel stack top and
     *                 setup the kernel entry point and stack of process
     *   hash_proc:    add proc into proc hash_list
     *   get_pid:      alloc a unique pid for process
     *   wakeup_proc:  set proc->state = PROC_RUNNABLE
     * VARIABLES:
     *   proc_list:    the process set's list
     *   nr_process:   the number of process set
     */

    //    1. call alloc_proc to allocate a proc_struct
    //    2. call setup_kstack to allocate a kernel stack for child process
    //    3. call copy_mm to dup OR share mm according clone_flag
    //    4. call copy_thread to setup tf & context in proc_struct
    //    5. insert proc_struct into hash_list && proc_list
    //    6. call wakeup_proc to make the new child process RUNNABLE
    //    7. set ret vaule using child proc's pid
    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(proc, stack, tf);

    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);

    ret = proc->pid;
fork_out:
    return ret;

bad_fork_cleanup_kstack:
    put_kstack(proc);
bad_fork_cleanup_proc:
    kfree(proc);
    goto fork_out;
}
                \end{minted}

                \mintinline{C}{do_fork}函数中，先调用\mintinline{C}{alloc_proc}分配一个进程控制块，再调用\mintinline{C}{setup_kstack}分配一个内核堆栈，再调用\mintinline{C}{copy_mm}将父进程的内存空间复制或者共享给子进程，再调用\mintinline{C}{copy_thread}设置子进程的中断处理，并设置上下文的变量。之后关中断，将新的进程控制块插入到全部进程链表与相同哈希链表中，再开中断。然后把子进程设置成状态为\mintinline{C}{RUNNABLE}，返回子进程的\mintinline{C}{pid}。
            }
            \questionandanswer[]{
                试简单分析 uCore中 内核线程的切换过程。（要求说明处理流程，相关的主要函数、它们的功能以及调用关系。）
            }{}
            {\kaishu
            \setlength{\parskip}{2em}
                切换线程主要使用的是\mintinline{C}{schedule}函数。

                \begin{minted}{C}
void
schedule(void) {
    bool intr_flag;
    list_entry_t *le, *last;
    struct proc_struct *next = NULL;
    local_intr_save(intr_flag);
    {
        current->need_resched = 0;
        last = (current == idleproc) ? &proc_list : &(current->list_link);
        le = last;
        do {
            if ((le = list_next(le)) != &proc_list) {
                next = le2proc(le, list_link);
                if (next->state == PROC_RUNNABLE) {
                    break;
                }
            }
        } while (le != last);
        if (next == NULL || next->state != PROC_RUNNABLE) {
            next = idleproc;
        }
        next->runs ++;
        if (next != current) {
            proc_run(next);
        }
    }
    local_intr_restore(intr_flag);
}
                \end{minted}

                在\mintinline{C}{schedule}中，我们要找到下一次调度的线程，即找到状态为\mintinline{C}{RUNNABLE}的线程，如果当前的线程为\mintinline{C}{idleproc}即CPU空闲的线程则从线进程链表头开始找（因为\mintinline{C}{idleproc}线程不在线程链表中），否则从当前线程的下一个线程开始找。找到需要调度的\mintinline{C}{RUNNABLE}线程后，如果新的线程和老的线程不是同一个线程，则需要调用\mintinline{C}{proc_run}进行切换。

                \begin{minted}{C}
// proc_run - make process "proc" running on cpu
// NOTE: before call switch_to, should load  base addr of "proc"'s new PDT
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;
            load_esp0(next->kstack + KSTACKSIZE);
            lcr3(next->cr3);
            switch_to(&(prev->context), &(next->context));
        }
        local_intr_restore(intr_flag);
    }
}
                \end{minted}

                在\mintinline{C}{proc_run}中，调用\mintinline{C}{load_esp0}设置任务状态段，调用\mintinline{C}{lcr3}加载新的进程的页表，之后调用\mintinline{C}{switch_to}保存老的进程的上下文，恢复新的进程的上下文。

                \begin{minted}{asm}
switch_to:                      # switch_to(from, to)

    # save from's registers
    movl 4(%esp), %eax          # eax points to from
    popl 0(%eax)                # save eip !popl
    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
    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
                \end{minted}

                在\mintinline{C}{switch_to}中，从\mintinline{C}{esp+4}的位置加载\mintinline{C}{eax}指针（即\mintinline{C}{&(prev->context)}），并把老的线程中当前的寄存器中的值都放到这个指针所在的\mintinline{C}{context}结构体中，再从\mintinline{C}{esp+8}的位置加载\mintinline{C}{eax}指针（即\mintinline{C}{&(next->context)}），并从这个指针的位置加载新的线程中的上下文寄存器的值，其中\mintinline{C}{eip}需要用\mintinline{C}{popl}和\mintinline{C}{pushl}来设置，不能使用\mintinline{C}{movl}来设置。
            }
        \end{enumerate}
    \end{enumerate}
\end{document}