My earliest test program was written to use one thread to write the letter A to the screen. The other thread wrote the letter B.
—— just for fun
linus在屏幕上交替打印A和B,验证操作系统进程切换是否能正常工作
emperorOS进程管理
上节我们实现了emperorOS的中断框架,在此基础之上,可以实现一个简单的进程管理器,PCB(Process Control Block) 进程控制块,是描述进程的数据结构,emperorOS里,我们定义proc为
1 | struct proc { |
- state 进程状态
- pid 进程号
- parent 父进程号
- context 上下文
- name 进程名
- pgd 页描述符(1MB section页)
进程调度实现
有两种调度方式,协作式Cooperative和抢占式Preemptive。协作式操作系统只能等待进程主动停止运行释放cpu,抢占式操作系统是由操作系统调度进程的运行,系统可以剥夺进程运行的权利。抢占式内核是指系统支持内核抢占。emperorOS所有进程共用同一个内核栈,系统在处于内核态会关闭中断,不支持内核抢占。
kernel完成启动后,首先proc_init初始化系统的第一个进程,然后proc_schd开始调度进程。proc_schd遍历procs列表,找到运行状态的进程,更新curproc,更新user页表,trap_return储存当前cpu上下文到context_schd。最后进入curproc的cpu上下文curproc-context,从而进入用户进程。
在用户进程运行当中,当发生外部中断或者异常,cpu进入特权模式,trap_enter储存cpu上下文到curproc-context。然后,进行相应的异常处理或者系统调用。最后,schd回到context_schd上下文,再次回到proc_schd遍历procs列表。
Linux内核是抢占式内核,kernel代码是可重入的,函数使用的所有变量都保存在调用堆栈,锁的作用就是kernel中非抢占区域的标记。emperorOS是非抢占内核,内核代码可以一直运行到完成而不被中断,无需要求内核代码可重入,也无需为每一个进程分配内核栈。
如果自旋锁已经被其它线程获取,那么另一个线程在获取锁的时候将循环等待。自旋锁只能锁当前的核,单cpu并且内核不可抢占的时候,自旋锁退化为空操作。
虚拟内存管理
当代操作系统内存分配庞大而繁杂,精确而动态,大部分32位操作系统都拥有两级页表,每页的粒度为4kb。
emperorOS用户程序只占虚拟内存最低1MB的空间,因此每次切换到用户进程前,loaduvm要替换当前用户进程相应的页描述符pgd
emperorOS系统调用
要完成两个进程交替打印ab,我们至少要实现fork和exec两个系统调用,fork创建子进程,exec从文件中载入进程。
用户程序init
系统启动后,载入elf程序bin/init
1 | .globl start |
创建一个子进程,父进程打印a,子进程打印b
用户程序printab
printab.S
1 | .globl start |
也可以用C程序实现,exec函数来加载elf文件
printab.c
1 | void printa() { |
不需要c启动初始化代码,加nostartfiles参数编译。得到elf文件
1 | arm-none-eabi-gcc -g -O0 -nostartfiles --specs=nosys.specs printab.c |
svc vs swi
以前叫swi指令(software interrupt),后来叫svc指令(Supervisor call)
svc指令产生svc中断异常,在操作系统中一般用于请求特权操作或系统资源。
eabi vs oabi
arm架构在演化过程中浮点单元也在不断演进,由此对应不同的abi(Application Binary Interface) ,oabi、eabi、eabihf。oabi old abi是arm架构第一个abi。
eabi和oabi一个很大的区别就是系统调用
1 | oabi swi syscall_number |
emperorOS使用的老式oabi的方式
Calling SWIs dynamically from an application
arm开发者文档中有一篇有趣的文章,讲述如何在应用中动态调用swi
elf解析
Executable and Linkable Format
elf有三种header
- 一个elf header
- 多个program header,描述操作系统运行时segment的布局
- 多个section header,描述程序编译时section的布局
segment和section在elf文件中可以是重叠的,是可执行文件的两种视角,大道至简,elf文件简单,兼容各种指令集架构。
exec系统调用
1 | make user/init.hex |
把init.hex填入initcode数组来编译到kernel中,这是系统的0号进程,用exec来加载运行elf文件bin/init
1 | void execve() { |
- exec解析elf,遍历每一个program header,把ph.off至ph.filesz中的代码载入到虚拟地址ph.vaddr
- 构建页描述符pgd
- 构造用户栈,压栈
- 删除原页描述符,释放原用户内存
fork系统调用
1 | void |
fork复制当前进程创建子进程,从procs中找到空位置,复制pcb进程控制块,复制内存到新的页描述符,设置返回值。
内存硬件
内存屏障指令
(arm11需要通过设置cp15协处理器)
DMB Data Memory Barrier 保证执行顺序
DSB Data Synchronization Barrier 保证内存访问指令完成顺序
ISB Instruction Synchronization Barrier 最严格,flush流水线
内存对齐
C语言编译器会自动帮助内存对齐
汇编需要align n伪指令,对齐2的n次方
页表内存参数
emperorOS disable了核心的cache/tlb
代码在github
1 | git clone -b syscall https://github.com/996refuse/emperorOS.git |
若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏
扫描二维码,分享此文章