XV6源代码阅读--进程与内存管理

由于工作和兴趣爱好的关系,接触了不少实时操作系统, 一般来说实时操作系统基本没有进程的概念了,无非是任务堆栈的切换。

一直对Linux,Windows这种带有进程的OS,很好奇,无奈,LINUX代码很庞大,很难整体把握。

所以去年一直在寻找带支持进程的OS, 要求简单,易懂,确实真找不到。最后找到了MIT教学用VX6,便深深的着迷了。

自从调试了VX6的源代码,发现用MMU来管理进程真是复杂,怪不得很少能找到支持进程的OS了。

通过参考了很多关于XV6的中文的blog,发现几乎没有对进程管理和MMU做阐述的。

于是我将去年的笔记部分公开了出来了(直接从word中黏贴出来的)。

 

进程

 

 

 

进程的线程  线程堆栈/用户堆栈 

进程的内核线程 内核堆栈

一个进程可能在内核线程中等待IO而导致block

P->pgdir 指向进程的页表

 

 

 

为什么不加载到物理的0地址而是0x100000处,因为0xa0000:0x100000IO设备区

 

 

第一个进程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_UCODEDPL_USER. tf->ds ES SS设置为 SEG_UDATA DPL_USER

         设置的cs会使代码段的CPL3,这样用户代码段只可以访问PTE_U的页

tf->eflags = FL_IF允许中断

tf->esp = PGSIZE  4K  

用户堆栈的空间最大只有4K??? 由于堆栈由高地址向地址(即由4K0地址), 用户代码和数据都需要占据空间呀,(代码从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自动压入     trapretiret弹出

|    CS    | 产生trap CPU自动压入     trapretiret弹出

|   EFLAGS | 产生trapCPU自动压入     trapretiret弹出

|    ESP   | 模式的ESP.产生trapCPU自动压入 如果已经在内核模式,就不会压入

|    SS    |<-- p->tf结构体最后.模式的SS.产生trapCPU自动压入,如果已经在内核模式,就不会压入

|---------- | 地址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\0";

char *argv[] = { init, 0 };

start:

Push argv

Push init

Push 0  应该是argv的结束符

Eax = 7 (exec系统调用号)

int 64 EXEC系统调用执行init程序

 

执行int 64之前


eax            0x7    

ecx            0x0     

edx            0x0    

ebx            0x0    

esp            0xff4  

ebp            0x0    

esi            0x0      

edi            0x0     

eip            0x11   

eflags         0x202  

cs             0x23  

ss             0x2b  

ds             0x2b  

es             0x2b  

fs             0x0     

gs             0x0    


0xff4处内存为:       0x00000000     0x0000001c     0x00000024    

print cpus[0].proc->kstack 0x8dfff000

可见内核堆栈使用的ESP=proc->kstack + KSTACKSIZE=0x8dfff000+0x1000=0x8e000000

 

 

 

 

 

执行int 64之后(导致进入vector64,建立trapframe,gdb调试已经执行完push 0)


eax            0x7    

ecx            0x0     

edx            0x0    

ebx            0x0    

esp            0x8dffffe8 

ebp            0x0    

esi            0x0      

edi            0x0     

eip            0x80106c10        

eflags         0x202  

cs             0x8     

ss             0x10  

ds             0x2b  

es             0x2b  

fs             0x0     

gs             0x0    


内核堆栈trapframe的内容如下

0x8dffffe8:        0x00000000  (push 0) errorno

                            0x00000013  eip

                            0x00000023  CS

                            0x00000202  eflags

0x8dfffff8:         0x00000ff4       用户空间esp

0x0000002b  用户空间ss

0x8e000000:

 

接下来的sys_execexec 都是在这个内核堆栈上。

 

exec

exec (char *path, char **argv)

主要执行以下内容

1.      ELF文件头,检测是否有效

2.      调用setupkvm分配页表pgdir 为什么要重新分配一个页表呢?

3.      分析elf各个段

a)        根据段大小,调用allocuvm为这些段分配页,注意仔细看代码

b)        调用loaduvm加载ELF的段到该段的虚拟地址对应的内核地址(虚拟地址转换成物理地址,p2v转换成内核地址) ,为什么不直接放到进程虚拟地址(对应的页已经分配了啦??),因为页表还没有加载呢

4.      调用allocuvm分配2个页,这两个页的虚拟地址接着之前的虚拟地址。

5.      调用clearpteu 清除第一个页的PTE_U标志(进程空间无法访问,应该作为guard page)

6.      Sp =sz sz为整个程序空间的长度包括堆栈,并根据argv 设置当前的堆栈

 

Init堆栈要如下:

低地址Y:  ustack[0] PC=0xffffffff

       ustack[1]字符串的个数N

            ustack[2] 内容为char *argv[]的地址X

         X:  ustack[3+0] 内容为argv[0]地址

            ustack[3+1] 内容为argv[1]地址

       ustack[3+2] 内容为argv[2]地址

       ustack[3+N] 内容为argv[3+N]地址

            ustack[3+N+1 ] 内容为0

argv[N]整个字符串可能好多字节 4字节对齐

argv[2]整个字符串可能好多字节 4字节对齐

argv[1]整个字符串可能好多字节 4字节对齐

高地址argv[0]整个字符串 可能好多字节 4字节对齐

可以的得知initcodeinitargv都被拷贝到堆栈了。 X处应该不会反回的吧,这些都浪费了。。

 

但是由于新的页表还没有加载,当前堆栈是无法直接访问的,如何将进程空间的initargv数据拷贝到堆栈中的呢,请参考 copyout函数

7.      设置path

8.      oldpgdir = proc->pgdir;  保存原来的页表

9.      proc->pgdir = pgdir; 更新进程的页表

10.  proc->sz = sz; 重新设置进程整个空间的大小

11.  proc->tf->eip =elf.entry;  设置eip的入口

12.    proc->tf->esp = sp; 设置堆栈spsp的值为地址Y

13.    switchuvm(proc);

14.    freevm(oldpgdir); 并返回

 

 

进程调度

上下文切换

从进程的内核线程 切换到 CPU的调度线程/scheduler thread

CPU的调度线程切换到 新进程的内核线程

 

CPU的调度线程/CPU内核线程 用于执行调度器

 

Scheduler

         得到状态RUNABLE的进程proc

Switchuvm(proc) 加载进程的页表

         设置状态为RUN

         Swtch(&cpu->scheduler,proc->context) CPU内核线程切换到进程的内核线程          

         Switchkvm

 

 

swtch

进程的内核线程会调用swtch来保存上下文(内核堆栈和内核寄存器) 返回到 调度器的上下文。

swtch用于切换内核堆栈和内核寄存器

Swtch调用代码如下:

extern struct cpu *cpuasm("%gs:0");       // Thiscpu.

extern struct proc *procasm("%gs:4");     // Currentproc on this cpu.

swtch(&cpu->scheduler,proc->context);

返汇编如下:

mov %gs:0x4,%eax      proc的内容eax

mov 0x1c(%eax),%eax   proc->context -> EAX

mov %eax,0x4(%esp)    proc->context 压入堆栈

mov %gs:0x0,%eax

add $0x4,%eax

mov %eax,(%esp)      &cpu->scheduler 压入堆栈

call swtch

 

swtch: // swtch(struct context **old,struct context *new);

 movl 4(%esp), %eax  ;eax内容为&cpu->scheduler

 movl 8(%esp), %edx  ;edx 内容为proc->context

 

 pushl %ebp

 pushl %ebx

 pushl %esi

 pushl %edi

 

 movl %esp, (%eax)  ;将当前内核堆栈ESP压入 cpu->scheduler  cpu->scheduler=esp

 movl %edx, %esp   ;proc->context 赋值 当前内核堆栈ESP esp = proc->context

 

 popl %edi

  popl%esi

 popl %ebx

 popl %ebp

  ret

 

问题:

Swtch 保存cpu内核线程的寄存器,然后就没有了。

如果下次切换到CPU内核线程,会出现什么情况呢?

         因为刚开始CPU内核线程使用的堆栈为stack, swtch只是将一些寄存器保存到cpu->scheduler没有保存ESP.

         CPU内核线程恢复时候,直接从cpu->scheduler上恢复寄存器,还能正确找到原来的堆栈stack吗?

回答:

         其实swtch已经保存了esp: cpu->scheduler =esp, cpu->scheduler指向CPU内核线程的内核堆栈栈顶

         下次CPU内核线程恢复运行时,esp=cpu->cheduler,即可从CPU内核堆栈上恢复寄存器

 

 

 

总结

CPU内核线程/调度线程

执行 scheduler()

1.      选择就绪的进程p, proc =p

2.      switchuvm(proc)

3.      swtch(&cpu->scheduler,proc->context)

4.      switchkvm

5.      proc = 0, 1开始重新执行

永远不会返回

可见proc表示当前进程的PCB

 

第一个进程的运行

mainc

调用mpmain

                   调用scheduler

                            调用swtch 保存CPU内核线程的ESP和寄存器,并切换到一个进程的的内核线程

                   返回到forkret

         返回到trapret

返回到用户进程

 

时间片轮转调度

 

定时中断 T_IRQ0 + IRQ_TIMER=32

 

情况1:普通情况,进程运行过程中产生定时中断

vector32 jmp alltraps      

         trap

                   walkup

yield

sched

swtch 切换到CPU的内核线程

返回到scheduler()switchkvm,选择kpgdir作为页表,

                            继续执行的scheduler()的第5步骤,又开始执行scheduler()的第1,2,3步骤(选择新的进程)

                            执行scheduler():swtch 切换到新进程的内核寄存器和内核堆栈

                   返回到新进程sched():swtch函数调用的下条指令

         返回到yield

返回到trap

         返回到alltraps

返回到新进程用户空间

 

进程调用Sched(),sched调用swtch切换到CPU调度线程

(先保存进程的寄存器和swtch函数的返回地址)

CPU调度线程调用Sched(),sched调用swtch切换到进程

(先保存CPU调度进程的寄存器和swtch函数的返回地址)

 

其实可以看到scheduler函数和sched函数是互相切换运行,又称co-routines

 

CPU处于IDLE状态(没有可运行的进程), IDLE状态的CPUscheduler函数一开始就上锁, 正在运行进程的其他cpu无法实施上下文切换或者进程相关的系统调用,也无法设置一个进程为可运行以至于让IDLECPU跳出调度循环。

接着关于lock,没看懂

 

 

情况2:在 cpu内核线程运行时(在执行scheduler()函数),产生定时中断

vector32

         Alltraps

                   Trap但是,proc0

         直接退出中断

 

额外说明:

比如定时中断发生在 情况1某个步骤,会不会产生中断嵌套呢?

应该不会,因为执行了swtch之后,内核堆栈切换到CPU内核堆栈上了,而中断使用的堆栈是什么?

经过调试发现,进入中断时候ESP有时0XFFFFB4(B4+4C(trapframe结构体的长度,由中断自行压入)),应该打断CPU内核线程

 有时0XFF1D60,应该是打断了进程内核线程 ,第一个进程初始化的时 kstack0xff1000,内核SP0Xff2000,那为什么不是0XFF1FB4呢?感觉是嵌套了,估计: 在内核模式下运行(中断,系统调用,异常,反正都是trap)trap模式,ESP已经从TSS中取出,如果在内核模式下再出现中断,那么不会重新从TSSESP

估计正确,根据某次调试结果:

进入vector32 esp0XFF0D60,强制跳过yield,vector32返回后,ESP0xff0D70,即压入16byte4个寄存器

(根据文档是errno EIP CS EFLAGS ,但是errorno不是由VECTOR32压入的吗?

调试:

某次ESP0xff0dc0

0xff0dc0: 0x103cf0(EIP) 0X08(CS)0X202(EFLAGS) 0X10140C()

向量返回后 ESP0xff0dcc  正好是弹出了3个寄存器

 

 

 

LOCKING

内存管理

X86页式管理

页表/PAGE TABLE

存放页表项, 2^20=1M个页表项/PAGE TABLEENTRY/PTE,

每个页表项指向物理页面4K        所以一个页表能表示4K*1M=4G

在页表中每个页表项占用4BYTE,所以一个页表占用4*1M=4M byte

 

PTE

一个PTE 中,PHYSICAL PAGE NUMBER /PPN占用20bit

线性地址的高20bit,低12bit作为页内偏移地址

 

X86的页表具体如下:

一个页表Page DirectoryTable/PDT 包好 1024 Page Table Page/PTP/Page Directory Entry

每个PDP 包含1024 32bitPTE

分页硬件将虚拟地址 10bit 用于指定PDE,如果该PDEPTE_P置位,继续,否则fault

分页硬件将虚拟地址中间10bit 用于指定PTE,如果该PTEPTE_P置位,继续,否则fault

 

 

 

线性地址转换原理,

CPU得到一个虚拟地址后,首先通过段式管理得到线性地址

取出线性地址的高20bits后,

CR3 页表寄存器

         页表项/PTE=页表基地址/PT+(线性地址>>20)

         物理页面=页表项的高20bit

         物理地址 = 物理页面+线性地址的低12bits

 

每个PTE中含有标志位来表示对应的线性地址的访问权限

PTE_P  表示当前PTE是否有效,如果访问无效的PTE会导致fault

PTE_W 可写,不可写的话只能读和执行

PTE_U  用户程序可以访问,否则只能内核程序访问

 

每个进程都有一个独立的页表,进程切换时候需要切换页表.

 

 

VX6内存管理

不同进程将用户空间转换到不同的物理页,所以进程有私有的用户空间,进程空间从0地址开始

每个进程的页表都映射内核空间(KERNBASE以上的地址),内核空间的PTE不会设置PTE_U

即虚拟地址XUKERNBASE~KERNBASE+PHYSTOP映射到0~PHYSTOP。理由是

1.      内核可以使用自己的代码和数据,不明白什么意思?

2.      内核有时会写物理page. 比如分配页表时,物理地址如果映射可以预测的虚拟地址,就很方便了。不明白?

猜想: 假设内核给某进程的虚拟地址0X0分配的物理页肯定在0~PHYSTOP范围内,那么内核可以直接访问该地址(位于XUKERNBASE~KERNBASE+PHYSTOP范围), 也可以该进程空间访问0x0(实际是通过页表通过访问该物理页)

缺点:

Xv6不能使用超过2G的物理内存. WINXP好像也不能使用2G的内存的。

 

每个进程的页表包含的用户空间(进程空间)和内核空间的映射。这样系统调用和中断就不用切换页表。

Xv6保证每个进程使用独立的用户空间(利用PTE_U实现), 保证每个进程虚拟地址连续并且开始于0地址.(利用页表将连续的虚拟地址映射到不连续的物理页上)

 

地址空间

PA物理地址地址范围 0~PHYSTOP

UVA进程空间虚拟地址 0~KERNBASE

KA内核空间虚拟地址 KERNBASE~ KERNBASE+PHYSTOP其中

         KERNBASE~KERNBASE+EXTMEMBIOS /IO

         KERNBASE+EXTMEM~KERNBASE+PHYSTOP 映射在 0~PHYSTOP

                  KERNBASE+EXTMEM ~ data xv6内核的text .rodata

                  data ~ end  xv6内核的dataBSS

                  end~ KERNBASE+PHYSTOP 分配其他用(如用户空间)

 

注意不管怎么样,进程空间虚拟地址和内核空间虚拟地址都是应设在物理地址0~PHYSTOP

 

进程空间虚拟地址转换为内核空间虚拟地址方法:

进程空间虚拟地址->进程页表->物理地址

内核空间虚拟地址=p2v(物理地址) ,当然内核空间也可以用进程空间虚拟地址来访问

 

物理页分配

内核从end~ KERNBASE+PHYSTOP这个内核空间虚拟地址范围内来分配页,所以这个页地址还是虚拟地址.

一般来说,这个页可以拿来直接使用了。但是什么情况下需要转换为物理地址:

当要生成页表的物理映射的时候,就需要v2p转换为物理地址

mem = kalloc();分配页

if(mappages(页表, 虚拟地址, 地址范围, v2p(mem)物理地址) 转换为物理地址

   

关于entry

所有的物理地址映射好之后, 分配器才可以初始化了freelist。但是创建一个页表(并初始化页表内的映射)需要分配器来分配物理页。 ---太费解了。。。

Xv6entry是利用独立的分配器来解决这个问题,尽管不支持释放页,受限于4M,但是足够分配一个内核页表kpgdir

 

 

Entry 使用的页表是entrypgdir,页大小为4M (cr4|=CR4_PSE)

问题1:进入main程序特别是kvmalloc之后,使用的4K, 这是如何区分的呢?

         CR4.PSE 置为 1 ,将开启 4M page,但在具体由相应的 page-translation tables 的属性来决定哪个是 4M pages,哪个是 4K pages

         查看 entrypgdir 数组一个元素 [0] = (0)| PTE_P | PTE_W | PTE_PS, PTE_PS应该就表示4MPAGE

 

Main

         kinit1(end,P2V(4*1024*1024)); end.text.data.bss之后的地址, P2V(4*1024*1024)0X80400000

         kvmalloc

         kinit2(P2V(4*1024*1024),P2V(PHYSTOP)); 0X80400000

 

为什么要两次调用?

Kinit2 使能locking

物理内存管理
kinit1/ kinit2

kinit1/ kinit2(void *vstart, void *vend)

         freerange(vstart,vend)

freerange

freerange(void *vstart, void *vend)

将内核空间虚拟地址从vstartvend这个范围以4K为单位连接起来 kmem.freelist

 

Kalloc

kmem.freelist分配一个页, 注意该页使用的是内核空间虚拟地址,要使用V2P才能转换成真正的物理地址。

没有特殊说明的话,就直接说明用kalloc函数分配一个物理页

 

虚拟内存管理/用户空间

Heapstack之上,可以使用sbrk扩展

Stack占用一个page,内容如下图.

         Xv6stack下面有一个guard page(未映射),stack溢出时(堆栈向下增长,使用guard page),导致页未映射异常。

allocuvm

allocuvm(pde_t *pgdir, uint oldsz, uintnewsz) 将进程空间内存由oldsz扩展到newsz

即需要额外分配 newsz-oldsz 空间

基本原理

1.      调用kalloc分配4K物理页, oldsz开始的4K映射到该物理页(这样进程的虚拟地址就连续了)

2.      oldsz+=4K

3.      oldsz < newsz? ,1,否则退出

loaduvm

loaduvm(pde_t *pgdir, char *addr, structinode *ip, uint offset, uint sz) 加载elf段到用户空间

1.      判断addr是否4k对齐

2.      根据addr调用walkpgdir寻找对应页的pte

3.      根据pte计算物理地址pa

4.      根据ip,offset(段在elf文件中的偏移),长度调用readi读取信息到 内核空间虚拟地址p2v(pa)

为什么不直接读到addr,请看exec

copyout

copyout(pde_t *pgdir, uint va, void *p,uint len) 从地址p处拷贝数据到va(包含va地址映射的页表pgdir非当前进程页表)

1.      pa =uva2ka(pgdir, (char*)va);根据pgdir计算vapa(其实是内核空间虚拟地址)

2.      p处数据拷贝到pa(因为是内核空间虚拟地址,所以内核可以访问)

 

uva2ka(pde_t *pgdir, char *uva)

1.      调用walkpgdir得到uvapte

2.      pte PTE_PPTE_U必须存在

3.      p2v(PTE_ADDR(*pte)) 根据PTE计算得到物理地址,在转换内核空间虚拟地址

虚拟内存管理/内核

链接脚本中定义了链接地址data (text段结束地址) end(bss段结束)

Kpgdir CPU内核线程使用的页表

 

 

setupkvm

         调用kalloc分配一个物理页

         调用mappages 设置页表,利用kmap映射区域如下

                   virt;     phys_start;    phys_end;  perm; kmap[] = {

KERNBASE, 0,             EXTMEM,    PTE_W // I/O space

KERNLINK, V2P(KERNLINK), V2P(data),0},     // kern text+rodata

data,     V2P(data),     PHYSTOP,  PTE_W}, // kern data+memory

DEVSPACE, DEVSPACE,      0,        PTE_W}, // more devices

虚拟地址到物理地址

虚拟地址0~KERNBASE即用户空间(text+data+stack+heap),对应的物理内存有内核来分配

虚拟地址KERNBASE~KERNBASE+EXTMEM映射到0~EXTMEMI/O地址空间 BIOS

虚拟地址KERNBASE+EXTMEM~data映射到EXTMEM~V2P(data) 内核textro

                 虚拟地址data~KERNBASE+PHYSTOP映射到V2P(data)~PHYSTOP,内核dataBSS段和可供内核分配的内存

        虚拟地址0xfe000000~0直接映射 (devices such as ioapic)

       

        物理地址到虚拟地址

        0~EXTMEMI/O地址空间 对应虚拟地址KERNBASE~KERNBASE+EXTMEM

        EXTMEM~V2P(data) 对应虚拟地址KERNBASE~KERNBASE+EXTMEM内核textro

        V2P(data)~PHYSTOP对应虚拟地址data~KERNBASE+PHYSTOP内核data,BSS段和可供内核分配的内存

        0xfe000000~0 对应虚拟地址0xfe000000~0 (devices such as ioapic)

 

物理地址V2P(end)~PHYSTOP(对应虚拟地址end~P2V(PHYSTOP)),这段内存内核用于分配内存等(包括用户空间)

 

Kvmalloc

kpgdir =setupkvm();

       switchkvm();

 

switchkvm

        使用v2p(kpgdir)作为CPU内核线程页表

 

Switchuvm

设置TSSss寄存器

设置TSSespproc->kstack +KSTACKSIZE 即设置内核堆栈esp

加载该TSS

加载进程页表pgdir

Trap处理

中断由内核来处理

进入ISR要保存寄存器, 从用户模式切换内核模式

退出ISR要恢复寄存器, 从内核模式切换用户模式

 

4级保存0~3, 内核模式使用0,用户模式使用3,当前的保护级别位于CSCPL字段内

中断子程序定义在IDT中,IDT256的条目,每条包含CSeip信息

0-31为软件中断 

32-63 硬件中断 INTERRUPT GATE 清除EFLAGFL标志  32 定时中断

64 系统调用   TRAP GATE,即不会清除EFLAGFL标志, 系统调用中允许中断。DPL_USER

不允许app调用其他中断(比如 int 非系统调用号),否则会进入异常 VECTOR[13].

 

 

 

Int n调用系统调用,n表示中断子程序在IDT的索引。运行系统调用指令,CPU的步骤如下:

1.      IDT中取出第n个索引的描述符

2.      检查CS中的CPL <= DPL, DPL在描述符中

3.      如果目标段选择字的PL ,保存ESPss CPU内部寄存器中

4.      TSS/任务段描述符加载ssESP(内核模式的)

5.      压栈原来的ss,原来的ESP (即用户模式的ESP,SS)

6.      压栈EFLAGS,CS,EIP,error(视情况)

7.      清空eflags的某些位,根据IDT描述符来设置CS,EIP

 

如果CPU在运行内核模式(已经在特权模式)不会做5步骤中压栈SS,ESP

 

中断使用的堆栈是内核堆栈。 TSS 在哪里设置的?switchuvm中,

每个进程都会公用TSS段,但是不同进程时,设置该TSS段的为进程的内核堆栈

 

Trap发生, CPU如果在内核模式,不需要做4,5步骤

 

 

系统调用

系统调用函数的代码如下:

exec:

mov   $0x1,%eax

int   64

ret

 

Vector64 jmp alltraps

         Trap

                   Syscall

                            sys_exec

                                     获得系统调用参数

                                     exec

 

alltraps中保存完用户寄存器后,开始设置DS,eS为内核数据段,之后都是内核操作啦,压入esp(指向当前的trap frame)作为C函数trap的参数,然后调用trap

 

trap

         如果是系统调用,首先从proc->tf->eax获得系统调用号 (EAX=7exec)

执行系统调用(首先获得系统调用的参数)

         将系统调用的返回值赋值给proc->tf->eax,这样作为系统调用函数的返回值返回给用户空间

 

Fork

*np->tf = *proc->tf; 拷贝整个trapframe

 

中断

设置eflagFL会允许中断

CLI 会清空EFLAGSFL

STI会设置EFLAGSFL

 

定时中断IRQ 0 使用vector32向量

Vector32 jmp alltrap

         Trap

                   wakeup

你可能感兴趣的:(OS/操作系统)