由于工作和兴趣爱好的关系,接触了不少实时操作系统, 一般来说实时操作系统基本没有进程的概念了,无非是任务堆栈的切换。
一直对Linux,Windows这种带有进程的OS,很好奇,无奈,LINUX代码很庞大,很难整体把握。
所以去年一直在寻找带支持进程的OS, 要求简单,易懂,确实真找不到。最后找到了MIT教学用VX6,便深深的着迷了。
自从调试了VX6的源代码,发现用MMU来管理进程真是复杂,怪不得很少能找到支持进程的OS了。
通过参考了很多关于XV6的中文的blog,发现几乎没有对进程管理和MMU做阐述的。
于是我将去年的笔记部分公开了出来了(直接从word中黏贴出来的)。
进程
进程的线程
线程堆栈/用户堆栈
进程的内核线程
内核堆栈
一个进程可能在内核线程中等待IO而导致block
P->pgdir
指向进程的页表
为什么不加载到物理的0地址而是0x100000处,因为0xa0000:0x100000是IO设备区
第一个进程initcode创建
main
callkvmalloc 设置CPU内核线程的页表kpgdir
Calluserinit
Callallocproc 初始化
用于进程内核线程执行的 PCB中部分数据结构
调用setupkvm
给进程分配页表pgdir
调用inituvm
调用kalloc分配一个物理页mem
将虚拟地址0映射到该物理页
mappages(0, v2p(mem));为什么V2P请看内存管理
将initcode拷贝该物理页
设置PCB
Tf->eip=0,表示从0地址开始执行
tf->cs
设置为SEG_UCODE和DPL_USER. tf->ds ES SS设置为
SEG_UDATA和 DPL_USER
设置的cs会使代码段的CPL为3,这样用户代码段只可以访问PTE_U的页
tf->eflags = FL_IF允许中断
tf->esp = PGSIZE
4K
用户堆栈的空间最大只有4K???
由于堆栈由高地址向地址(即由4K向0地址),
用户代码和数据都需要占据空间呀,(代码从0地址开始)
allocproc
allocproc
创建进程都会调用allocproc
1 从ptable.proc[NPROC]找到一个可用的PCB
p
2 调用kalloc给进程内核线程分配物理页作为内核堆栈p->kstack,为什么是物理内存?此时和逻辑地址什么关系?
3 设置pid
4 设置内核堆栈
4.1 sp
= p->kstack + KSTACKSIZE- sizeof (struct trapframe); p->tf= sp
设置tf的地址
4.2 sp
-= 4; *(uint*)sp = trapret 设置堆栈内容
4.3 sp
-= sizeof *p->context; p->context= sp
设置context的地址
4.4 p->context->eip
= forkret设置堆栈内容
注意整个过程没有记录内核的ESP?
从下图可以看到p->context就是指向ESP,而且
swtch(&cpu->scheduler,proc->context);
参数就是进程ESP
内核堆栈
-----------
<-- p->kstack 低地址
|
|
|
(empty) |
|
|
|
edi | <-- 第一次为p->context (内核寄存器)第一个元素
|
esi |
|
ebx |
|
ebp |
|
eip | 第一次为forkret函数地址,
p->context的最后一个元素
|
| 第一次此处为trapret(fortret调用结束后的返回的地址).如果是alltraps,此处压入ESP
|
edi |<--p->tf开始(用户寄存器).alltraps压栈结束(pushal压的最后一个reg).trapret手动开始弹出
|
esi |
|
ebp |
|
oesp |
|
ebx |
|
edx |
|
ecx |
|
eax | 由于pushal压入的第一个寄存器
|
GS |
|
FS |
|
ES |
|
DS | 此处开始由alltraps压入
|
trapno | 由vectorX压入的X编号,比如由vector64压入的是64
|
errno | 由vectorX压入的错误号0
trapret手动结束弹出
|
EIP | 产生trap
时CPU自动压入
trapret中iret弹出
|
CS |
产生trap
时CPU自动压入
trapret中iret弹出
|
EFLAGS | 产生trap时CPU自动压入
trapret中iret弹出
|
ESP | 用户模式的ESP.产生trap时CPU自动压入
如果已经在内核模式,就不会压入
|
SS |<-- p->tf结构体最后.用户模式的SS.产生trap时CPU自动压入,如果已经在内核模式,就不会压入
|---------- |
地址为p->kstack +KSTACKSIZE
高地址
栈底
问题:p->context中为什么要保存这些寄存器?
注意这些寄存器是calleesave register?
被调用函数需要保存这些寄存器,所以需要压栈
第一个进程initcode运行
main
Userinit
mpmain
Scheduler
switchuvm(p);
swtch(&cpu->scheduler,proc->context); 切换到用户进程的内核堆栈堆栈
forkret
trapret
用户空间
调度器scheduler()调用的swtch函数首先esp=p->context,然后从进程内核堆栈恢复进程的内核寄存器,即返回到forkret
函数(p->context->eip)。forkret函数返回到trapret
(注意fortret函数中没有局部变量,即没有堆栈操作,所以allocproc在设置内核堆栈时候将trapret放在forkret的后面)。Trapret模拟trap(中断/异常/系统调用)的返回,进而返回到用户模式(trapret中会弹出EIP等)0地址,寄存器值如下
eax
0x0
ecx
0x0
edx
0x0
ebx
0x0
esp
0x1000
ebp
0x0
esi
0x0
edi
0x0
eip
0x0
eflags
0x202
cs
0x23
ss
0x2b
ds
0x2b
es
0x2b
fs
0x0
gs
0x0
可见代码段,数据段和堆栈都位于4K之内
第一次系统调用exec
Initcode.s
首先会系统调用exec加载init程序来替换initcode进程的内存,代码
char init[] = "/init