基于ARM的自制最小操作系统Open-OS:实现

通过上一篇文章的介绍,我想读者应该对多任务操作系统有了一个粗略整体的认识,虽然是一个简陋的认识,但是对后面的学习却至关重要。有了这个整体认识,剩下的就是实现了。

在构建这个OS的过程中,会涉及到一些硬件处理器的知识和基本的软件知识:

内存管理单元(mmu)

缓存(Cache)

编译环境(Makefile)

程序的结构(ELF)

链接和加载

ABI/EABI的一些规范

C和汇编的接口APCS

GCC工具集的使用 等等

当然会逐步的展开去讲解,比如涉及到cache时,不会一上来就说cache的原理,讲cache的寄存器每个bit的含义,然后进行源码分析,这样也许不仅不会帮助到初学者,反而可能让想学习的人打了退堂鼓。因此在介绍的过程中我都会帮助读者建立一个整体的认识,在整体的认识下,读者就可以自己去深入细节了(硬件、软件是如何具体的实现),在深入细节的过程中来加强整体的认识。

例如:有的读者在学习了第一章的知识,就可以以自己的代码方式去实现多任务切换(细节)。

说明

构建这个ARM OS的不是为了供商业使用(没有必要),市场上有很多成熟的系统可供使用,真正的目的是为了学习,通过渐进式的方式讲解操作系统的概念和实现原理,为后面的嵌入式boot/linux驱动开发的学习打下坚实的基础。

函数调用堆栈

在上一篇文章提到了ARM处理器的基本模型(存储程序计算机):存储器取指令-执行指令-再取指令-再执行无限循环往复,同时还有一个重要的计算机基础内容:堆栈。

在计算器的石器时代,人们都是使用汇编语言进行程序开发,堆栈的概念并不是很重要。在有了高级语言之后,像C语言的函数调用必须借助堆栈机制。

引出问题

这里先要引出一个问题:什么是ABI?什么是EABI?嵌入式开发过程中常常会遇到很多奇怪的问题,也许答案就在这里。

ABI是“Application Binary Interface”的缩写,即应用程序二进制接口。它是编译器和我们编写汇编代码时需遵循的规范。

ABI既然是一个规范,对于ARM来说其实就是针对ARM 体系结构的应用程序二进制接口 (ABI) 的文档集,这些文档包括 ARM 过程调用标准 (APCS)、ARM ELF、ARM DWARF、基础平台 ABI (BPABI)、C++ ABI、异常处理 ABI、运行时 ABI 和 C 库 ABI。

这里我们举个简单的例子来给大家说明,例如ARM 32bit架构的ABI规定了基本的数据类型,这些数据类型所占用的内存字节数。

基于ARM的自制最小操作系统Open-OS:实现

那么各个厂商针对ARM 32架构的编译器的就必须符合ARM的ABI规范,例如,C语言的char类型一般就分配单个字节内存(字节对齐同样也有ABI规范)。

那我们能不能定义char类型占用10个byte。答案是可以,自己造处理器,然后自己定义ABI规范。

既然编译器都遵守相同的规范,那么符合ABI接口的对象文件(OBJ)链接就可以在一起,而不需要考虑它们的源文件,同时符合ABI接口的可执行文件运行在任何支持ABI接口的系统中。

延伸

Linux内核关于ABI的配置选项

<code>make menuconfig
        Kernel Features --->
                [ ] Use the ARM EABI to compile the kernel
                [ ] Allow old ABI binaries to run with this kernel (EXPERIMENTAL)/<code>

ABI规范里面我们重点关注下AAPCS这篇文档。下面主要来说下AAPCS主要的几个关键点

定义基本的数据类型

C语言中存在各种基本数据类型,其所占用的字节数是由处理器对应的ABI规范定义的。下图列出了ARM 32处理器ABI规范

基于ARM的自制最小操作系统Open-OS:实现

规范字节对齐处理

分配寄存器的功能

规定栈帧结构

函数参数的传递

函数返回值

关于ARM更多的ABI规范,这里不再一一赘述了,可参考ARM官方网站的相关ARM手册资料。

· ABI Introduction

· ABI Advisory Note 1

· Procedure Call Standard for the ARM Architecture Documentation

· ELF for the ARM Architecture Documentation

· DWARF for the ARM Architecture

· Base Platform ABI for the ARM Architecture

· C++ ABI for the ARM architecture Documentation

· Exception handling ABI for the ARM architecture Documentation

· Run-time ABI for the ARM Architecture

· C Library ABI for the ARM architecture Documentation

· Support for Debugging Overlaid Programs

· Addenda to, and Errata in, the ABI for the ARM Architecture Documentation

· Differences between v1 and v2 of the ABI for the ARM Architecture

实现

在实现之前,我们先来看下运行效果,程序中设计了四个任务,然后每个任务轮流执行,在linux环境下面输入(qemu的使用可参考网络资料):

qemu-arm a.out

基于ARM的自制最小操作系统Open-OS:实现

OS代码构成

基于ARM的自制最小操作系统Open-OS:实现

OS编译

基于ARM的自制最小操作系统Open-OS:实现

代码

pch.h

<code>
#define MAX_TASK_NUM 4
#define KERNEL_STACK_SIZE 1024*2*2

struct _thread {
\tunsigned long ip;
\tunsigned long sp;
};


typedef struct _pcb {
\tint pid;
\tvolatile long state; // -1:unrunnable,0 run >0: stoped
\tunsigned long stack[KERNEL_STACK_SIZE];
\tstruct _thread thread;
\tunsigned long task_entry;
\tstruct _pcb *next;
}pcb_t;/<code>

os.c

<code>#include <stdio.h>
#include <string.h>
#include "pcb.h"

static void my_process(void);
static void my_schedule(void);
static pcb_t task[MAX_TASK_NUM];

static pcb_t * my_current_task = NULL;

//static volatile int my_need_sched = 0;

int main(void)
{
int i = 0;
int pid = 0;
unsigned int cpsr = 0;
printf("==>this is my kernel\\n");\t
//init process 0
task[0].pid = 0;
task[0].state = 0;
task[0].task_entry = (unsigned long)my_process;
task[0].thread.ip = (unsigned long)my_process;
task[0].thread.sp = (unsigned long)&task[0].stack[KERNEL_STACK_SIZE-1];
task[0].next = &task[0];
printf("==>sp=%x,ip=%x\\n",task[0].thread.sp,task[0].thread.ip);
for(i = 1; i < MAX_TASK_NUM; i ++){
\tmemcpy(&task[i],&task[0],sizeof(pcb_t));
task[i].pid = i;
\ttask[i].state = -1;
task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];
\ttask[i].thread.ip = (unsigned long)my_process;
\ttask[i].next = task[i-1].next;
task[i-1].next = &task[i];
\tprintf("==>sp=%x,ip=%x\\n",task[i].thread.sp,task[i].thread.ip);
}
my_current_task = &task[0];

\tasm volatile(

\t\t"mov sp,%1\\n\\t"
\t\t"mov pc,%0\\n\\t"
\t\t:
\t\t:"r" (task[0].thread.ip),"r" (task[0].thread.sp)\t
\t);

\treturn 0;
}

static void my_schedule(void){
\tpcb_t *next,*prev;
\tif(my_current_task == NULL || my_current_task->next == NULL){
\t\treturn;
\t}
\tprintf(">>>>my_schedule<<<<\\n");
\t//schedule
\tnext = my_current_task->next;
\tprev = my_current_task;
\tif(next->state == 0){
\t\tmy_current_task = next;
\t\tprintf( ">>>>switch %d to %d <<<<\\n",prev->pid,next->pid);
#if 1
\t\tasm volatile(
\t\t\t"stmfd sp!,{r0-r12,lr}\\n\\t"
\t\t\t"str sp,%0\\n\\t"
\t\t\t"adrl r8,rettt\\n\\t"
\t\t\t"str r8,%1\\n\\t"
\t\t\t"ldr pc,%3\\n\\t"
\t\t\t"rettt:\\n\\t"
\t\t\t"ldr sp,%2\\n\\t"
\t\t\t"ldmfd sp!,{r0-r12,lr}\\n\\t"
\t\t\t:"=m" (prev->thread.sp),"=m" (prev->thread.ip)
\t\t\t:"m" (next->thread.sp), "m" (next->thread.ip)
\t\t\t:"r8"
\t\t);
#endif\t\t\t
\t}else{
\t\tnext->state = 0;
\t\tmy_current_task = next;
\t\tprintf(">>>>new switch %d to %d <<<<\\n",prev->pid,next->pid);
\t\t//switch to new process
#if 1
\t\tasm volatile(
\t\t\t"stmfd sp!,{r0-r12,lr}\\n\\t"
\t\t\t"str sp,%0\\n\\t"
\t\t\t"adrl r8,rettt\\n\\t"
\t\t\t"str r8,%1\\n\\t"
\t\t\t"ldr r8,%2\\n\\t"
\t\t\t"ldr r9,%3\\n\\t"
\t\t\t"mov sp,r8\\n\\t"

\t\t\t"mov pc,r9\\n\\t"
\t\t\t: "=m" (prev->thread.sp), "=m" (prev->thread.ip)
\t\t\t: "m" (next->thread.sp), "m" (next->thread.ip)
\t\t\t: "r8","r9"\t\t\t
\t\t);
#endif\t\t

\t}\t
}
static void my_process(){
int i = 0;
while(1){
i ++;
if(i%100000000 == 0){
printf("this is process %d -\\n",my_current_task->pid);
my_schedule();
printf("this is process %d +\\n",my_current_task->pid);

}
}
}/<code>

读者可以尝试在自己的虚拟机上编译运行,看下效果,关于代码具体细节这里就不在解释了。

关于OS的说明:

这个OS是基于孟宁老师linux3.6 X86的教学使用的。这里修改为基于ARM的,并且后面会在这个OS基础之上进行扩展,添加一些新的功能特性。

孟宁老师的项目地址:https://github.com/mengning/mykernel

预告

在做这个项目的时候,每次调试我都是手动输入编译命令,编译是非常频繁、动作周而复始,然而随着项目的扩张,源码文件越来越多,是否还要继续这样?有没有什么工具能够帮助我们来构建一个专业和高效的开发环境?答案是有。

后面将会讲到如何使用make来搭建一个开发环境。除了开发环境是不是还有别的需要做?

上面os实现的是各个任务按照一定的时间轮流执行,其实这个就称之为调度器。

任务调度算法:

根据一个标准,找到下一个将要运行的任务,然后将正在运行的任务和将要运行的任务做一次上下文切换。那么这个标准就是我们要说的调度算法。

后面会将现在的OS调度算法进行扩充,能够支持基于优先级的调度算法。

延伸

基于优先级和时间片、以及这两种算法的不同组合衍生出不同的调度算法?linux的调度算法?有兴趣的读者可以深入。



分享到:


相關文章: