内核不拿命中断来挡刀,谁来护你家应用?”——从鸿蒙内核(LiteOS)的调度到内存,再到线程通信,我都摊开了说
开篇语
哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛
今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。
我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!
前言
说实话,第一次摸一个小而美的实时内核,你很容易被它“我很轻,我很快”的自信骗到:代码量少,接口也不多,仿佛“写俩任务、丢个队列、开个中断”就能立地成佛。可一旦系统开始抖、任务互咬、内存碎得像饼干渣,你立刻就想问——**内核到底在背后做了什么魔术?**别急,今天我们把鸿蒙内核(LiteOS)里最关键的三根骨头——任务调度、内存管理、线程通信与同步——一次说透。不拧巴,不装深,把底层逻辑讲清、把坑点摊平、把代码跑起来。
目录
-
- 就绪队列与位图
- 抢占、时间片与调度锁
- 中断上下文与延迟调度
- 优先级反转与继承/上限
-
- 固定块池 vs. 小对象池(slab-like)
- 通用堆(TLSF/buddy 思维)
- 栈与溢出保护、MPU/栈哨兵
-
- 信号量、互斥量、事件标志组
- 消息队列、邮箱/管道
- 从 ISR 到任务的“无阻塞交接”
开篇三问:RTOS 的“快”到底快在哪?
**快,不是频率高;是决策更快。**RTOS 核心不在“算得多”,而在“在正确的时间点,让正确的任务立刻运行”。所以三个关键词:
- O(1) 调度:就绪优先级一眼看穿;
- 中断延迟小:ISR 短平快,唤醒任务别绕路;
- 内存确定性:分配释放可预期,不在关键路径里炸雷。
任务调度原理:用 O(1) 的方式保护你的实时性
1) 就绪队列与位图:最快找到“下一个”
典型 LiteOS 风格:为每个优先级维护一个就绪链表,再配一张位图(bitmap)标记“哪个优先级非空”。挑任务时,用 CLZ/CTZ 之类的硬件指令在 O(1) 时间找到最高优先级非空位。
// 伪代码:位图+就绪队列挑选最高优先级任务
#define MAX_PRIO 32
static uint32_t ready_bitmap;
static ListHead ready_queues[MAX_PRIO];
static inline int highest_ready_prio(void) {
// 以最低位为最高优先级,找第一个置位位
return __builtin_ctz(ready_bitmap); // O(1)
}
static inline void set_ready(int prio, TCB *t) {
list_add_tail(&t->node, &ready_queues[prio]);
ready_bitmap |= (1u << prio);
}
static inline void clr_ready_if_empty(int prio) {
if (list_empty(&ready_queues[prio])) {
ready_bitmap &= ~(1u << prio);
}
}
时间片:同优先级多任务时,按轮转切片跑;高优先级一来,直接抢占。
调度锁:在临界区用 sched_lock++/-- 把调度器“按住”,结束后统一决策,避免抖动。
2) 抢占、时间片与调度点
- 抢占:新就绪任务优先级更高→立刻触发 PendSV(Cortex-M)或软中断→上下文切换。
- 时间片:SysTick 减片到 0→仅在同优先级内轮转,不影响更高优先级。
void SysTick_Handler(void) {
tick_count++;
if (curr->time_slice > 0 && --curr->time_slice == 0) {
curr->time_slice = DEFAULT_SLICE;
if (has_other_same_prio_ready()) trigger_sched();
}
timer_wheel_tick(); // 软定时器推进
}
3) 中断上下文与延迟调度(deferred scheduling)
ISR 只做两件事:采样/清空硬件状态、把工作“丢”给任务。在 ISR 里唤醒任务通常只标记和入队,把真正的切换延迟到退出中断后进行(尾部触发调度),降低中断延迟。
4) 优先级反转:要么继承,要么上限
当低优先级拿着互斥量,高优先级来要锁就卡住,中优先级又来“插队”——经典反转。两招:
- 优先级继承(PI):临时把持锁者抬到等待者最高优先级;
- 优先级上限(PC):进入临界区即“升格”到上限。
LiteOS 场景多用 互斥量+PI,实时性友好。
内存管理策略:别把堆当垃圾桶
1) 固定块池:小对象就用小桶装
**固定块池(fixed-size pool)**对小对象超友好:常数时间分配,碎片可控。
- 典型做法:按 16B/32B/64B … 几档建池;
- 优点:分配/释放 O(1),适合中断和关键路径;
- 缺点:利用率与粒度之间要权衡。
typedef struct Chunk { struct Chunk *next; } Chunk;
typedef struct {
size_t blk_size;
Chunk *free_list;
} FixedPool;
void *fp_alloc(FixedPool *p) {
if (!p->free_list) return NULL;
Chunk *c = p->free_list;
p->free_list = c->next;
return (void*)c;
}
void fp_free(FixedPool *p, void *ptr) {
Chunk *c = (Chunk*)ptr;
c->next = p->free_list;
p->free_list = c;
}
2) 通用堆:TLSF/buddy 思路,追求“可预期”
**TLSF(Two-Level Segregate Fit)**常见于对实时性要求高的堆:O(1) 分配/释放 + 低碎片。buddy 也常见但可能更“块状”。建议:
- 大对象走通用堆,小对象走池子;
- 尽量避免在 ISR 使用堆;
- 为有生命周期的对象引入对象池,减少抖动。
3) 栈与保护:别让溢出变悬案
- 每个任务独立栈,创建即定长,调小心抖,调大心浪费;
- 栈哨兵/MPU:编译期放守卫词或用 MPU 把下界设不可写;
- 定期采集栈高水位做巡检。
线程通信与同步机制:消息要走对路,锁要上得巧
1) 信号量(Semaphore)
- 计数型:资源配额(如可用缓冲数);
- 二值型:ISR→Task 的轻量通知。
注意:不要用信号量实现互斥,会放大反转风险。
// 伪 API:ISR give,Task take(带超时)
void EXTI_IRQHandler(void) {
clear_irq_flag();
sem_give_from_isr(&g_evtSem, &need_sched); // 只标记,退出中断再调度
}
2) 互斥量(Mutex)+ 优先级继承
- 持锁者因等待更高优先级任务而临时提升;
- 严格禁止在持锁区内阻塞或使用会阻塞的 API(如队列接收带超时)。
mutex_lock(m); // 进入临界,可能触发 PI
critical_update();
mutex_unlock(m); // 退出恢复
3) 事件标志组(Event Flags)
位级组合等待,一次满足多条件。配合 AND/OR + CLEAR 策略做状态机又丝滑又省事。
// 等待 0x03 两位都置位(AND),到达后清除
event_wait(&evt, 0x03, EVENT_AND | EVENT_CLEAR, timeout);
4) 消息队列 / 邮箱 / 管道
- 消息队列:小消息、固定长度、拷贝式传输;
- 邮箱:传指针/句柄,零拷贝但要小心生命周期;
- 管道:流式字节,适合串口日志等。
typedef struct { uint8_t buf[32]; } Msg32;
MsgQ q;
msgq_create(&q, sizeof(Msg32), 16);
void producer(void *arg) {
Msg32 m = { .buf = "TEMP:25.1" };
msgq_send(&q, &m, 10); // 10 tick 超时
}
void consumer(void *arg) {
Msg32 m;
if (msgq_recv(&q, &m, WAIT_FOREVER) == OK) {
handle(m.buf);
}
}
实战演示:传感器驱动→中断投递→任务融合(完整代码)
目标:外部传感器触发中断→ISR 拉取寄存器→投递指针到队列→处理任务做校准与上报。过程中演示ISR 最小化、队列零拷贝、互斥保护 I²C 总线、事件标志收敛。
// ====== 全局对象 ======
#define SAMPLE_POOL_SIZE 16
typedef struct {
uint32_t ts;
int16_t raw[6];
} Sample;
static Sample g_samples[SAMPLE_POOL_SIZE];
static FixedPool g_samplePool; // 小对象固定池
static MsgQ g_mbox; // 邮箱:传指针
static Mutex g_i2cMux; // I²C 互斥
static Event g_sysEvt; // 事件:0x01 - 已联网;0x02 - 云侧可写
// ====== 传感器中断服务:快进快出 ======
void SENSOR_IRQHandler(void) {
clear_sensor_irq();
Sample *s = fp_alloc(&g_samplePool);
if (!s) { /* 统计丢包 */ return; }
s->ts = read_hw_ticks();
sensor_pull_regs((uint8_t*)s->raw, sizeof(s->raw)); // SPI/I2C 寄存器读(轮询+限时)
msgq_send_from_isr(&g_mbox, &s); // 丢指针,零拷贝
// 调度延迟到中断退出
}
// ====== 采集处理任务:队列取样→校准→上报 ======
void sampler_task(void *arg) {
while (1) {
Sample *s = NULL;
if (msgq_recv(&g_mbox, &s, WAIT_FOREVER) != OK) continue;
// 互斥保护 I²C 共享总线(带 PI 防反转)
mutex_lock(&g_i2cMux);
int ok = sensor_self_check();
mutex_unlock(&g_i2cMux);
if (ok) {
float xyz[3];
calibrate_6axis(s->raw, xyz);
// 等待“已联网 & 云可写”两个条件
event_wait(&g_sysEvt, 0x03, EVENT_AND | EVENT_CLEAR, 100);
cloud_publish_xyz(xyz);
}
fp_free(&g_samplePool, s);
}
}
// ====== 网络状态回调:合并为事件位 ======
void net_ready_cb(void) { event_set(&g_sysEvt, 0x01); }
void cloud_writable_cb(void){ event_set(&g_sysEvt, 0x02); }
// ====== 初始化:任务、池子、队列、互斥、事件 ======
void app_init(void) {
fp_init(&g_samplePool, g_samples, sizeof(Sample), SAMPLE_POOL_SIZE);
msgq_create(&g_mbox, sizeof(Sample*), 16);
mutex_create(&g_i2cMux, MUTEX_PRIO_INHERIT);
event_init(&g_sysEvt);
task_create("sampler", sampler_task, STACK_2K, PRIO_HIGH, NULL);
enable_sensor_irq();
}
关键点复盘
- ISR 只做状态清理 + 采样 + 投递指针;
- 小样本走固定池,分配/释放 O(1);
- 队列传指针避免大拷贝;
- I²C 用互斥 + PI 防止反转;
- 事件标志把跨模块条件并起来,减少“我等你、你等我”的尴尬。
调参心法与线上排障清单
调参心法
- 优先级分层:ISR 回调唤醒的处理任务给高优先级;日志/上报靠后。
- 时间片只给“平权兄弟”:实时路径尽量单任务独占优先级。
- 内存双轨:小对象池化,大对象上堆;关键路径禁堆。
- 定时器分层:硬件滴答只推进时间轮,耗时操作交给任务。
排障清单(真香版)
- 调度锁泄漏:
sched_lock不归零,系统“假死”; - 优先级反转:高优先级等锁被中优先级“顶着”——打开 PI 或PC;
- 队列阻塞链:一个消费者卡住,生产者全堵;监控队列深度与超时返回;
- 内存碎片:堆分配抖动大→引入对象池,定期做高水位与碎片统计;
- 栈溢出:打开栈哨兵/MPU,抓一次性硬错;
- ISR 干坏事:ISR 里调了会阻塞的 API?立刻摘掉。
- 滴答风暴:软定时器太多→批量处理 + 合并唤醒,避免频繁触发调度。
结语:内核给你的是确定性,你要给它的是克制
实时系统不是“跑得更快”,而是“永远在关键时刻不掉链子”。LiteOS 给你的,是位图调度的 O(1)、ISR 的短平快、内存的可预期。你需要回馈它的,是不在 ISR 玩花活、不在关键路径乱分配、不让低优先级拿着锁去喝咖啡。
最后留个小问题:**你现在系统里,真的每一把锁都值得那把锁的存在吗?**😉
附:最小任务/信号量/队列样例(可拼出你的“骨架工程”)
void worker(void *arg) {
while (1) {
if (sem_take(&g_sem, 50) == OK) {
// do work
} else {
// timeout path: health check / low-power hint
}
task_yield(); // 同优先级礼让,避免独占
}
}
int main(void) {
kernel_init();
sem_create(&g_sem, 0, 1);
msgq_create(&g_logQ, sizeof(LogMsg), 32);
task_create("worker", worker, STACK_1K, PRIO_MED, NULL);
task_create("logger", logger_task, STACK_1K, PRIO_LOW, NULL);
kernel_start(); // 不再返回
return 0;
}
… …
文末
好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。
… …
学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!
wished for you successed !!!
⭐️若喜欢我,就请关注我叭。
⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。
版权声明:本文由作者原创,转载请注明出处,谢谢支持!
- 点赞
- 收藏
- 关注作者
评论(0)