结合代码再看进程管理(上)

一、基本概念

1. 线程与进程

进程拥有一个存放程序和数据的的虚拟地址空间以及其他资源。一个进程基于程序的指令流执行,其执行过程可能与其它进程的执行过程交替进行。因此,一个具有执行状态(运行态、就绪态等)的进程是一个被操作系统分配资源(比如分配内存)并调度(比如分时使用CPU)的单位。在大多数操作系统中,这两个特点(拥有资源,交替执行)是进程的主要本质特征。但这两个特征相对独立,操作系统可以把这两个特征分别进行管理。

对属于同一进程的所有线程而言,这些线程共享进程的虚拟地址空间和其他资源,但每个线程都有一个独立的栈,还有独立的线程运行上下文,用于包含表示线程执行现场的寄存器值等信息。可以把拥有资源所有权的单位通常仍称作进程,对资源的管理成为进程管理;把指令执行流的单位称为线程,对线程的管理就是线程调度和线程分派。

在多线程环境中,进程被定义成资源分配与保护的单位,与进程相关联的信息主要有存放进程映像的虚拟地址空间等,在创建进程时必须要完成进程映像的初始化。在一个进程中,可能有一个或多个线程,每个线程有线程执行状态(运行、就绪、等待等),保存上次运行时的线程上下文、线程的执行栈等。考虑到CPU有不同的特权模式,参照进程的分类,线程又可进一步细化为用户线程和内核线程。

在Linux中,CPU只关心一个一个的独立执行流,无论进程内部只有一个执行流还是有多个执行流,CPU都是以task_struct为单位进行调度的;
Linux下并不存在真正的多线程,而是用进程模拟的。如果要支持真的线程(TCB)会提高操作系统的复杂程度。而线程的和进程的控制块基本是类似实现的,因此Linux直接复用了进程控制块,所以Linux中的所有执行流都叫做轻量级进程;
在Linux中都没有真正意义的线程,所以也就没有真正意义上的线程相关的系统调用,但是Linux提供了轻量级进程相关的库和接口,例如vfork函数和原生线程库pthread。

2. 进程的状态与转换

进程在其生命周期内,由于系统中各进程之间的相互制约关系及系统的运行环境的变化,使得进程的状态也在不断地发生变化(一个进程会经历若干不同状态)。通常进程有以下5 种状态,前 3 种是进程的基本状态。

  • 创建态。 进程正在被创建,尚未转到就绪态。创建进程通常需要多个步骤:
    (1)首先申请一个空白的 PCB,并向 PCB 中填写一些控制和管理进程的信息;
    (2)然后由系统为该进程分配运行时所必需的资源;
    (3)最后把该进程转入就绪态。
  • 就绪态。 进程获得了除处理机外的一切所需资源,一旦得到处理机,便可立即运行。系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列。
  • 运行态。 进程正在处理机上运行。在单处理机环境下,每个时刻最多只有一个进程处于运行态。
  • 阻塞态,又称等待态。 进程正在等待某一事件而暂停运行,如等待某资源为可用(不包括处理机)或等待输入/输出完成。即使处理机空闲,该进程也不能运行。
  • 结束态。 进程正从系统中消失,可能是进程正常结束或其他原因中断退出运行。进程需要结束运行时,系统首先必须将该进程置为结束态,然后进一步处理资源释放和回收等工作。

注意区别就绪态和等待态: 就绪态是指进程仅缺少处理机,只要获得处理机资源就立即运行;而等待态是指进程需要其他资源(除了处理机)或等待某一事件。之所以把处理机和其他资源划分开,是因为在分时系统的时间片轮转机制中,每个进程分到的时间片是若干毫秒。也就是说,进程得到处理机的时间很短且非常频繁,进程在运行过程中实际上是频繁地转换到就绪态的;而其他资源(如外设)的使用和分配或某一事件的发生(如I/O 操作的完成)对应的时间相对来说很长,进程转换到等待态的次数也相对较少。这样来看,就结悉和等待态是进程生命周期中两个完全不同的状态,显然需要加以区分。

下图说明了 5 种进程状态的转换,而 3 种基本状态之间的转换如下:

  • 就绪态→运行态: 处于就绪态的进程被调度后,获得处理机资源(分派处理机时间片),于是进程由就绪态转换为运行态。
  • 运行态→就绪态: 处于运行态的进程在时间片用完后,不得不让出处理机,从而进程由运行态转换为就绪态。此外,在可剥夺的操作系统中,当有更高优先级的进程就绪时,调度程序将正在执行的进行转换为就绪态,让更高优先级的进程执行。
  • 阻塞态(等待态)→就绪态: 进程等待的事件到来时,如I/O操作结束或中断结束时,中断处理程序必须把相应的进程的状态由阻塞态转化为就绪态。

在这里插入图片描述

因此,一个具有执行状态(运行态、就绪态等)的进程是一个被操作系统分配资源(比如分配内存)调度(比如分时使用CPU) 的单位。在大多数操作系统中,这两个特点是进程的主要本质特征。这样可以把拥有资源所有权的单位通常仍称作进程,对资源的管理成为进程管理;把指令执行流的单位称为线程,对线程的管理就是线程调度和线程分派。对属于同一进程的所有线程而言,这些线程共享进程的虚拟地址空间和其他资源,但每个线程都有一个独立的栈,还有独立的线程运行上下文,用于包含表示线程执行现场的寄存器值等信息

3. 进程的控制

进程控制的主要功能是对系统中的所有进程实施有效的管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能。在操作系统中,一般把进程控制用的程序段称为原语,原语的特点是执行期间不允许中断,它是一个不可分割的基本单位。

3.1 进程的创建

允许一个进程创建另一个进程。此时创建者称为父进程,被创建的进程称为子进程可以继承父进程所拥有的资源。 当子进程被撤销时,应将其从父进程那里获得的资源全部还给父进程。此外,在撒销父进程时,必须同时撤销其所有的子进程。在操作系统中,终端用户登录系统、作业调度、系统提供服务、用户程序的应用请求等都会引起进程的创建。操作系统创建一个新进程的过程如下 (创建原语):

  • 为新进程分配一个唯一的进程标识号,并申请一个空白的 PCB(PCB 是有限的)。若PCB申请失败,则创建失败(应当在程序编写中考虑到这个问题)。
  • 为进程分配资源。为新进程的程序和数据及用户栈分配必要的内存空间(在 PCB 中体现)。注意,若资源不足(如内存空间),则并不是创建失败,而是处于阻塞态,等待内存资源。
  • 初始化 PCB,主要包括初始化标志信息、初始化处理机状态信息和初始化处理机控制信息,以及设置进程的优先级等。
  • 若进程就绪队列能够接纳新进程,则将新进程插入就绪队列,等待被调度运行。

3.2 进程的终止

引起进程终止的事件主要有:
①正常结束,表示进程的任务已完成并准备退出运行。
②异常结束,表示进程在运行时,发生了某种异常事件,使程序无法继续运行,如存储区越界、保护错、非法指令、特权指令错、运行超时、算术运算错、I/O 故障等。
③外界干预,指进程应外界的请求而终止运行,如操作员或操作系统干预、父进程请求和父进程终止。
操作系统终止进程的过程如下 (撤销原语):

  • 根据被终止进程的标识符,检索 PCB,从中读出该进程的状态。
  • 若被终止进程处于执行状态,立即终止该进程的执行,将处理机资源分配给其他进程。
  • 若该进程还有子孙进程,则应将其所有子孙进程终止。
  • 将该进程所拥有的全部资源,或归还给其父进程,或归还给操作系统。
  • 将该 PCB 从所在队列(链表)中删除。

3.3 进程的阻塞和唤醒

正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成,新数据尚未到达或无新工作可做等,由系统自动执行阻塞原语(Block),使自己由运行态变为阻塞态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞态。阻塞原语的执行过程如下:

  • 找到将要被阻塞进程的标识号对应的 PCB。
  • 若该进程为运行态,则保护其现场将其状态转为阻塞态,停止运行。
  • 把该 PCB 插入相应事件的等待队列,将处理机资源调度给其他就绪进程。

当被阻塞进程所期待的事件出现时,如它所启动的I/O 操作已完成或其所期待的数据已到达,由有关进程(比如,释放该 I/O设备的进程,或提供数据的进程)调用唤醒原语(Wakeup),将等待该事件的进程唤醒。唤醒原语的执行过程如下:

  • 在该事件的等待队列中找到相应进程的 PCB。
  • 将其从等待队列中移出,并置其状态为就绪态。
  • 把该 PCB 插入就绪队列,等待调度程序调度。

需要注意的是,Block 原语和 Wakeup 原语是一对作用刚好相反的原语,必须成对使用。Block原语是由被阻塞进程自我调用实现的,而 Wakeup 原语则是由一个与被唤醒进程合作或被其他相关的进程调用实现的。

3.4 进程的切换

对于通常的进程而言,其创建、撤销及要求由系统设备完成的 I/O操作,都是利用系统调用而进入内核,再由内核中的相应处理程序子以完成的。进程切换同样是在内核的支持下实现的,因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。
进程切换是指处理机从一个进程的运行转到另一个进程上运行,在这个过程中,进程的运行环境产生了实质性的变化。进程切换的过程如下:

  • 保存处理机上下文,包括程序计数器和其他寄存器。
  • 更新 PCB 信息。
  • 把进程的 PCB 移入相应的队列,如就绪、在某事件阻塞等队列。
  • 选择另一个进程执行,并更新其 PCB。
  • 更新内存管理的数据结构。
  • 恢复处理机上下文。

注意,进程切换与处理机模式切换是不同的,模式切换时,处理机逻辑上可能还在同一进程中运行。若进程因中断或异常进入核心态运行,执行完后又回到用户态刚被中断的程序运行,则操作系统只需恢复进程进入内核时所保存的 CPU 现场,而无须改变当前进程的环境信息。但若要切换进程,当前运行进程改变了,则当前进程的环境信息也需要改变。

注意:“调度”和“切换”的区别。调度是道决定资源分配给哪个进程的行为,是一种决策行为;切换是指实际分配的行为,是执行行为。一般来说,先有资源的调度,然后才有进程的切换。

二、代码实现

1. 内核线程与用户进程

内核线程是一种特殊的进程,内核线程与用户进程的区别有两个:内核线程只运行在内核态而用户进程会在在用户态和内核态交替运行;所有内核线程直接使用共同的内核内存空间,不需为每个内核线程维护单独的内存空间而用户进程需要维护各自的用户内存空间。从内存空间占用情况这个角度上看,我们可以把线程看作是一种共享内存空间的轻量级进程。

在Linux系统中,关于进程的创建我们知道fork()是生成一个子进程,并且其完全继承了父进程的的信息,父进程中的fork()函数返回子进程的PID,而子进程的返回PID为0,也就是说fork()函数调用一次返回两次。而如果我们想在子进程中运行我们的代码,而不是父进程的,需要使用exec()函数装载我们的代码。这两个函数究竟做了什么,通过源代码来分析一下。

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;

    //1. call alloc_proc to allocate a proc_struct
    if ((proc = alloc_proc()) == NULL) {
        goto fork_out;
    }

    proc->parent = current;
    assert(current->wait_state == 0);
	//2. call setup_kstack to allocate a kernel stack for child process
    if (setup_kstack(proc) != 0) {
        goto bad_fork_cleanup_proc;
    }
    //3. call copy_mm to dup OR share mm according clone_flag
    if (copy_mm(clone_flags, proc) != 0) {
        goto bad_fork_cleanup_kstack;
    }
    //4. call copy_thread to setup tf & context in proc_struct
    copy_thread(proc, stack, tf);

    bool intr_flag;
    //5. insert proc_struct into hash_list && proc_list
    local_intr_save(intr_flag);
    {
        proc->pid = get_pid();
        hash_proc(proc);
        set_links(proc);
    }
    local_intr_restore(intr_flag);
	//6. call wakup_proc to make the new child process RUNNABLE
    wakeup_proc(proc);
	//7. set ret vaule using child proc's pid
    ret = proc->pid;
fork_out:
    return ret;

bad_fork_cleanup_kstack:
    put_kstack(proc);
bad_fork_cleanup_proc:
    kfree(proc);
    goto fork_out;
}

fork()是生成一个子进程,并且其继承父进程的信息是通过copy_thread()函数完成的,来看看此函数做了什么。可以看到子进程复制了父进程中断栈的内容并且初始化了自己上下文的内容。

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; //此空间中拷贝在kernel_thread函数建立的临时中断帧的初始值
    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);
}

2. 进程的管理

在进程管理方面,主要涉及进程控制块中与内存管理相关的部分,包括建立进程的页表和维护进程可访问空间(可能还没有建立虚实映射关系)的信息;加载一个ELF格式的程序到进程控制块管理的内存中的方法; 在进程复制(fork)过程中,把父进程的内存空间拷贝到子进程内存空间的技术。另外一部分与用户态进程生命周期管理相关,包括让进程放弃CPU而睡眠等待某事件;让父进程等待子进程结束;一个进程杀死另一个进程;给进程发消息;建立进程的血缘关系链表。

2.1 进程的创建

// do_execve - call exit_mmap(mm)&pug_pgdir(mm) to reclaim memory space of current process
//           - call load_icode to setup new memory space accroding binary prog.
int
do_execve(const char *name, size_t len, unsigned char *binary, size_t size) {
    struct mm_struct *mm = current->mm;
    if (!user_mem_check(mm, (uintptr_t)name, len, 0)) {
        return -E_INVAL;
    }
    if (len > PROC_NAME_LEN) {
        len = PROC_NAME_LEN;
    }

    char local_name[PROC_NAME_LEN + 1];
    memset(local_name, 0, sizeof(local_name));
    memcpy(local_name, name, len);

    if (mm != NULL) {
        lcr3(boot_cr3);
        if (mm_count_dec(mm) == 0) {
            exit_mmap(mm);
            put_pgdir(mm);
            mm_destroy(mm);
        }
        current->mm = NULL;
    }
    int ret;
    if ((ret = load_icode(binary, size)) != 0) {
        goto execve_exit;
    }
    set_proc_name(current, local_name);
    return 0;

execve_exit:
    do_exit(ret);
    panic("already exit: %e.\n", ret);
}

加载应用程序执行码到当前进程的新创建的用户态虚拟空间中。这里涉及到读ELF格式的文件,申请内存空间,建立用户态虚存空间,加载应用程序执行码等。load_icode()函数进行了这些操作

/* load_icode - load the content of binary program(ELF format) as the new content of current process
 * @binary:  the memory addr of the content of binary program
 * @size:  the size of the content of binary program
 */
static int
load_icode(unsigned char *binary, size_t size) {
    if (current->mm != NULL) {
        panic("load_icode: current->mm must be empty.\n");
    }

    int ret = -E_NO_MEM;
    struct mm_struct *mm;
    /*
     * (1) create a new mm for current process
     * 调用mm_create函数来申请进程的内存管理数据结构mm所需内存空间,并对mm进行初始化;
     */
    if ((mm = mm_create()) == NULL) {
        goto bad_mm;
    }
    /*
     * (2) create a new PDT, and mm->pgdir= kernel virtual addr of PDT
     * 调用setup_pgdir来申请一个页目录表所需的一个页大小的内存空间,并把描述ucore内核虚空间映射的内核页表(boot_pgdir所指)
     * 的内容拷贝到此新目录表中,最后让mm->pgdir指向此页目录表,这就是进程新的页目录表了,且能够正确映射内核虚空间;
     */
    if (setup_pgdir(mm) != 0) {
        goto bad_pgdir_cleanup_mm;
    }
    /*
     * (3) copy TEXT/DATA section, build BSS parts in binary to memory space of process
     * 根据应用程序执行码的起始位置来解析此ELF格式的执行程序,并调用mm_map函数根据ELF格式的执行程序说明的各个段
     * (代码段、数据段、BSS段等)的起始位置和大小建立对应的vma结构,并把vma插入到mm结构中,
     * 从而表明了用户进程的合法用户态虚拟地址空间;
     */
    struct Page *page;
    /*(3.1) get the file header of the bianry program (ELF format)*/
    struct elfhdr *elf = (struct elfhdr *)binary;
    /*(3.2) get the entry of the program section headers of the bianry program (ELF format)*/
    struct proghdr *ph = (struct proghdr *)(binary + elf->e_phoff);
    /*(3.3) This program is valid?*/
    if (elf->e_magic != ELF_MAGIC) {
        ret = -E_INVAL_ELF;
        goto bad_elf_cleanup_pgdir;
    }

    uint32_t vm_flags, perm;
    struct proghdr *ph_end = ph + elf->e_phnum;
    for (; ph < ph_end; ph ++) {
    /*(3.4) find every program section headers*/
        if (ph->p_type != ELF_PT_LOAD) {
            continue ;
        }
        if (ph->p_filesz > ph->p_memsz) {
            ret = -E_INVAL_ELF;
            goto bad_cleanup_mmap;
        }
        if (ph->p_filesz == 0) {
            continue ;
        }
    /*
     * (3.5) call mm_map fun to setup the new vma ( ph->p_va, ph->p_memsz)
     * 调用根据执行程序各个段的大小分配物理内存空间,并根据执行程序各个段的起始位置确定虚拟地址,
     * 并在页表中建立好物理地址和虚拟地址的映射关系,然后把执行程序各个段的内容拷贝到相应的内核虚拟地址中,
     * 至此应用程序执行码和数据已经根据编译时设定地址放置到虚拟内存中了;
     */
        vm_flags = 0, perm = PTE_U; //内核指定PTE_U特权级
        if (ph->p_flags & ELF_PF_X) vm_flags |= VM_EXEC;
        if (ph->p_flags & ELF_PF_W) vm_flags |= VM_WRITE;
        if (ph->p_flags & ELF_PF_R) vm_flags |= VM_READ;
        if (vm_flags & VM_WRITE) perm |= PTE_W;
        if ((ret = mm_map(mm, ph->p_va, ph->p_memsz, vm_flags, NULL)) != 0) {
            goto bad_cleanup_mmap;
        }
        unsigned char *from = binary + ph->p_offset;
        size_t off, size;
        uintptr_t start = ph->p_va, end, la = ROUNDDOWN(start, PGSIZE);

        ret = -E_NO_MEM;

     /*(3.6) alloc memory, and  copy the contents of every program section (from, from+end) to process's memory (la, la+end)*/
        end = ph->p_va + ph->p_filesz;
     /*(3.6.1) copy TEXT/DATA section of bianry program*/
        while (start < end) {
            if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
                goto bad_cleanup_mmap;
            }
            off = start - la, size = PGSIZE - off, la += PGSIZE;
            if (end < la) {
                size -= la - end;
            }
            memcpy(page2kva(page) + off, from, size);
            start += size, from += size;
        }

      /*(3.6.2) build BSS section of binary program*/
        end = ph->p_va + ph->p_memsz;
        if (start < la) {
            /* ph->p_memsz == ph->p_filesz */
            if (start == end) {
                continue ;
            }
            off = start + PGSIZE - la, size = PGSIZE - off;
            if (end < la) {
                size -= la - end;
            }
            memset(page2kva(page) + off, 0, size);
            start += size;
            assert((end < la && start == end) || (end >= la && start == la));
        }
        while (start < end) {
            if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
                goto bad_cleanup_mmap;
            }
            off = start - la, size = PGSIZE - off, la += PGSIZE;
            if (end < la) {
                size -= la - end;
            }
            memset(page2kva(page) + off, 0, size);
            start += size;
        }
    }
    /*
     * (4) build user stack memory
     * 需要给用户进程设置用户栈,为此调用mm_mmap函数建立用户栈的vma结构,明确用户栈的位置在用户虚空间的顶端,
     * 大小为256个页,即1MB,并分配一定数量的物理内存且建立好栈的虚地址<-->物理地址映射关系;
     */
    vm_flags = VM_READ | VM_WRITE | VM_STACK;
    if ((ret = mm_map(mm, USTACKTOP - USTACKSIZE, USTACKSIZE, vm_flags, NULL)) != 0) {
        goto bad_cleanup_mmap;
    }
    assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-PGSIZE , PTE_USER) != NULL);
    assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-2*PGSIZE , PTE_USER) != NULL);
    assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-3*PGSIZE , PTE_USER) != NULL);
    assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-4*PGSIZE , PTE_USER) != NULL);
    
    /*
     * (5) set current process's mm, sr3, and set CR3 reg = physical addr of Page Directory
     * 至此,进程内的内存管理vma和mm数据结构已经建立完成,于是把mm->pgdir赋值到cr3寄存器中,
     * 即更新了用户进程的虚拟内存空间,此时的initproc已经被hello的代码和数据覆盖,成为了第一个用户进程,
     * 但此时这个用户进程的执行现场还没建立好;
     */
    mm_count_inc(mm);
    current->mm = mm;
    current->cr3 = PADDR(mm->pgdir);
    lcr3(PADDR(mm->pgdir));

    /*
     * (6) setup trapframe for user environment
     * 先清空进程的中断帧,再重新设置进程的中断帧,使得在执行中断返回指令“iret”后,能够让CPU转到用户态特权级,
     * 并回到用户态内存空间,使用用户态的代码段、数据段和堆栈,且能够跳转到用户进程的第一条指令执行,
     * 并确保在用户态能够响应中断;
     */
    struct trapframe *tf = current->tf;
    memset(tf, 0, sizeof(struct trapframe));
    /* LAB5:EXERCISE1 YOUR CODE
     * should set tf_cs,tf_ds,tf_es,tf_ss,tf_esp,tf_eip,tf_eflags
     * NOTICE: If we set trapframe correctly, then the user level process can return to USER MODE from kernel. So
     *          tf_cs should be USER_CS segment (see memlayout.h)
     *          tf_ds=tf_es=tf_ss should be USER_DS segment
     *          tf_esp should be the top addr of user stack (USTACKTOP)
     *          tf_eip should be the entry point of this binary program (elf->e_entry)
     *          tf_eflags should be set to enable computer to produce Interrupt
     */
    tf->tf_cs = USER_CS;
    tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS;
    tf->tf_esp = USTACKTOP;
    tf->tf_eip = elf->e_entry;
    tf->tf_eflags = FL_IF;
    ret = 0;
out:
    return ret;
bad_cleanup_mmap:
    exit_mmap(mm);
bad_elf_cleanup_pgdir:
    put_pgdir(mm);
bad_pgdir_cleanup_mm:
    mm_destroy(mm);
bad_mm:
    goto out;
}

我们知道程序头记录着各个段的信息,建立虚拟地址空间时,需要将这些信息反映到mm的数据结构中,其内容的填写主要是通过mm_map()函数完成的。

int
mm_map(struct mm_struct *mm, uintptr_t addr, size_t len, uint32_t vm_flags,struct vma_struct **vma_store) {
    uintptr_t start = ROUNDDOWN(addr, PGSIZE), end = ROUNDUP(addr + len, PGSIZE);
    if (!USER_ACCESS(start, end)) {
        return -E_INVAL;
    }

    assert(mm != NULL);

    int ret = -E_INVAL;

    struct vma_struct *vma;
    if ((vma = find_vma(mm, start)) != NULL && end > vma->vm_start) {
        goto out;
    }
    ret = -E_NO_MEM;

    if ((vma = vma_create(start, end, vm_flags)) == NULL) {
        goto out;
    }
    insert_vma_struct(mm, vma);
    if (vma_store != NULL) {
        *vma_store = vma;
    }
    ret = 0;

out:
    return ret;
}

当用户进程的初始化完成后,将按产生系统调用的函数调用路径原路返回,执行中断返回指令“iret”后,将切换到用户进程的第一条语句位置_start处开始执行。

2.2 进程的回收

当进程执行完它的工作后,就需要执行退出操作,释放进程占用的资源。首先由进程本身完成大部分资源的占用内存回收工作,然后由此进程的父进程完成剩余资源占用内存的回收工作。

// do_exit - called by sys_exit
//   1. call exit_mmap & put_pgdir & mm_destroy to free the almost all memory space of process
//   2. set process' state as PROC_ZOMBIE, then call wakeup_proc(parent) to ask parent reclaim itself.
//   3. call scheduler to switch to other process
int
do_exit(int error_code) {
    if (current == idleproc) {
        panic("idleproc exit.\n");
    }
    if (current == initproc) {
        panic("initproc exit.\n");
    }
    
    struct mm_struct *mm = current->mm;
    if (mm != NULL) { //current->mm != NULL,表示是用户进程,则开始回收此用户进程所占用的用户态虚拟内存空间;
        lcr3(boot_cr3); //切换到内核态的页表上
        /*
         * mm_count减1后为0(表明这个mm没有再被其他进程共享,可以彻底释放进程所占的用户虚拟空间了),
         * 则开始回收用户进程所占的内存资源
         */
        if (mm_count_dec(mm) == 0) { 
            /*
             * 调用exit_mmap函数释放current->mm->vma链表中每个vma描述的进程合法空间中实际分配的内存,
             * 然后把对应的页表项内容清空,最后还把页表所占用的空间释放并把对应的页目录表项清空;
             */
            exit_mmap(mm);
            /*调用put_pgdir函数释放当前进程的页目录所占的内存*/
            put_pgdir(mm);
            /*调用mm_destroy函数释放mm中的vma所占内存,最后释放mm所占内存*/
            mm_destroy(mm);
        }
        /*
         * 此时设置current->mm为NULL,表示与当前进程相关的用户虚拟内存空间和对应的内存管理成员变量
         * 所占的内核虚拟内存空间已经回收完毕;
         */
        current->mm = NULL;
    }
    /*
     * 此时当前进程已经不能被调度了,需要此进程的父进程来做最后的回收工作(即回收描述此进程的内核栈和进程控制块)
     */
    current->state = PROC_ZOMBIE;
    current->exit_code = error_code;
    
    bool intr_flag;
    struct proc_struct *proc;
    local_intr_save(intr_flag);
    {
        proc = current->parent;
        /*
         * 如果当前进程的父进程current->parent处于等待子进程状态:
         * 则唤醒父进程(即执行“wakup_proc(current->parent)”),让父进程帮助自己完成最后的资源回收;
         */
        if (proc->wait_state == WT_CHILD) {
            wakeup_proc(proc);
        }
        /*
         * 如果当前进程还有子进程,则需要把这些子进程的父进程指针设置为内核线程initproc,
         * 且各个子进程指针需要插入到initproc的子进程链表中。如果某个子进程的执行状态是PROC_ZOMBIE,
         * 则需要唤醒initproc来完成对此子进程的最后回收工作。
         */
        while (current->cptr != NULL) {
            proc = current->cptr;
            current->cptr = proc->optr;
    
            proc->yptr = NULL;
            if ((proc->optr = initproc->cptr) != NULL) {
                initproc->cptr->yptr = proc;
            }
            proc->parent = initproc;
            initproc->cptr = proc;
            if (proc->state == PROC_ZOMBIE) {
                if (initproc->wait_state == WT_CHILD) {
                    wakeup_proc(initproc);
                }
            }
        }
    }
    local_intr_restore(intr_flag);
    
    schedule(); //执行schedule()函数,选择新的进程执行
    panic("do_exit will not return!! %d.\n", current->pid);
}
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是浩浩子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值