首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

实现一个最简单的嵌入式操作系统(四)

实现一个最简单的嵌入式操作系统(四)

任务结构数组(或链表)的实现

我们的任务结构就采用链表形式吧,但其长度是限定了的,头指针是一个全局指针变量(
指针变量是一个无符号整型指针,其指针本身所在的地址是在BSS段,但其指向的内容是分
配在堆上的一片内存),分配内核内存的函数就用kmalloc吧,kmalloc函数需要自己编写
呵,为了简单,这个函数只接受一个参数,就是所需分配大小,这个函数做得很简单,首先
有一个全局针指,它在初始化时指向了整个堆的起始位置,并且固定大小,就是所谓的内核
堆栈,在内核堆栈之后就是用户堆栈,由于总共有十个任务,当然不包括内核本身的任务,
所以整个堆栈就平均分成十一部分,注意:在所有任务初始化完成之后,还有一个步骤就是
将内核这个任务移到用户态,相当于要将自己的任务结构的堆栈指针修改一下就行了),
判断大小是否超出了内核堆的可分配范围,还有一点,需要维护内核堆和其它任务的堆,
需要进行分块,并且有一个全局的内存使用标识,就用数组吧,简单,0表示相应的内存
部分未占用,1就表示占用,对应的kfree就相当于把标志置0),
对于内存的维护,比较复杂,为了简单,就定为4K,并且不能进行大于四K的内存申请,因为
大于4K之后,由于没有虚拟地址的概念,就不能实现堆上的连续分配地址,当然在栈上分配
是可以大于4K的,栈是由编译器和CPU所决定了的

任务结构包括:
1.所剩的时间片
2.本任务所指向的代码段内存地址,这里也就是函数入口地址
3.本任务所指向的数据段地址,这里的数据段被包含进了整个内核中,所以并没有用,作为保留
4.本任务的函数体是否存在,也就是否会被调度
5.本任务所使用的栈指针
6.本任务所使用的堆指针
7.本任务的标识,用0代表是IDLE,1代表是其它进程
8.所有寄存器的值
9.当前PC值,初始化时被置成了函数入口地址

首先讲解一下任务数组结构的初始化:
将先定义一个全局指针,然后将此指针强制转换为一个任务结构指针,并通过kmalloc函在内核所占用的堆(前而讲过内核的堆的起始就是整个堆的起始)上去分配十个任务结构所占的内存,这里是绝不会超过4K的并且为这十个任务结构赋值,将第一个任务置为IDLE,时间片为20,代码段内存地址为main函数的的地址,数据段地址忽略,函数体存在,可以被调度,栈指针指向的位置根据以下来计算:

假定每个给每个任务可使用的堆栈设定为64K,而整个堆的起始位置是0x20030000,那么第一个堆指针所指向的就是0x20030000,栈就是0x20030000+64K的位置,第二个以后就以此类推注意:在初始化任务结构之前,不允许系统使用堆,但可以使用栈,那么内核任务栈部分就分成了两个,在未进行调度之前,栈就是上一页中第二步中所设的栈,那么上一页设置堆栈的时候就得注
意必须将堆栈空间设成十个64K再加上在本步骤使用以前的最大可能所需的栈空间

 

再讲解一下任务切换时所要做的事情:

进入整个中断处理入口时,会将所有寄存器推入IRQ栈之中,并把值拷贝到当前任务结构相应的字段当中,并取出被中断的进程的当前PC值存入当前任务结构中的相应字段中,接下就判别中断类型,以进入相应的中断处理函数,这里就会进入do_timer函数中,以下就是进入此函数之后的流程:
内核中还有一个全局指针,就是当前任务指针,它本身也是在系统BSS段中,它的定义如上一步中的那个全局指针一样,当由系统时钟中断之后,就取出这个全局指针,上一步初始化完成之后,还会把这个指针指向第一个任务结构所在位置,也就是0x20030000处,那么就取出这个任务结构中的时间片字段,判断其是否为0,若为0,就进行以下的操作:保存用户态下的栈指针至当前任务结构,保存堆指针,并将搜索一下可以被调度的任务结构,并将巳挝窠峁垢掣鼻叭挝裰刚耄眯枰腥挝袂谢槐晔叮吮晔锻且桓鋈直淞浚潜桓沉顺踔担岱旁谡鱿低车腄ATA段中,返回do_timer函数。若不为0,就进行以下操作:
将时间片减一,返回do_timer函数接下来判断任务切换标识,若为0,则进行以下操作:
不需要进行任务切换,所有寄存器出栈(这里的栈指的是IRQ栈),重新开启中断,切换到用户模式,加载当前任务结构中的当前PC值字段,以退出中断处理程序若此标识为1,则执行以下操作:
就需要进行任务切换,让所有寄存器出栈(这里的栈指的是IRQ栈),将当前任务结构中的所有寄存器的值恢复到相应寄存器中,将用户态下的栈指针恢复至当前任务结构栈指针,将堆指针恢复至当前任务结构堆指针,并把需要进行任务切换标识恢复为0,重新开启中断,切换到用户模式,任务切换是通过加载PC值来实现的,也就是通过加载当前任务结构中的当前PC值字段,以退出中断处理程序


系统调用的实现

本系统是完全可以不实现系统调用的,因为没有实现内核态和用户态的保护,完全可以不实现自己的C库,所有的函数都像kmalloc之类的实现一样,在内核中直接写函数原型,但为了以后扩展,还是说一下系统调用,这里以malloc系统调用来实现


首先说明还有一个堆指针(前面在kmalloc时有一个堆指针,不过那个堆指针是为内核任务,中断处理所提供),这里这个堆指针是用于用户态的,它在系统初始化完成之前会赋上初值,其初值就是第一个任务结构所使用的堆的起始位置,也就是在内核所使用的堆加上64K的位置

函数库中的malloc函数实现步骤如下:
1.首先检测申请大小是否超出了4K,若超出4K,就返回错误
2.进行系统调用(这里用_syscall1,并只传递一个参数(所需分配大小)系统调用函数_syscall1的实现:
1.将寄存器压入堆栈(这里的栈指向就是当前任务的栈)

2.将系统调用号1放至R0,参数放入R1

3.发出SWI指令以产生SWI中断(就是所说的软中断,陷阱)
此时系统发生中断,会进入SWI中断处理入口,下面说一下SWI入口函数的实现
1.取出R0的值,判断其值,进入相应的分支处理代码段

2.在此进入_malloc处理代码段,取出R1的值,然后再得到前面所说的当前堆指针,并申请对应数据块大小,置用于内存占用标识的相应字段,将当前堆指针放入R0,移动当前堆指针,改变当前任务结构的堆指针,切换到用户态,返回SWI中断系统调用_syscall1的返回处理:为了简单,在从内核态返回用户态时,不再进行任务的重新调度,所以上面的步骤就相对简单

1.当从SWI中断返回后,系统就运行在了用户态,此时取出R0的值,并赋值给需要申请内存的指针

2.在用户态弹出寄存器,返回到上一层函数
malloc函数的返回,此时malloc函数直接返回指针就行了,整个malloc的流程就结束了,其它的系统调用同这个过程类似

到此为止,这个操作系统初步实现了,但好像什么事情都不能做,如果让它支持串口中断的话,或许可以做那么一点点事情,比如像单片机那样的功能,整个系统的难点就是中断处理和任务切换,在本例中,由于ARM不支持像0x86那样的CPU级的保护模式,所以进行任务切换的时候,就得自己通过加载PC值的方法来实现,呵,因为我想不到更好的办法,但这个办法有一个不好解决的地方,就是寄存器入栈和出栈的保护,在进入中断时,必须保护寄存器,但如果需要进行重新调度,就得从中断上下文切换到进程上下文中,如何从中断上下文切换到进程上下文呢??我在这里所采用的方法很笨拙:
1.首先让寄存器入栈
2.让寄存器保存至当前任务结构数组,被中断掉的进程的PC值保存至任务结构
3.处理timer中断
4.如果进行任务切换,寻找下一个可调度的进程,然后把当前任务结构指下刚搜索到
的任务结构,让寄存器出栈,恢复当前任务结构里的值到寄存器,恢复堆栈指针,切换到用户态,通过加载当前任务结构的PC值来恢复被挂起的进程这里在中断上下文中使用了任务结构,这在LINUX上好像是不这样用的,中断上下文和进程上下文是两个不同的概念,中断上下文中不能访问进程上下文里的任务结构,我实在想不出有什么办法来实现进程调度了,所以请看到我这则文章的人提出好一点的方法

RTOS 与RISC 应如何结合

电子技术 对于今天复杂的嵌入式应用来说,采用实时操作系统(RTOS)作为软件基础平台是一个良好抉择。RTOS 中有一个核心负责处理器专项任务,例如CPU 的分配与调度、寄存器上下文变换和存储器管理。核心的周围是完成RTOS 服务的例行程序库,它们执行各种系统级功能,在应用程序运行时发挥一定的作用。应用程序被分解为一组任务;RTOS 调度器根据某些多任务调度算法让这些任务得以控制CPU。一个应用任务(通常使用汇编语言以外的其他语言写成)为了得到RTOS 服务,需调用相应的应用程序界面(API)功能。应用程序假如是用C 语言或其它语言写成的,则RTOS 及其API 库实际上掩盖了处理器的内部工作机理(不管它是CISC 还是RISC,是8 位、16 位、还是32 位,等等),因而应用软件工程师不必太多地考虑实际使用的处理器。 上述是从外部看到的情景。但到内部去看,从RTOS 的角度观察,情景就大不相同。 RISC 机器不同于CISC 微处理器和微控制器,这些差别往往要求对有关的操作系统给与一些特殊考虑。RISC 机器是为高速度而发明的,它们运行于很高的时钟速度,通常是在一个周期内执行完指令。他们往往采用多极流水线,因而在指令流出该流水线而进入执行单元的同时,还可以进行指令与数据的预取以及转移地址的估算。当流水线的流动被打断时,比如遇到某些转移指令或跳跃指令时,其性能便受到伤害。所以,如果把这种情况减少到最低程度,就可以达到最大的性能。简而言之,RISC 机器就像是1 级方程式赛车,在笔直的车道上跑得很快,但在拐角处必须减速。 在采用多任务RTOS 时应用程序中有许多“拐角”,这是因为“笔直的”应用程序或RTOS 可能引起正常的处理流发生改变。例如在多任务应用中,许多个任务都共用处理器,它们都调用RTOS 服务,使RTOS 在其内部做许多工作(并非都是“笔直车道”),以便完成所请求的操作,并决定接着应运行哪个任务,再把CPU 的控制权交给该任务。所有这些都会引起处理流发生改变,而外部中断则更易引起这种改变。由此可见,高速笔直应用程序开足马力一直运行下去的希望落空了。如果RTOS 在通过“拐角”时减速,应用程序的性能也要遭殃。因此,RISC 处理器上的RTOS 成功的一个关键是有能力迅速通过这些“拐角”。每当RTOS 服务请求或中断引起任务的处理流发生改变时,CPU 的寄存器上下文就必须进行相应的管理,以保证同一任务重现获得CPU 的控制权时处理工作能正确继续下去。 RISC 机器通常采用数量很大的寄存器,这要求RTOS 设计时给与特殊考虑。当应用任务调用一个RTOS 服务时,RTOS 究竟需要多少个寄存器来保存上下文?RTOS 中的功能需用多少个寄存器?中断需用多少个?如果保存用的寄存器太少,RTOS 显然就会出错;这是设计 RTOS 之初的唯一考虑。反过来,如果要处理的寄存器太多,虽然问题不太明显,但性能肯定要下降。幸运的是,RTOS 设计师在作出这些决定时可以从处理器的应用二进制界面(ABI)技术规范得到帮助。 ABI 通常是在RISC 处理器厂商的支持下写成的(例如PowerPC 嵌入式ABI 就是在 Motorola 公司的赞助下写成的),它是对编译器、汇编器和调试器以及RTOS 构件等软件工具的开发人员很有用的一套规则。ABI 中含有:在函数调用期间用于传递变量的寄存器用途,堆栈帧规则,留用的寄存器,以及对编译器或调试器设计师有用的其他信息的定义。对于 RTOS 时基本上自给自足的软件,因此ABI 中的大部分规范不考虑RTOS 设计师的需要。但是,堆栈帧规则和保留寄存器定义对于设计高效的RTOS 很有用。 以保留寄存器为例。他们是非易失性的,在上下文保存与恢复操作期间需要由RTOS 管理的处理器上下文部分就不必把保留寄存器考虑进去。由于只需保存和恢复易失性的寄存器,所以每当RTOS 在处理器的上下文进行操作时,RTOS 可节省几个周期。鉴于每秒钟发生的上下文管理操作达数万次之多,因而即使节省几个周期也可使CPU 有更多的时间去做其它工作。 RTOS 设计师将乐意利用ABI 的堆栈帧规则来确保:任务栈定界于正确的边界上,栈指示器被调整到正确的数值,在保存上下文期间RTOS 建立的帧反映了ABI 所规定的格式。遵守ABI 规范会得到重大的好处:可以用遵循ABI 标准的调试器去查看堆栈。按照处理器的ABI 规范来实现RTOS 还会带来另一个看不到的好处:可与各种工具互操作。遵循ABI 标准的RTOS 应兼容于任何ABI 标准的工具链。这样的兼容性会使RTOS 开发者和用户感到高兴。RTOS 开发者之所以高兴,是因为再也不必为了与所有的非兼容工具配合工作而被迫进行软件移植。用户之所以高兴,是因为RTOS、运行库和调试器现在可以浑然一体进行工作。双方都节省了时间,提高了工作效率。 选择语言在开发RISC 处理器上的RTOS 时,RTOS 设计师还必须选用最佳的语言。RISC 处理器是很复杂的,它们的汇编语言往往最适合不怕反复调试的人使用。那些爱闹情绪的人往往改用较简单的解决方法,例如使用编译器来轻松地产生程序码。不管采用哪一种语言,选择编译器是非常重要的,因为编译器决定了产生的RTOS 程序码在RISC 处理器上运行的好坏。例如,循环的编写方法可能影响到指令流水线的连续性。流水线的处理流中断将需要清理和重新注入处理流,引起处理效率下降,其积累效应会极大地降低效能。RTOS 设计师在为 RISC 处理器编写程序时通常要采用不同于应用程序开发者的方法,使流水线只在万不得已时才中断。使处理流发生中断的另一个途径是通过中断或其它类型的例外操作。中断不仅会引起正常处理流中断,还要求为它进行中断服务,因而会使系统性能变差,如果处理不当的话还会影响其响应速度。虽然几乎每种系统都是如此,但基于RISC 处理器的系统尤为明显,这是因为它们很大的处理器上下文和硬件中断设计对软件来说是很不友好的。中断是正常处理的一种例外情况;流行的RTOS 设计都尽可能快地去处理中断,但对于运行RTOS 的RISC 处理器来说,要定义“快捷”的含义并不总是轻而易举。每当发生中断时,有关的中断服务例程(ISR)必须保存某些或全部的处理器上下文,辨认中断源,向引起中断的设备提供服务,再恢复正常的处理通路。在ISR 处理工作中,保存处理器上下文的这部分工作通常是这样完成的;先禁止处理器的中断系统,使可恢复的状态可以顺利保存而不会被破坏。但是,由于中断被禁止,其它的设备就不能请求服务,必须等到ISR 重新启动中断,允许新的中断被识别。当处理器有大量的寄存器需要保存时(典型的RISC 机器就是如此),保存上下文所需的时间要占用很多的机器周期,增加了系统的中断等待时间,降低了中断响应的速度。在理想情况下,一旦处理器的上下文保存好了,就应当允许接受新的中断,但是,这会带来一种复杂局面:已经激活的ISR 可能被另一个设备所中断。如果系统有中断优先级或分开的向量,则很容易管理多个中断。但是,如果像PowerPC 或ARM 中那样,所有的外部中断都要通过一个或两个向量输入,则重入式中断服务就是不得不面对的实际问题。虽然允许中断肯定可以改善对其他中断请求的响应速度,但处理重入式中断是复杂的任务,RTOS 必须能适应这种情况,不让任一中断只得到部分的服务。那么,哪一种方法能更快完成中断服务呢?是采用较简单的RTOS,在ISR 工作期间一直禁止中断,完全避免重入式中断的出现?还是及时允许中断输入,让ISR 和RTOS 有处理重入式中断的能力。对于上述的问题有可能没有一个恰当的答案,这是因为在决定快与慢是应用软件的考虑起着主要作用。为了让操作系统知道中断引起的事件,ISR 可能需要调用一个或多个RTOS 服务。那么,在第一种处理方法中是否需要在RTOS 服务期间也禁止中断?如果这么做的话,中断等待时间肯定会明显加长,使系统响应明显变差。简而言之,中断响应的快慢最终可能要看究竟需要哪一类系统响应。如果采用简单的RTOS,则ISR 程序可能较简单,但代价是系统响应较差。如果采用智能的RTOS 和较复杂的ISR 程序,则系统响应性能可能较好。最终的分析意见如下:对于使用RISC 处理器的嵌入式应用来说,应用需要与所选择的 RTOS 存在千丝万缕的联系。但有一点需要牢记,非常适合于应用软件和RISC 处理器的 RTOS 将能在笔直通道上快跑,还能以较快速度通过拐角。反之,如果配合不好的话,他在直道上仍然像一条猎狗,但在拐角处就变成一支普通狗。 UDN2944W UDN2954W UDN2961W UDN2962W UDN2962W-1 UDN2966W-2 UDN2974W UDN2981A UDN2982LW UDN2993LB UDN5714M UDN6118A UDN6118A-2 UDQ2543B ULN2001A ULN2001AN ULN2002AN ULN2003 ULN2003A ULN2003L ULN2004A ULN2004A(C) ULN2004AN ULN2033A ULN2064B ULN2067NE ULN2076B ULN2204A-2B ULN2802A ULN2803 ULN2803A ULN2803LW ULN2804A ULN2804LW ULN3782M ULN3839A-1 ULN3839A-2 ULN3839R-1 ULN3859A ULQ2003A ULQ2437M ULQ2803A

[此贴子已经被作者于2005-12-30 0:21:54编辑过]

返回列表