【MIT 6.S081】笔记
学习MIT 6.S081过程中的一些理解,并非完整的操作系统理论知识笔记
进程
- 内核将每个进程与一个进程标识符pid关联
fork
-
进程创建子进程的函数
-
创建子进程的过程可以视作,父进程的所有资源复制一份,即父子进程拥有独立的资源,子进程与父进程代码相同但是从
fork()
函数之后开始执行 -
进程用
exit
函数退出,子进程向父进程返回 ,子进程的函数返回 -
父进程用
wait
函数等待子进程完成
exec
- 从指定的文件中读取并加载指令,并替代当前调用进程的指令(丢弃了原进程的资源),原进程不需要单独写返回(完全替代了原进程)
I/O
- 系统调用
read
和write
从文件描述符所指的文件中读或者写 n 个字节。read(fd, buf, n)
从fd
读最多 n 个字节(fd
可能没有 n 个字节),将它们拷贝到buf
中,然后返回读出的字节数。
管道
-
进程间通信
-
读和写描述符
用户/内核模式
- 普通权限/特定权限指令
页表
-
内存管理单元(MMU,Memory Management Unit)将指令中的(虚拟)地址转换成物理地址
-
SATP的寄存器会保存页表的物理地址(指向最高一级的页表)
-
index和offset,index用来查找page,offset对应的是一个page中的哪个字节
-
一般三级页表
Traps(X86的中断)
- 用户程序想要进入内核(设备信号
interrupt
、非法操作exception
),OS需要在用户/内核模式间转换 - 系统必须保存寄存器以备将来的状态恢复
Page fault
信息:引起page fault 的内存地址、原因类型、程序计数值
lazy allocation
内核不提前分配物理内存,应用程序使用到新申请的内存时会触发page fault,希望内核能够分配一个内存page,并且重新执行指令。
p->sz
指向stack的顶,heap的底,新旧p->sz
之间就是未分配的部分。
在page fault handler中,通过kalloc函数分配一个内存page;初始化这个page内容为0;将这个内存page映射到user page table中在page fault handler中,通过kalloc函数分配一个内存page;初始化这个page内容为0;将这个内存page映射到user page table中
0 fill on demand
用户程序的地址空间:text区域(指令),data区域(初始化了的全局变量),BSS区域(未被初始化、初始化为0的全局或者静态变量)
物理内存只分配一个page内容是0,虚拟空间的全0的page全映射到这一个物理内存(只读),需要写的时候page fault,重新给个page。
copy-on-write fork(COW fork)
创建子进程时,直接共享父进程的物理内存page,设置子进程的PTE指向父进程对应的物理内存page,PTE设只读防止被修改。触发page fault后拷贝新page给子进程,二者PTE就可设可读可写了。
重新执行用户指令是指调用userret函数(lab 4见过)
PTE最后两位RSW保留给supervisor software(内核),将bit8标识为当前是一个copy-on-write page。
但由于共享,需要对于每一个物理内存page的引用进行计数,确保释放是安全的。
Demand paging
线程
线程就是单个串行执行代码的单元,它只占用一个CPU并且以普通的方式一个接一个的执行指令。
线程的状态包含线程的状态包含:程序计数器、寄存器、栈。
多线程的并行运行:多CPU、一个CPU切换多线程。
XV6内核共享内存,支持内核线程的概念,每个用户进程都有一个内核线程来执行来自用户进程的系统调用。所有的内核线程都共享了内核内存。
每一个用户进程都有独立的内存地址空间,包含一个线程控制用户进程代码指令的执行,用户线程之间没有共享内存。
每个CPU核都有一个线程调度器(Scheduler)。
位于内核的定时器中断处理程序,自愿把CPUyield给线程调度器。
pre-emptive scheduling:将一个RUNNING转换成RUNABLE。
一个用户进程切换到另一个用户进程:
-
第一个用户进程接入内核(一个定时器中断强迫CPU从用户空间进程切换到内核,trampoline代码将用户寄存器保存于用户进程对应的trapframe对象中),保存用户进程的状态并运行第一个用户进程的内核线程(usertrap)。
-
从第一个用户进程的内核线程(包括一些工作和swtch;swtch函数会保存用户进程P1对应内核线程的寄存器至context对象,恢复之前这个CPU核的调度器线程保存的寄存器和stack pointer,之后就在调度器线程的context下执行schedulder函数中;schedulder再调用swtch,保存自己的寄存器到调度器线程的context对象,找到第二个进程之前保存的context,恢复其中的寄存器,第二个进程之前的swtch函数会被恢复,并返回到进程P2所在的系统调用或者中断处理程序中)切换到第二个用户进程的内核线程。
-
之后,第二个用户进程的内核线程暂停自己,并恢复第二个用户进程的用户寄存器。
-
最后返回到第二个用户进程继续执行。
用户进程的内核线程context对象保存在用户进程对应的proc结构体中;调度器线程没有对应的进程和proc结构体,所以调度器线程的context对象保存在cpu结构体中(对应一个CPU核,有一个context字段)。
context保存在trapframe中不是不可以,但是设计成两个也许是简化/让代码清晰。