跳转至

LuatOS 框架的设计原理

作者:朱天华 | 最后修改:2025-12-05

Hello,大家好,我是朱天华。

欢迎大家来到合宙LuatOS直播课堂,一起学习LuatOS课程。

第一部分:LuatOS课程背景

因为今天是我们LuatOS系列课程的第006讲,所以在这里我就不重复讲解整个LuatOS课程的背景了;

如果您还不清楚LuatOS课程背景,可以访问:LuatOS课程背景 这个链接,进行了解;

第二部分:LuatOS 框架的设计原理,讲哪些内容

在LuatOS系列课程的第001讲,我们一起探讨学习了:LuatOS框架的使用;

今天是第006讲,我们一起来深入理解下LuatOS框架的内部设计原理;

今天的课程主要从以下几方面进行讲述:

首先,从以下几方面快速地复习一下LuatOS框架中的几个概念:

  1. 三个核心概念:任务(task),消息(message),定时器(timer)
  2. 一个调度器:sys.run()函数;

以上这些理论知识的内容和LuatOS课程的第001讲 LuatOS框架的使用重复,所以在本讲中仅仅对一些核心知识点进行简单的复习回顾,为我们深入理解LuatOS框架的内部设计原理打好基础;

如果你还不清楚这些理论知识内容,可以访问:LuatOS 框架的使用(第二部分和第三部分) 这个链接,再学习一下;

其次,从以下几点,深入学习LuatOS框架的内部设计原理:

1、sys.lua源码分析

  • sys核心库是LuatOS运行框架库,是LuatOS应用程序运行的核心大脑;

  • 我们要深入学习LuatOS框架的内部设计原理,就是要深入学习sys核心库的内部设计实现原理;

  • sys.lua源文件是sys核心库的代码实现,点击这里可以看到sys.lua源码

  • 所以本讲课程中,我们会重点分析sys.lua的源码实现;

2、按照以下顺序对sys.lua进行源码分析

  • 任务(task),消息(message),定时器(timer)对应的API内部数据结构和业务逻辑源码分析

  • 调度器(sys.run())的内部数据结构和业务逻辑源码分析

  • 最后结合一个实际的MQTT项目,再把task,message,timer关联起来,总体分析一下task和各种消息驱动的完整处理过程以及对应的源码设计

第三部分:复习LuatOS框架中的几个概念

在LuatOS中,有三个核心概念和一个调度器:

  1. 三个核心概念:任务(task),消息(message),定时器(timer)
  2. 一个调度器:sys.run()函数;

3.1 LuatOS任务(task)

3.1.1 FreeRTOS task和LuatOS task

从上面这张图中我们可以看到,有两种任务:

  1. 第一种是LuatOS内核固件中的任务,也就是FreeRTOS创建的任务;这种任务我们把它命名为FreeRTOS task;
  2. 第二种是LuatOS项目应用脚本中的任务,也就是用户脚本代码中调用sys.taskInit和sys.taskInitEx两个API创建的任务,这种任务我们把它命名为LuatOS task;严格意义上说,LuatOS task并不是真正的task,而是利用了Lua中的协程(coroutine)概念,来等价实现的一种task效果,因为task的受众面比协程的受众面要广的很多,所以我们在LuatOS中,将协程(coroutine)包装成了task的概念,这样更容易理解和使用;关于协程(coroutine)的知识,在本文接下来的第六部分内容中,我们会接触到。

在这里我使用一个形象的比喻,争取可以让大家更加直观的理解这两种任务的联系和区别:

  1. 有一个森林,这个森林就是FreeRTOS;

  2. 森林里生长了很多棵大树,每一棵大树都是FreeRTOS创建的一个任务,就是FreeRTOS task;

  3. 这些大树分成了两种:

a. 一种是长出了树干,树干上还有很多树枝,这一种大树只有一棵,就是Lua虚拟机任务

b. 一种是只长了光秃秃的树干,树干上没有树枝,这种大树有很多,除Lua虚拟机之外,其余所有任务都是这种大树

  1. Lua虚拟机任务这棵大树的树干上长出的所有树枝,就是一个个的LuatOS task

根据以上描述画一张简图如下

区别项 FreeRTOS task LuatOS task
任务优先级 不同的task支持不同的优先级,也支持相同的优先级 task没有优先级的概念
任务调度 1、高优先级的task准备就绪后,可以抢占当前正在运行的低优先级task的执行权
2、相同优先级的task,采取时间片轮转的方式获得执行权
采用的是 抢占式+时间片轮转 的调度方式
1、当前正在执行的task,必须自己主动让出执行权,其他task才有可能执行;
2、也就是说,我正在运行,其他task别想打断我,我只要自己不想停,其他task都没有机会得到运行
采用的是 协作式 的调度方式
多任务访问共享资源 提供了临界区、互斥锁、信号量等方法实现了共享资源的安全访问,防止数据竞争或者资源冲突 不需要复杂的共享资源安全访问机制,完全靠协作式的任务调度来控制,使用起来非常简单

3.1.2 两种LuatOS task(基础task和高级task)

怎么创建一个task,在sys核心库中提供了两个api:

sys.taskInit(task_func, ...)

sys.taskInitEx(task_func, task_name, non_targeted_msg_cbfunc, ...)

这两个api分别创建两种task,首先我们给这两种task起个名字,一种叫基础task,一种叫高级task;

sys.taskInit(task_func, ...)创建的task是基础task;

sys.taskInitEx(task_func, task_name, non_targeted_msg_cbfunc, ...)创建的task是高级task;

从设计原理的角度来看,基础task和高级task的区别是:

(1) 所有的基础task共享一个全局消息队列;

(2) 每个高级task都有自己独立的定向消息队列,同时又能使用全局消息队列;

3.1.3 LuatOS task内部运行环境和外部运行环境

在LuatOS应用脚本开发过程中,我们所编写的应用脚本代码,存在两种业务运行的逻辑环境:

  1. 一种是在task的任务处理函数内部的业务逻辑环境中运行,我们简称为:在task内部运行;
  2. 一种是在task的任务处理函数外部的业务逻辑环境中运行,我们简称为:在task外部运行;

怎么理解这两种业务逻辑运行环境?我们看下面这张图

看右边生长出分支的这棵大树,这棵大树就是FreeRTOS创建的Lua虚拟机task,是一个FreeRTOS task;

在这个Lua虚拟机FreeRTOS task上,这棵大树再分为两部分:

  1. 树干部分:树干部分运行的业务逻辑环境就是LuatOS task外部运行环境;
  2. 树枝部分:每个树枝都是一个独立的LuatOS task,树枝部分运行的业务逻辑环境就是LuatOS task内部运行环境;

在这里,大家只要知道有task内部运行和task外部运行两种环境即可,接下来我们本文第四部分讲解内部设计原理时,会进一步去学习理解;

3.1.4 LuatOS task状态切换

3.2 LuatOS消息(message)

LuatOS 的消息机制是LuatOS task协作和事件驱动编程的核心部分,消息机制包括消息的发布和订阅处理、消息的发送和接收处理;

3.2.1 三种消息队列

消息需要存储到消息队列中,消息队列中的消息遵循先进先出(FIFO)原则;

内核消息队列

FreeRTOS创建的每一个task都有一个消息队列,这种消息队列就叫做内核消息队列;

内核消息队列是FreeRTOS直接管理的,存储每个FreeRTOS task内核消息的队列;

LuatOS应用全局消息队列

所有LuatOS task应用共享一个消息队列,这个消息队列就叫做LuatOS应用全局消息队列;

LuatOS应用全局消息队列是LuatOS虚拟机直接管理的;

LuatOS基础task可以接收处理这个消息队列中的消息,LuatOS高级task可以接收处理这个消息队列中的消息;

LuatOS外部运行环境中的业务也可以接收处理这个消息队列中的消息;

LuatOS应用定向消息队列

每个LuatOS 高级task都有一个消息队列,这个消息队列就叫做LuatOS应用定向消息队列;

LuatOS应用定向消息队列是LuatOS虚拟机直接管理的;

每个定向消息队列和一个LuatOS高级task存在唯一的绑定关系;

只有绑定的LuatOS高级task才可以接收处理这个消息队列中的消息;(虽然从源码设计上看,所有的LuatOS高级task都可以接收处理定向消息队列中的消息,但是这样使用的话,理解起来会比较混乱)

3.2.2 六种消息

根据消息存储使用的消息队列类型以及消息发送方的不同,可以把消息划分为以下六类:

3.3 LuatOS定时器(timer)

对于LuatOS应用程序来说,定时器本质上也算是一种特殊的消息,因为定时器太常用了,所以把他单独拎出来,单独的一个章节进行讲解;

定时器分类如下图所示:

3.4 LuatOS调度器(sys.run())

sys核心库是LuatOS应用程序运行的核心大脑,sys.run()是sys核心库的大脑,负责整个LuatOS应用脚本程序的调度和管理,是LuatOS应用程序的调度器;

sys.run()非常重要,但是sys.run()使用起来非常简单,仅仅在main.lua的最后一行调用sys.run()即可。

虽然sys.run()使用起来非常简单,但是如果大家对sys.run()的运行原理有一个总体性的理解和认识,对开发LuatOS应用项目来说,帮助很大。

所以在这里,我先对sys.run()内部的工作原理做一个简化后的总体介绍,至于更详细的原理介绍,我们会在本文的第四部分进行详细讲解;

我们看上面这张图:

1、LuatOS内核固件中的FreeRTOS会创建一个Lua虚拟机任务;

2、Lua虚拟机任务的处理函数中,首先进行初始化:

  • 在内核固件的C代码中,加载Lua标准库和LuatOS核心库;

  • 从LuatOS的脚本分区找到main.lua

  • 开始逐行嵌套解析执行main.lua中的脚本代码(加载必要的扩展库脚本文件和自己开发的应用脚本文件,并且运行这些脚本文件的初始化代码)

3、运行main.lua的最后一行代码sys.run()

4、sys.run()中的实现是一个while true循环,在这个循环内,不断地从内核消息队列和LuatOS应用消息队列中读取消息,并且分发消息给接收者进行处理。

第四部分:sys.lua源码分析方法

通过本文前三部分的描述,我们对LuatOS运行框架的基础理论知识做了简单的回顾;

接下来我们进入本讲的重点内容:学习理解LuatOS运行框架的内部设计源码;

  • sys核心库是LuatOS运行框架库,是LuatOS应用程序运行的核心大脑;
  • 我们要深入学习LuatOS框架的内部设计原理,就是要深入学习sys核心库的内部设计实现原理;
  • sys.lua源文件是sys核心库的代码实现,点击这里可以看到sys.lua源码
  • 所以本讲课程中,我们接下来会重点分析sys.lua的源码实现;通过分析sys.lua源码,从而来学习理解LuatOS框架的设计原理;

sys.lua是sys核心库的内部实现;

说到核心库,我们再来回顾一下LuatOS软件架构,如下图所示:

之前我们讲述LuatOS软件架构时,当时有说,LuatOS核心库是用C语言开发的,这个描述并不准确,在核心库中,有一个特例,就是sys核心库,这个核心库是用Lua语言开发的,并且源码也开放了;

虽然sys核心库是用Lua语言开发的,但是它还是一个核心库,在编译内核固件的时候,会把sys.lua源文件集成到编译好的内核固件中对外发布;

我们今天采用以下方式来学习sys.lua源码设计:

  1. 在目前的sys.lua源文件中增加详细的注释和运行日志,分析代码设计
  2. 使用LuatOS PC模拟器,运行demo代码+第1步增加运行日志的sys.lua,通过分析运行日志,进一步理解sys.lua的设计原理

在模拟器上运行demo代码的方法,大家可以参考这里

本讲中使用的demo代码参考luatos_framework ;模拟器运行时的命令如下:

luatos --llt=H:\Luatools\project\luatos_framework_luatos_task_Air8000.ini

要深入理解LuatOS框架的设计原理,其实就是深入学习sys.lua中的以下几个概念:

1、一个调度器:sys.run()

2、三个概念:任务(task),消息(message),定时器(timer)

3、五张表:

  • 高级task任务列表(taskList)

  • 全局消息订阅表(subscribers),全局消息队列(messageQueue)

  • 定时器处理表(timerPool),定时器回调函数参数表(para)

第五部分:LuatOS 调度器(sys.run())内部设计

LuatOS调度器sys.run函数的主体处理逻辑参考下图中黄色背景部分

从这张图可以看出,在LuatOS内核固件中有一个FreeRTOS,FreeRTOS运行起来之后,创建了很多任务,有软件定时器任务,TCP/IP协议栈任务,文件系统任务,Lua虚拟机任务等。

其中Lua虚拟机任务和LuatOS项目的应用软件关系最为密切;

Lua虚拟机任务运行起来之后,经过必要的初始化动作,就会去寻找main.lua脚本文件,找到之后,从main.lua的第一行代码开始解析执行,main.lua会执行必要的初始化动作并且加载运行其他的Lua脚本应用功能模块,main.lua的最后一行代码为sys.run(),sys.run()是一个while true的循环函数,实际上也是Lua虚拟机任务的处理函数;

在这个while循环里面,不断的分发处理各种消息,调度LuatOS项目应用软件的正常运行。

现在,我们一起看下刚才描述的这个过程的源码,在阅读源码之前,我们先看下面这张框图,描述了主要源码函数和代码段的调用过程:

第六部分:LuatOS 任务(task)内部设计

6.1 sys.taskInit(创建并且运行基础task)

sys.taskInit(task_func, ...)

6.1.1 源码分析

function sys.taskInit(fun, ...)
    -- 创建协程,返回协程对象co
    -- 创建一个Lua协程,协程的处理函数为fun,返回一个协程对象co
    -- 执行这一行代码之后,这个协程并不会运行,而是处于挂起状态
    local co = coroutine.create(fun)

    -- 启动运行协程,传入协程对象和任务处理函数携带的可变参数
    sys.coresume(co, ...)
    return co
end

我们接下来再看sys.coresume(co, ...)的源码

-- 对coroutine.resume加一个修饰器用于捕获协程错误
--local rawcoresume = coroutine.resume
local function wrapper(co,...)
    local arg = {...}
    -- arg[1]为false,表示协程执行出错
    -- 此时arg[2]为错误信息描述字符串,例如:[string "hello_luatos.lua"]:27: attempt to perform arithmetic on a string value
    if not arg[1] then
        -- 可以获取到完整的当前协程的调用堆栈信息(包括函数调用链和行号),例如:
        -- stack traceback:
        -- [string "scheduling.lua"]:27: in function <[string "scheduling.lua"]:19>
        local traceBack = debug.traceback(co)

        -- arg[2]为错误信息描述:包括文件名,行数,错误信息字符串,例如:
        -- [string "scheduling.lua"]:27: attempt to perform arithmetic on a string value
        traceBack = (traceBack and traceBack~="") and (arg[2].."\r\n"..traceBack) or arg[2]
        log.error("coroutine.resume",traceBack)
        --if errDump and type(errDump.appendErr)=="function" then
        --    errDump.appendErr(traceBack)
        --end

        -- 此处COROUTINE_ERROR_ROLL_BACK变量设为了true,表示:
        -- 如果某一个协程运行错误,则启动一个500毫秒的定时器,500毫秒之后,执行assert(false, traceBack)
        -- 最终表现为:日志中输出traceBack的信息,然后Lua虚拟机异常退出,15秒后自动重启软件;例如:
        -- Luat:
        -- [string "sys.lua"]:434: [string "scheduling.lua"]:27: attempt to perform arithmetic on a string value
        -- stack traceback:
        --         [string "scheduling.lua"]:27: in function <[string "scheduling.lua"]:19>
        -- Lua VM exit!! reboot in 15000ms
        -- 根据这里的代码设计,我们可以知道,如果应用脚本中某一个task(协程)运行异常时,此时其余task的都还可以正常运行
        -- 如果不想自动重启软件,则可以在main.lua中的最开始的位置,设置:
        -- _G.COROUTINE_ERROR_ROLL_BACK = false
        -- _G.COROUTINE_ERROR_RESTART = false
        if _G.COROUTINE_ERROR_ROLL_BACK then
            sys.timerStart(assert,500,false,traceBack)
        elseif _G.COROUTINE_ERROR_RESTART then
            rtos.reboot()
        end
    end
    return ...
end

sys.coresume = function(...)
    local arg = {...}
    -- 此处先执行coroutine.resume(...),表示运行协程,运行时传入了协程对象和处理函数的可变参数
    -- coroutine.resume返回多个值:
    -- 第一个值是布尔值,表示协程是否执行成功
    -- 如果执行成功,第一个返回值为true
    --             后续的返回值是协程运行中,通过coroutine.yield接口使这个协程挂起时传递的参数;
    --                        或者是协程正常运行结束的返回值(当协程执行完毕时);
    -- 如果执行中出现错误,第一个返回值为false,第二个返回值是错误信息
    return wrapper(arg[1], coroutine.resume(...))
end

通过分析sys.taskInit这个api的源码,我们可以看出,LuatOS的task概念,实际上是对Lua语言的协程(coroutine)概念的一层封装,因为task的受众面比协程的受众面要广的很多,所以我们在LuatOS中,将协程(coroutine)包装成了task的概念,这样更容易理解和使用;

6.1.2 Lua语言中协程(coroutine)概念的理解

在LuatOS的sys.lua中,把Lua中的协程概念包装成了task概念,来实现多任务的编程效果;

所以我们要想深入理解sys.lua中的内部设计,势必要对Lua语言中的协程概念有一个基本的认识;

在 Lua 语言中,协程(coroutine)是一种用户态的轻量级线程,它允许程序在执行过程中暂停和恢复,从而实现协作式的多任务处理。与操作系统的线程不同,协程的调度完全由程序自身控制,而非由操作系统内核调度;

1、在LuatOS的sys.lua中,仅仅关注协程的三种状态:

挂起(suspended)状态,以下两种情况,协程处于挂起状态:

  • coroutine.create(f)创建协程后,未主动运行这个协程;
  • 协程运行过程中,被本协程内部调用coroutine.yield(...)暂停;

运行(running)状态

  • 处于挂起状态的协程co,在此协程外执行coroutine.resume(co[, ...])则恢复为运行状态;

死亡(dead)状态:以下两种情况,协程处于死亡状态:

  • 协程正常运行结束;
  • 协程运行过程中出现异常结束;

2、协程的核心特点

  • 协作式:协程必须主动让出执行权(通过调用coroutine.yield(...)),其他协程才有机会运行。

  • 状态保留:协程暂停挂起时会保存当前执行状态(如变量、栈),恢复时从暂停处继续。

6.1.3 应用示例

针对刚才对sys.taskInit源码的分析,接下来我们一起在模拟上运行一个demo示例,加深一下对sys.taskInit api内部设计的理解;

这个demo示例的完整代码链接:scheduling.lua main.lua

核心代码片段如下(重点关注黄色背景的代码):

main.lua

-- 以下两行代码是为了演示:task运行异常时,不自动重启软件的功能配置
-- _G.COROUTINE_ERROR_ROLL_BACK = false
-- _G.COROUTINE_ERROR_RESTART = false

scheduling.lua

local function task1_func()
    local count = 0
    while true do
        count = count + 1
        log.info("task1_func", "运行中,计数:", count)
        -- 等待500ms
        sys.wait(500)  

        -- 下一行代码是为了演示:task运行过程中,出现异常错误
        -- count = count + "hdjks"       
    end
end

-- 创建并启动第一个task
-- 运行这个task的任务处理函数task1_func
sys.taskInit(task1_func)

首先我们分析一下,在这个demo示例中,使用sys.taskInit(task1_func)创建的任务,任务处理函数的task1_func()的源码+sys.lua源码,分析下代码是如何调度运行的;

接下来我们使用模拟器,针对以下几种场景,实际跑一下这个demo示例看看运行效果:

模拟器运行指令如下:

luatos --llt=H:\Luatools\project\luatos_framework_luatos_task_Air8000.ini

1、打开 scheduling.lua中的count = count + "hdjks"

  1. 最终运行的核心日志为
[2025-12-01 21:48:13.844][00000000.106] I/user.task1_func 运行中,计数: 1
[2025-12-01 21:48:13.845][00000000.106] I/user.task_scheduling after task1 and before task2
[2025-12-01 21:48:13.845][00000000.107] I/user.task2_func 运行中,计数: 1
[2025-12-01 21:48:14.049][00000000.310] I/user.task2_func 运行中,计数: 2
[2025-12-01 21:48:14.247][00000000.509] E/user.coroutine.resume [string "scheduling.lua"]:28: attempt to perform arithmetic on a string value
stack traceback:
        [string "scheduling.lua"]:28: in function <[string "scheduling.lua"]:19>
[2025-12-01 21:48:14.353][00000000.615] I/user.task2_func 运行中,计数: 3
[2025-12-01 21:48:14.660][00000000.921] I/user.task2_func 运行中,计数: 4
[2025-12-01 21:48:14.750][00000001.012] E/main Luat:
[2025-12-01 21:48:14.751][00000001.012] E/main [string "sys.lua"]:444: [string "scheduling.lua"]:28: attempt to perform arithmetic on a string value
stack traceback:
        [string "scheduling.lua"]:28: in function <[string "scheduling.lua"]:19>
[2025-12-01 21:48:14.755][00000001.017] E/main Lua VM exit!! reboot in 15000ms

2、打开 scheduling.lua中的count = count + "hdjks"

  1. sys.lua中的return wrapper(arg[1], coroutine.resume(...)) 修改为 return coroutine.resume(...)

  2. 最终运行的核心日志为

[2025-12-01 21:42:14.186][00000000.067] I/user.task1_func 运行中,计数: 1
[2025-12-01 21:42:14.186][00000000.067] I/user.task_scheduling after task1 and before task2
[2025-12-01 21:42:14.187][00000000.067] I/user.task2_func 运行中,计数: 1
[2025-12-01 21:42:14.424][00000000.305] I/user.task2_func 运行中,计数: 2
[2025-12-01 21:42:14.732][00000000.613] I/user.task2_func 运行中,计数: 3
[2025-12-01 21:42:15.040][00000000.921] I/user.task2_func 运行中,计数: 4
[2025-12-01 21:42:15.346][00000001.227] I/user.task2_func 运行中,计数: 5
[2025-12-01 21:42:15.654][00000001.535] I/user.task2_func 运行中,计数: 6
[2025-12-01 21:42:15.963][00000001.844] I/user.task2_func 运行中,计数: 7
[2025-12-01 21:42:16.271][00000002.152] I/user.task2_func 运行中,计数: 8
[2025-12-01 21:42:16.582][00000002.462] I/user.task2_func 运行中,计数: 9
[2025-12-01 21:42:16.889][00000002.770] I/user.task2_func 运行中,计数: 10
[2025-12-01 21:42:17.195][00000003.076] I/user.task2_func 运行中,计数: 11

3、打开 scheduling.lua中的count = count + "hdjks"

  1. 打开main.lua中的

  2. _G.COROUTINE_ERROR_ROLL_BACK = false

  3. _G.COROUTINE_ERROR_RESTART = false

  4. 最终运行的核心日志为

[2025-12-01 21:47:06.842][00000000.041] I/user.task1_func 运行中,计数: 1
[2025-12-01 21:47:06.843][00000000.042] I/user.task_scheduling after task1 and before task2
[2025-12-01 21:47:06.843][00000000.042] I/user.task2_func 运行中,计数: 1
[2025-12-01 21:47:07.109][00000000.308] I/user.task2_func 运行中,计数: 2
[2025-12-01 21:47:07.309][00000000.508] E/user.coroutine.resume [string "scheduling.lua"]:28: attempt to perform arithmetic on a string value
stack traceback:
        [string "scheduling.lua"]:28: in function <[string "scheduling.lua"]:19>
[2025-12-01 21:47:07.416][00000000.615] I/user.task2_func 运行中,计数: 3
[2025-12-01 21:47:07.725][00000000.924] I/user.task2_func 运行中,计数: 4
[2025-12-01 21:47:08.029][00000001.228] I/user.task2_func 运行中,计数: 5
[2025-12-01 21:47:08.335][00000001.534] I/user.task2_func 运行中,计数: 6
[2025-12-01 21:47:08.646][00000001.845] I/user.task2_func 运行中,计数: 7
[2025-12-01 21:47:08.956][00000002.155] I/user.task2_func 运行中,计数: 8
[2025-12-01 21:47:09.264][00000002.463] I/user.task2_func 运行中,计数: 9
[2025-12-01 21:47:09.572][00000002.771] I/user.task2_func 运行中,计数: 10
[2025-12-01 21:47:09.882][00000003.081] I/user.task2_func 运行中,计数: 11
[2025-12-01 21:47:10.190][00000003.389] I/user.task2_func 运行中,计数: 12
[2025-12-01 21:47:10.496][00000003.695] I/user.task2_func 运行中,计数: 13
[2025-12-01 21:47:10.803][00000004.002] I/user.task2_func 运行中,计数: 14
[2025-12-01 21:47:11.110][00000004.309] I/user.task2_func 运行中,计数: 15
[2025-12-01 21:47:11.415][00000004.614] I/user.task2_func 运行中,计数: 16

6.2 sys.taskInitEx(创建并且运行高级task)

sys.taskInitEx(task_func, task_name, non_targeted_msg_cbfunc, ...)

6.2.1 数据结构(taskList)

-- 高级task的任务列表
-- 数据结构如下:
-- local taskList = 
-- {
--     ["task_name1"] =
--     {
--         -- 定向消息队列,用来存储sys.sendMsg("task_name1", param1, param2, param3, param4)
--         -- 给"task_name1"的高级task发布的param1, param2, param3, param4消息参数
--         -- table.insert(taskList["task_name1"].msgQueue, {param1, param2, param3, param4})
--         msgQueue = {{param1, param2, param3, param4}, {}, ...}, 

--         -- sys.waitMsg(taskName, target, ms)时,如果传入了ms超时时长,
--         -- 并且当前task在阻塞等待target消息;
--         -- 则To表示阻塞等待过程中,是否因为超时原因退出了阻塞等待状态
--         To = false, 

--         -- 非目标消息回调函数
--         cb = cbFun
--     }, 

--     ["task_name2"] =
--     {
--     },
-- 
--     ...
-- }
local taskList = {}

6.2.2 源码分析

--- 创建一个任务线程,在模块最末行调用该函数并注册模块中的任务函数,main.lua导入该模块即可
-- @param fun 任务函数名,用于resume唤醒时调用
-- @param taskName 任务名称,用于唤醒任务的id
-- @param cbFun 接收到非目标消息时的回调函数
-- @param ... 任务函数fun的可变参数
-- @return co  返回该任务的线程号
-- @usage sys.taskInitEx(task1,'a',callback)
function sys.taskInitEx(fun, taskName, cbFun, ...)
    if taskName == nil then
        log.error("sys", "taskName is nil", debug.traceback())
        return
    end
    -- 在高级task任务列表中,申请此高级任务对应的定向消息队列、阻塞等待定向消息超时标志、非目标消息回调函数
    taskList[taskName]={msgQueue={}, To=false, cb=cbFun}

    -- 创建一个基础task
    return sys.taskInit(fun, ...)
end

从上面这段代码可以看到,使用sys.taskInitEx创建一个高级task,和使用sys.taskInit创建一个基础task相比,最核心的区别是多申请了一个高级task全局信息表{msgQueue={}, To=false, cb=cbFun},这个全局信息表是高级task处理定时器消息和定向消息的关键;下图中高级task指向的定向消息队列,对应的就是高级task全局表中的msgQueue={}

6.2.3 应用示例

使用sys.taskInitEx创建一个高级task,和使用sys.taskInit创建一个基础task相比,最核心的区别是多申请了一个高级task全局信息表{msgQueue={}, To=false, cb=cbFun},其余实现原理和sys.taskInit完全相同,所以此处不再对sys.taskInitEx结合实际的demo示例来进一步理解了;

在后续的消息和定时器章节会进一步深入演示学习。

6.3 sys.taskDel(清除高级task的内存资源)

sys.taskDel(task_name)

6.3.1 源码分析

--- 删除由taskInitEx创建的任务线程
-- @param taskName 任务名称,用于唤醒任务的id
-- @return 无
-- @usage sys.taskDel('a')
function sys.taskDel(taskName)
    -- 释放taskName对应的高级task全部信息表资源,
    -- 释放的是taskList[taskName] 对应的 {msgQueue={}, To=false, cb=cbFun}占用的内存

    -- 因为taskList是全局变量,会一直存在;
    -- 如果taskList[taskName]没有赋值为nil,则taskList[taskName]一直引用{msgQueue={}, To=false, cb=cbFun}
    -- 此时Lua的垃圾回收机制永远不会自动回收{msgQueue={}, To=false, cb=cbFun}所占用的内存

    -- 所以在高级task的任务处理函数执行结束,每个return语句正常退出前,或者最后执行结束自然退出前,
    -- 都要调用sys.taskDel(taskName),释放{msgQueue={}, To=false, cb=cbFun}占用的内存

    -- 如果高级task的任务处理函数运行过程中,发生异常退出,
    -- 这种情况下,即使我们写了sys.taskDel(taskName),也不会被执行
    -- 遇到这种问题,需要怎么处理呢?分为以下两种情况:
    -- 1、目前的LuatOS设计,当task运行异常退出,默认会自动重启软件系统,
    --    这种情况下,软件都重启了,就不用考虑未释放的内存问题了;
    -- 2、如果你看懂了sys.lua的内部设计,当task运行异常退出时,可以控制不重启软件系统,
    --    这种情况下,就需要修改sys.lua的内部设计,可以自动释放异常退出的高级task对应的信息表资源内存,
    --    目前还没有实现这种功能,后续如果客户有需求,可以开发支持
    taskList[taskName]=nil
end

6.3.2 应用示例

针对刚才对sys.taskDel源码的分析,接下来我们一起在模拟上运行一个demo示例,加深一下对sys.taskDelapi内部设计的理解;

这个demo示例的完整代码链接:memory_task_delete.lua main.lua

核心代码片段如下(重点关注黄色背景的代码):

main.lua

-- 以下两行代码是为了演示:task运行异常时,不自动重启软件的功能配置
-- _G.COROUTINE_ERROR_ROLL_BACK = false
-- _G.COROUTINE_ERROR_RESTART = false

memory_task_delete.lua

local function led_task_func()
    for i=1, 1000 do
        sys.sendMsg("led_task", "targeted_msg_"..i)
    end

    -- 下一行代码会导致task运行异常
    -- 在此处task异常退出,不会执行后续的代码
    -- dhjsk = las + 2

    -- 有选择的打开或者关闭下一行代码
    -- 可以对比看出sys.taskDel对内存释放的作用
    sys.taskDel("led_task")
end

接下来我们使用模拟器,针对以下三种场景,实际跑一下这个demo示例看看运行效果:

1、打开 memory_task_delete.lua中的sys.taskDel("led_task")

  1. 关闭 memory_task_delete.lua中的dhjsk = las + 2

  2. 关闭main.lua中的

  3. _G.COROUTINE_ERROR_ROLL_BACK = false

  4. _G.COROUTINE_ERROR_RESTART = false

  5. 最终运行的核心日志为

[2025-12-02 15:45:50.610][00000000.155] I/user.before led task
[2025-12-02 15:45:50.611][00000000.156] I/user.mem.lua 2097144 43536 58168
[2025-12-02 15:45:50.613][00000000.157] I/user.after led task
[2025-12-02 15:45:51.461][00000001.006] I/user.mem.lua 2097144 52640 272776
[2025-12-02 15:45:52.469][00000002.014] I/user.mem.lua 2097144 52640 272776
[2025-12-02 15:45:53.483][00000003.027] I/user.mem.lua 2097144 52640 272776
[2025-12-02 15:45:54.489][00000004.034] I/user.mem.lua 2097144 52640 272776
[2025-12-02 15:45:55.504][00000005.049] I/user.mem.lua 2097144 52640 272776
[2025-12-02 15:45:56.508][00000006.052] I/user.mem.lua 2097144 52640 272776
[2025-12-02 15:45:57.520][00000007.064] I/user.mem.lua 2097144 52640 272776

2、关闭 memory_task_delete.lua中的sys.taskDel("led_task")

  1. 关闭 memory_task_delete.lua中的dhjsk = las + 2

  2. 关闭main.lua中的

  3. _G.COROUTINE_ERROR_ROLL_BACK = false

  4. _G.COROUTINE_ERROR_RESTART = false

  5. 最终运行的核心日志为

[2025-12-02 15:58:50.197][00000000.052] I/user.before led task
[2025-12-02 15:58:50.197][00000000.052] I/user.mem.lua 2097144 43496 58232
[2025-12-02 15:58:50.201][00000000.056] I/user.after led task
[2025-12-02 15:58:51.159][00000001.013] I/user.mem.lua 2097144 192424 269096
[2025-12-02 15:58:52.166][00000002.020] I/user.mem.lua 2097144 192424 269096
[2025-12-02 15:58:53.171][00000003.026] I/user.mem.lua 2097144 192424 269096
[2025-12-02 15:58:54.173][00000004.028] I/user.mem.lua 2097144 192424 269096
[2025-12-02 15:58:55.181][00000005.036] I/user.mem.lua 2097144 192424 269096
[2025-12-02 15:58:56.191][00000006.045] I/user.mem.lua 2097144 192424 269096
[2025-12-02 15:58:57.201][00000007.055] I/user.mem.lua 2097144 192424 269096
[2025-12-02 15:58:58.206][00000008.061] I/user.mem.lua 2097144 192424 269096
[2025-12-02 15:58:59.206][00000009.061] I/user.mem.lua 2097144 192424 269096

3、打开 memory_task_delete.lua中的sys.taskDel("led_task")

  1. 打开 memory_task_delete.lua中的dhjsk = las + 2

  2. 打开main.lua中的

  3. _G.COROUTINE_ERROR_ROLL_BACK = false

  4. _G.COROUTINE_ERROR_RESTART = false

  5. 最终运行的核心日志为

[2025-12-02 16:02:25.871][00000000.090] I/user.before led task
[2025-12-02 16:02:25.871][00000000.091] I/user.mem.lua 2097144 43704 58232
[2025-12-02 16:02:25.873][00000000.092] E/user.coroutine.resume [string "memory_task_delete.lua"]:47: attempt to perform arithmetic on a nil value (global 'las')
stack traceback:
        [string "memory_task_delete.lua"]:47: in function <[string "memory_task_delete.lua"]:42>
[2025-12-02 16:02:25.873][00000000.092] I/user.after led task
[2025-12-02 16:02:26.786][00000001.006] I/user.mem.lua 2097144 192480 274888
[2025-12-02 16:02:27.786][00000002.005] I/user.mem.lua 2097144 192480 274888
[2025-12-02 16:02:28.791][00000003.011] I/user.mem.lua 2097144 192480 274888
[2025-12-02 16:02:29.806][00000004.025] I/user.mem.lua 2097144 192480 274888
[2025-12-02 16:02:30.814][00000005.034] I/user.mem.lua 2097144 192480 274888
[2025-12-02 16:02:31.820][00000006.040] I/user.mem.lua 2097144 192480 274888
[2025-12-02 16:02:32.825][00000007.045] I/user.mem.lua 2097144 192480 274888
[2025-12-02 16:02:33.826][00000008.046] I/user.mem.lua 2097144 192480 274888

4、打开 memory_task_delete.lua中的sys.taskDel("led_task")

  1. 打开 memory_task_delete.lua中的dhjsk = las + 2

  2. 关闭main.lua中的

  3. _G.COROUTINE_ERROR_ROLL_BACK = false

  4. _G.COROUTINE_ERROR_RESTART = false

  5. 最终运行的核心日志为

[2025-12-02 16:20:55.361][00000000.068] I/user.before led task
[2025-12-02 16:20:55.362][00000000.068] I/user.mem.lua 2097144 43640 59032
[2025-12-02 16:20:55.364][00000000.070] E/user.coroutine.resume [string "memory_task_delete.lua"]:49: attempt to perform arithmetic on a nil value (global 'las')
stack traceback:
        [string "memory_task_delete.lua"]:49: in function <[string "memory_task_delete.lua"]:42>
[2025-12-02 16:20:55.364][00000000.071] I/user.after led task
[2025-12-02 16:20:55.794][00000000.500] E/main Luat:
[2025-12-02 16:20:55.796][00000000.502] E/main [string "sys.lua"]:448: [string "memory_task_delete.lua"]:49: attempt to perform arithmetic on a nil value (global 'las')
stack traceback:
        [string "memory_task_delete.lua"]:49: in function <[string "memory_task_delete.lua"]:42>
[2025-12-02 16:20:55.799][00000000.505] E/main Lua VM exit!! reboot in 15000ms

通过这个demo示例,我们可以看出sys.taskDel函数对于内存释放的作用,目前的LuatOS设计,只需要记住一点就行了:

高级task的任务处理函数中,在调用return语句返回前,或者任务处理函数的最后一行代码,调用sys.taskDel即可;

第七部分:LuatOS 定时器(timer)内部设计

7.1 数据结构(timerPool和para)

-- 定时器处理表
-- 表中记录了两种类型的定时器
-- 第一种定时器记录的是定时器id和对应的回调函数
-- 第二种定时器记录的是定时器id和对应的task对象
-- 数据结构如下:
-- local timerPool = 
-- {
--     -- id1表示:通过sys.timerStart或者sys.timerLoopStart接口创建定时器时,sys.lua内部自动分配的定时器id
--     -- func1表示:定时器对应的回调函数,例如:sys.timerStart(blink_on, 5000)中的回调函数blink_on
--     [id1] = func1,

--     -- id2表示:通过sys.wait或者sys.waitUntil接口创建定时器时动态分配的定时器id
--     -- co2表示:创建定时器时,当前正常运行的协程(task)对象,例如:schedulig.lua中sys.wait所处的task对象
--     [id2] = co2,
-- 
--     ...
-- }
local timerPool = {}

-- 定时器回调函数参数表
-- 此处的定时器指的是timerPool中记录的第一种定时器
-- 数据结构如下:
-- local para = 
-- {
--     -- id1表示:通过sys.timerStart或者sys.timerLoopStart或者sys.waitMsg接口创建定时器时,sys.lua内部自动分配的定时器id
--     -- {...}表示:定时器对应的回调函数参数,例如:sys.timerStart(blink_on, 5000, "red", 1)中的{"red", 1}
--     [id1] = {...},

--     ...
-- }
local para = {}

7.2 api源码分析

现在,我在vscode上打开已经添加了详细注释的sys.lua,对以下每个api的源码进行分析(仅分析和定时器有关的源码部分),为了让大家对定时器的使用有一个更全局的认识,所以也会分析一部分LuatOS仓库中开源的内核C代码

sys.timerStart(cbfunc, timeout, ...)(创建并且运行单次定时器)
sys.timerLoopStart(cbfunc, timeout, ...)(创建并且运行循环定时器)
sys.timerStop(cbfunc_or_timerid, ...)(停止并且删除一个定时器)
sys.timerStopAll(cbfunc)(停止并且删除cbfunc指定的所有定时器)
sys.wait(timeout)(在task中阻塞等待一段时间)
sys.waitUntil(msg, timeout)(在task中阻塞等待一段时间或者阻塞等待一个全局消息msg)
sys.waitMsg(task_name, msg, timeout)(在task中阻塞等待一段时间或者阻塞等待一个定向消息msg)

下面列举的函数调用栈,供分析源码时参考使用:

  1. Lua->C:sys.timerStart,sys.timerLoopStart,sys.wait,sys.waitUntil,sys.waitMsg->rtos.timer_start
  2. C: l_rtos_timer_start->luat_timer_start->xTimer接口
  3. C:luat_timer_callback->luat_msgbus_put
  4. Lua->C->Lua:sys.run->rtos.receive->luat_msgbus_get->l_timer_handler(返回rtos.MSG_TIMER,timer_id, repeat三个参数)

7.3 完整业务处理过程

7.4 应用示例

刚才我们已经分析了和定时器操作有关的sys.lua源码,在本讲的最后一章,我会基于一个实际的MQTT demo项目,逐行分析项目中应用脚本源码和sys.lua源码,让大家在实际的项目中深刻理解LuatOS运行框架中的消息设计原理;

第八部分:LuatOS 消息(message)内部设计

8.1 数据结构(subscribers和messageQueue)

-- 全局消息订阅表
-- 数据结构如下:
-- local subscribers = 
-- {
--     [msg1] = 
--     {
--         cbfunc1 = true,  -- sys.subscribe(msg1, cbfunc1)
--         task1 = true,    -- 在task1的处理函数中,调用sys.waitUntil(msg1[, timeout])
--         cbfunc2 = true,
--         task2 = true,
--         ......
--         cbfuncN = true,
--         taskN = true,
--     },

--    [msg2] = { ...... },
--    [msg3] = { ...... },
--    ......,
--    [msgN] = { ...... }
-- }
local subscribers = {}

-- 全局消息队列
-- 数据结构如下:
-- local messageQueue = 
-- {
--     [1] = {msg1, {...}},    --sys.publish(msg1, ...)
--     [2] = {msg2, {...}},
--     [3] = {msg3, {...}},

--     ......,
--     [n] = { ...... }
-- }
local messageQueue = {}

下图中的全局消息队列,对应的就是messageQueue ={}

8.2 api源码分析

现在,我在vscode上打开已经添加了详细注释的sys.lua,对以下每个api的源码进行分析(仅分析和消息处理有关的源码部分),为了让大家对消息的使用有一个更全局的认识,所以也会分析一部分LuatOS仓库中开源的内核C代码

sys.publish(msg, ...)(发布一条全局消息)
sys.subscribe(msg, msg_cbfunc)(订阅一个全局消息的回调函数)
sys.unsubscribe(msg, msg_cbfunc)(取消订阅一个全局消息的回调函数)
sys.waitUntil(msg, timeout)(在task中阻塞等待一个全局消息,超时时长timeout)
sys.sendMsg(task_name, msg, arg2, arg3, arg4)(向名称为task_name的task发布一个定向消息)
sys.waitMsg(task_name, msg, timeout)(在task中阻塞等待名称为msg的定向消息)
sys.cleanMsg(task_name)(清除名称为task_name的task的所有定向消息)

在这些api中,消息的发送和接收api容易混用,组合使用关系参考下表(每一行的两个单元格所表示的api必须组合使用):

消息发送api 消息接收api
sys.publish(msg, ...) sys.subscribe(msg, msg_cbfunc)
sys.publish(msg, ...) sys.waitUntil(msg, timeout)
sys.sendMsg(task_name, msg, arg2, arg3, arg4) sys.waitMsg(task_name, msg, timeout)

下面列举五种消息应用场景的完整业务处理过程:

8.3 内核非定时器消息完整业务处理过程(以 串口接收到新数据消息 为例)

  1. uart.on(1, "receive", recv_cb)->l_uart_on
  2. luat_irq_uart_cb->luat_msgbus_put
  3. sys.run()->rtos.receive->luat_msgbus_get->l_uart_handler(在这里执行执行uart.on注册的回调函数recv_cb,然后返回空数据,sys.lua中的rtos.receive返回值为nil)

8.4 系统应用全局消息完整处理过程(以"IP_READY"为例)

  1. 在用户脚本代码中,根据具体的业务逻辑,订阅全局消息"IP_READY"
  2. 当某一种网卡网络环境准备就绪时,在内核固件的netif_ip_event_cb中,主动调用sys.lua中定义的_G.sys_pub = sys.publish,实际上就是调用sys.publish发布一条全局消息"IP_READY",存储到sys.lua中的全局消息表messageQueue中
  3. 在sys.lua中,消息分发处理:sys.run->dispatch

8.5 用户应用全局消息完整处理过程

  1. 在用户脚本代码中,根据具体的业务逻辑,订阅全局消息
  2. 在用户脚本的代码,调用sys.publish发布全局消息
  3. 在sys.lua中,通过sys.run调度器,执行dispatch分发处理全局消息

8.6 系统应用定向消息完整处理过程(以socket.EVENT为例)

  1. l_socket_callback,当内核固件中的某一个socket产生异步事件时(例如socke接收到新数据),主动调用sys.lua中定义的_G.sys_send = sys.sendMsg,实际上就是调用sys.sendMsg给指定Lua脚本中的高级task发送一条定向消息,存储到此高级task对应的定向消息队列taskList[task_name].msgQueue中
  2. 在用户脚本代码中,通过sys.waitMsg去处理定向消息

8.7 用户应用定向消息完整处理过程

  1. 在用户脚本代码中,调用sys.sendMsg发送定向消息
  2. 在用户脚本代码中,调用sys.waitMsg处理定向消息

8.8 应用示例

刚才我们已经分析了和消息操作有关的sys.lua源码,在本讲的最后一章,我会基于一个实际的MQTT demo项目,逐行分析项目中应用脚本源码和sys.lua源码,让大家在实际的项目中深刻理解LuatOS运行框架中的消息设计原理;

第九部分:基于MQTT demo项目,再次全面理解LuatOS框架的内部设计运行原理

讲到最后,我会基于一个实际的MQTT demo项目,逐行分析项目中应用脚本源码和sys.lua源码,让大家在实际的项目中深刻理解LuatOS运行框架的内部设计原理;

9.1 demo项目总体设计框图

demo项目的总体设计框图如下:

9.2 demo项目源码+sys.lua源码综合分析

这个mqtt demo代码中的注释比较详细,sys.lua代码注释也比较详细,接下来我用vscode直接打开这份demo项目sys.lua,和大家一起分析下源码设计;