进程是指一个具备必定独立功能的程序在一个数据集合上的一次动态执行过程数据结构
源代码在通过编译连接以后生成了可执行文件,再由操做系统进行加载而且进行一些堆栈的分配才是进程并发
操做系统管理控制进程运行所用的信息集合函数
PCB是进程存在的惟一标志ui
每一个进程都在操做系统中有一个对应的PCBthis
进程控制块主要包含的就是进程的标识信息,处理机现场保存和进程控制信息spa
控制信息:操作系统
进程建立线程
用户请求建立一个新进程,正在运行的进程执行了建立进程的系统调用,而且加入到就绪队列指针
进程执行rest
内核对就绪队列进行调度,到执行该进程
进程等待
运行中的进程可能会进入阻塞状态,好比进行IO的等待或者须要的数据没有到达
进程抢占
运行中的进程可能时间片被用完或者高优先级进程被唤醒致使了进程被抢占进入阻塞状态
进程唤醒
被阻塞须要的资源能够被知足就可能被唤醒进入就绪队列
进程结束
进程完成任务或者被迫结束
线程是进程的一部分,描述指令流执行状态。它是进程中的指令执行流的最小单元,是CPU调度的基本单位。
进程由一组相关资源构成,包括地址空间(代码段、数据段)、打开的文件等各类资源
线程描述在进程资源环境中的指令流执行状态
一个进程中能够同时存在多个线程,各个线程之间能够并发地执行,各个线程之间能够共享地址空间和文件等资源
用户线程是基于在用户态本身实现的线程库函数来完成对线程的管理,包括线程的建立、终止、同步和调度
这样内核并不知道用户线程的存在,因此每一个线程控制块都由线程库函数来维护,也不要在用户态和内核态进行切换,开销会较小,可是对于若是线程发生阻塞的话,因为操做系统并不知道用户级线程的状况,因此会形成整个进程的阻塞,而且除非当前运行线程主动放弃,它所在进程的其余线程没法抢占CPU
由内核经过系统调用实现的线程机制,由内核完成线程的建立、终止和管理
由内核维护PCB和TCB,线程执行系统调用而被阻塞不影响其余线程,线程的建立、终止和切换相对较大,以线程为单位进行CPU时间分配
暂停当前运行进程,从运行状态变成其余状态,调度另外一个进程从就绪状态变成运行状态
要完成进程切换就须要对切换前的进程进行进程上下文的保存,而且在切换后对进程上下文的恢复
进程控制块:内核为每一个进程维护了对应的进程控制块(PCB),内核将相同状态的进程的PCB放置在同一队列
fork/exec:
fork() 建立一个继承的子进程
复制父进程的全部变量和内存,复制父进程的全部CPU寄存器(有一个寄存器例外),fork()执行过程对于子进程而言,是在调用时间对父进程地址空间的一次复制,对于父进程fork() 返回child PID, 对于子进程返回值为0
fork()的地址空间复制
fork()执行过程对于子进程而言,是在调用时间对父进程地址空间的一次复制,对于父进程fork() 返回child PID, 对于子进程返回值为0
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 }; static struct proc_struct *alloc_proc(void) { struct proc_struct *proc = kmalloc(sizeof(struct proc_struct)); if (proc != NULL) { proc->state = PROC_UNINIT; //设置进程为未初始化状态 proc->pid = -1; //未初始化的的进程id为-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; }
proc_struct便是进程控制块的结构体,alloc_proc函数来负责分配一个新的struct proc_struct结构,根据提示咱们须要初始化一些变量
建立内核线程的主要工做由do_fork来完成资源的分配
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:调用alloc_proc()函数申请内存块,若是失败,直接返回处理 if ((proc = alloc_proc()) == NULL) { goto fork_out; } //2.将子进程的父节点设置为当前进程 proc->parent = current; //3.调用setup_stack()函数为进程分配一个内核栈 if (setup_kstack(proc) != 0) { goto bad_fork_cleanup_proc; } //4.调用copy_mm()函数复制父进程的内存信息到子进程 if (copy_mm(clone_flags, proc) != 0) { goto bad_fork_cleanup_kstack; } //5.调用copy_thread()函数复制父进程的中断帧和上下文信息 copy_thread(proc, stack, tf); //6.将新进程添加到进程的hash列表中 bool intr_flag; local_intr_save(intr_flag); { proc->pid = get_pid(); hash_proc(proc); //创建映射 nr_process ++; //进程数加1 list_add(&proc_list, &(proc->list_link));//将进程加入到进程的链表中 } local_intr_restore(intr_flag); // 7.一切就绪,唤醒子进程 wakeup_proc(proc); // 8.返回子进程的pid ret = proc->pid; fork_out: return ret; bad_fork_cleanup_kstack: put_kstack(proc); bad_fork_cleanup_proc: kfree(proc); goto fork_out; }
先看调度函数,就只是一个简单FIFO模型
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); }
调度完成由proc_run来切换和开始执行进程
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); } }
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
这些指令完成了保存前一个进程的其余 7 个寄存器到 context 中的相应域中。至此前一个进程的执行现场保存完毕。
再日后是恢复向一个进程的执行现场,这其实就是上述保存过程的逆执行过程,即从 context 的高地址的域 ebp 开始,逐一把相关域的值赋值给对应的寄存器。
完成应用程序的加载的函数是do_evecve,其中最主要的是load_icode,这个函数用来将ELF可执行二进制文件加载到当前内存中来
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); }
首先为加载新的执行码作好用户态内存空间清空准备。若是mm不为NULL,则设置页表
为内核空间页表,且进一步判断mm的引用计数减1后是否为0,若是为0,则代表没有进
程再须要此进程所占用的内存空间,为此将根据mm中的记录,释放进程所占用户空间内
存和进程页表自己所占空间。最后把当前进程的mm内存管理指针为空。因为此处的
initproc是内核线程,因此mm为NULL,整个处理都不会作
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 if ((mm = mm_create()) == NULL) { goto bad_mm; } //(2) create a new PDT, and mm->pgdir= kernel virtual addr of PDT 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 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; 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 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 mm_count_inc(mm); current->mm = mm; current->cr3 = PADDR(mm->pgdir); lcr3(PADDR(mm->pgdir)); //(6) setup trapframe for user environment 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_create函数来申请进程的内存管理数据结构mm所需内存空间,并对mm进行初
始化;
调用setup_pgdir来申请一个页目录表所需的一个页大小的内存空间,并把描述ucore内核
虚空间映射的内核页表(boot_pgdir所指) 的内容拷贝到此新目录表中,最后让mm->pgdir指向此页目录表,这就是进程新的页目录表了,且可以正确映射内核虚空间;
根据应用程序执行码的起始位置来解析此ELF格式的执行程序,并调用mm_map函数根
据ELF格式的执行程序说明的各个段(代码段、数据段、BSS段等) 的起始位置和大小建
立对应的vma结构,并把vma插入到mm结构中,从而代表了用户进程的合法用户态虚拟
地址空间;
调用根据执行程序各个段的大小分配物理内存空间,并根据执行程序各个段的起始位置
肯定虚拟地址,并在页表中创建好物理地址和虚拟地址的映射关系,而后把执行程序各
个段的内容拷贝到相应的内核虚拟地址中,至此应用程序执行码和数据已经根据编译时
设定地址放置到虚拟内存中了
须要给用户进程设置用户栈,为此调用mm_mmap函数创建用户栈的vma结构,明确用户
栈的位置在用户虚空间的顶端,大小为256个页,即1MB,并分配必定数量的物理内存且
创建好栈的虚地址<-->物理地址映射关系
至此,进程内的内存管理vma和mm数据结构已经创建完成,因而把mm->pgdir赋值到cr3
寄存器中,即更新了用户进程的虚拟内存空间,此时的initproc已经被hello的代码和数据
覆盖,成为了第一个用户进程,但此时这个用户进程的执行现场还没创建好
先清空进程的中断帧,再从新设置进程的中断帧,使得在执行中断返回指令“iret”后,能 够让CPU转到用户态特权级,并回到用户态内存空间,使用用户态的代码段、数据段和 堆栈,且可以跳转到用户进程的第一条指令执行,并确保在用户态可以响应中断