xv6阅读汇报-2
- xv6中的进程线程相关的模块有
types.h param.h memlayout.h defs.h x86.h asm.h mmu.h elf.h vm.c proc.h proc.c swtch.S kalloc.c
-
type.h
主要用于声明一些数据类型的简化名称,和声明页表指针的数据类型。 -
param.h
主要用于声明基本的一些常量,包括内核栈大小等。 -
memlayout.h
主要用于声明一些和内存与地址相关的常量与方法,包括虚拟内存转物理内存的方法以及物理内存转虚拟内存的方法等。 -
defs.h
声明在之后文件中要用到的函数。 -
x86.h
让c代码使用特殊的x86汇编的一些函数包括outb等,并声明trapframe。 -
asm.h
一些汇编上的宏定义 -
mmu.h
定义x86内存管理单元,包括控制寄存器CR上的不同位代表的含义。 -
elf.h
ELF可执行文件的格式,包括ELF头的数据结构等。 -
vm.c
一些cpu虚拟内存管理的函数,包括建立内核页面setpkvm()
、加载inintcode.s
的inituvm()
、切换当前进程页表的switchuvm()
,用于切换到内核的页表的switchkvm()
,用于拷贝父进程空间到子进程空间的copyuvm()
。 -
proc.h
声明了CPU在内核中的数据结构表示struct cpu
,进程在内核中的数据结构struct proc
,上下文在内核中的数据结构struct context
。 -
proc.c
声明了关于进程的核心函数,进程表的数据结构struct ptable
,初始进程initproc
,初始pid,获取当前cpu的函数mycpu()
,获取当前进程的函数myproc()
,建立第一个进程的函数userinit()
,增加当前进程大小的函数growproc()
,新建子进程的fork()
,退出进程的exit()
,等待子进程退出的wait()
,内核运行时用于进程调度的循环scheduler()
,切换到内核的sched()
,放弃cpu占用的yield()
,fork返回时会调用的forkret()
,切换成SLEEPING状态sleep()
,解除睡眠队列上的所有进程wakeup1()``````wakeup()
区别在于之前有没有上锁,杀死进程kill()
,和向控制台上打印当前进程状态的procdump()
-
swtch.S
声明用于上下文切换的swtch
函数. -
kalloc.c
声明一些物理内存分配的函数,包括分配的kalloc()
和用于free的kfree()
。 - 总体上来说这样设计的原因是可以实现一个简单的小型操作系统所必须的一些内容。关于进程的部分,这样的设计可以简单的实现多进程的并发,实现其功能。
- 什么是进程,什么是线程?操作系统的资源分配单位和调度单位分别是什么?XV6 中的进程和线程分别是什么,都实现了吗?
在xv6系统中,我们使用一个叫做proc
的结构体来保存进程中的元素,同时xv6系统中没有实现线程。
- 一个进程是一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。有多种状态模型,其中三状态表示的是:运行状态、就绪状态、和等待状态。
- 线程是进程的一部分,描述指令流执行状态,表示进程子任务的执行。它是进程中的指令执行流的最小单元,是 CPU调度的基本单位。
- 操作系统中资源分配的基本单位是进程,在引入了线程的操作系统中,线程是调度的基本 单位,否则进程是调度的基本单位。
- 进程管理的数据结构是什么?在 Windows,Linux,XV6 中分别叫什么名字?其中包含哪些内容?操作系统是如何进行管理进程管理数据结构的?它们是如何初始化的?
- 进程管理的数据结构是进程控制块PCB。
- PCB在windows系统中被称作EPROCESS。
其中包含的内容过多了,这里就不一一列举了,挑几个细说一下。
包含的内容有KPROCESS结构(内核进程块),windows中的EPROCESS在内核层执行体中,而KPROCESS作为EPROCESS的结构在内核层的真内核中,在EPROCESS里面的KPROCESS结构体被命名为PCB域,其中细分的内容也很多。主要与进程的内存环境相关,比如页目录表、交换状态等。同时还有线程相关的一些属性,比如线程列表以及线程所需要的优先级、时限设置等。
Process-Lock(进程锁结构体),用于windows系统中进程同步的锁机制。
ActiveProcessLinks 是双链表的节点。在windows进程中EPROCESS以双链表形式被组织起来。
UniqueProcessId,就是进程的pid。
- 对于linux系统中PCB是task_struct结构体。
其中主要内容包括:
- state进程的状态,一共有八种,比如:正在执行、停止执行、阻塞、等待回收等。
- PID进程标识符。
- stack进程内核栈。
- flags标记。
- 表示进程亲属关系的一些成员
- ptrace系统调用
- Performance Event 一个性能诊断工具
- 进程调度 包括实时优先级范围等
- 进程地址空间
- 一些判断标志
- 时间 包括已经运行的时间等
- 关于信号处理的一些数据结构
- ……
- xv6的PCB就是
proc.h
中的proc
结构体。其内容包括:-
uint sz
:进程的内存空间大小。 -
pde_t* pgdir
:进程的页表。 -
char *kstack
:进程的内核栈指针 -
enum procstate state
:进程的状态(包括六种UNUSED, EMBRYO, SLEEPING, RUNNABLE, RUNNING, ZOMBIE
) -
int pid
:进程的pid -
struct proc *parent
:进程的父进程指针。 -
struct trapframe *tf
:位于x86.h
,是中断进程后,需要恢复进程继续执行所保存的寄存器内容。 -
struct context *context
:切换进程所需要保存的进程状态。切换进程需要维护的寄存器内容,定义在proc.h
中。 -
void *chan
:不为0时,保存该进程睡眠时的队列。 -
int killed
:不为0时,则该进程被杀死。 -
struct file *ofile[NOFILE]
:打开文件的组。 -
struct inode *cwd
:进程当前的目录。 -
char name[16]
:进程名。
-
- 对于xv6,每个进程都有一个运行线程(或简称为线程)来执行进程的指令。线程可以被暂时挂起,稍后再恢复运行。系统在进程之间切换实际上就是挂起当前运行的线程,恢复另一个进程的线程。线程的大多数状态(局部变量和函数调用的返回地址)都保存在线程的栈上。每个进程都有用户栈和内核栈(p->kstack)。当进程运行用户指令时,只有其用户栈被使用,其内核栈则是空的。然而当进程(通过系统调用或中断)进入内核时,内核代码就在进程的内核栈中执行;进程处于内核中时,其用户栈仍然保存着数据,只是暂时处于不活跃状态。进程的线程交替地使用着用户栈和内核栈。要注意内核栈是用户代码无法使用的,这样即使一个进程破坏了自己的用户栈,内核也能保持运行。当进程使用系统调用时,处理器转入内核栈中,提升硬件的特权级,然后运行系统调用对应的内核代码。当系统调用完成时,又从内核空间回到用户空间:降低硬件特权级,转入用户栈,恢复执行系统调用指令后面的那条用户指令。线程可以在内核中“阻塞”,等待 I/O, 在 I/O 结束后再恢复运行。
- 在xv6中进程的初始化会调用
allocproc()
函数,上锁后函数寻找一个PCB中状态为UNUSED
即未被使用的进程。然后把他的状态改成EMBRYO
并分配一个pid。然后调用kalloc()
函数来给进程分配用户栈,分配陷阱帧(trapframe)。然后通过设置返回程序计数器的值,使得新进程的内核线程首先运行在forkret
的代码中,然后返回到trapret
中运行。这里调用的过程重点是是将p->context->eip
的值设为相应的地址,即在后面调用上下文切换的switch.S
中的switch函数返回时ret
指令会一次调用context转换后栈中最底下的forkret,即此时的context->eip
。此时的trapret在上述进程栈的正上方,这样forkret之后,弹出相应的栈大小,会调用proc-context
上面位置的trapret()
。
- 进程有哪些状态?请画出 XV6 的进程状态转化图。在 Linux,XV6 中,进程的状态分别包括哪些?你认为操作系统的设计者为什么会有这样的设计思路?
-
进程状态一般有三状态模型、五状态模型和七状态模型。一般来说,三状态模型是指:就绪、运行、等待。七状态是指:创建状态、退出状态、运行状态、就绪状态、阻塞状态、阻塞挂起状态、就绪挂起状态
-
xv6进程包括UNUSED, EMBRYO, SLEEPING, RUNNABLE, RUNNING, ZOMBIE这6种状态。即未使用态、初始态、等待态、就绪态、运行态、僵尸态。
WechatIMG3.png -
产生这样的设计思路是因为,可以实现多道程序的交叉运行, 和方便设计各种进程调度算法。在时间片轮转调度算法中RUNNING的程序在时间片到的时候就会转成RUNNABLE同时让出CPU。 而正在RUNNING的程序如果遇到资源申请,就会主动放弃CPU进SLEEPING状态。但是SLEEPING状态的程序如果得到资源之后并不能马上进入RUNNING而是先进入就绪队列等待CPU调度。
-
Linux的进程状态可分为
R(TASK_RUNNING)
即可执行状态,S(TASK_INTERRUPTIBLE)
即可中断的睡眠状态,D(TASK_UNINTERRUPTIBLE)
即不可中断的睡眠状态,T(TASK_STOPPED or TASK_TRACED)
即暂停状态或跟踪状态,Z(TASK_DEAD-EXIT_ZOMBIE)
即进程成为僵尸进程,X(TASK_DEAD-EXIT_DEAD)
即进程等待将被销毁。
- 如何启动多进程(创建子进程)?如何调度多进程?调度算法有哪些?操作系统为何要限制一个CPU最大支持的进程数?XV6中的最大进程数是多少?如何执行进程的切换? 什么是进程上下文?多进程和多CPU有什么关系?
- 除了第一个进程之外,进程(子进程)都是有父进程创建的。即父进程调用
fork()
函数。过程如下:- 获取当前的进程,其过程为在调用
myproc
函数中调用mycpu
来表示当前的cpu。 - 调用
allocproc
初始化该proc。 - 以一次一页的方式复制父进程地址空间,若失败则回收空间并报报错。
- 子进程继承父进程的大小,设置父进程为当前进程,继承trapframe。
- 设置trapframe的eax为0,这样通过中断返回的时候,会返回0。
- 获得子进程继承的打开的文件和当前工作目录,以及父进程的名字
- 在上锁的情况下设置子进程当前状态为就绪(RUNABLE)
- 最后返回子进程当前的pid
- 获取当前的进程,其过程为在调用
- 多进程的调度方式如下:在
main.c
中经过一系列初始化之后,内核会进入scheduler
中,首先内核会允许中断,然后加锁,再循环体中找到一个就绪状态的进程。然后调用switchuvm()
切换到该进程的页表。然后使用swtch()
切换到该进程中运行,该函数会按照proc中保存的上下文去切换上下文。然后再切回内核的页表最后再释放锁。 - 除此之外,在进程运行的过程中,时钟(如同上一章提到的)会发送一个中断的信号给内核,强制结束进程只能在某时间片上运行。大概过程是调用
trap
函数调用yield
函数,yield
调用sched
调用swtch
,返回原先保存的上下文cpu->scheduler
。 - 除此之外,还有
exit``````sleep
等调出方式。 - 调度算法有:先来先服务、短作业优先算法、最高响应比优先算法、时间片轮转算法、高响应比优先算法、多级反馈队列算法,公平共享调度算法等。
- 操作系统设置最大进程数的原因是,不能让进程无限的占用空间导致最终内存资源耗尽。对于xv6来说
#define NPROC 64
可知一共有64个进程。 - 就如上文所说的进程切换在xv6中主要有中断和scheduler来设置,除此之外对于用户来说,进程的切换是由于进程状态的改变引起的。
- xv6的进程上下文是指的括当前进程的程序计数器PC和当前运行的CPU中各个寄存器的内容。 当进程切换和发生中断的时候这些信息要保存下来以便于下次运行时使用。在xv6中进程上下文是
proc->context
。其他的系统中可能还包括用户栈、用户数据、状态寄存器、和系统上的一些其他内容。 - 多进程和多cpu没有直接关系。但是多cpu下,多进程可以有原先的并发执行变成同时的并行执行。
- 内核态进程是什么?用户态进程是什么?它们有什么区别?
- 内核态进程:一个进程因系统调用陷入内核代码中执行时,此时该进程的特权级是最高的,就是在内核态中运行中的进程。
- 用户态进程:一个进程执行用户自己代码时处于用户态,特权级最低,这样的进程就是用户态进程。
- 区别在于:上一次汇报中的内核态与用户态的区别,为了防止用户对计算机指令的乱用而区分。并不一定是两个进程,只是进程在权限位上产生不同的情况。
- 进程在内存中是如何布局的,进程的堆和栈有什么区别?
-
进程在内存中的布局在前面已经讲过了。
WechatIMG4.png -
大概就是上面这样,
-
代码段 : 保存程序的执行码。在进程并发时, 代码段是共享的且只读的,在存储器中只需有一个副本。
-
数据段 : 此段又称为初始化数据段,它包含了程序中已初始化的全局变量、 全局静态变量、 局部静态变量 。
-
栈 : 程序执行前静态分配的内存空间,栈的大小可在编译时指定 , Linux 环境下默认为 8M。 栈段是存放程序执行时局部变量、函数调用信息、中断现场保留信息的空间。程序执行时,CPU 堆栈段指针会在栈顶根据执行情况进行上下移动。
-
堆 : 程序执行时, 按照程序需要动态分配的内存空间。malloc 、 calloc 、 realloc 函数分配的空间都在堆上分配。
-
对于xv6来说
- 请结合代码简述proc.c 文件中的fork、wait、exit函数分别完成了什么功能。
- fork在前面第四题已经提过了这里不多说了,功能上就是创建一个子进程。
- wait函数:
- 用
myproc()
获取当前进程,加锁后进入无限循环 - 查找当前进程的子进程
- 如果没有或者当前进程被杀死了返回-1
- 否则判断子进程是否处于僵尸状态,若是则设置其为
UNUSED
,并且重置该进程上所有的内容,包括释放栈空间等。 - 返回该子进程的pid
- 用
- exit函数:
- 用
myproc()
获取当前进程,检查是否是初始进程,若是则对控制台输出init exiting
再调用exit()自身。 - 然后关闭所有文件。
- 调用
iput
函数把对当前目录的引用从内存中删除。 - 上锁后唤醒当前进程的父进程,以及以上的进程。
- 把当前进程的所有子进程都划归为用户初始进程initproc的子进程中。
- 把当前进程改为僵尸进程,并从使用进程调度器调度下一个可执行进程。
- 用
- 其他你认为有趣有价值的问题。
-
控制寄存器cr:CR0中含有控制处理器操作模式和状态的系统控制标志,CR1不用,CR2含有导致页错误的线性地址,CR3含有页目录表物理内存的基地址,CR4上面的内容比较多用于控制实模式与保护模式。
-
第一个进程的调用:在之
entry()
调用之后操作系统进入main
函数,在其中的userinit()
函数中调用了allocproc
。在调用之后userinit
调用setupkvm
然后,userinit
调用inituvm
,分配一页物理内存,将虚拟地址0映射到那一段内存,并将并把initcode这段代码拷贝到那一页中。接下来,userinit
把trapframe设置为用户模式。最后userinit
将p->state
设置为 RUNNABLE,使进程能够被调度。 -
smp架构是每个处理器分配了一个调度函数。
网友评论