简介:《MELIS 用户编程手册-Orange 编程指南》PDF 面向使用 F1C100S、荔枝派nano 及同类 SDK 进行嵌入式 GUI 开发的工程师与学习者,帮助梳理 MELIS 图形子系统中的窗口管理、图层与屏幕配置、Pipe 数据通路、Alpha 混合和 Colorkey 等关键机制。全文约81页,按文档编号 SW1107REF001 V1.0 组织,目录从分类、关系、图层属性、屏幕属性、优先级概念延伸到 GUI_FrmWinCreate、GUI_ManWinCreate、窗口删除与焦点管理等接口,并给出 Alpha 计算公式和 Colorkey 运算说明,便于开发时查阅和对照实现。资源包仅含1个 PDF 文件,大小4.47MB,单文件即完整手册,适合按章节检索或打印备查。已有586人学习,可作为嵌入式图形界面开发、SDK 移植与显示问题排查的参考资料。
1. MELIS 的 Orange 编程指南解决的是“从裸机到多任务”这道坎
板子能点灯、串口能打印,是很多人跨进嵌入式的第一步;第二步往往就卡住了——按键要响应、屏幕要刷新、串口要收数据、还要定时采样,一个while(1)里塞不下,勉强塞下了也开始丢事件。MELIS 的这份 Orange 编程指南解决的正是这一步:把应用拆成若干个任务,用内核提供的信号量、消息队列、定时器把它们串起来,再通过统一的设备接口去碰硬件,而不是让每个模块各自去操作寄存器。它面向的是已经能独立写裸机小项目、但没在实时内核上做过完整应用的工程师。手册篇幅不短,真正要先读透的是三块:任务与优先级、同步原语的选型、设备句柄的打开与释放。剩下的文件系统、网络、音频这些章节,等这三块立住了再翻,效率差很多。
2. 任务模型:MELIS 下优先级、栈和阻塞点的三组参数怎么定
任务创建那一页,手册往往只有半页,参数也就五六个,但这五六个参数决定了后面所有问题的形态。我在实际项目里调过的绝大多数“跑飞”“卡死”“偶发重启”,追到根上都能落回三件事:优先级排得不对、栈给得不够、任务里没有阻塞点。这一章就按这三件事展开。
2.1 从 main 到第一个任务:启动链路上到底发生了什么
上电之后的链路大致是:引导代码把内核搬进内存,内核初始化时钟节拍、内存堆和内部设备表,然后创建第一个初始任务,再由这个初始任务去调用用户写的main。这意味着main本身已经运行在一个任务上下文里了,它不该再写死循环,而是创建好业务任务之后尽快返回或者主动挂起。
常见的一个误用是把业务主体直接写在main里,然后在里面开一个大循环。这样跑起来的现象是:别的任务优先级再高也调度不进来,因为初始任务的优先级往往不低,而且它从不阻塞。正确做法是main只做“建任务 + 建同步对象 + 返回”这三件事。
建任务的接口在头文件里通常是esKRNL_前缀的风格,不同发行版可能略有差异,以手上 SDK 的声明为准:
#include <stdio.h> #include "meli.h" /* 头文件名以你手上的 SDK 为准 */ static void app_task(void *arg) { (void)arg; for (;;) { /* 业务主体。注意:这个循环里必须存在阻塞点 */ do_work(); esKRNL_TimeDly(10); /* 单位是 tick;tick 配成 1ms 时就是 10ms */ } } int main(void) { int tid = esKRNL_TCreate(app_task, /* 入口函数,签名必须是 void (*)(void *) */ NULL, /* 传给入口的参数,不要指向栈上的局部变量 */ 2048, /* 栈大小,单位要先确认是字节还是字 */ 10, /* 优先级,数字越小越优先 */ 0, /* 同优先级轮转的时间片,不做轮转填 0 */ "app"); /* 任务名,挂死的时候能打印出来定位 */ if (tid <= 0) { printf("task create failed\n"); return -1; } return 0; /* 别在这里写 while(1) */ }参数里最容易踩的是栈大小的单位。有的内核按字节算,有的按unsigned int的个数算,差四倍,按字节给 2048、实际按字算就只给了 512 字节,跑两天没事,一进深一点的函数就崩。判断方法很土但有效:建一个只做printf的任务,栈给 256,如果直接崩,说明单位是字节;如果能跑,说明单位是字。第二个容易踩的是入口函数签名,写成void app_task(void)编译能过(C 里参数不匹配只是警告),运行起来拿到的arg就是垃圾值。
2.2 优先级怎么排:按截止时间,不按“重要程度”
手册里只会写“数字越小优先级越高、支持抢占”。真正的设计问题是你怎么给每个任务定这个数字。我一般按周期来:周期越短、截止时间越紧的任务,优先级越高。串口收包这类几十毫秒就要把数据取走的任务,排在显示刷新前面;显示刷新排在日志落盘前面。这跟业务上“哪个功能更重要”没关系,纯粹是时间约束。
给一张常见分配的参考表:
| 任务类型 | 典型周期 | 建议优先级区间 | 说明 |
|---|---|---|---|
| 硬实时采集/协议解析 | 1~10ms | 5~12 | 超时即丢数据,必须最先跑 |
| 人机交互/按键扫描 | 10~20ms | 12~20 | 慢一点人感觉不到 |
| 业务状态机 | 20~100ms | 20~30 | 大量逻辑在这里 |
| 显示刷新 | 30~50ms | 25~35 | 与业务同级会互相拖 |
| 日志/存储 | 事件驱动 | 30~40 | 丢一两条可以接受 |
排完之后必须回头检查一件事:有没有两个任务优先级相同但都长时间不阻塞。如果有,要么给它们不同的优先级,要么给时间片让它轮转,否则先跑起来的那个会把另一个饿死。这个现象特别隐蔽——日志里只有 A 在跑,B 一行都没输出,看上去像 B 没被创建,其实是 B 从来没被调度。
优先级反转是另一个坑。低优先级任务持有互斥量、高优先级任务等它、中优先级任务又把低优先级任务抢占掉,结果高优先级任务等了远超预期的时间。解决办法是用带优先级继承的互斥量,而不是用二值信号量当锁。手册里这两类原语的接口长得几乎一样,选错了不会报错,只会在压力大的时候偶发超时。
2.3 栈深怎么估:把 printf 和浮点一起算进去
栈溢出是嵌入式里最像“玄学”的故障:改一行无关的代码就好了,加一条打印就崩,换个编译选项又活了。原因是溢出之后踩到的是相邻任务的控制块或者别的任务的栈,症状跟原因完全无关。
估栈我一般按下面这张表逐项加:
| 组成 | 估算方式 | 备注 |
|---|---|---|
| 函数调用深度 | 最深调用链 × 每层 32~64 字节 | 用编译器生成的调用图看,别靠感觉 |
| 局部数组/缓冲区 | 直接取最大那个 | 大缓冲区改成静态或堆分配 |
printf系列 | 留 256~512 字节 | 格式化解析栈开销很大 |
| 浮点运算 | 留 128~256 字节 | 软浮点比硬浮点更吃栈 |
| 中断嵌套 | 每层中断栈 × 最大嵌套层数 | 有的内核中断用当前任务栈 |
| 余量 | 上面总和 × 0.3 | 别省这点 |
估完之后用填充法实测一遍,这比任何估算都可靠:
#define STACK_MAGIC 0xA5A5A5A5u /* 在任务创建后、正式跑业务前,把整段栈写满魔数; 运行一段时间(覆盖所有业务分支)后回查,从栈底往上数连续魔数的长度 */ unsigned int stack_used_peak(unsigned int *stack_base, unsigned int words) { unsigned int i = 0; while (i < words && stack_base[i] == STACK_MAGIC) { i++; } return (words - i) * sizeof(unsigned int); /* 已用峰值的近似值 */ }逻辑很直白:栈是从高地址往低地址长的,栈底那一段如果一直是魔数,说明从来没被用到。用「总大小 − 未用部分」就是峰值占用量。参数words是栈区按字计的长度,stack_base是栈区起始地址,有的内核提供查询当前任务栈基址的接口,没有的话可以在创建任务时自己记录。
判据是峰值占用不超过总栈的 70%。超过就加,不要赌。我见过一个项目为了省 RAM 把栈压到刚好够用,实验室跑了一周没问题,现场因为一路串口噪声触发了重传分支,调用链深了两层,直接进异常。
3. 任务间通信:信号量、互斥量与消息队列在 Orange 编程指南里怎么选
同步原语的接口都很短,创建、等待、释放三步,看一遍就会用;难的是选型。选错了不会立刻出问题,而是在并发压力上来之后以超时、丢数据、卡死的形式冒出来。这一章把四类原语的边界划清楚,再落到消息队列的参数和中断上下文的规则上。
3.1 四类原语的适用边界
| 原语 | 语义 | 典型场景 | 用错的后果 |
|---|---|---|---|
| 二值信号量 | 0/1 计数 | 中断通知任务「有事了」 | 当锁用时没有优先级继承,会反转 |
| 计数信号量 | 0~N 计数 | 管理 N 个相同的资源(缓冲块、连接槽) | 计数上限设错,资源被重复占用 |
| 互斥量 | 独占 + 优先级继承 | 保护共享缓冲区、共享外设 | 在中断里调用会直接崩 |
| 消息队列 | 定长消息 FIFO | 任务间传数据块、事件带参数 | 消息体太大,拷贝开销吃掉 CPU |
| 事件标志组 | 位掩码 | 一个任务等多个条件「都到齐」 | 用队列硬凑,逻辑变复杂 |
一个很实用的判断顺序:先问「我是在传数据,还是在传一个信号」。传数据用消息队列,传信号用信号量。再问「这个资源同一时刻只能一个人用吗」,是就用互斥量。最后问「我要等的是不是多个条件的组合」,是就用事件标志组。
有一种常见误用是把消息队列当锁用:往队列里放一个令牌,谁取到谁进临界区。这样能用,但一旦持有令牌的任务卡住,其他任务全部超时,而且没有优先级继承。用互斥量三行代码的事,别绕。
3.2 消息队列:深度、超时、丢不丢
消息队列有三个参数必须一起定:消息大小、队列深度、等待超时。这三个不是独立的。
- 消息大小:按最大那条消息定,不要为了「以后可能变大」直接给到 1KB,因为队列是静态分配的,深度 × 大小就是常驻内存。
- 队列深度:等于「生产者的最大突发量 ÷ 消费者的单次处理能力」,再留 30% 余量。深度给 1 等于没有缓冲,生产者和消费者严格串行。
- 等待超时:生产者侧我一般填 0(不等待,满了就丢并计数),消费者侧填一个有限值(比如 100 tick),不填永久等待。永久等待意味着队列一旦出问题,任务就静默挂死,日志里什么都看不到。
typedef struct { unsigned int ts; int value; } sensor_frame_t; static volatile unsigned int g_dropped = 0; static void producer_task(void *arg) { int qid = (int)(long)arg; sensor_frame_t f; for (;;) { f.ts = esKRNL_TickGet(); f.value = read_sensor(); /* 第三个参数 0:队列满时不等待,立刻返回错误。 对采样类数据来说,丢掉最新一帧比阻塞采样任务更划算 */ if (esKRNL_QPost(qid, &f, 0) != 0) { g_dropped++; /* 丢弃计数比死等更有诊断价值 */ } esKRNL_TimeDly(5); } } static void consumer_task(void *arg) { int qid = (int)(long)arg; sensor_frame_t f; for (;;) { /* 超时 100 tick:宁可报错也不要静默挂死 */ if (esKRNL_QPend(qid, &f, 100) == 0) { process_frame(&f); } else { /* 超时是正常路径的一部分,不要在这里刷错误日志 */ heartbeat(); } } }代码里两个细节值得说。第一,QPost传的是结构体地址,内核会把内容拷进队列,所以f是栈变量没问题;但如果你传的是指针、队列里存的是指针,那f就不能是栈上的,这个区别在手册里通常只有一句话。第二,消费者超时后做的是心跳,不是打印错误。超时在高负载下是正常现象,把它当异常刷日志,日志本身又会拖慢系统,形成正反馈。
还有一个隐性问题:g_dropped这种计数变量,如果只在生产者任务里写、在别的任务里读,读到的可能是个中间值。32 位对齐的整型在多数平台上是原子的,但别依赖这个。要么加临界区,要么接受它是近似值并在注释里写清楚。
3.3 中断里能做什么:ISR 与任务的分工
中断服务程序的第一原则是短。手册里会明确列出哪些接口不能在中断里调,这份清单建议直接抄到项目规范里,别靠记。
ISR 里我一般只做三件事:清中断标志、把数据搬进一个预分配的环形缓冲、投递一个信号量或消息。所有解析、计算、打印、写文件,全部放到任务里。原因不只是「中断里不能阻塞」,还有一个更实际的:中断执行期间会屏蔽同级或更低级中断,ISR 里多待 100 微秒,可能就丢掉一帧串口数据。
规则清单(贴在工位上那种): 1. ISR 里不调用任何带超时参数的接口 2. ISR 里不调用 printf / 日志接口 3. ISR 里不调用 malloc / free 4. ISR 里不做浮点运算(除非确认上下文已保存) 5. ISR 里操作共享变量,用临界区保护,不用信号量保护最后一条容易搞混:临界区保护的是「几个指令的原子性」,信号量保护的是「一段时间内的独占」。ISR 和任务之间改同一个 32 位计数器,用临界区;ISR 通知任务去处理一整块缓冲区,用信号量。
临界区的粒度也要控制。常见写法是把整个函数包进esKRNL_EnterCritical()和esKRNL_ExitCritical(),函数里再调几个别的函数,结果临界区长达几百微秒,中断延迟直接超标。原则是临界区里只放「读-改-写」那几条语句,其他全部挪出去。
4. 设备访问:把 open/read/write/ioctl 这套接口用对的四个前提
设备访问这一章在手册里是最像「说明书」的部分:每个设备一节,列一串接口。真正上手时卡住的不是接口怎么调,而是句柄什么时候失效、多个任务能不能同时用一个句柄、ioctl 的命令字该由谁来定义。这一章按这四个前提讲。
4.1 句柄、引用计数与多任务共享
设备访问通常有两层接口:一层是 POSIX 风格的open/read/write/ioctl/close,一层是内核内部的esDEV_前缀接口。应用层优先用前者,移植性好;只有在需要拿到底层参数或者前者没覆盖的功能时才下沉到后者。
int fd = open("/dev/uart0", O_RDWR); if (fd < 0) { /* 打开失败绝大多数是三种原因:设备名拼错、驱动没编进固件、被独占 */ return -1; } struct uart_cfg cfg = { .baud = 115200, .databits = 8, .stopbits = 1, .parity = 0, }; /* 配置类操作走 ioctl,不要试图用 write 去做 */ if (ioctl(fd, UART_IOC_SET_CFG, &cfg) < 0) { close(fd); /* 错误分支也要关,这条最容易忘 */ return -1; }几个必须确认的点:
| 问题 | 怎么确认 | 踩错的表现 |
|---|---|---|
| 设备名对不对 | 看内核启动日志里的设备注册打印 | open一直返回负值 |
| 能不能多任务同时 open | 同一个设备名连续 open 两次看返回值 | 第二次失败,或第二次把第一次挤掉 |
| 读是阻塞还是非阻塞 | 看 open 时有没有O_NONBLOCK | 任务在读上挂死,或者读不到就疯狂空转 |
| 关闭后句柄是否失效 | 关掉后立刻再 read 一次 | 偶发野指针,症状随机 |
「同一个设备被两个任务同时 open」这个问题,不同驱动的实现不一样:有的支持引用计数,两个句柄共用一份硬件状态;有的直接返回失败;有的会重置硬件,导致第一个任务的数据流断掉。手册里未必写,最稳的做法是应用层约定「一个设备只由一个任务持有」,其他任务通过消息队列把请求转过去。这就是常说的设备管理任务模式,多一层转发,换来的是状态可预期。
连接错误处理也必须成套。每条open都要有对应的close,包括所有失败分支和超时分支。句柄泄漏在嵌入式上不像在 PC 上那么宽容,一般几十个句柄就是上限,泄漏完了之后所有open全失败,而现象是「系统运行几个小时后所有外设都不工作了」,很难往泄漏上想。
4.2 ioctl 命令字:别让应用层知道寄存器地址
ioctl最容易失控的地方是命令字。项目里常见一种写法:应用层直接传一个寄存器偏移和值下去,驱动里*(volatile unsigned int *)addr = val。这样能跑,但从此应用层和硬件绑死,换一颗芯片要改所有应用代码。
我一般按功能分类定义命令字,一个命令字对应一个结构体,结构体里只放业务语义,不放地址:
/* 命令字按功能分组:0x01xx 配置类,0x02xx 状态类,0x03xx 控制类 */ #define UART_IOC_SET_CFG 0x0101 /* 参数:struct uart_cfg * */ #define UART_IOC_GET_STAT 0x0201 /* 参数:struct uart_stat * */ #define UART_IOC_FLUSH_TX 0x0301 /* 参数:NULL */ struct uart_stat { unsigned int rx_bytes; unsigned int tx_bytes; unsigned int overflow_cnt; /* 溢出计数,排查丢数据的关键指标 */ unsigned int err_cnt; };参数说明写清楚三件事:传的是值还是指针、指针指向的结构体大小、调用是否可能阻塞。这三件事没写,下一个人接手就得去读驱动源码。
ioctl的返回值约定也要统一:成功返回 0,参数错误返回一个固定负值,硬件错误返回另一个固定负值。应用层据此判断是重试还是上报,比统一返回 -1 有用得多。
4.3 延时与定时:三种「等一会儿」别混用
MELIS 里至少有三种时间相关的手段,用错了时序精度差一个数量级:
| 手段 | 精度 | 上下文 | 适用场景 |
|---|---|---|---|
esKRNL_TimeDly(n) | 1 tick 起 | 仅任务 | 任务周期控制、让出 CPU |
| 软定时器 | 1 tick 起 | 回调在特定上下文 | 超时判定、周期回调 |
| 硬件定时器 | 微秒级 | 中断 | 精确定时、PWM、脉冲测量 |
esKRNL_TimeDly(1)的实际延时是 1 到 2 个 tick 之间,因为它对齐到下一个时钟节拍。想要稳定周期,不要用「延时固定值」的写法,而要用绝对时间累加:
unsigned int next = esKRNL_TickGet(); for (;;) { do_work(); next += 10; /* 目标周期 10 tick */ int now = (int)(next - esKRNL_TickGet()); if (now > 0) { esKRNL_TimeDly(now); /* 扣掉本次工作耗时 */ } /* now <= 0 说明本周期已经超时,不再延时,直接进入下一轮, 同时应该累加一个超时计数用于事后分析 */ }这段代码的意义是把「工作耗时」从周期里扣掉,而不是固定延时 10 tick 再加工作耗时。前者周期是 10,后者周期是 10 + 工作耗时,任务一忙起来周期就漂,采样率跟着漂。
软定时器的回调上下文必须确认清楚:如果回调在中断里执行,里面就不能有阻塞调用;如果回调在一个专门的任务里执行,那这个任务的优先级会直接影响所有定时器的精度。这一条手册里通常有写,但很容易看漏。
5. 避坑与排查:MELIS 应用跑飞的五种典型现场
调试实时系统最难受的地方是现象和原因离得远。下面五条是这类项目里反复出现的问题,每条按「看到什么 → 为什么 → 怎么办」写,遇到相似症状可以按顺序对号入座。
第一条:某个任务完全不执行,日志里一行都没有。现象是系统整体还在跑,串口有别的任务在输出,唯独某个任务的打印从来没出现过,加断点也进不去。原因大概率是优先级被饿死:比它优先级高的任务在循环里没有阻塞点,或者它的优先级被设成了和某个忙任务相同但时间片为 0。排查方法是先把这个任务的优先级临时提到最高,如果立刻就能跑了,问题就确认了。解决不是简单调优先级,而是回头检查那个高优先级任务的循环里有没有TimeDly或者等待原语——一个不阻塞的高优先级任务,等于把内核变成了单任务。
第二条:串口输出偶尔乱码、半包、两段日志粘在一起。现象是单任务测试时打印完全正常,多任务一开就偶发乱码。原因是多个任务并发调用同一个设备的write,一次写操作的多个字节被另一个任务插进来了。注意write本身是原子的不代表「一次业务日志」是原子的,一条日志可能要调好几次write。解决办法是在应用层给这个设备配一把互斥量,把「组装 + 写出」整体包起来。如果不想每个调用点都加锁,就封装一个log_printf(),锁在里面,外部无感。
第三条:运行几小时到几天后偶发重启,重启位置每次都不一样。现象是长时间跑才出问题,加日志之后问题消失或者换了个地方出现——这是典型的栈溢出或者数组越界。原因是踩到了相邻任务的控制块,破坏的是调度器的数据结构。排查按三步走:先把所有任务的栈统一翻倍,如果问题消失,基本确认是栈;再用前面说的魔数填充法测出峰值占用,按 70% 判据重新分配;最后检查所有sprintf、memcpy、数组下标,尤其是「长度来自外部输入」的那些。这类问题不要靠猜,靠工具和判据。
第四条:定时器回调里做了稍重的操作,系统整体变卡。现象是开了某个周期性定时器之后,按键响应变慢、串口开始丢数据。原因是回调在中断上下文执行,里面调了printf或者做了浮点运算,单次执行时间从几微秒涨到几百微秒,中断延迟全线超标。解决办法是把回调缩到只投递一个信号量或消息,实际处理放到任务里。判断方法很直接:在回调入口和出口各翻转一个 GPIO,用示波器看高电平宽度,超过 50 微秒就该拆了。
第五条:系统跑一段时间后所有外设都不工作,重启就好。现象是open开始全部返回失败,之前打开的句柄读写也异常。原因是句柄泄漏或者设备被异常路径占住:某条错误分支直接return了,没有close。排查方法是在open和close处各加一个计数,运行一段时间后打印,两个数应该相等。不相等就沿着调用链找漏掉的close。这条的预防成本很低——写代码时养成「open 之后立刻把 close 写出来,再填中间的逻辑」的习惯,能省掉后面几天的排查。
提示:这五条的共同点是「现象出现在 A,原因在 B」。所以排查时不要从现象所在模块开始看,先看调度和内存这两个全局性的东西。
6. 把手册变成工程模板:一份初始化骨架与上线前自检表
手册读完一遍,最容易发生的事是:当时都懂,三个月后接手的人(包括未来的自己)从零再读一遍。我的习惯是把手册里的东西固化成一份初始化骨架,新项目直接拷,改参数不改结构。
骨架的顺序是有讲究的,不能乱:先建同步对象,再建设备句柄,最后建任务。原因是任务一旦跑起来就会去用信号量和句柄,如果这些东西还没准备好,任务第一轮就拿到空句柄,然后进入一个莫名其妙的错误分支。很多「偶发启动失败」都是这个顺序问题——不是每次都失败,取决于任务调度和初始化的相对速度。
int app_init(void) { /* 1. 先建同步对象,此时还没有任何任务在跑 */ g_lock = esKRNL_MutexCreate(); g_evt = esKRNL_SemCreate(0); g_queue = esKRNL_QCreate(16); /* 深度 16,消息大小按最大帧定 */ if (!g_lock || !g_evt || !g_queue) { return -1; } /* 2. 再打开设备,失败要能回滚到上一步 */ g_fd = open("/dev/uart0", O_RDWR); if (g_fd < 0) { esKRNL_QDel(g_queue); /* 建了多少就删多少,别懒 */ esKRNL_SemDel(g_evt); esKRNL_MutexDel(g_lock); return -1; } /* 3. 最后建任务,从低优先级往高优先级建 */ if (esKRNL_TCreate(log_task, NULL, 1024, 30, 0, "log") <= 0 || esKRNL_TCreate(work_task, NULL, 2048, 20, 0, "work") <= 0) { return -1; } return 0; }这里有个细节值得单独说:任务按「低优先级先建」的顺序创建。如果反过来,高优先级任务先跑起来,可能立刻去用一个还没创建的低优先级任务该产出的数据,然后进入超时分支。按序创建能把这个窗口关掉。
上线前我会过一遍这张自检表,每一项都对应一个具体的检查动作,不是「感觉没问题」:
| 检查项 | 通过标准 | 怎么测 |
|---|---|---|
| 每个任务都有阻塞点 | 单次循环最长执行时间远小于周期 | 翻转 GPIO 测高电平宽度 |
| 栈峰值占用 | 小于总栈的 70% | 魔数填充法,跑满所有分支后回查 |
| 句柄配平 | open 次数等于 close 次数 | 计数打印,跑 24 小时后比对 |
| 中断执行时间 | 单次 ISR 小于 50 微秒 | 示波器测 ISR 首尾翻转的 GPIO |
| 队列丢弃率 | 正常负载下接近 0,压力下可观测 | 打印丢弃计数,不要静默丢 |
| 错误分支回滚 | 初始化失败能干净退出 | 人为让设备打开失败,看是否复位 |
最后说一个我自己的教训。早些年做一个多任务采集的项目,功能全部跑通之后就交付了,栈按「编译不报错、跑一天没事」的标准给的。三个月后现场反馈偶发重启,日志什么都抓不到。后来花了两周定位到是一个很少触发的错误分支调用链比正常路径深了三层,栈溢出踩了调度器。从那之后我的习惯就变成:栈按峰值占用的 1.5 倍给,宁可贵一点 RAM,也不要留一个只在现场才出现的黑匣子。同样的道理适用于队列深度和超时值——参数不是调到「能跑」就够,要调到「出问题时我能看见」。希望帮到你。
本文还有配套的精品资源,点击获取