嵌入式实时操作系统的设计与开发(二)

文章详细阐述了加载应用程序到内存的过程,特别是针对裸板程序的加载域和运行域。接着介绍了RTOS(实时操作系统)的概念,以及线程作为基本执行单元的角色。文中提到了线程的状态、调度策略和资源管理,特别强调了线程的优先级和内存保护机制在多线程环境中的重要性。此外,还讨论了aCoral这种实时操作系统中的线程创建、调度和管理机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

加载应用程序与创建程序运行环境

将应用程序从Flash加载到RAM的实现代码是一定在启动代码中的。
计算机系统的运行其实是CPU到相应的内存地址去取回指令,然后译码并执行指令,再依次从下一个地址取指、执行,而程序就是指令与数据的集合。
程序的运行就是CPU从程序中取出指令、执行指令,当需要时,再从程序中取得需要的数据。
对于没有任何操作系统或裸板上的应用程序,它的执行会设计许多步骤。
需要了解,在给定的CPU体系下,裸板应用程序的镜像文件的组成方式,这里镜像文件就是可执行文件。
由ADS编译链接器生成的ARM镜像文件,当都由一个或多个域组成,域有两种:加载域与运行域。

  1. 加载域:程序被加载到内存的地方。
  2. 运行域:程序在内存中具体运行时所占有的地方。

一个域由一个或多个输出段组成,而一个输出段由一个或多个输入段组成。
输入段的属性有三种:RO(只读)、RW(可读写)、ZI(可读写但是未被初始化且需初始化为0)。

  1. RO代表的是一个源文件中指令代码与常量,这些在程序中不能改变。
  2. RW代表一个源文件中已经被初始化的变量(全局变量),这些变量可以修改和读取,并且已经被初始化为一个确定的值。
  3. 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所在进程是不能相互访问彼此之间的变量的。

  1. 地址保护。每个进程都有自己的地址空间,如果当前进程跨界访问了其它进程的区域,则会出错,就访问不了这个地址,这种地址保护需要硬件有存储保护单元MPU(Memory Protection Unit)的支持。
  2. 虚拟地址。各个进程仅管访问同一地址,但是由于虚拟地址机制,它们对应的物理地址是不一样的,所以读取的值就会不一样。虚拟地址需要硬件有内存管理单元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成员赋值。
在这里插入图片描述
线程创建的最后一步就是将其挂到就绪队列上,之后由调度机制来负责具体任务调度。

调度策略分类

一种调度策略对应一种线程。

  1. 普通线程(通用策略创建的线程)。这种线程,需要人为指定CPU、优先级信息,通过acoral_thread_create创建。
  2. 分时线程(分时策略创建的线程)。aCoral支持相同优先级的线程,对于相同优先级的线程,默认采用FIFO方式调度。当用户需要线程以分时的方式和其它线程共享CPU时,可以将线程设置为分时线程。需要注意(1)只存在相同优先级的分时策略,不同优先级线程之间不存在所谓的分时策略,而是按优先级抢占策略来调度的。(2)必须是两厢情愿的,当a分时后,它执行指定时间片后会将CPU移交给b。
  3. 周期线程(周期策略创建的线程)。这种线程每隔一个固定时间就要执行一次。这种需求在嵌入式实时系统较常见,如信号采集系统有一个采样周期,每隔一段时间要采集一路信号。
  4. RM线程(RM策略创建的线程)。RM是一种可以满足任务截止时间的强实时调度算法,这种策略需要周期性线程策略的支持。
  5. 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等硬件资源的管理由裸板程序时代进入操作系统时代。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

饼干饼干圆又圆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值