37行) 动态内存分配与释放 kalloc.c中是对内存空间进行管理,kfree是回收一段内存,而kalloc是分配一段内存。xv6的内存管理是十分简单的。由于内存分配是以连续的段的方式进行的,通过单链表方式链接空闲的段。因此经过一定时间的分配,空闲空间将由一个个地址不连续的段组成。xv6用freelist将其按起始地址从左至右排列串起来。每次回收时,将回收的段加入段序列 中。如果发现新加入段之后能够合并,则将其合并成一个段。当进行分配时,则遍历整个链表,直到找到一个比需求大的段,则将相应的段分配出去。 初始化后 第一次分配 第N次分配和释放后 第五章 进程管理与调度 1.概述 本章将给出xv6进程管理实现的概貌。读者将学习以下一些内容: ? ? ? ? ? ? ? ? ? 什么是进程? xv6的进程管理数据结构(进程控制块,PCB)包含哪些内容? xv6是如何组织进程管理数据结构? xv6如何进行进程管理初始化的? xv6怎样启动多进程的(即需要做哪些初始化工作)? xv6何时进行进程调度? xv6是如何调度进程的(如何选择进程占用CPU运行)? xv6是如何完成进程切换的? xv6如何启动并执行用户态的进程的? 进程的概念 程序与进程的概念是不可分的。当用户在计算机上运行一个程序时,此程序对应的进程就诞生了,并实际完成各种程序提供的功能,而用户关闭一个程序时,进程也随之终止。程序是为了完成某项任务编排的语句序列,它要告诉计算机如何执行,因此程序是需要通过CPU来运行的,且在程序的运行过程中需要占有计算机的各种资源(比如内存等)才能运行下去。如果计算机系统在任一时刻限制只有一个程序在运行,则程序在整个运行过程中独占计算机中的全部资源,这样整个程序运行和管理就简单了。就象在一个家中只住了一个人A,他想看书就到书房去看书,想睡觉就到睡房的床上去睡觉,想看电视就到电视厅看电视,想吃饭就去餐厅吃饭,没人和他抢占资源。但为了提高计算机系统的资源利用率,需要支持多个程序并发执行。这就会带来许多新的问题,如资源的共享与竞争,同步与互斥等。比如此人与B成家并有了小孩C,那就是三口之家同时住一套房,当A想去看足球比赛直播电视节目的时候,如果发现电视厅已经有B在坐着看连续剧电视节目了,A就得等待或干别的事情。除非A在买一个电视,并在另外一个房间看他的球比赛直播。由于程序是静态的,我们看到的程序是存储在存储介质(如硬盘、光盘等)上的,它无法反映出程序执行过程中的动态特性,而且程序在执行过程中会不断申请资源或释放资源,这样让程序作为共享资源的基本单位是不合适的,所以需要引入一个概念,它能描述程序的执行过程而且可以作为共享资源的基本单位,这个概念就是进程。简单地说,一个进程是一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。每个进程都是整个应用的某一部分。操作系统在逻辑上维护了进程的运行状态信息,即与进程运行直接相关的CPU寄存器和栈空间(只有这样才能实现进程切换)。操作系统根据当前进程的情况设置进程的状态,并根据进程的状态(比如优先级)进行选择一个进程占用CPU并运行,这个过程成为调度。 进程的状态
进程从诞生到死亡要经历若干个阶段,也会有生老病死。简单地说,进程有三种状态:就绪、执行、等待。多种原因可以导致创建一个进程,比如,当操作系统把一个程序从硬盘调入内存后,在开始执行前,操作系统就要为此程序创建一个对应的进程。又比如,一个进程可以自己创建一个子进程,子进程被创建后就是在内存中,处于就绪态,所谓就绪态就是万事具备,只欠CPU这个东风了;一旦进程占有了CPU,就可以执行实际的工作了,其状态就变成了执行态;进程在执行中如果需要等待某个资源(如等硬盘输入数据),则进程会放弃CPU,且其状态就变为等待态,这时操作系统又会从处于就绪态的另一个进程中挑选一个进程占有CPU,则这另一个进程的状态就变成了执行态。当前一个进程所等到数据到来后,处于等待态的前一个进程又被唤醒,并把状态变成为就绪态。
在实际的操作系统中,进程的状态往往多于三种,比如找xv6中,进程具有多种状态,包括:EMBRO, SLEEPING, RUNNABLE, RUNNING和ZOMBIE。定义在proc.h文件中:
enum proc_state { UNUSED, EMBRYO, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
状态的含义如下:
? UNUSED:进程未被创建(即进程控制块空闲)时的状态;
? EMBRYO:需要分配一个进程控制块且找到一个处于UNUSED状态的进程控制块时,
把此进程控制块状态设置为要使用的状态;
? SLEEPING:进程由于等待某资源等原因无法执行,进入睡眠状态,即等待态; ? RUNNABLE:进程获得了除CPU之外的所有资源,处于可运行状态,即就绪态; ? RUNNING:进程获得CPU,正在运行的状态,即执行态; ? ZOMBIE:进程结束的状态
这几个状态之间的转化如图5.1(左)所示。 由于是SMP架构,因此每个CPU获取进程调度的权利是相同的。CPU在scheduler里面进行轮询操作,每次从线程池中选择一个
RUNNABLE的进程进行运行。直到运行完毕,或一单位时间片结束,或者进程主动yield或sleep。CPU与进程池的关系如图5.1(右)所示。
图5.1 状态转换的过程[即哪些事件促使了状态的转换 TODO] 进程控制块 程序的运行是通过进程体现的,操作系统对进程进行管理和控制,那么操作系统怎么了解到进程的状态并掌握进程占有的资源分配呢,而且进程做状态转换时CPU的现场保存在那呢?这实际是通过进程控制快(Process Control Block, 简称PCB)。PCB是进程 的唯一标志,在其中记录了进程的全部信息,相当于进程的档案。操作系统通过PCB感知进程的存在,通过PCB了解进程和控制 进程的运行。在xv6中,所有的CPU共享一个进程控制块池,即源代码中为proc.c中的proc(即进程控制块)数组。在这 个进程数组保存的进程控制块结构分成两类:一类是未使用的进程控制块结构,另一类是正在使用的进程控制块结构。每次要创建一个进程时,只需要从进程控制块数组中取得一个未使用进程控制块结构进行相应的处理即可。 2.代码分析 结构与变量 proc.h
文件中定义了几个关键的数据结构。
其中context是在内核进行上下文切换需要保存的寄存器(定义在proc.h中):
struct context { int eip; int esp; int ebx; int ecx; int edx; int esi; int edi; int ebp; };
[Q]:为何不保存eax?
proc结构描述了进程运行需要的数据:
struct proc {
char *mem; // Start of process memory (kernel address)
uint sz; // Size of process memory (bytes)
char *kstack; // Bottom of kernel stack for this process
enum proc_state state; // Process state
int pid; // Process ID struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan int killed; // If non-zero, have been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for current interrupt char name[16]; // Process name (debugging) };
其中:
? ? ? ? ? ? ? ?
mem记录了进程在内核的起始地址; sz是记录进程所占有的内存空间大小; kstack是进程在内核态的栈; state是进程的状态; pid是进程的ID;
chan不为NULL时,是进程睡眠时所挂的睡眠队列; killed不为0时,表示进程被杀死了; ofile数组是进程打开的文件数组;