加载应用程序与创建程序运行环境
将应用程序从Flash加载到RAM的实现代码是一定在启动代码中的。
计算机系统的运行其实是CPU到相应的内存地址去取回指令,然后译码并执行指令,再依次从下一个地址取指、执行,而程序就是指令与数据的集合。
程序的运行就是CPU从程序中取出指令、执行指令,当需要时,再从程序中取得需要的数据。
对于没有任何操作系统或裸板上的应用程序,它的执行会设计许多步骤。
需要了解,在给定的CPU体系下,裸板应用程序的镜像文件的组成方式,这里镜像文件就是可执行文件。
由ADS编译链接器生成的ARM镜像文件,当都由一个或多个域组成,域有两种:加载域与运行域。
- 加载域:程序被加载到内存的地方。
- 运行域:程序在内存中具体运行时所占有的地方。
一个域由一个或多个输出段组成,而一个输出段由一个或多个输入段组成。
输入段的属性有三种:RO(只读)、RW(可读写)、ZI(可读写但是未被初始化且需初始化为0)。
- RO代表的是一个源文件中指令代码与常量,这些在程序中不能改变。
- RW代表一个源文件中已经被初始化的变量(全局变量),这些变量可以修改和读取,并且已经被初始化为一个确定的值。
- ZI代表未被初始化且初始化为0的变量(ZI段在镜像文件中并不占空间)。
输出段由一个和多个属性相同的输入段组成。
一个简单裸板ARM镜像文件在外部存储器中的大致结构为:
在该镜像文件结构中,ZI段没有占应有的数据空间,只有一些必要的信息。RW输出段紧跟在RO输出段之后,加载域的RO起始位置与运行域时的起始位置相同。
但在ARM镜像文件执行时,RW段可以不与RO段连续,ZI段与RW段连续。这样设计的原因是让应用程序能充分使用系统有限的内存。
进行完应用程序运行域的生成后,程序就可以开始运行了。
参数的设置是将应用程序不同属性输出段放在RAM的相应位置,这样镜像不必要连续存放,能更充分利用有限的存储资源。
跳转到主程序
跳转到主程序可通过一条bl指令完成。
bl my_MainLoop;
内核基础
RTOS:RTOS可以简单认为是功能强大的主控程序,它嵌入在目标机代码中,系统复位后首先执行;在硬件基础上为应用软件建立一个功能强大的运行环境,用户的应用程序都运行于RTOS之上。
RTOS内含一个实时内核,将CPU、定时器、中断、I/O等资源集中管理起来,为用户提供一套标准API;并可根据各个任务的优先级合理安排任务在CPU上执行。
编写内核
aCoral线程
aCoral调度的基本单位是线程,aCoral的一个线程也可称为一个任务。
真正的RTOS,基本上没有做到进程,只是停留在多线程,因为多进程要解决很多问题,且需要硬件支持,这样就使得系统复杂了,可能影响系统实时性。
线程和进程的区别:
线程之间是共享地址的,也就是说当前线程的地址对于其它线程的地址是可见的,如果修改了地址的内容,其它线程是可以知道,并且能访问的。
int i = 1;
test(){
sleep(10s);
printf("%d",i);
}
int main(){
create_task(test,....);
i++;
}
如果create_task对应的是创建线程的接口,则test输出2,如果是创建进程的接口,则test输出1。
如果是多进程,main函数所在进程和test所在进程是不能相互访问彼此之间的变量的。
- 地址保护。每个进程都有自己的地址空间,如果当前进程跨界访问了其它进程的区域,则会出错,就访问不了这个地址,这种地址保护需要硬件有存储保护单元MPU(Memory Protection Unit)的支持。
- 虚拟地址。各个进程仅管访问同一地址,但是由于虚拟地址机制,它们对应的物理地址是不一样的,所以读取的值就会不一样。虚拟地址需要硬件有内存管理单元MMU(Memory Management Unit)的支持。
所以进程之间相互独立、隔离,一个进程的崩溃或错误操作不会影响其它进程。但是无法直接访问全局变量,因为全局变量都变成了进程范围内的全局变量。
所以RTOS很少支持多进程,一是RTOS从单片机发展来的,硬件不支持;二是进程间通信、互斥的开销太大,导致系统复杂,对注重实时性的应用来说,代价太大。
描述线程
aCoral是多线程嵌入式实时操作系统,线程就是一段代码的执行体。
ACORAL_COMM_THREAD test3(acoral_u32 timer){
while(1){
acoral_delay_self(timer);
}
}
void test_delay_init(){
acoral_print("%d\n",i);
id = acoral_create_thread(test3,256,500+i*18,"delay",i+1,-l);
}
test_delay_init()创建了34个线程,这些线程都执行相同的代码,即test3。
相同的执行代码为什么是不同的线程呢?因为它们有不同的执行环境,所以线程保护了执行代码+执行环境。
执行环境就是“堆栈+寄存器”。
在aCoral中,线程控制块TCB(Task Control Block)是acoral_thread_t。
typedef struct{
acoral_res_t res;
#ifdef CFG_CMP
acoral_spinlock_t move_lock;
#endif
acoral_u8 state;
acoral_u8 prio;
acoral_8 CPU;
acoral_u32 CPU_mask;
acoral_u8 policy;
acoral_list_t ready;
acoral_list_t timeout;
acoral_list_t waiting;
acoral_list_t global_list;
acoral_evt_t *evt;
acoral_u32 *stack;
acoral_u32 *stack_buttom;
acoral_u32 stack_size;
acoral_u32 delay;
acoral_u32 slice;
acoral_char *name;
acoral_id console_id;
void* pricate_data;
void* data;
}acoral_thread_t;
- res:线程控制块是一种资源,因此拥有一个称为res的结构体成员。
- move_lock:用以支持自旋锁,使aCoral支持多核CMP(Chip Multi-Processors)。自旋锁是专为防止多核/处理器并发而引入的一种锁机制。
- state:线程状态,有五种线程状态ACORAL_THREAD_STATE_READY、ACORAL_THREAD_STATE_SUSPEND、ACORAL_THREAD_STATE_EXIT、ACORAL_THREAD_STATE_RELEASE、ACORAL_THREAD_STATE_RUNNING。
ACORAL_THREAD_STATE_EXIT意味着某个线程退出了,不会再参与调度,但此时该线程的资源,如线程控制块TCB、堆栈等资源还未释放,而ACORAL_THREAD_STATE_RELEASE状态意味着可以释放这些资源。 - prio:优先级。
- CPU:aCoral是一款支持多核的RTOS,这个指示线程在哪个CPU上执行。当前aCoral尚不支持线程迁移:线程创建时在哪个CPU,以后的整个执行过程也都是在该CPU上。
- CPU_mask:指示线程可以在哪些CPU上执行,如0x1表示线程只可以在CPU0上执行,0x3表示线程可在CPU0、CPU1上执行。
- policy:线程调度策略,如时间片轮转、先来先服务、周期性调度策略,一种策略对应一类线程。
- ready、waiting、timeout、global_list:这4个acoral_list_t成员主要是用来将线程结构挂载到相应链表队列上。
(1)ready:当用户调用了acoral_rdy_thread或acoral_resume_thread接口时,就会将线程挂到就绪队列acoral_ready_queue上。
(2)waiting:当用户调用了acoral_udrdy_thread或acoral_delay_self接口时,就会将线程挂到延时队列timer_delay_queue上。
(3)timeout:当线程因为申请某种资源而被阻塞,且超过了预先设置的时间时,则会将线程挂到超时链表队列上。
(4)global_list:用来将线程挂到全局线程链表。 - evt:指向线程占用的事件(信号量、互斥量、邮箱等),当线程退出时,必须释放该事件。
- stack:指示线程的堆栈。在当前线程被其它线程抢占,并切换到其它线程时,当前线程的stack会赋值为CPU堆栈寄存器sp的值。每个线程都有自己的堆栈,用以存放自己的运行环境,当任务切换时,存放被切换进程的运行环境,恢复新线程的运行环境。
- stack_buttom:这是栈底。一个线程的堆栈是有大小的,当堆栈指针超过了栈底,这时sp指向的内存地址已经不是本线程的内存空间,可能会破坏其它线程的数据结构,严重时会导致系统崩溃。
- stack_size:堆栈大小。
- delay:当用户需要延迟某个线程的执行时,用它来指定延迟的时间,单位是Ticks。当用户调用acoral_delay_self时传入的时间参数转化为Ticks再赋给delay。
- slice:线程执行的时间片,用于同优先级且支持时间片轮转策略的线程调度,内核将根据各个线程的slice来调度线程。
- name:线程名字。
- console_id:线程控制台ID号。
- private_data:长久备用数据指针,目前用于线程策略私有数据指针。
- data:临时备用数据指针。
TCB里的stack成员隐含了该线程的执行代码信息,因为当任务切换时,stack将保存被切换线程的PC指针,PC指向线程的当前执行代码。
res结构体的定义
typedef union{
acoral_id id;
acoral_u16 next_id;
}acoral_res_t;
线程控制块是一种资源,id表示线程的资源ID,当某个资源空闲时,id的高16位表示该资源在资源池的编号,分配后表示该资源的ID。
next_id表示下一资源的ID,它是个空闲链表指针,指向下一个空闲的资源的编号,属于资源ID的一部分。
res代表了资源的ID,资源ID由资源类型Type和空闲内存池ID两部分组成。
aCoral定义了6种资源类型:线程型、事件型、时钟型、驱动型、GUI型、用户使用型。
#define ACORAL_RES_THREAD 1
#define ACORAL_RES_EVENT 2
#define ACORAL_RES_TIMER 3
#define ACORAL_RES_DRIVER 4
#define ACORAL_RES_GUI 5
#define ACORAL_RES_USER 6
如果资源为线程,则其类型Type为1。
aCoral采用了资源池的内存管理方式,而资源池由结构acoral_pool_t定义。
空闲内存池ID由aCoral内存管理模块在初始化分配内存时,根据当前内存块数确定。
aCoral启动完成后,若用户要创建某一新线程,将调用函数acoral_get_free_pool(),从空闲内存资源池中获取一空闲内存,并获取其ID号,将申请的内存空间供该线程使用。
typedef struct{
void *base_adr;//在空闲时指向下一个pool,否则为管理的资源的基地址。
void *res_free;//指向下一空闲资源
...
}acoral_pool_t;
资源池初始化
void acoral_pools_cinit(void);
创建某一资源池
acoral_err acoral_create_pool(acoral_pool_ctrl_t *pool_strl);
aCoral的优先级与数字大小成反比,即:数字越大,优先级越低。
#ifdef CFG_THRD_POSIX
#define ACORAL_MAX_PRIO_NUM ((CFG_MAX_THREAD+CFG_POSIX_START_NUM+1) & 0xff)
#else
#define ACORAL_MAX_PRIO_NUM ((CFG_MAX_THREAD+1) & 0xff)
#define ACORAL_INIT_PRIO 0 #aCoral的初始优先级为0
#define ACORAL_MAX_PRIO 1 #aCoral的最高优先级为1
#define ACORAL_MIN_PRIO ACORAL_MAX_PRIO_NUM-1 #最小优先级是总的优先级数减一
一般情况下ACORAL_MAX_PRIO_NUM-1=100,如果为了支持POSIX线程标准((CFG_MAX_THREAD+CFG_POSIX_START_NUM+1) & 0xff) =130
acoral_list_t是一个双向链表,如果将aCoral配置为支持CMP,acoral_list_t还定义了自旋锁acoral_spinlock_t。
struct acoral_list{
struct acoral_list *prev,*next;
#ifdef CFG_CMP
acoral_spinlock_t lock;
#endif
};
这种通过TCB成员定义的结构(acoral_list_t)来挂到相应链表队列上的方式的优点是:可以用相同数据处理方式来描述所有双向链表,不用再单独为各个链表编写各种函数。
线程优先级
aCoral的就绪队列采用的是优先级链表,每个优先级是一个链表,相同优先级的线程都挂在此链表上。
对于RTOS,几乎都是采用了基于优先级的抢占调度策略。
为了支持基于优先级的抢占调度,aCoral的优先级通过acoral_prio_array来定义。
struct acoral_prio_array{
acoral_u32 num; //就绪任务总数
acoral_u32 bitmap[PRIO_BITMAP_SIZE];//标识某一优先级是否有就绪队列,这样才能确保以O(1)复杂度找出最高优先级的线程。
//PRIO_BITMAP_SIZE由系统配置的优先级总数确定
acoral_queue_t queue[ACORAL_MAX_PRIO_NUM];//优先级链表数组,数组成员是一个链表队列acoral_queue_t,挂在该优先级的就绪队列。
}
优先级位图数组bitmap[PRIO_BITMAP_SIZE]每个变量都是32位。
每个变量从右到左的每一位依次代表一个优先级(bitmap的每个变量共代表了32个优先级),而每一位由“0”,“1”两个可能值。
- “0”:该位对应的优先级没有任务就绪。
- “1”:该位对应的优先级有任务就绪。
调度策略
aCoral把线程相关的操作统称为线程调度。
把调度分为两层,上层策略,下层机制,采用策略与机制分离的原则,可以灵活方便地扩展调度策略,而不改变底层的调度机制。
- 调度策略就是如何确定线程的CPU、优先级prio等参数,线程是按照什么策略来调度。一种策略对应一种线程。
- 调度机制根据调度策略来安排任务的具体执行,如何创建线程?…
线程调度分层结构
调度策略本质就是调度算法,即确定任务执行顺序的规则。
调度策略目前包括通用策略、分时策略、周期策略和RM策略,用户还可以自行扩展新的调度策略。
当用户创建线程时,需要指定某种调度策略,并找到对应的策略控制块,再为TCB成员赋值。
线程创建的最后一步就是将其挂到就绪队列上,之后由调度机制来负责具体任务调度。
调度策略分类
一种调度策略对应一种线程。
- 普通线程(通用策略创建的线程)。这种线程,需要人为指定CPU、优先级信息,通过acoral_thread_create创建。
- 分时线程(分时策略创建的线程)。aCoral支持相同优先级的线程,对于相同优先级的线程,默认采用FIFO方式调度。当用户需要线程以分时的方式和其它线程共享CPU时,可以将线程设置为分时线程。需要注意(1)只存在相同优先级的分时策略,不同优先级线程之间不存在所谓的分时策略,而是按优先级抢占策略来调度的。(2)必须是两厢情愿的,当a分时后,它执行指定时间片后会将CPU移交给b。
- 周期线程(周期策略创建的线程)。这种线程每隔一个固定时间就要执行一次。这种需求在嵌入式实时系统较常见,如信号采集系统有一个采样周期,每隔一段时间要采集一路信号。
- RM线程(RM策略创建的线程)。RM是一种可以满足任务截止时间的强实时调度算法,这种策略需要周期性线程策略的支持。
- POSIX线程(POSIX策略创建的线程)。POSIX线程属于非实时线程,这类线程的主要特点是越公平越好,这种线程的调度算法是电梯调度算法。
typedef struct{
acoral_list_t list; //策略链表结点,用于将策略挂到策略链表上去。
acoral_u8 type; //策略类型ACORAL_SCHED_POLICY_COMM、ACORAL_SCHED_POLICY_SLICE、ACORAL_SCHED_POLICY_PERIOD、ACORAL_SCHED_POLICY_RM、ACORAL_SCHED_POLICY_POSIX
acoral_id (*policy_thread_init)(acoral_thread_t*,void (*route)(void *args),void *,void *); //策略初始化函数,用于确定线程的CPU、优先级prio等
void (*delay_deal)(); //与延时相关的处理函数,period、slice等策略都要用到类似的延时机制。
acoral_char *name; //用于传递某种调度策略所需要的参数,每种策略对应一种数据结构,用来保存线程的参数,不同策略需要的参数不同,用户创建线程时传递的数据结构也不一样,比如普通策略的参数只有CPU、prio
}acoral_sched_policy_t;
查找调度策略
当用户想根据某种调度策略创建线程时,须根据TCB的policy成员值从策略控制块链表中查找到相应的结点,将信息取出赋值给相应的TCB成员。
具体查找过程:
acoral_sched_policy_t *acoral_get_policy_ctrl(acoral_u8 type){
acoral_list_t *tmp,*head;
acoral_sched_policy_t *policy_ctrl;
head = &policy_list.head;
tmp = head;
for(tmp=head->next;tmp!=head;tmp=tmp->next){
policy_ctrl = list_entry(tmp,acoral_sched_policy_t,list);
if(policy_ctrl->type == type){
return policy_ctrl;
}
}
return NULL;
}
注册调度策略
若要在aCoral扩展新的调度策略并生效,须进行注册,注册后,用户才能通过此策略创建特定类型的线程。
注册就是将用户自己定义的调度策略挂载到策略控制块上,放在队列尾(将策略挂到策略链表上)。
void acoral_register_sched_policy(acoral_sched_policy_t *policy){
acoral_list_add2_tial(&policy->list,&policy_list.head);
}
通用调度策略在进行通用调度策略初始化com_policy_init()时注册
void comm_policy_init(){
comm_policy.type = ACORAL_SCHED_POLICY_COMM;
comm_policy.policy_thread_init = comm_policy_thread_init;//绑定策略初始化函数
...
acoral_register_sched_policy(&comm_policy);//绑定完后,对策略进行注册,挂载到策略控制块链表的尾部。
}
通用调度策略初始化(comm_policy_init())是在aCoral调度策略初始化(acoral_sched_policy_init())时被调用的。
void acoral_sched_policy_init(){
acoral_list_init(&policy_list.head);
comm_policy_init();
#ifdef CFG_THRD_SLICE
slice_policy_init();
#endif
#ifdef CFG_THRD_PERIOD
period_policy_init();
#endif
#ifdef RM_THRD_SLICE
rm_policy_init();
#endif
#ifdef CFG_THRD_POSIX
posix_policy_init();
#endif
}
aCoral调度策略初始化(acoral_sched_policy_init())在aCoral系统初始化时启用。
// 内核各模块初始化
void acoral_module_init(){
/*中断系统初始化*/
acoral_intr_sys_init();
/*内存管理系统初始化*/
acoral_mem_sys_init();
/*资源管理系统初始化*/
acoral_res_sys_init();
/*线程管理系统初始化*/
acoral_thread_sys_init();
/*时钟管理系统初始化*/
acoral_time_sys_init();
/*事件系统初始化*/
acoral_evt_sys_init();
#ifdef CFG_DRIVER
acoral_drv_sys_init();
#endif
}
acoral_start()是内核各模块初始化的入口,也是aCoral系统初始化的入口,当CPU启动完成后,就会通过
ldr pc,=acoral_start
进入acoral_start(),开始aCoral的启动工作。至此,对CPU等硬件资源的管理由裸板程序时代进入操作系统时代。