写这篇文章的目的,主要是给那些新手看的,以满足他们的好奇心,让他们知道写OS会经历哪些过程。至于老手,都已经会写OS了,本文就没啥吸引力了。大家瞅瞅就行 。
1. 背景
有现成的bootloader、OS、芯片驱动、产品,CPU是Xtensa DC_D_233L。我们有其它部分的源代码,但OS是其它公司开发的,源码没有。
2. 设计目的
在DC_D_233L 上运行的多线程操作系统。当前已有bootloader、CPU内部模块的驱动、官方GDB(不是linux中的GDB)等。
3. 实现步骤
(1)了解OS的结构;
(2)查阅xtensa官方手册,了解芯片工作机制;
(3)分析xtensa官方手册,提取出与OS相关的内容;
(4)在纸上或电子文档中写下OS运行过程;
(5)检查第(4)步的结果是否可行;;
(6)编写OS代码,在电路板或仿真器上运行。
4.了解OS的结构
多线程OS的结构如下:
(1)内存管理;
(2)中断和异常处理;
(3)线程调度;
(4)系统调用;
(5)文件系统(暂不实现);
(6)兼容现有OS接口;
多线程和多进程非常不同,多进程的文件系统是一大难点。多线程可就简单好多好多了,不用实现文件系统、代码常驻在内存中,这些都省好多事情。
5 查阅xtensa官方手册,了解芯片工作机制
芯片工作机制,包含如下内容:
(1)CPU寻址过程;
(2)发生中断时,硬件对中断的处理过程;
上述2项内容是必须要了解的。第(1)步牵涉到CPU寻址硬件的工作流程,它会使用很多硬件结构和寄存器,它们可能会成为单个线程正常工作的一部分。第(2)步是为了线程切换做准备,因为10ms定时中断一定会使用这种机制进行线程切换。
xtensa官方文档很多,为了实现上述2个目的,需要做如下2步:
(1)浏览多个官方文档,找出与上述目的相关的所有文档;
(2)对第(1)步的文档进行重要性排序,先看最重要的文档;
本人第(1)步唯一找到的文档是微处理器编程手册,紧接着就开始看了。坦白说,这份文档真不错,感觉是手把手教人如何写OS,就连代码设计的背后考虑,它都进行了详细说明。文档信息整理,这是本人现在面对的问题。这个编程手册才200多页,瞅瞅ARM的那些手册,动辄上千页,整理信息是很容易迷路和失去耐心的。抱着目的看文档,能有效解决这个问题。写OS是个艰苦的活,整理文档信息是第一步,是不可避免的。
6. 分析xtensa官方手册,提取出与OS相关的内容
这次的目的性非常强。首先是对总结内容的排序:
(1)先做内存管理的总结。内存管理肯定排在线程调度、系统调用前面,但是如果内存访问时没有触发异常或引发中断,则中断和异常处理是不必要的;内存管理却是时时都要用的。因此先做内存管理的总结,直到其完成为止。
(2)再做中断和异常处理的总结。中断和异常处理肯定排在系统调用前面,另外在中断里可以进行线程调度(如10ms定时中断),因此再做中断和异常处理的总结。
(3)再做线程调度的总结。它肯定要排在系统调用的前面。
(4)再做系统调用的总结。
(5)兼容现有OS接口:这个暂时不考虑,等到写出了OS,再做兼容不迟。
在总结过程中,我先在纸质笔记本上记录心中所想(纸张比电子文档更让人能思考),后来发现图表在纸质笔记本上写着不方便(经常删除内容),因此后来改用电子文档做总结思考。但是,总是在思考成熟后,比如在提出一个疑问并获得答案后,才会在电子文档上做记录。例如,内存寻址步骤是什么?每步做了什么事?内存寻址的参与对象有哪些?这步做下来,A3纸用了29页。
7. 设计的细化
7.1 设计目标
分2步实现我们的最终目标:
(1)写出最小的、能正常工作的OS,OS越简单越好,效率高低无所谓;
(2)在第一步的基础上,逐步扩充功能;
采用上述策略的原因是:
(1)一次性实现所有功能,难度大;
(2)某些功能并非必需;
(3)一次性实现所有功能,不利于程序调试;
(4)一次性实现所有功能,不能增加软件实现者的信心(非常重要!);
上述方案可以让我们一开始,就集中注意力在验证文档总结是否正确、核心模块如何实现上,非核心模块先不做。这种方式可以让我们在初期就发现设计缺陷。当然了,我们的设计仍是大而全的,只是先实现核心模块而已。
7.2 内存管理的设计
内存管理的目标:
(1)不启用MMU(也就是说,不使用虚拟地址到物理地址的转换硬件---非常重大的设计决策);
(2)可以随时分配内存、随时释放内存;
(3)内存大小的改变不影响程序的改动;
因为上述目标,使得内存管理蜕变为一个算法。
7.3 中断和异常处理的设计
(1)必须能够处理各种中断;
(2)支持高频率的中断处理(此为通信芯片,物理层中断非常频繁);
(3)支持硬件中断嵌套,不支持软件中断嵌套(这个决策与芯片有关);
7.4 线程调度的设计
(1)线程可以创建无数个;
(2)线程可以手动退出,也可以自动退出;
(3)提供线程间通信方法;
(4)线程调度算法易于修改;
(6)所有线程都是privileged权限;
(7)线程的优先级可以在创建线程时设置(此为通信芯片,某些线程优先级很高);
(8)线程调度接口简单;
7.5 系统调用的设计
系统调用函数分为2类:
(1)线程切换类;
(2)非线程切换类。
之所以分类,是因为中断处理函数开放给用户编写,用户可能会在其中胡乱调用系统调用函数。所以对每个系统调用函数分类,使得在中断中只能调用非线程切换类函数。
8. 实现策略
8.1 最终目标
#include <stdio.h>
#include "OS_API.h"
void *taskA(void *arg)
{
u32 i;
while (1) {
for (i = 0; i < 2000000; i++);
schedule();
}
}
void *taskB(void *arg)
{
u32 i;
while (1) {
for (i = 0; i < 2000000; i++);
schedule();
}
}
int main()
{
u32 i;
/* 1: register main() as a task, and put it to task chain, this has
* been done statically
*/
/* 2: */
mem_init();
/* 3: */
interrupt_init();
/* 4: must be done after memory init */
sched_init();
/* 5: start testing */
thread_create(taskA, NULL);
thread_create(taskB, NULL);
while (1) {
for (i = 0; i < 2000000; i++);
schedule();
}
return 1;
}
上述代码只有创建线程和线程调度,没有使用内存分配算法和开启中断。这符合先前的简化目标,因为我们仍处于验证设计是否可行的阶段,此时不应把代码弄复杂。
8.2 系统初始化
分类 内容 初始化顺序
PC寄存器,SP寄存器 21
全局设置——> PS寄存器 14
WINDOWBASE寄存器 11
windows机制 WINDOWSTART寄存器 12
a0寄存器,a1寄存器 13
中断机制 INTENABLE寄存器 1
INTERRUPT寄存器 2
ICOUNTLEVEL寄存器 6
debug异常机制 IBREAKENABLE寄存器 5
DBREAKCn寄存器 4
zero-overhead机制 LCOUNT寄存器 3
时间计算 CCOUNT寄存器 7
扩展L32R模式 LITBASE寄存器 8
PTEVADDR寄存器 15
MMU机制 RASID寄存器 16
ITLBCFG寄存器,DTLBCFG寄存器 17
协处理器 CPENABLE寄存器 9
浮点寄存器 FSR寄存器,FCR寄存器 10
MMU机制 指令cache,数据cache 18
TLB entries或CACHEATTR寄存器 19
其它寄存器
其它硬件结构
ROM解压 从ROM中解压到内存中 20
调用第1个C函数 调用main() 22
8.3 内存管理初始化
存储访问中的参与对象:
ITLB,DTLB组
ITLBCFG,DTLBCFG寄存器
RASID寄存器
PTEVADDR寄存器(存放页表的基地址)
cache结构
EXCVADDR寄存器
在CPU上电启动后,应做如下初始化工作:(没有列出先后顺序)
(1)ITLB组和DTLB组中,除了way 6,其余的way都置为invalid。
(2)设置ITLBCFG寄存器和DTLBCFG寄存器,以设置way4, way5, way6的页表大小。
(3)设置RASID寄存器的值为0x04030201。
(4)清空cache中所有内容。
(5)不设置PTEVADDR寄存器。因为此时位于ring 0,寻址可以使用way6,故不会发生硬件自动填充TLB的情况。
在main()运行之前:
(1)定义1个数组avail_memory[内存字节大小/ (1024 × 4 ×8)],用每个成员的每个bit表示1个4KB的内存;bit = 0表示该4KB没有被使用,bit = 1表示该4KB正在被使用;
(2)将数组avail_memory[]的每个成员都置为0;
(3)定义存储分配函数为:
void *emalloc(u32 size); // 从主内存中分配size个字节的存储区
void efree(void *addr); // 从主内存中释放以addr为起始地址的存储区
【注】之所以叫emalloc(),efree(),是不想与malloc()和free()混淆。
在mem_init()内部:
(1)将avail_memory[]数组清零;
8.4中断初始化
main()运行之前:
(1)定义1个数组:void (*interrupt_handler)[32];
interrupt_init()内部:
(1)将interrupt_handler[]数组全部设置为NULL;
(2)将10ms定时中断的中断优先级设置为最低(为了嵌套中断能工作更顺利);
8.5 线程调度初始化
main()运行之前:
(1)将main()定义成1个线程;
这步一定要将main()定义为1个线程,原因是这样的:main()一定会创建新线程,当新线程被中断或主动进行线程切换时,如果main()不是一个线程(从而它不在全局的线程链表中),则线程调度时,永远不会选择并切换回main()。
sched_init()内部:
空;
thread_create()内部:
(1)使用phy_malloc()为每个新线程分配PCB;
(2)对该新PCB进行结构方面的初始化;
(3)对该新PCB进行栈上内容的初始化;
9. 实现总结
(1)查阅并总结文档非常耗时,占整个时间的2/3;编码及测试只占1/3;
(2)尽量看官方文档,官方文档比网络上的二手资料准确;
(3)先写核心模块,再逐步扩充OS功能。OS功能众多,难道要一次性全部写完?对本人而言,一次性写完难度太高了,根本完成不了。所以在设计阶段,就应经想好实现模块的先后顺序。初期实现目标代码,当然是越简单越好(否则就没信心了),无所谓效率,能进行线程创建、线程切换就行,此时不要求线程退出(线程就是无限循环)。这个阶段伴随着设计方案的验证、设计方案的修改、芯片理解的校正等,逐步实现线程切换。一旦实现线程切换,则可以加上内存分配、互斥量、条件变量等,再加上中断和异常处理、定时器,形成可以工作的OS;
(4)xtensa GDB可以进行软件层的线程切换(Keil也可以),它是个非常强大的程序调试工具;