深入了解QK内核:特性、使用与移植
1. QK内核基础特性
QK作为一个抢占式内核,在空闲处理方面与非抢占式普通内核有所不同。QK_onIdle()回调函数在中断未锁定的状态下被调用,并且无需解锁中断(这与QF_onIdle()回调函数不同)。此外,在QK_onIdle()内部切换到低功耗睡眠模式时,也无需锁定中断,这种切换是安全的,不会引发竞态条件,因为只要有事件需要处理,抢占式内核就不会将上下文切换回空闲循环。
QActive_start()函数用于初始化事件队列并启动活动对象QK任务。对于传统内核,QActive_start()的第五和第六个参数分别代表私有堆栈内存及其大小。而在QK内核中,第五个参数用作指向QK任务的线程本地存储(TLS)的指针,第六个参数则作为标志位掩码,代表任务的属性,例如任务是否使用协处理器。
QK内核使用原生的QF事件队列QEQueue,需要使用QEQueue_init()函数进行初始化。活动对象的QF优先级在活动对象内部设置,然后将活动对象添加到QF框架中。任务标志存储在osObject数据成员中,指向该任务TLS的指针存储在thread数据成员中,最后初始化活动对象的内部状态机。
2. 高级QK特性
2.1 优先级上限互斥锁
QK是一个抢占式内核,在QK任务之间共享资源时需要格外小心。理想情况下,QF活动对象应仅通过事件进行通信,不共享任何资源。但如果选择共享某些资源,则需要对这些资源的访问进行互锁。
一种强大的保证资源互斥访问的方法是使用QF宏QF_INT_LOCK()和QF_INT_UNLOCK()实现的临界区机制,对于非常短的访问,这可能是最有效的同步机制。不过,QK还支持优先级上限互斥锁,以防止在访问共享资源时发生任务级别的抢占。优先级上限互斥锁可以避免优先级反转问题,并且比中断锁定更具选择性,因为所有优先级高于优先级上限的任务(和中断)仍能正常运行。
以下是使用QK优先级上限互斥锁保护共享资源的示例代码:
void your_function(arguments) {
// (1) 提供一个类型为QMutex的临时互斥锁变量
QMutex mutex;
...
// (2) 调用QK_mutexLock()锁定互斥锁
mutex = QK_mutexLock(PRIO_CEILING);
// (3) 安全访问共享资源
You can safely access the shared resource here
// (4) 调用QK_mutexUnlock()解锁互斥锁
QK_mutexUnlock(mutex);
...
}
具体操作步骤如下:
1. 提供一个类型为QMutex(仅为一个字节)的临时互斥锁变量。
2. 调用QK_mutexLock()函数锁定互斥锁,该函数需要优先级上限参数,通常选择为可能使用要保护的共享资源的最高优先级任务的优先级,这个优先级通常在编译时就已知。
3. 访问共享资源。
4. 调用QK_mutexUnlock()函数解锁互斥锁。
互斥锁变量仅临时使用,应用程序中使用的互斥锁数量没有限制,原则上互斥锁锁甚至可以嵌套,但这不一定是好的设计。
QK互斥锁的实现代码如下:
QMutex QK_mutexLock(uint8_t prioCeiling) {
uint8_t mutex;
QK_INT_LOCK_KEY_
QK_INT_LOCK_();
// (1) 保存当前QK优先级
mutex = QK_currPrio_;
// (2) 如果优先级上限超过当前QK优先级
if (QK_currPrio_ < prioCeiling) {
// (3) 提高当前QK优先级
QK_currPrio_ = prioCeiling;
}
QK_INT_UNLOCK_();
// (4) 返回原始QK优先级
return mutex;
}
void QK_mutexUnlock(QMutex mutex) {
QK_INT_LOCK_KEY_
QK_INT_LOCK_();
// (5) 比较当前QK优先级和互斥锁参数
if (QK_currPrio_ > mutex) {
// (6) 恢复保存的优先级
QK_currPrio_ = mutex;
// (7) 调用QK调度器处理潜在的同步抢占
QK_SCHEDULE_();
}
QK_INT_UNLOCK_();
}
锁定互斥锁实际上是将当前QK优先级提高到优先级上限水平,这样可以防止优先级低于或等于优先级上限的所有任务进行抢占,从而保护资源。QK互斥锁是非阻塞机制,如果一个需要保护共享资源的任务正在运行,说明所有更高优先级的任务都没有事件需要处理,因此只需防止可能访问该资源的更高优先级任务启动,就足以保证对资源的互斥访问。
2.2 线程本地存储(TLS)
线程本地存储(TLS)是一种变量分配机制,每个现存线程都有该变量的一个实例。以Newlib标准C运行时库为例,其设施是可重入的,但需要正确集成到多线程环境中。例如,ANSI C标准中的errno设施,在多线程环境下,如果errno是一个所有线程共享的全局变量,就会出现重入问题。
Newlib通过将errno重新定义为一个宏,间接引用一个名为_impure_ptr的全局指针来解决这个问题。_impure_ptr指向一个struct _reent类型的结构,该结构包含传统的errno值以及许多其他元素。每个线程都有自己的_reent结构副本,并且在上下文切换时,_impure_ptr指针会切换到当前活动线程的_reent结构。
QK通过提供上下文切换钩子QK_TLS()来支持TLS概念,该钩子在处理不同任务优先级时被调用。以下是qk_port.h中定义的QK_TLS()宏,用于在上下文切换时重新分配Newlib的_impure_ptr:
#define QK_TLS(act_) (_impure_ptr = (struct _reent *)(act_)->thread)
虽然QK_TLS()宏会自动切换_impure_ptr,但你需要负责在每个活动对象中分配_reent结构,并在启动时通过将TLS指针作为QActive_start()函数的第五个参数来告知QK每个活动对象的TLS位置。需要注意的是,当前QK中TLS的实现假设线程本地存储既不在ISR中也不在空闲循环(QK_onIdle()回调函数内部)中访问。
2.3 扩展上下文切换(协处理器支持)
C编译器生成的中断上下文保存和恢复通常只包括CPU核心寄存器,不包括各种协处理器的寄存器,如浮点协处理器、专用DSP引擎、专用基带处理器、视频加速器等。如果多个任务使用这些协处理器,对于抢占式内核来说就会出现问题。
QK内核以通用的方式支持扩展上下文切换,可针对各种协处理器和硬件加速器进行定制。扩展上下文切换的设计会尽量减少额外开销,仅在必要时保存和恢复扩展上下文。基本假设是ISR和QK空闲循环都不使用协处理器,因此仅在一个任务抢占另一个任务时才需要保留扩展上下文,同步上下文切换通常不需要扩展。
异步抢占在中断退出时由QK_ISR_EXIT()宏处理,以下是QK_ISR_EXIT()宏的伪代码:
#define QK_ISR_EXIT() do {
// 锁定中断
Lock interrupts
// 向中断控制器发送EOI指令
Send the EOI instruction to the interrupt controller
--QK_intNest_;
if (QK_intNest_ == 0) {
// 调用扩展调度器
QK_scheduleExt_();
}
} while (0)
扩展调度器QK_scheduleExt_()的代码如下:
#ifndef QF_INT_KEY_TYPE
void QK_scheduleExt_(void) {
#else
void QK_scheduleExt_(QF_INT_KEY_TYPE intLockKey_) {
#endif
uint8_t p;
// 确保QK调度器仅在任务级别被调用
Q_REQUIRE(QK_intNest_ == (uint8_t)0);
if (QPSet64_notEmpty(&QK_readySet_)) {
// 确定最高优先级就绪任务的优先级
QPSet64_findMax(&QK_readySet_, p);
if (p > QK_currPrio_) {
// 保存初始优先级
uint8_t pin = QK_currPrio_;
QActive *a;
#ifdef QK_TLS
uint8_t pprev = pin;
#endif
#ifdef QK_EXT_SAVE
if (pin != (uint8_t)0) {
// 获取被抢占活动对象的指针
a = QF_active_[pin];
// 保存扩展上下文
QK_EXT_SAVE(a);
}
#endif
do {
QEvent const *e;
// 获取活动对象的指针
a = QF_active_[p];
// 更新当前任务优先级
QK_currPrio_ = p;
#ifdef QK_TLS
if (p != pprev) {
// 切换新的线程本地存储
QK_TLS(a);
pprev = p;
}
#endif
// 解锁中断
QK_INT_UNLOCK_();
// 获取下一个事件
e = QActive_get_(a);
// 调度事件到活动对象
QF_ACTIVE_DISPATCH_(&a->super, e);
// 垃圾回收事件
QF_gc(e);
// 锁定中断
QK_INT_LOCK_();
if (QPSet64_notEmpty(&QK_readySet_)) {
// 确定最高优先级就绪活动对象
QPSet64_findMax(&QK_readySet_, p);
} else {
p = (uint8_t)0;
}
} while (p > pin);
// 恢复初始优先级
QK_currPrio_ = pin;
#if defined(QK_TLS) || defined(QK_EXT_RESTORE)
if (pin != (uint8_t)0) {
// 获取被抢占活动对象的指针
a = QF_active_[pin];
#ifdef QK_TLS
// 恢复原始TLS
QK_TLS(a);
#endif
#ifdef QK_EXT_RESTORE
// 恢复扩展上下文
QK_EXT_RESTORE(a);
#endif
}
#endif
}
}
}
QK_EXT_SAVE()和QK_EXT_RESTORE()宏允许为给定任务保存和恢复尽可能多的协处理器上下文,需要为使用的所有扩展上下文提供每个任务的内存。
3. 移植QK
当使用QF与QK抢占式内核时,无需将QF框架移植到内核,因为QF和QK已经集成。但仍需要将QK内核移植到目标CPU和编译器。由于QK内核的简单性,移植相对容易,只需要在qep_porth.h中提供编译器特定的精确宽度整数类型,在qf_port.h中配置QF,最后在qk_port.h中提供中断锁定策略和中断进入/退出。大多数情况下,QK可以使用C编译器生成的ISR,无需编写任何特定于平台的QK源文件。
QK可以移植到满足以下要求的处理器和编译器:
1. 处理器支持能够容纳相当数量数据(至少256字节或更多)的硬件堆栈。
2. C或C++编译器能够生成可重入代码,特别是能够在堆栈上分配自动变量。
3. 可以从C语言中锁定和解锁中断。
4. 系统提供时钟滴答中断(通常为10到100Hz)。
综上所述,QK内核具有丰富的特性和良好的可移植性,在多任务处理和资源共享方面提供了有效的解决方案。通过合理使用其高级特性,可以提高系统的性能和稳定性。
深入了解QK内核:特性、使用与移植
4. 操作步骤总结与对比
为了更清晰地理解QK内核的使用和特性,下面对前面提到的关键操作步骤进行总结,并通过表格形式对比不同特性的使用方法。
| 特性 | 操作步骤 |
|---|---|
| 优先级上限互斥锁 |
1. 提供一个类型为QMutex的临时互斥锁变量;
2. 调用QK_mutexLock()函数,传入优先级上限参数锁定互斥锁; 3. 访问共享资源; 4. 调用QK_mutexUnlock()函数解锁互斥锁。 |
| 线程本地存储(TLS) |
1. 在每个活动对象中分配_reent结构;
2. 在启动时将TLS指针作为QActive_start()函数的第五个参数; 3. 利用QK_TLS()宏在上下文切换时切换_impure_ptr。 |
| 扩展上下文切换(协处理器支持) |
1. 在中断退出时,QK_ISR_EXIT()宏调用扩展调度器QK_scheduleExt_;
2. 在QK_scheduleExt_中,根据情况保存和恢复扩展上下文(使用QK_EXT_SAVE()和QK_EXT_RESTORE()宏)。 |
通过这个表格,可以快速对比不同特性的操作步骤,便于在实际应用中选择合适的方法。
5. 流程图展示
为了更直观地理解QK内核的一些关键流程,下面使用mermaid格式绘制流程图。
5.1 优先级上限互斥锁使用流程
graph TD;
A[定义QMutex变量] --> B[调用QK_mutexLock()];
B --> C[访问共享资源];
C --> D[调用QK_mutexUnlock()];
这个流程图展示了优先级上限互斥锁的使用流程,从定义变量开始,到锁定互斥锁、访问资源,最后解锁互斥锁。
5.2 扩展上下文切换流程
graph TD;
A[中断退出] --> B[QK_ISR_EXIT()宏];
B --> C{QK_intNest_ == 0?};
C -- 是 --> D[调用QK_scheduleExt_];
D --> E{是否有抢占?};
E -- 是 --> F[保存扩展上下文];
F --> G[执行任务调度];
G --> H[恢复扩展上下文];
C -- 否 --> I[结束];
E -- 否 --> I[结束];
这个流程图展示了扩展上下文切换的流程,从中断退出开始,根据中断嵌套情况决定是否调用扩展调度器,在有抢占的情况下保存和恢复扩展上下文。
6. 注意事项与最佳实践
在使用QK内核的过程中,有一些注意事项和最佳实践可以帮助开发者更好地发挥其性能。
- 资源共享方面 :尽量遵循理想情况,让QF活动对象仅通过事件进行通信,避免共享资源。如果必须共享资源,优先考虑使用优先级上限互斥锁,因为它可以避免优先级反转问题,并且比中断锁定更具选择性。
- 线程本地存储(TLS) :确保在每个活动对象中正确分配_reent结构,并在启动时正确传递TLS指针。同时,要注意当前QK中TLS的实现假设线程本地存储既不在ISR中也不在空闲循环中访问。
- 扩展上下文切换(协处理器支持) :在使用扩展上下文切换时,要根据实际情况合理配置QK_EXT_SAVE()和QK_EXT_RESTORE()宏,为使用的所有扩展上下文提供每个任务的内存。
7. 总结
QK内核作为一个抢占式内核,在空闲处理、资源共享、线程管理和协处理器支持等方面都有独特的设计和实现。通过优先级上限互斥锁可以有效地保护共享资源,避免优先级反转问题;线程本地存储(TLS)机制为多线程环境下的可重入库提供了支持;扩展上下文切换则解决了多个任务共享协处理器时的上下文保存和恢复问题。
在移植方面,由于QK内核的简单性,只需要满足一定的处理器和编译器要求,就可以相对容易地将其移植到目标平台。
在实际应用中,开发者可以根据具体需求选择合适的特性和操作方法,遵循注意事项和最佳实践,以提高系统的性能和稳定性。同时,通过流程图和表格等工具,可以更好地理解和掌握QK内核的使用流程。
超级会员免费看
96

被折叠的 条评论
为什么被折叠?



