由于工作和兴趣爱好的关系,接触了不少实时操作系统, 一般来说实时操作系统基本没有进程的概念了,无非是任务堆栈的切换。
一直对Linux,Windows这种带有进程的OS,很好奇,无奈,LINUX代码很庞大,很难整体把握。
所以去年一直在寻找带支持进程的OS, 要求简单,易懂,确实真找不到。最后找到了MIT教学用VX6,便深深的着迷了。
自从调试了VX6的源代码,发现用MMU来管理进程真是复杂,怪不得很少能找到支持进程的OS了。
通过参考了很多关于XV6的中文的blog,发现几乎没有对进程管理和MMU做阐述的。
于是我将去年的笔记部分公开了出来了(直接从word中黏贴出来的)。
进程的线程 线程堆栈/用户堆栈
进程的内核线程 内核堆栈
一个进程可能在内核线程中等待IO而导致block
P->pgdir 指向进程的页表
为什么不加载到物理的0地址而是0x100000处,因为0xa0000:0x100000是IO设备区
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
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? 被调用函数需要保存这些寄存器,所以需要压栈
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之内
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_exec和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字节对齐
可以的得知initcode中init和argv都被拷贝到堆栈了。 从X处应该不会反回的吧,这些都浪费了。。。
但是由于新的页表还没有加载,当前堆栈是无法直接访问的,如何将进程空间的init和argv数据拷贝到堆栈中的呢,请参考 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; 设置堆栈sp,sp的值为地址Y
13. switchuvm(proc);
14. freevm(oldpgdir); 并返回
上下文切换
从进程的内核线程 切换到 CPU的调度线程/scheduler thread
从CPU的调度线程切换到 新进程的内核线程
CPU的调度线程/CPU内核线程 用于执行调度器
得到状态RUNABLE的进程proc
Switchuvm(proc) 加载进程的页表
设置状态为RUN
Swtch(&cpu->scheduler,proc->context) 由CPU内核线程切换到进程的内核线程
Switchkvm
进程的内核线程会调用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状态的CPU的scheduler函数一开始就上锁, 正在运行进程的其他cpu无法实施上下文切换或者进程相关的系统调用,也无法设置一个进程为可运行以至于让IDLECPU跳出调度循环。
接着关于lock,没看懂
情况2:在 cpu内核线程运行时(在执行scheduler()函数),产生定时中断
vector32
Alltraps
Trap但是,proc为0,
直接退出中断
额外说明:
比如定时中断发生在 情况1某个步骤,会不会产生中断嵌套呢?
应该不会,因为执行了swtch之后,内核堆栈切换到CPU内核堆栈上了,而中断使用的堆栈是什么?
经过调试发现,进入中断时候ESP有时0XFFFFB4(B4+4C(trapframe结构体的长度,由中断自行压入)),应该打断CPU内核线程
有时0XFF1D60,应该是打断了进程内核线程 ,第一个进程初始化的时 kstack为0xff1000,内核SP为0Xff2000,那为什么不是0XFF1FB4呢?感觉是嵌套了,估计: 在内核模式下运行(中断,系统调用,异常,反正都是trap)是trap模式,ESP已经从TSS中取出,如果在内核模式下再出现中断,那么不会重新从TSS取ESP。
估计正确,根据某次调试结果:
进入vector32 ,esp为0XFF0D60,强制跳过yield,vector32返回后,ESP为0xff0D70,即压入16个byte,4个寄存器
(根据文档是errno, EIP, CS, EFLAGS ,但是errorno不是由VECTOR32压入的吗?
调试:
某次ESP为0xff0dc0
0xff0dc0: 0x103cf0(EIP) 0X08(CS)0X202(EFLAGS) 0X10140C()
向量返回后 ESP为0xff0dcc 正好是弹出了3个寄存器
页表/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 个 32bit的PTE
分页硬件将虚拟地址 头10bit 用于指定PDE,如果该PDE的PTE_P置位,继续,否则fault
分页硬件将虚拟地址中间10bit 用于指定PTE,如果该PTE的PTE_P置位,继续,否则fault
线性地址转换原理,
CPU得到一个虚拟地址后,首先通过段式管理得到线性地址
取出线性地址的高20bits后,
CR3 页表寄存器
页表项/PTE=页表基地址/PT+(线性地址>>20)
物理页面=页表项的高20bit
物理地址 = 物理页面+线性地址的低12bits
每个PTE中含有标志位来表示对应的线性地址的访问权限
PTE_P 表示当前PTE是否有效,如果访问无效的PTE会导致fault
PTE_W 可写,不可写的话只能读和执行
PTE_U 用户程序可以访问,否则只能内核程序访问
每个进程都有一个独立的页表,进程切换时候需要切换页表.
不同进程将用户空间转换到不同的物理页,所以进程有私有的用户空间,进程空间从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内核的data段BSS段
end~ KERNBASE+PHYSTOP 分配其他用(如用户空间)
注意不管怎么样,进程空间虚拟地址和内核空间虚拟地址都是应设在物理地址0~PHYSTOP
进程空间虚拟地址转换为内核空间虚拟地址方法:
进程空间虚拟地址->进程页表->物理地址
内核空间虚拟地址=p2v(物理地址) ,当然内核空间也可以用进程空间虚拟地址来访问
物理页分配
内核从end~ KERNBASE+PHYSTOP这个内核空间虚拟地址范围内来分配页,所以这个页地址还是虚拟地址.
一般来说,这个页可以拿来直接使用了。但是什么情况下需要转换为物理地址:
当要生成页表的物理映射的时候,就需要v2p转换为物理地址
mem = kalloc();分配页
if(mappages(页表, 虚拟地址, 地址范围, v2p(mem)物理地址) 转换为物理地址
所有的物理地址映射好之后, 分配器才可以初始化了freelist。但是创建一个页表(并初始化页表内的映射)需要分配器来分配物理页。 ---太费解了。。。
Xv6在entry是利用独立的分配器来解决这个问题,尽管不支持释放页,受限于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应该就表示4M的PAGE
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(void *vstart, void *vend)
freerange(vstart,vend)
freerange(void *vstart, void *vend)
将内核空间虚拟地址从vstart到vend这个范围以4K为单位连接起来 kmem.freelist
从kmem.freelist分配一个页, 注意该页使用的是内核空间虚拟地址,要使用V2P才能转换成真正的物理地址。
没有特殊说明的话,就直接说明用kalloc函数分配一个物理页
Heap为stack之上,可以使用sbrk扩展
Stack占用一个page,内容如下图.
Xv6在stack下面有一个guard page(未映射),当stack溢出时(堆栈向下增长,使用guard page),导致页未映射异常。
allocuvm(pde_t *pgdir, uint oldsz, uintnewsz) 将进程空间内存由oldsz扩展到newsz
即需要额外分配 newsz-oldsz 空间
基本原理
1. 调用kalloc分配4K物理页, 将oldsz开始的4K映射到该物理页(这样进程的虚拟地址就连续了)
2. oldsz+=4K
3. oldsz < newsz? 是,走1,否则退出
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(pde_t *pgdir, uint va, void *p,uint len) 从地址p处拷贝数据到va(包含va地址映射的页表pgdir非当前进程页表)
1. pa =uva2ka(pgdir, (char*)va);根据pgdir计算va的pa(其实是内核空间虚拟地址)
2. 将p处数据拷贝到pa(因为是内核空间虚拟地址,所以内核可以访问)
uva2ka(pde_t *pgdir, char *uva)
1. 调用walkpgdir得到uva的pte
2. 该pte 的 PTE_P和PTE_U必须存在
3. p2v(PTE_ADDR(*pte)) 根据PTE计算得到物理地址,在转换内核空间虚拟地址
链接脚本中定义了链接地址data (text段结束地址) end(bss段结束)
Kpgdir CPU内核线程使用的页表
调用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~EXTMEM即I/O地址空间 BIOS
虚拟地址KERNBASE+EXTMEM~data映射到EXTMEM~V2P(data) 内核text和ro段
虚拟地址data~KERNBASE+PHYSTOP映射到V2P(data)~PHYSTOP,内核data段BSS段和可供内核分配的内存
虚拟地址0xfe000000~0直接映射 (devices such as ioapic)
物理地址到虚拟地址
0~EXTMEM即I/O地址空间 对应虚拟地址KERNBASE~KERNBASE+EXTMEM
EXTMEM~V2P(data) 对应虚拟地址KERNBASE~KERNBASE+EXTMEM内核text和ro段
V2P(data)~PHYSTOP对应虚拟地址data~KERNBASE+PHYSTOP内核data,BSS段和可供内核分配的内存
0xfe000000~0 对应虚拟地址0xfe000000~0 (devices such as ioapic)
物理地址V2P(end)~PHYSTOP(对应虚拟地址end~P2V(PHYSTOP)),这段内存内核用于分配内存等(包括用户空间)
kpgdir =setupkvm();
switchkvm();
使用v2p(kpgdir)作为CPU内核线程页表
设置TSS的ss寄存器
设置TSS的esp为proc->kstack +KSTACKSIZE 即设置内核堆栈esp
加载该TSS段
加载进程页表pgdir
中断由内核来处理
进入ISR要保存寄存器, 从用户模式切换内核模式
退出ISR要恢复寄存器, 从内核模式切换用户模式
4级保存0~3, 内核模式使用0,用户模式使用3,当前的保护级别位于CS的CPL字段内
中断子程序定义在IDT中,IDT中256的条目,每条包含CS,eip信息
0-31为软件中断
32-63 硬件中断 INTERRUPT GATE 清除EFLAG的FL标志 32 定时中断
64 系统调用 TRAP GATE,即不会清除EFLAG的FL标志, 系统调用中允许中断。DPL_USER。
不允许app调用其他中断(比如 int 非系统调用号),否则会进入异常 VECTOR[13].
Int n调用系统调用,n表示中断子程序在IDT的索引。运行系统调用指令,CPU的步骤如下:
1. 从IDT中取出第n个索引的描述符
2. 检查CS中的CPL <= DPL, DPL在描述符中
3. 如果目标段选择字的PL
4. 从TSS/任务段描述符加载ss和ESP(内核模式的)
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=7是exec)
执行系统调用(首先获得系统调用的参数)
将系统调用的返回值赋值给proc->tf->eax,这样作为系统调用函数的返回值返回给用户空间
*np->tf = *proc->tf; 拷贝整个trapframe
设置eflag中FL会允许中断
CLI 会清空EFLAGS中FL
STI会设置EFLAGS中FL
定时中断IRQ 0 使用vector32向量
Vector32 jmp alltrap
Trap
wakeup