在这一章中将进一步解释进程的动态的概念,提供创建、销毁、等待、信息等系统调用,并开发一个shell作为user_app之一。我们会重新设计TaskControlBlock的结构,来满足我们对于进程创建,销毁的一系列需求。

进程ID

​ 进程ID的分配采用了和前面一样的栈式分配,不断向后移动current标志,表示最大已分配id号。如果要进行回收,则直接回收到recycled中,同时如果recycled中如果存在已经回收的id,则不会向后移动current。最后在这个模块还要说明如何获取kernel stack,和之前保持一致,不过app_id换成了pid

进程控制块及其操作

​ 对于进程控制块,分成初始化后不变和可变的两部分。结构如下:

pub struct TaskControlBlock {
    pub pid: PidHandle,
    pub kernel_stack: KernelStack,
    inner UPSafeCell<TaskControlBlockInner>,
}

pub struct TaskControlBlockInner {
    pub trap_cx_ppn: PhysPageNum,
    pub base_size: usize,
    pub task_cx: TaskContext,
    pub task_status: TaskStatus,
    pub memory_set: MemorySet,
    pub parent: Option<Weak<TaskControlBlock>>,
    pub children: Vec<Arc<TaskControlBlock>>,
    pub exit_code: i32,
}

new方法增加了pid的申请。同时增加了forkexec方法,用于拷贝和替换上下文,达到再创建一个进程和执行一个程序的效果。

pub fn exec(&self, elf_data: &[u8]) {
    let (memory_set, user_sp, entry_point) = MemorySet::from_elf(elf_data);
    let trap_cx_ppn = memory_set
    	.translate(VirtAddr::from(TRAP_CONTEXT).into())
    	.unwrap()
    	.ppn();
    
    let mut inner = self.inner_exclusive_access();
    inner.memory_set = memory_set;
    inner.trap_cx_ppn = trap_cx_ppn;
    inner.base_size = user_sp;
    let trap_cx = inner.get_trap_cx();
    *trap_cx = TrapContext::app_init_context(
    	entry_point,
        user_sp,
        KERNEL_SPACE.exclusive_access().token(),
        self.kernel_stack.get_top(),
        trap_handler as usize,
    );
}

​ 这里直接将trap上下文和memory_set即映射空间进行替换,这样返回时会直接restore我们刚刚放入的上下文,达到执行一个程序的目的。

pub fn fork(self: &Arc<Self>) -> Arc<Self> {
	let mut parrent_inner = self.inner_exclusive_access();
	let memory_set = MemorySet::from_existed_user(&parent_inner.memory_set);
    let trap_cx_ppn = memory_set
    	.translate(VirtAddr::from(TRAP_CONTEXT).into())
    	.unwrap()
    	.ppn()
   	let pid_handle = pid_alloc();
    let kernel_stack = KernelStack::new(&pid_handle);
    let kernel_stack_top = kernel_stack.get_top();
    let task_control_block = Arc::new(TaskControlBlock{
        pid: pid_handle,
        kernel_stack,
        inner: unsafe {
            UPSafeCell::new(TaskControlBlockInner {
                trap_cx_ppn,
                base_size: parent_inner.base_size,
                task_cx: TaskContext::goto_trap_return(kernel_stack_top),
                task_status: TaskStatus::Ready,
                memory_set,
                parent: Some(Arc::downgrade(self)),
                children: Vec::new(),
                exit_code: 0,
            })
        },
    });
    parent_inner.children.push(task_control_block.clone());
    let trap_cx  = task_control_block.inner_exclusive_access().get_trap_cx();
    trap_cx.kernel_sp = kernel_stack_top;
    task_control_block
}

​ 这里调用了from_existed_user函数对于父进程的地址空间进行拷贝,同时获取新获取空间的root_ppn分配的物理帧,非陪内核占空间,重新申请一个task_control_block。同时设置父子进程关系,更换fork生成的trap上下文中的内核栈指针位置,这个指针的位置应该由新生成的pid决定。

数据结构关系

​ 如上图所示,这是进程调度时的框架图,处理结构体内部只会维护一个idle_task_cxcurrentidle_task_cx会不断循环让后只是调度下一个程序,schedule函数本身运行在操作系统的启动栈,切换的进程各自运行在自己的内核栈,进行切换时的调度对于两个进程的trap执行流就是不可知的。处理器结构体的任务就是维护当前正在运行的任务,

TaskManager内部就是一个VecDeque一个双向队列,可以从末尾加入一个进程控制块,从队首去取出进程控制块。

运行过程

​ 这里起始于右上角的find_app_data_by_name,这个函数会根据app的名字来获取app在整个存储队列中的位置,TaskControlBlocknew方法中会调用from_elf解析数据,生成控制块后将这个结构压入Manager的队列。processor中会调度schedule启动这个进程控制块,执行initproc后,会执行fork系统调用,根据from_existed_user拷贝一份地址映射,并生成新的进程控制块,这个控制块也会被加入队列。initproc返回后会执行yield,这是idle会被用于调度下一个进程,user_shell会被调度执行,get_char会不断获取字符,如果已经键入一个命令,会先进行一次fork,根据结果,主进程进行wait,子进程执行结束后将状态设置为zombie,并设置exit_codewait系统调用在检查到子进程执行完毕后,会结束loop