Skip to content

27_软件定时器篇

kuangyufei edited this page Sep 5, 2022 · 1 revision

本篇关键词:、、、

下载 >> 离线文档.鸿蒙内核源码分析(百篇博客分析.挖透鸿蒙内核).pdf

任务管理相关篇为:

本篇说清楚定时器的实现

读本篇之前建议先读v08.xx 鸿蒙内核源码分析(总目录) 其余篇。

运作机制

  • 软件定时器,是基于系统Tick时钟中断且由软件来模拟的定时器。当经过设定的Tick数后,会触发用户自定义的回调函数。
  • 软件定时器是系统资源,在模块初始化的时候已经分配了一块连续内存。
  • 软件定时器使用了系统的一个队列和一个任务资源,软件定时器的触发遵循队列规则,先进先出。定时时间短的定时器总是比定时时间长的靠近队列头,满足优先触发的准则。
  • 软件定时器以Tick为基本计时单位,当创建并启动一个软件定时器时,鸿蒙会根据当前系统Tick时间及设置的定时时长确定该定时器的到期Tick时间,并将该定时器控制结构挂入计时全局链表。
  • 当Tick中断到来时,在Tick中断处理函数中扫描软件定时器的计时全局链表,检查是否有定时器超时,
  • 若有则将超时的定时器记录下来。Tick中断处理函数结束后,软件定时器任务(优先级为最高)被唤醒,在该任务中调用已经记录下来的定时器的回调函数。

定时器长什么样?

typedef VOID (*SWTMR_PROC_FUNC)(UINTPTR arg);//函数指针, 赋值给 SWTMR_CTRL_S->pfnHandler,回调处理
typedef struct tagSwTmrCtrl {//软件定时器控制块
    SortLinkList stSortList;//通过它挂到对应CPU核定时器链表上
    UINT8 ucState;      /**< Software timer state *///软件定时器的状态
    UINT8 ucMode;       /**< Software timer mode *///软件定时器的模式
    UINT8 ucOverrun;    /**< Times that a software timer repeats timing *///软件定时器重复计时的次数
    UINT16 usTimerID;   /**< Software timer ID *///软件定时器ID,唯一标识,由软件计时器池分配
    UINT32 uwCount;     /**< Times that a software timer works *///软件定时器工作的时间
    UINT32 uwInterval;  /**< Timeout interval of a periodic software timer *///周期性软件定时器的超时间隔
    UINT32 uwExpiry;    /**< Timeout interval of an one-off software timer *///一次性软件定时器的超时间隔
#if (LOSCFG_KERNEL_SMP == YES)
    UINT32 uwCpuid;     /**< The cpu where the timer running on *///多核情况下,定时器运行的cpu
#endif
    UINTPTR uwArg;      /**< Parameter passed in when the callback function
                             that handles software timer timeout is called *///回调函数的参数
    SWTMR_PROC_FUNC pfnHandler; /**< Callback function that handles software timer timeout */	//处理软件计时器超时的回调函数
    UINT32          uwOwnerPid; /** Owner of this software timer *///软件定时器所属进程ID号
} SWTMR_CTRL_S;//变量前缀 uc:UINT8  us:UINT16 uw:UINT32

解读

  • 在多CPU核情况下,定时器是跟着CPU走的,每个CPU核都维护着独立的定时任务链表,上面挂的都是CPU核要处理的定时器。
  • stSortList的背后是双向链表,这对钩子在定时器创建的那一刻会钩到CPU的swtmrSortLink上去。
  • pfnHandler定时器时间到了的执行函数,由外界指定。uwArg为回调函数的参数
  • ucMode 为定时器模式,软件定时器提供了三类模式

    单次触发定时器,这类定时器在启动后只会触发一次定时器事件,然后定时器自动删除。 周期触发定时器,这类定时器会周期性的触发定时器事件,直到用户手动停止定时器,否则将永远持续执行下去。 单次触发定时器,但这类定时器超时触发后不会自动删除,需要调用定时器删除接口删除定时器。

  • ucState 定时器状态。

    OS_SWTMR_STATUS_UNUSED(定时器未使用) 系统在定时器模块初始化时,会将系统中所有定时器资源初始化成该状态。 OS_SWTMR_STATUS_TICKING(定时器处于计数状态) 在定时器创建后调用LOS_SwtmrStart接口启动,定时器将变成该状态,是定时器运行时的状态。 OS_SWTMR_STATUS_CREATED(定时器创建后未启动,或已停止) 定时器创建后,不处于计数状态时,定时器将变成该状态。

定时器分类

定时器是指从指定的时刻开始,经过一定的指定时间后触发一个事件,例如定个时间提醒晚上9点准时秒杀。定时器有硬件定时器和软件定时器之分:

  • 硬件定时器是芯片本身提供的定时功能。一般是由外部晶振提供给芯片输入时钟,芯片向软件模块提供一组配置寄存器,接受控制输入,到达设定时间值后芯片中断控制器产生时钟中断。硬件定时器的精度一般很高,可以达到纳秒级别,并且是中断触发方式。

  • 软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受数目限制的定时器服务。

鸿蒙内核提供软件实现的定时器,以时钟节拍(OS Tick)的时间长度为单位,即定时数值必须是 OS Tick 的整数倍,例如鸿蒙内核默认是10ms触发一次,那么上层软件定时器只能是 10ms,20ms,100ms 等,而不能定时为 15ms。

定时器怎么管理?

LITE_OS_SEC_BSS SWTMR_CTRL_S    *g_swtmrCBArray = NULL;     /* First address in Timer memory space *///定时器池
LITE_OS_SEC_BSS UINT8           *g_swtmrHandlerPool = NULL; /* Pool of Swtmr Handler *///用于注册软时钟的回调函数
LITE_OS_SEC_BSS LOS_DL_LIST     g_swtmrFreeList;            /* Free list of Software Timer *///空闲定时器链表

typedef struct {//处理软件定时器超时的回调函数的结构体
    SWTMR_PROC_FUNC handler;    /**< Callback function that handles software timer timeout  */	//处理软件定时器超时的回调函数
    UINTPTR arg;                /**< Parameter passed in when the callback function
                                    that handles software timer timeout is called */	//调用处理软件计时器超时的回调函数时传入的参数
} SwtmrHandlerItem;

解读

三个全局变量可知,定时器是通过池来管理,在初始化阶段赋值。

  • g_swtmrCBArray 定时器池,初始化中一次性创建1024个定时器控制块供使用
  • g_swtmrHandlerPool 回调函数池,回调函数也是统一管理的,申请了静态内存保存。 池中放的是 SwtmrHandlerItem 回调函数描述符。
  • g_swtmrFreeList 空闲可供分配的定时器链表,鸿蒙的进程池,任务池,事件池都是这么处理的,没有印象的自行去翻看。 g_swtmrFreeList上挂的是一个个的 SWTMR_CTRL_S
  • 要搞明白 SWTMR_CTRL_SSwtmrHandlerItem的关系,前者是一个定时器,后者是定时器时间到了去哪里干活。

初始化 -> OsSwtmrInit

#define LOSCFG_BASE_CORE_SWTMR_LIMIT 1024 // 最大支持的软件定时器数
LITE_OS_SEC_TEXT_INIT UINT32 OsSwtmrInit(VOID)
{
    UINT32 size;
    UINT16 index;
    UINT32 ret;
    SWTMR_CTRL_S *swtmr = NULL;
    UINT32 swtmrHandlePoolSize;
    UINT32 cpuid = ArchCurrCpuid();
    if (cpuid == 0) {//确保以下代码块由一个CPU执行,g_swtmrCBArray和g_swtmrHandlerPool 是所有CPU共用的
        size = sizeof(SWTMR_CTRL_S) * LOSCFG_BASE_CORE_SWTMR_LIMIT;//申请软时钟内存大小 
        swtmr = (SWTMR_CTRL_S *)LOS_MemAlloc(m_aucSysMem0size); /* system resident resource */ //常驻内存
        if (swtmr == NULL) {
            return LOS_ERRNO_SWTMR_NO_MEMORY;
        }

        (VOID)memset_s(swtmrsize0size);//清0
        g_swtmrCBArray = swtmr;//软时钟
        LOS_ListInit(&g_swtmrFreeList);//初始化空闲链表
        for (index = 0; index < LOSCFG_BASE_CORE_SWTMR_LIMIT; index++swtmr++) {
            swtmr->usTimerID = index;//按顺序赋值
            LOS_ListTailInsert(&g_swtmrFreeList&swtmr->stSortList.sortLinkNode);//通过sortLinkNode将节点挂到空闲链表 
        }
        //想要用静态内存池管理,就必须要使用LOS_MEMBOX_SIZE来计算申请的内存大小,因为需要点前缀内存承载头部信息。
        swtmrHandlePoolSize = LOS_MEMBOX_SIZE(sizeof(SwtmrHandlerItem), OS_SWTMR_HANDLE_QUEUE_SIZE);//计算所有注册函数内存大小
        //规划一片内存区域作为软时钟处理函数的静态内存池。
        g_swtmrHandlerPool = (UINT8 *)LOS_MemAlloc(m_aucSysMem1swtmrHandlePoolSize); /* system resident resource *///常驻内存
        if (g_swtmrHandlerPool == NULL) {
            return LOS_ERRNO_SWTMR_NO_MEMORY;
        }

        ret = LOS_MemboxInit(g_swtmrHandlerPoolswtmrHandlePoolSizesizeof(SwtmrHandlerItem));//初始化软时钟注册池
        if (ret != LOS_OK) {
            return LOS_ERRNO_SWTMR_HANDLER_POOL_NO_MEM;
        }
    }
    //每个CPU都会创建一个属于自己的 OS_SWTMR_HANDLE_QUEUE_SIZE 的队列
    ret = LOS_QueueCreate(NULLOS_SWTMR_HANDLE_QUEUE_SIZE&g_percpu[cpuid].swtmrHandlerQueue0sizeof(CHAR *));//为当前CPU core 创建软时钟队列 maxMsgSize:sizeof(CHAR *)
    if (ret != LOS_OK) {
        return LOS_ERRNO_SWTMR_QUEUE_CREATE_FAILED;
    }

    ret = OsSwtmrTaskCreate();//每个CPU独自创建属于自己的软时钟任务,统一处理队列
    if (ret != LOS_OK) {
        return LOS_ERRNO_SWTMR_TASK_CREATE_FAILED;
    }

    ret = OsSortLinkInit(&g_percpu[cpuid].swtmrSortLink);//每个CPU独自对自己软时钟链表排序初始化,为啥要排序因为每个定时器的时间不一样,鸿蒙把用时短的排在前面
    if (ret != LOS_OK) {
        return LOS_ERRNO_SWTMR_SORTLINK_CREATE_FAILED;
    }

    return LOS_OK;
}

解读:

  • 每个CPU核都是独立处理定时器任务的,所以需要独自管理。OsSwtmrInit是负责初始化各CPU核定时模块功能的,注意在多CPU核时,OsSwtmrInit会被多次调用。
  • cpuid == 0代表主CPU核, 它最早执行这个函数,所以g_swtmrCBArrayg_swtmrHandlerPool是共用的,系统默认最多支持 1024 个定时器和回调函数。
  • 每个CPU核都创建了自己独立的 LOS_QueueCreate队列和任务OsSwtmrTaskCreate,并初始化了swtmrSortLink链表,关于链表排序可前往系列篇总目录 排序链表篇查看。

定时任务 -> 最高优先级

LITE_OS_SEC_TEXT_INIT UINT32 OsSwtmrTaskCreate(VOID)
{
    UINT32 retswtmrTaskID;
    TSK_INIT_PARAM_S swtmrTask;
    UINT32 cpuid = ArchCurrCpuid();//获取当前CPU id

    (VOID)memset_s(&swtmrTasksizeof(TSK_INIT_PARAM_S), 0sizeof(TSK_INIT_PARAM_S));//清0
    swtmrTask.pfnTaskEntry = (TSK_ENTRY_FUNC)OsSwtmrTask;//入口函数
    swtmrTask.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;//16K默认内核任务栈
    swtmrTask.pcName = "Swt_Task";//任务名称
    swtmrTask.usTaskPrio = 0;//哇塞! 逮到一个最高优先级的任务 @note_thinking 这里应该用 OS_TASK_PRIORITY_HIGHEST 表示
    swtmrTask.uwResved = LOS_TASK_STATUS_DETACHED;//分离模式
#if (LOSCFG_KERNEL_SMP == YES)
    swtmrTask.usCpuAffiMask   = CPUID_TO_AFFI_MASK(cpuid);//交给当前CPU执行这个任务
#endif
    ret = LOS_TaskCreate(&swtmrTaskID&swtmrTask);//创建任务并申请调度
    if (ret == LOS_OK) {
        g_percpu[cpuid].swtmrTaskID = swtmrTaskID;//全局变量记录 软时钟任务ID
        OS_TCB_FROM_TID(swtmrTaskID)->taskStatus |= OS_TASK_FLAG_SYSTEM_TASK;//告知这是一个系统任务
    }

    return ret;
}

解读:

  • 内核为每个CPU处理单独创建任务来处理定时器, 任务即线程, 外界可理解为内核开设了一个线程跑定时器。
  • 注意看任务的优先级 swtmrTask.usTaskPrio = 0; 0是最高优先级! 这并不多见! 内核会在第一时间响应软时钟任务。
  • 系列篇CPU篇中讲过每个CPU都有自己的任务链表和定时器任务,g_percpu[cpuid].swtmrTaskID = swtmrTaskID; 表示创建的任务和CPU具体核进行了捆绑。从此swtmrTaskID负责这个CPU的定时器处理。
  • 定时任务是一个系统任务,除此之外还有哪些是系统任务?
  • 任务入口函数OsSwtmrTask ,是任务的执行体,类似于[Java 线程中的run()函数]
  • usCpuAffiMask代表这个任务只能由这个CPU核来跑

队列消费者 -> OsSwtmrTask

//软时钟的入口函数,拥有任务的最高优先级 0 级!
LITE_OS_SEC_TEXT VOID OsSwtmrTask(VOID)
{
    SwtmrHandlerItemPtr swtmrHandlePtr = NULL;
    SwtmrHandlerItem swtmrHandle;
    UINT32 retswtmrHandlerQueue;

    swtmrHandlerQueue = OsPercpuGet()->swtmrHandlerQueue;//获取定时器超时队列
    for (;;) {//死循环获取队列item,一直读干净为止
        ret = LOS_QueueRead(swtmrHandlerQueue&swtmrHandlePtrsizeof(CHAR *), LOS_WAIT_FOREVER);//一个一个读队列
        if ((ret == LOS_OK) && (swtmrHandlePtr != NULL)) {
            swtmrHandle.handler = swtmrHandlePtr->handler;//超时中断处理函数,也称回调函数
            swtmrHandle.arg = swtmrHandlePtr->arg;//回调函数的参数
            (VOID)LOS_MemboxFree(g_swtmrHandlerPoolswtmrHandlePtr);//静态释放内存,注意在鸿蒙内核只有软时钟注册用到了静态内存
            if (swtmrHandle.handler != NULL) {
                swtmrHandle.handler(swtmrHandle.arg);//回调函数处理函数
            }
        }
    }
}

解读

  • OsSwtmrTask是任务的执行体,只做一件事,消费定时器回调函数队列。
  • 任务在跑一个死循环,不断在读队列。关于队列的具体操作不在此处细说,系列篇中已有专门的文章讲解,可前往查看。
  • 每个CPU核都有属于自己的定时器回调函数队列,里面存放的是时间到了回调函数。
  • 但队列的数据怎么来呢? OsSwtmrTask只是在不断的消费队列,那生产者在哪里呢? 就是 OsSwtmrScan

队列生产者 -> OsSwtmrScan

LITE_OS_SEC_TEXT VOID OsSwtmrScan(VOID)//扫描定时器,如果碰到超时的,就放入超时队列
{
    SortLinkList *sortList = NULL;
    SWTMR_CTRL_S *swtmr = NULL;
    SwtmrHandlerItemPtr swtmrHandler = NULL;
    LOS_DL_LIST *listObject = NULL;
    SortLinkAttribute* swtmrSortLink = &OsPercpuGet()->swtmrSortLink;//拿到当前CPU的定时器链表

    swtmrSortLink->cursor = (swtmrSortLink->cursor + 1) & OS_TSK_SORTLINK_MASK;
    listObject = swtmrSortLink->sortLink + swtmrSortLink->cursor;
	//由于swtmr是在特定的sortlink中,所以需要很小心的处理它,但其他CPU Core仍然有机会处理它,比如停止计时器
    /*
     * it needs to be carefully coped with, since the swtmr is in specific sortlink
     * while other cores still has the chance to process it, like stop the timer.
     */
    LOS_SpinLock(&g_swtmrSpin);

    if (LOS_ListEmpty(listObject)) {
        LOS_SpinUnlock(&g_swtmrSpin);
        return;
    }
    sortList = LOS_DL_LIST_ENTRY(listObject->pstNextSortLinkListsortLinkNode);
    ROLLNUM_DEC(sortList->idxRollNum);

    while (ROLLNUM(sortList->idxRollNum) == 0) {
        sortList = LOS_DL_LIST_ENTRY(listObject->pstNextSortLinkListsortLinkNode);
        LOS_ListDelete(&sortList->sortLinkNode);
        swtmr = LOS_DL_LIST_ENTRY(sortListSWTMR_CTRL_SstSortList);

        swtmrHandler = (SwtmrHandlerItemPtr)LOS_MemboxAlloc(g_swtmrHandlerPool);//取出一个可用的软时钟处理项
        if (swtmrHandler != NULL) {
            swtmrHandler->handler = swtmr->pfnHandler;
            swtmrHandler->arg = swtmr->uwArg;

            if (LOS_QueueWrite(OsPercpuGet()->swtmrHandlerQueueswtmrHandlersizeof(CHAR *), LOS_NO_WAIT)) {
                (VOID)LOS_MemboxFree(g_swtmrHandlerPoolswtmrHandler);
            }
        }

        if (swtmr->ucMode == LOS_SWTMR_MODE_ONCE) {
            OsSwtmrDelete(swtmr);

            if (swtmr->usTimerID < (OS_SWTMR_MAX_TIMERID - LOSCFG_BASE_CORE_SWTMR_LIMIT)) {
                swtmr->usTimerID += LOSCFG_BASE_CORE_SWTMR_LIMIT;
            } else {
                swtmr->usTimerID %= LOSCFG_BASE_CORE_SWTMR_LIMIT;
            }
        } else if (swtmr->ucMode == LOS_SWTMR_MODE_NO_SELFDELETE) {
            swtmr->ucState = OS_SWTMR_STATUS_CREATED;
        } else {
            swtmr->ucOverrun++;
            OsSwtmrStart(swtmr);
        }

        if (LOS_ListEmpty(listObject)) {
            break;
        }

        sortList = LOS_DL_LIST_ENTRY(listObject->pstNextSortLinkListsortLinkNode);
    }

    LOS_SpinUnlock(&g_swtmrSpin);
}

解读

  • OsSwtmrScan 函数是在系统时钟处理函数 OsTickHandler 中调用的,它就干一件事,不停的比较定时器是否超时
  • 一旦超时就把定时器的回调函数扔到队列中,让 OsSwtmrTask去消费。

总结

  • 定时器池 g_swtmrCBArray 存储内核所有的定时器,默认1024个,各CPU共享这个池
  • 定时器响应函数池g_swtmrHandlerPool 存储内核所有的定时器响应函数,默认1024个,各CPU也共享这个池
  • 每个CPU核都有独立的任务(线程)来处理定时器, 这个任务叫定时任务
  • 每个CPU核都有独立的响应函数队列swtmrHandlerQueue,队列中存放该核时间到了的响应函数SwtmrHandlerItem
  • 定时任务的优先级最高,循环读取队列swtmrHandlerQueueswtmrHandlerQueue中存放是定时器时间到了的响应函数。并一一回调这些响应函数。
  • OsSwtmrScan负责扫描定时器的时间是否到了,到了就往队列swtmrHandlerQueue中扔。
  • 定时器有多种模式,包括单次,循环。所以循环类定时器的响应函数会多次出现在swtmrHandlerQueue中。

百文说内核 | 抓住主脉络

  • 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。
  • 与代码需不断debug一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。
  • 百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,百篇博客系列目录如下。

按功能模块:

基础知识 进程管理 任务管理 内存管理
双向链表 内核概念 源码结构 地址空间 计时单位 优雅的宏 钩子框架 位图管理 POSIX main函数 调度故事 进程控制块 进程空间 线性区 红黑树 进程管理 Fork进程 进程回收 Shell编辑 Shell解析 任务控制块 并发并行 就绪队列 调度机制 任务管理 用栈方式 软件定时器 控制台 远程登录 协议栈 内存规则 物理内存 内存概念 虚实映射 页表管理 静态分配 TLFS算法 内存池管理 原子操作 圆整对齐
通讯机制 文件系统 硬件架构 内核汇编
通讯总览 自旋锁 互斥锁 快锁使用 快锁实现 读写锁 信号量 事件机制 信号生产 信号消费 消息队列 消息封装 消息映射 共享内存 文件概念 文件故事 索引节点 VFS 文件句柄 根文件系统 挂载机制 管道文件 文件映射 写时拷贝 芯片模式 ARM架构 指令集 协处理器 工作模式 寄存器 多核管理 中断概念 中断管理 编码方式 汇编基础 汇编传参 链接脚本 内核启动 进程切换 任务切换 中断切换 异常接管 缺页中断
编译运行 调测工具
编译过程 编译构建 GN语法 忍者无敌 ELF格式 ELF解析 静态链接 重定位 动态链接 进程映像 应用启动 系统调用 VDSO 模块监控 日志跟踪 系统安全 测试用例

百万注源码 | 处处扣细节

  • 百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。

  • < gitee | github | coding | gitcode > 四大码仓推送 | 同步官方源码。

关注不迷路 | 代码即人生

期间不断得到小伙伴的支持,有学生,有职场新人,也有老江湖,在此一并感谢,大家的支持是前进的动力。尤其每次收到学生的赞助很感慨,后生可敬。 >> 查看捐助名单

据说喜欢 点赞 + 分享 的,后来都成了大神。:)

Clone this wiki locally