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 2 3 4 5 6 7 8 struct proc { enum procstate state; // Process state int pid; // Process ID struct proc *parent; struct context context; char name[16]; // Process name (debugging) uint32_t pgd; // page descriptor };
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 2 3 4 5 6 7 8 9 10 11 12 .globl start start: ldr r0, =fn ldr r1, =argv swi #4 fn: .string "0:bin/init" .align 2 // 地址类型,需要对齐4字节 argv: .word 0
创建一个子进程,父进程打印a,子进程打印b
用户程序printab printab.S
1 2 3 4 5 6 7 8 9 10 11 .globl start start: swi #1 cmp r0, #0 beq 2f 1: swi #2 b 1b 2: swi #3 b 2b
也可以用C程序实现,exec函数来加载elf文件
printab.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 void printa() { asm ("swi #2\n\t"); } void printb() { asm ("swi #3\n\t"); } int fork() { int res = 0; asm volatile ( "swi #1\n\t" "mov %[reg], r0" : [reg] "=r" (res) : : "r0" ); return res; } int _start() { int res = fork(); while (1) { if (res) { printa(); } else { printb(); } } return 0; }
不需要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 2 oabi swi syscall_number eabi r7=syscall_number; swi 0x0
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系统调用
把init.hex填入initcode数组来编译到kernel中,这是系统的0号进程,用exec来加载运行elf文件bin/init
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 void execve() { ... char* mem = kalloc(); int i, off; for (i = 0, off = elf.phoff; i < elf.phnum; i++, off += sizeof(ph)) { res = f_lseek(&fil, off); if (res) while (1); res = f_read(&fil, &ph, sizeof(ph), &count); if (res) while (1); printf("elf: proghdr %d bytes loaded\n", count); if (ph.type != ELF_PROG_LOAD) { continue; } if (ph.memsz < ph.filesz) while (1); res = f_lseek(&fil, ph.off); if (res) while (1); res = f_read(&fil, mem+ph.vaddr, ph.filesz, &count); if (res) while (1); printf("elf: segment %d bytes loaded\n", count); } uint32_t pgd = createuvm(0, 0, mem); // Push argument strings, prepare user stack. uint32_t sp = (uint32_t)mem+0x00100000; #define MAXARG 128 uint32_t ustack[MAXARG + 1]; int argc = 0; for (argc = 0; argv[argc]; argc++) { if (argc >= MAXARG) while(1); sp = (sp - (strlen(argv[argc]) + 1)) & (~3); memmove((void *)sp, argv[argc], strlen(argv[argc]) + 1); ustack[argc] = sp; } ustack[argc] = 0; sp -= (argc + 1) * 4; curproc->context.r[0] = argc; curproc->context.r[1] = sp; memmove((void *)sp, ustack, (argc + 1) * 4); // Save program name for debugging. char *last, *s; for (last = s = path; *s; s++) { if (*s == '/') { last = s + 1; } } safestrcpy(curproc->name, last, sizeof(curproc->name)); uint32_t oldpgd = curproc->pgd; curproc->pgd = pgd; curproc->context.r[15] = elf.entry; curproc->context.r[14] = 0xdeadbeef; // fake return PC, never exit now curproc->context.r[13] = sp & 0x000fffff; // reset fp if compile with nostartfiles // curproc->context.r[11] = curproc->context.r[13]; freeuvm(oldpgd); // exec returns only if an error has occurred //curproc->context.r[0] = -1; }
exec解析elf,遍历每一个program header,把ph.off至ph.filesz中的代码载入到虚拟地址ph.vaddr
构建页描述符pgd
构造用户栈,压栈
删除原页描述符,释放原用户内存
fork系统调用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void proc_fork(void) { struct proc *p; // Allocate process. for(p = procs; p < &procs[NPROC]; p++) { if(p->state == UNUSED) { break; } } p->state = RUNNABLE; p->pid = nextpid++; p->parent = curproc; p->context = curproc->context; strncpy(p->name, curproc->name, 16); p->pgd = copyuvm(curproc->pgd, kalloc()); // in the child, fork returns 0 p->context.r[0] = 0; // in the parent, fork returns child pid curproc->context.r[0] = p->pid; }
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