现在的位置: 首页 > 综合 > 正文

ecos内核概览–bakayi译

2013年02月25日 ⁄ 综合 ⁄ 共 9098字 ⁄ 字号 评论关闭

 

最近正在学习
ecos
嵌入式系统,国内关于这玩意的东西还真是少之又少啊,闲着没事也翻译了两篇,看了下面这篇翻译,觉得比自己翻译的好多了,虽然
bakayi
翻译的是
ecos2.0
版,现在
ecos
都到
3.0
了,但是仔细看了看,发现就这篇概览来讲,
ecos2.0

3.0
似乎没什么区别。美中不足的是,这个家伙貌似就翻译了这么一篇
~


注:原文出处

http://blog.csdn.net/bakiya/archive/2008/04/25/2329124.aspx


名称

内核

ecos
内核概览

描述

内核是
ecos
的一个关键包。它提供了开发多线程应用程序的核心方法。

1.
创建新线程,在系统启动或者已经运行的时候。

2.
控制不同的线程,比如操作线程的优先级。

3.
一个调度器,决定哪个线程当前可以运行。

4.
一组同步元语,允许多线程通讯和安全共享数据。

5.
集成系统中断和异常。

在其它一些操作系统内核中,一般会提供一些额外的功能,比如内核还会提供内存申请,并且设备驱动也作为内核的一部分。而在
ecos
中,内存申请模块被放在了一个独立的包中,同样,每个设备驱动也被放在一个独立的包中。我们可以用
ecos
的配置技术(
configtool
),来将应用程序需要的包整合在一起。

ecos
内核包也是一个可选的。你完全可以写一个单线程的程序,不需要用到内核的任何功能,比如
RedBoot
。它是一个典型的基于中心循环检测的程序,它会不停的检查设备,当有
I/O
发生时,作出相应处理。每次循环都会有一个小的计数,用来指示
I/O
事件与循环检测之间的时间。当需求简单直接的时候,应用程序就可以用循环检测来实现,这样就可以避免多线程的同步问题了。当需求变得复杂的时候,就适合用多线程来解决,这时就需要内核包了。实际上
ecos
中一些更高级的包,比如
TCP/IP
协议栈,它内部就使用了多线程。因此,当应用需要用到这些包的时候,内核就必须包含,而不再是可选的了。

内核的功能可以通过两种方式来使用。内核提供了它自己的
API
,比如
cyg_thread_create

cyg_mutex_lock
等一些函数。这些函数可以直接被应用程序或者其它的包调用。还有一种方式是使用一些包中提供的兼容
API
,比如
POSIX
或μ
ITRON
。这些兼容层允许应用程序调用标准的
API
,比如
pthread_create
,这些
API
由底层的
ecos
内核
API
实现。应用程序中使用兼容
API
可以使程序更加简单,也可以在其它系统中减少代码量,共享代码,方便移植。

尽管不同的兼容层在内核上有着相同的需求,比如创建一个新的线程,但它们还是有一些准确的语义差别。比如,严格的μ
ITRON
要求内核的时间片轮转被关闭。这主要通过
ecos
的配置技术来完成。内核会提供大量的配置选项来控制这些差别和一些兼容层的特殊设置选项。这样会导致两种结果。第一,通常在一个
ecos
配置中,不会同时存在两种不同的兼容层,因为它们对内核的要求有冲突。第二,内核的
API
在语义上只能宽松的定义,因为有很多的配置选项。比如,
cyg_mutex_lock
只会试图锁住一个
mutex
,但是当
mutex
锁住以后,不同的配置选项会决定不同的行为,并且很可能会引起优先级倒置。

内核可选的特性会导致其它一些问题,特别是设备驱动层。不管有没有内核,一个设备驱动应该正确工作。但是,系统的一些部分,特别是中断处理,在多线程和单线程的时候有着不同的实现。为了处理这种语义差别,
HAL
包提供了一个驱动
API
,比如
cyg_drv_interrupt_attach
。当选择了内核的时候,这些
API
直接映射到内核提供的函数,比如
cyg_interrupt_attach
。当没有选择内核的时候,驱动
API
会自己实现,但是这种实现会比内核的实现要简单的多,因为它假定系统处在单线程环境中。

调度器

当一个系统包含多线程的时候,就需要一个调度器来决定哪个线程可以在当前运行。
ecos
的内核可以被配置成两种--
bitmap

MLQ

bitmap
调度器更高效,但是有数量限制。大多数系统会安装
MLQ
调度器。其它一些调度器会在将来加入进来,或者作为现有内核包的扩展,或者作为一个独立的包。

两种调度器都使用简单的数字优先级来决定哪个线程应该运行。优先级的级别用数字表示,可以通过
CYGNUM_KERNEL_SCHED_PRIORITIES
选项配置,但是一个典型的系统一般会有
32

个优先级别。因此线程的优先级一般会在
0--31
范围内。
0
是最高级别,
31
是最低级别。通常只有系统的空闲线程会运行在最低级别。线程的优先级是绝对的,因此内核只会在所有的高级别线程阻塞的时候,才会运行低级别的线程。

bitmap
调度器只允许每个级别一个线程,所以如果系统配置成
32
级别,那么就最多只有
32
个线程
--
仍然满足大多数应用程序。一个简单的
bitmap
调度器可以被用来追踪当前哪些线程可以运行。它还可以追踪哪些线程正在等待
mutex
或其它的同步元语。识别最高级别的线程是否可以运行或正在等待其它线程是一个很简单的位操作,并且一个数组的索引操作可以被用来得到线程自身的数据结构。这让
bitmap
调度器处理很快,并且有完全的确定性。

MLQ
调度器则提供多个线程共用一个优先级别,这意味着系统对线程的数量没有限制,只要在系统内存允许的条件下。但是操作,像查找最高级别的可运行线程,将会比
bitmap
开销更大。

另外,
MLQ
调度器支持时间片轮转,帮助调度器在一定的时钟
ticks
到来时,自动在多个优先级相同的线程间选择运行。时间片轮转只会发生在在两个可运行的线程处在同一优先级别,并且没有更高级别的可运行线程存在的时候。如果时间片轮转被关闭了,那么一个线程就不能抢占另一个相同级别的线程,只能等到那个线程运行完成或者被阻塞的时候(比如等待一个同步元语),才能运行。配置选项
CYGSEM_KERNEL_SCHED_TIMESLICE


CYGNUM_KERNEL_SCHED_TIMESLICE_TICKS

控制着时间片轮转。
bitmap
调度器不支持时间片轮转,它只允许一个级别一个线程,所以不可能存在相同优先级别的线程抢占问题。

另外一个影响
MLQ
调度器的重要配置是
CYGIMP_KERNEL_SCHED_SORTED_QUEUES
。它决定当一个线程被阻塞的时候(比如等待一个事件还未发生的信号量),将如何选择。系统默认的行为是先进先出
(FIFO)
队列。比如,当几个线程等待一个信号量,事件发生的时候,最先调用
cyg_semaphore_wait
入队的线程被唤醒。这就使得入队出队的操作既简单又非常快。但是,如果有几个不同优先级别的线程进入队列的时候,就很可能不是最高级别的线程最先被唤醒。实际上这是一个很少见的问题:通常最多只会有一个线程在等待队列,或者有多个线程,但它们处在同一级别。但是如果应用程序确实如此,那么就需要将配置选项
CYGIMP_KERNEL_SCHED_SORTED_QUEUES
打开。这有几个缺点:只要有线程入队,就需要做更多的工作,调度器也会被锁起来,因此系统延迟会有错误。如果
bitmp
调度器被启用,那么优先级队列会自动去掉,不需要任何改动。

一些内核的功能目前只被
MLQ
调度器所支持,而
bitmap
则没有支持。这些包括
SMP
系统,保护优先级倒置的解决方案:共用优先级和优先级继承。

同步元语

ecos
内核提供了一组同步元语:共用体

(mutex)

、条件变量
(condition variables)
、信号量
(semaphore)
、信箱
(mail
box)

、事件标志
(event flag)

mutex
和其它的同步元语作用非常不同。
mutex

允许多个线程安全地共享一个资源:一个线程锁住
mutex
,然后操作共享资源,最后解锁
mutex
。其它的同步元语则通常用来在线程间通讯,或者是在一个中断发生的时候,随
ISR
之后的
DSR
与线程通讯。

当一个线程锁住一个
mutex
需要等待某个条件变成真的时候,你应该使用一个条件变量。条件变量本质上是提供给一个线程等待的空间,其它的线程或
DSR
可以使用它来唤醒那个线程。当一个线程等待一个条件变量的时候,它会在之前释放
mutex
,并且在唤醒前重新要求得到
mutex
,然后才能接着处理。这些操作都是原子的,所以竞争条件的概念没有引入进来。

信号量通常用在一件特殊的事件发生的情况下。一个等待线程将一直等待直到事件发生,而另一个发射线程或
DSR
会通告该事件。这个信号量是和数字相关的,所以如果事件发生在多个连续的时间点上,信息不会丢失,等待在相关的数字下的信号量会被处理。

信箱通常也被用来指示某种特殊的时间发生,并且允许在时间发生的时候交换一项数据。典型的数据项是一个指向某个数据结构的指针。正因为需要储存这些额外的数据,所以信箱只有一定的容量。如果一个线程收到邮件的速度要快于它所能处理的速度,那么为了避免溢出,它会被阻塞直到再次获得足够的空间。这意味着邮箱通常不能被
DSR
用来唤醒一个线程,而典型的用途是用于线程间的通讯。

事件标志可以被用来等待一定数目的不同事件,当有一件或多件事件发生的时候被唤醒。这通过一个代表不同事件的位掩码来完成。和信号量不同,它并不需要追踪事件的序号,实际上只要有事件发生就可以了。和邮箱不同的是,事件发生时它不能发送额外的数据,但这也意味着它不会引起溢出,所以既可以被用在
DSR
和线程之间,又可以用在线程之间。

ecos
的通用
HAL(
硬件抽象层
)
包提供了自己的设备驱动

API

,其中也包含了以上的同步元语。它允许一个中断的
DSR
可以向上层代码通告事件。如果配置中加入了内核,那么驱动
API
会映射到等价的内核
API
上,这样中断就可以和线程交互。如果内核没有包含,应用程序也简单的运行在单线程环境下,驱动
API
就完全由
HAL
实现,同时也不用担心多线程问题,实现也更加简单。

线程和中断处理

 

在普通的操作期间,处理器将会运行在系统的多个线程中的一个上。它或许是一个应用程序的线程,也或许是一个
TCP/IP
协议内部的系统线程,又或者是一个空闲线程。在某个时间,中断发生了,这会将处理器的使用权暂时交给中断处理。当中断处理完毕,系统的调度器会决定把处理器控制权交给被中断的线程还是其它可以运行的线程。

线程和中断处理程序必须是可以交互的。如果一个线程正在等待一些
I/O
操作完成,与那个
I/O
相关的中断处理程序就可以通知线程操作已完成。这有几种方法来实现。一个最简单的方法就是设置一个
volatile
变量,线程可以间歇性检测,直到变量被设置,很可能这个间歇的睡眠时间是一个时钟周期。间歇性检测意味着
cpu
时间对其它运行的线程变得不可用,这或许可以被一些应用程序接受,但不是所有。每隔一个时钟周期检测一次使得开销很小,但是意味着不能检测到一个时钟周期内发生的
I/O
事件,典型的系统中这个时钟周期是
10
毫秒。这样一个延迟或许能被一些应用程序接受,但是不是所有。

一个好点的解决方案或许是用一个同步元语。中断处理程序可以发送一个条件变量,发射一个信号量,或者是一个其他的同步元语。线程会执行一个等待的同步元语。这样在
I/O
事件发生前就不会浪费任何的
cpu
周期了,并且线程也可以立即运行起来(假设没有更好级别的线程正在等待运行)。

同步元语会创建共享数据,所以要特别注意引起并发访问的问题。如果一个被中断的程序仅仅是执行一些计算,那么中断处理程序可以很安全的操作同步元语。但是如果被中断的程序正处在内核调用中时,很有可能内核数据遭到破坏。

一个避免此问题的方法就是,在一个内核关键区域内,禁止中断。在大多数的体系中,这非常容易实现也非常快捷,但是它将意味着中断会被经常禁止很长一段时间。对一些应用来说可能不是一个问题,但是对于要求有最快中断响应的嵌入式应用来说,内核禁止中断的机制将不能够满足它。

为了解决这个问题,
ecos
内核使用了两级结构来处理中断。与每个中断向量相关的是一个中断服务程序
(ISR)
,它可以以最快的速度运行,所以可以服务硬件。但是,
ISR
只可以调用系统小部分的内核函数,大多数和中断子系统相关,并且它不能调用使用任何唤醒线程的调用。如果一个
ISR
检测到一个
I/O
操作完成,线程应该被唤醒时,它会调用一个相关联的延迟服务程序
(DSR)

DSR
可以调用更多的内核函数,比如,发送一个条件变量,或这发射一个信号量。

禁止中断会阻止
ISR
运行,但是在系统的极少数部分,会禁止中断很短一段时间。让线程禁止中断的一个主要原因为了操作
ISR
所共享的数据。例如,如果一个线程需要加入一块缓冲区到一个链表中,但是
ISR
很可能会移除这个缓冲区的时候,线程就会禁止中断,从而操作这个链表。如果这个时候硬件产生一个中断,它将被推迟到中断打开后处理。

类似中断的禁止与打开,内核也有一个调度器锁。有几种内核函数像
cyg_mutex_lock

cyg_semaphore_post
都要求得到调度器锁,以便操作内核数据,完成后解开调度器锁。如果一个中断引起的
DSR
被调用,但是调度器被锁上,它就会被延迟处理。只有当调度器解锁后,它才可以继续运行。这或许会发送同步事件,唤醒高级别的线程。

例如,设想一下下面的情景。系统有一个高级别的线程
A
,负责处理来自外部设备的数据。当数据可用的时候,设备会发起一个中断。同时有两个线程
B

C,
正在执行计算,偶尔会写入一些分类信息到屏幕上。屏幕是一个共享的资源,所以一个
mutex
被用来控制访问。

在一个特殊的时刻,线程
A
似乎被阻塞了,等待一个信号量或者是其它的同步元语,直到数据变得可用。线程
B
或许正在处理一些运算,线程
C
正在等待下个时间片。中断被打开,调度器也被解锁,因为没有任何线程正在进行内核操作。就在这个时刻,中断发生了,接着相应的
ISR
开始运行。这个
ISR
操作硬件,确定数据可用,想要通过发送一个信号量来唤醒线程
A
。但是
ISR
不能直接调用
cyg_semaphore_post
,所以它要求相应的
DSR
运行。现在没有其它的中断发生,所以内核开始检查
DSR
。它发现有一个
DSR
正待处理,并且调度器没有锁上,所以
DSR
可以马上运行起来,发送一个信号量。这样就会使得线程
A
变成可运行态,调度器的数据也相应调整。当
DSR
返回时,线程
B
就不是最高级别的可运行线程了,而线程
A
则得到了
cpu
的控制权。

在上面这个列子中,没有内核数据在中断发生的那一瞬间被操作,但是我们可以想象。假设线程
B
完成当前的计算任务,想要写入结果到屏幕上。它会要求得到
mutex
,从而操作屏幕。现在假设线程
B
得到时间片,开始运行,而线程
C
也完成了计算想要写入数据到屏幕上。线程
C
先调用了
cyg_mutex_lock
。这个时候线程
B
把调度器锁上,检查
mutex
的当前状态,发现
mutex
已经被其它的线程得到了,于是调度器终止了当前的线程,选择了其它可以运行的线程。刚好另外一个中断发生在
cyg_mutex_lock
的调用期间,导致
ISR
立即运行。
ISR
决定唤醒线程
A
,所以它调起
DSR,
返回内核。这个时候系统有一个待处理的
DSR
,但是调度器仍然被锁住,所以
DSR
不能立即运行起来。而调用
cyg_mutex_lock
的线程继续运行,直到某个时刻解开调度器。待处理的
DSR
才可以运行,安全得发送信号量,唤醒线程
A

如果
ISR
直接调用
cyg_mutex_lock
而不是把它留给
DSR
的化,很有可能内核数据会遭到破坏。例如内核可能完全失去对某个线程的追踪,从而导致这个线程永远不会再次运行。两个级别的中断处理机制,
ISR

DSR
,可以有效得防止这些问题,而不需要禁止中断。

调用
context

ecos
定义了很多
context
。每个
context
只允许一定的调用,例如大多数线程操作或同步元语不能在
ISR context
调用。这些不同的
context
有:初始化、线程、
ISR

DSR


ecos
启动的时候,它会经历一系列阶段,包括设置硬件,调用
C++
静态构造。在这期间,中断被禁止,调度器也被锁上。当一个配置包含内核,最后的操作是调用
cyg_scheduler_start.
在这个时候中断被打开,调度器被解锁,控制权交给最高优先级的线程。如果配置同样包含了
C
库,那么通常
C
库的启动包会创建一个线程来调用应用程序的入口函数
main

一些应用程序的代码同样可以在调度器启动之前运行,这些代码就运行在初始化
context
。如果应用程序部分或完全由
C++
写成,那么任何静态对象的构造器会运行。相应地,应用程序代码可以定义一个函数
cyg_user_start
,它将在
C++
静态构造器运行之后被调用。这样就允许应用程序完全由
C
来写。

void

cyg_user_start(void)

{

   

/*

在这里执行应用程序的特定初始化动作
*/

}

应用程序并不一定要提供这个函数,因为系统提供了一个默认的,但是并不做任何事情。

在静态构造器和
cgy_user_start
里,最典型的操作,包括创建新线程、同步元语、设置报警器、注册应用程序指定的中断处理程序。实际上,对于大多数应用程序来说,这些创建的操作一般都发生在这个时候,使用静态申请的数据,避免动态申请内存或其它花费。

代码运行在初始化
context
,中断被关闭,调度器被锁上。在这个时候,系统不能保证运行在一个完全一致的状态,所以拒绝打开中断和解锁调度器。一个结果就是,初始化代码不能使用同步元语,比如用
cyg_semaphore_wait
等待一个外部的事件。锁上和解锁
mutex
也是不允许的:没有其它任何线程正在运行,所以能够保证
mutex
还没有被锁上,因此,上锁的操作永远不会阻塞线程。当在内部使用一个
mutex
来调用库函数的时候,这会非常有用。

在启动阶段的最后,系统将调用
cyg_scheduler_start,
然后大量的线程就可以开始运行了。在线程
context
,几乎所有的内核函数都可用。但是中断相应的操作可能会有一些限制,这取决于目标硬件。例如,硬件可能会要求在控制回到线程
context
之前,在
ISR

DSR
中得到应答,在这种情况下,
cyg_interrupt_acknowledge
必须被线程调用。

在任何时候,处理器可能接收到一个外部中断请求,导致控制权从当前线程转移。典型的例子是,一个
ecos
提供的
VSR
,会运行并准确的确定那个中断发生。这时
VSR
会选择对应的
ISR
,它可以被
HAL
、设备驱动、或者应用程序提供。在这段期间,系统运行在
ISR context
,大多数的内核调用都被禁止。这些包括大量的同步元语,所以一个
ISR
不能发送一个信号量,指示某个事件发生。通常在
ISR
内唯一被允许的操作就是和中断相关的子系统,例如屏蔽一个中断或者是应答一个已经处理的中断。另外,在
SMP
系统中,还可以使用
spinlocks

当一个
ISR
返回时,他将要求相应的
DSR
尽快的安全运行起来,然后就系统运行在
DSR context
。这个
context
也允许报警器函数,线程也可以通过锁上调度器而临时得到运行。在
DSR context
,只有一定的内核函数可以被调用,然而也比
ISR context
多多了。较为特殊的是,它允许使用同步元语,但是不能产生阻塞。这些包括
cyg_semaphore_post, cyg_cond_signal, cyg_cond_broadcast,
cyg_flag_setbits, and cyg_mbox_tryput.

不允许可以产生阻塞的同步元语,包括

cyg_semaphore_wait, cyg_mutex_lock, or cyg_mbox_put

。调用这些函数会使系统挂掉。

有关各种内核函数的文档给出了更多的细节,关于正确的
context

错误处理和断言

在许多
API
中,每个函数都会对参数的正确性,或者系统的状态作出验证。这样可以确保每个函数都可以被正确的使用,比如,应用程序不会试图对一个信号量像共用体
(mutex)
一样操作。如果根据返回的一个错误代码检测到一个错误,比如
POSIX
函数
pthread_mutex_lock
可以返回不同的错误代码,像
EINVAL

EDEADLK
等。这样做会有一些问题,尤其是在嵌入式系统中:

1.
执行这些检查,不管在
mutex lock

内还是在其它的函数中,都需要额外的
cpu
周期,并且会明显地增加代码大小。即使程序编写完全正确,并且调用系统函数的参数有意义,并在正确的条件下,这些开销仍然存在。

2.
返回错误代码只在一种情况下有用,即调用代码能够识别这些错误代码并作出合理处理。实际通常调用者会忽略一些错误,因为程序员“知道”函数被正确的使用。如果程序员犯了错误,那么一个错误的条件验证将被检测到,但是程序却会继续执行,最后以一种神秘的方式失败。

3.
如果调用者一直检查错误代码,那么将增加更多的
cpu
周期,更多的代码。

4.
通常没有方法可以恢复错误代码,所以如果程序代码检查到一个错误比如
EINVAL
,它所能做的,只能是挂起程序。

ecos
内核采取了一种不同的方式。一些函数,比如

cyg_mutex_lock

,不会返回一个错误代码。作为替代,他包含大量断言,这些断言能被打开或关闭。在开发期间,断言通常都被打开,内核的函数会进行参数检查和一些系统检查。如果一个问题被检测到,那么断言就会失败,从而应用程序被终止。在一个典型的调试中,程序员会设置一些断点,然后检查系统状态,准确地知道将要发生的事情。在开发的最后阶段,通常会通过配置选项关闭断言,这样所有的断言就会在编译阶段被清楚。这样做有一个假定:所有的程序代码
bug
都已经得到最好的解决了,代码必须可以操作信号量像操作共用体一样,但是不会出错。这样做有几个好处:

1.
在最终的程序中,没有检查参数的系统开销。所有这些开销都在编译阶段清除掉了。

2.
因为最终的程序不会忍受额外的开销,开发期间系统做更多的工作也是合理的。特别是断言可以测试更多的错误条件和复杂的错误。当以个错误被检测到的时候,一条错误的信息比一个错误的代码有用的多。

3.
程序不需要处理内核函数的返回值。这可以简化程序代码。

4.
如果错误被检测到,断言失败,程序立即挂起。没有忽略错误条件的可能,因为程序代码不会检查返回的错误代码。

尽管没有内核函数返回错误代码,它们很多会返回一个状态条件。例如,函数
cyg_semaphore_timed_wait
一直等待,直到一个事件发生,或者一定的时钟周期完成。通常调用者直到等待操作完

抱歉!评论已关闭.