物联网操作系统的任务调度初步认识

随风

任务调度是操作系统的基本功能。任务调度使得用户程序感觉自己在独占cpu。但是cpu是有限的,任务又那么多,操作系统是如何做到合理分配cpu时间的呢。其实rtos基本上是根据优先级抢占式调度,然后如果任务优先级相同就按照时间轮转片调度。

时间轮转片算法并不难,然而我阅读了大量的时间轮转片介绍的博客始终没有具体的硬件实现介绍。大部分博客都是仿真实现,这其实和真实的内核实现有着很大的差距。因为仿真切换你根本没法实现硬件中断机制,而真实的内核是利用中断机制来实现任务调度的。

为什么内核要使用中断机制而不是直接像普通程序那样编写。这其实非常显然,内核调度肯定占用的资源越低越好,实时性越高越好。而中断机制就能提供这样的属性。中断机制可以说是计算机最具革命性的一个设计,它大大提高了计算机系统的响应速度。

当看到利用中断机制的介绍时候我脑海里已经浮现了一种内核调度的实现方法。利用systick中断,systick中断实现流水灯相信大家都曾经编写过,因此想到利用systick来进行任务切换是一种非常自然的想法,只要设置好中断的时间间隔,那么就实现了时间片的设计。事实上tos系统同样利用了systick中断。下面是tos系统时基的入口。

void SysTick_Handler(void)
{
	if (tos_knl_is_running())
  {
    tos_knl_irq_enter();
    tos_tick_handler();
    tos_knl_irq_leave();
  }
}

但是仅仅利用systick中断肯定是要出问题的。因为单片机还要处理其它中断,在Cortex-M3中,如果OS在某个中断活跃时,抢占了该中断,而且又发生了任务调度,并执行了任务,切换到了线程运行模式,将直接触发Fault异常。这肯定是不行的,但你肯定会说不如直接把systick设置为最低优先级不就行了吗?当然可以,异常确实不会发生了,然而你仔细想想会发生什么?

仔细想好像也没问题啊。的确很难想到,但是我们处理中断的流程是怎么样的?第一步是关中断,然后保存现场再开中断,执行程序,最后再关中断,恢复现场然后开中断。这样做就是为了现场不被破坏同时保证更高优先级的中断能够正常被响应。一般OS在调度任务时,也是要关闭中断,也就是进入临界区,而OS任务调度是要耗时的,这就会出现一种情况:
在任务调度期间,如果新的外部中断发生,CPU将不能够快速响应处理。这对于智能驾驶这样的场景你能忍受吗?

到了这里你似乎走到了穷途末路。的确只利用systick在软件层面已经没有更好的办法了。但是狡猾的内核大佬利用了一个叫做pendsv的中断,这个中断很奇怪,它能够像普通外设中断一样悬起。它是系统级别的异常,但它又天生支持缓期执行。有了它我们就能轻松避免了不能快速处理的问题,pendsv会自动等待所有的中断处理完毕再进行任务切换的操作。

到了这里我们已经得到了一种较为成熟的os调度方案。

  1. 滴答定时器中断,制作业务调度前的判断工作,不做任务切换。
  2. 触发PendSV,PendSV并不会立即执行,因为PendSV的优先级最低,如果此时正好有中断请求,那么先响应中断,最后等到所有优先级高于PendSV的中断都执行完毕,再执行PendSV,进行任务调度。

那么tos系统也是这样实现的吗?我们看看源码。那么我们要启动tos系统,入口肯定要调用启动的api,源代码api头文件注释是get the kernel start to run, which means start the multitask scheduling.中文意思就是启动内核,意味着启动多任务调度。

__API__ k_err_t tos_knl_start(void)
{
    if (tos_knl_is_running()) {
        return K_ERR_KNL_RUNNING;
    }

    k_next_task = readyqueue_highest_ready_task_get();
    k_curr_task = k_next_task;
    k_knl_state = KNL_STATE_RUNNING;
    cpu_sched_start();

    return K_ERR_NONE;
}

可以看到前几行都是检测和获取任务的一些信息,重点在于 cpu_sched_start() ,这个是用来启动cpu任务调度的。刚才那个加了宏_api_表明它还是属于用户api,下面正式进入内核世界。

可奇怪的是它的实现c文件很短,就又调用了一个函数 port_sched_start 。

__KERNEL__ void cpu_sched_start(void)
{
    port_sched_start();
}

继续前进已经没有c文件了,迎面而来的是汇编语言。我熟悉8051汇编,但是对于arm的汇编,我并不是很熟悉,但是又不是考试,还是可以查手册的。粗略看了看,大概知道这是一段很重要的代码。

NVIC_INT_CTRL   EQU     0xE000ED04                              ; Interrupt control state register.
NVIC_SYSPRI14   EQU     0xE000ED20                              ; System priority register (priority 14).
NVIC_PENDSV_PRI EQU     0x00FF0000                              ; PendSV priority value (lowest).
NVIC_PENDSVSET  EQU     0x10000000                              ; Value to trigger PendSV exception.

我们看一下注释,这不就是设置pendsv为设置为最低优先级吗,到这里我们还不清楚它与systick的关系,但至少已经证实它肯定利用了pendsv。幸运的是我滚动一下鼠标就发现的

pendsv的中断处理子程序(汇编我们还是不要叫函数吧)

这段代码很复杂,但是只要有点51单片机的功底,还是猜得出它在干什么,其实就是操作系统最重要的工作,任务堆栈的切换。它借用了寄存器实现的现场的保护。至此我们完全证明tos系统利用pendsv进行上下文切换。

我们还看到这样一段注释

@ set pendsv priority lowest
@ otherwise trigger pendsv in port_irq_context_switch will cause a context swich in irq
@ that would be a disaster

翻译过来跟我们上面描述的完全一致。

我们再去找systick,得到了这个函数

__API__ void tos_tick_handler(void)
{
    if (unlikely(!tos_knl_is_running())) {
        return;
    }

    tick_update((k_tick_t)1u);

#if TOS_CFG_TIMER_EN > 0u && TOS_CFG_TIMER_AS_PROC > 0u
    timer_update();
#endif

#if TOS_CFG_ROUND_ROBIN_EN > 0u
    robin_sched(k_curr_task->prio);
#endif
}

unlikely这是内核的一种优化函数,暂时不用管它,我们全力看 robin_sched ()函数。

__KERNEL__ void robin_sched(k_prio_t prio)
{
    TOS_CPU_CPSR_ALLOC();
    k_task_t *task;

    if (k_robin_state != TOS_ROBIN_STATE_ENABLED) {
        return;
    }

    TOS_CPU_INT_DISABLE();

    task = readyqueue_first_task_get(prio);
    if (!task || knl_is_idle(task)) {
        TOS_CPU_INT_ENABLE();
        return;
    }

    if (readyqueue_is_prio_onlyone(prio)) {
        TOS_CPU_INT_ENABLE();
        return;
    }

    if (knl_is_sched_locked()) {
        TOS_CPU_INT_ENABLE();
        return;
    }

    if (task->timeslice > (k_timeslice_t)0u) {
        --task->timeslice;
    }

    if (task->timeslice > (k_timeslice_t)0u) {
        TOS_CPU_INT_ENABLE();
        return;
    }

    readyqueue_move_head_to_tail(k_curr_task->prio);

    task = readyqueue_first_task_get(prio);
    if (task->timeslice_reload == (k_timeslice_t)0u) {
        task->timeslice = k_robin_default_timeslice;
    } else {
        task->timeslice = task->timeslice_reload;
    }

    TOS_CPU_INT_ENABLE();
    knl_sched();
}

从源码中可以看到,时间片调度算法的实现非常简单,当时钟节拍来临的时候,将就绪列表中第一个任务控制块的时间片值递减,如果递减到0,则移到就绪列表的队尾去,让出此次执行机会,内核发生调度。我们重点看 knl_sched()函数。

__KERNEL__ void knl_sched(void)
{
    TOS_CPU_CPSR_ALLOC();

    if (knl_is_inirq()) {
        return;
    }

    if (knl_is_sched_locked()) {
        return;
    }

    TOS_CPU_INT_DISABLE();
    k_next_task = readyqueue_highest_ready_task_get();
    if (knl_is_self(k_next_task)) {
        TOS_CPU_INT_ENABLE();
        return;
    }

    cpu_context_switch();
    TOS_CPU_INT_ENABLE();
}

这个就是检查一下当前是否有中断发生,还有检查一下锁是否释放,然后调用函数 TOS_CPU_INT_DISABLE()关闭中断,调用 cpu_context_switch() 进行上下文切换。不出意外,终点一定是汇编实现。

果然我们已经离开了c语言世界,进入汇编世界。看代码

port_context_switch
    LDR     R0, =NVIC_INT_CTRL
    LDR     R1, =NVIC_PENDSVSET
    STR     R1, [R0]
    BX      LR

这是什么?这是什么?触发pendsv异常,到了这里我们已经破解了tos系统的任务调度器的基本原理。这表明tos系统和freertos的实现机制是大差不差的。

内核是非常复杂的东西,我们一定要充满敬畏,不要随意去修改,但是了解内核还是很有必要的,否则应用层的很多东西我们理解可能都是错误的。在此,真的太佩服太佩服内核大佬了,瞬间觉得应用层开发那点困难不算什么了。

最后来个传送门

物联网操作系统的初步探索

随风

近几年物联网飞速发展,市面上也出现了很多物联网操作系统。比较有名的有rt-thread,tencentos-tiny以及鸿蒙等。这些物联网系统各有差异,这符合物联网碎片化的生态现状。下面我将以tencentos-tiny为例子分析一下目前物联网操作系统的一些特点。

打开tencentos-tiny的官网,映入眼帘的是比较熟悉的代码结构。分别是内核,驱动,网络以及一些辅助的工具,这和大多数的嵌入式操作系统是类似的。

我们再来看看下面这张图。

这张图是tos系统官网给出的架构,可以看出它的模块化设计是非常优秀的。内核只包含了最为重要的特性,即任务调度,时间以及内存管理,以及ipc通讯的组件。而物联网的工具箱则被放置于内核之外。我们用vscode阅读一下它的内核源码。

可以看到它的确非常的精简,每个功能的实现代码有些甚至只有几百行。这样精简的实现,使得它可以轻松移植到stm32f1这样的芯片。

但是如果进行全量移植的话,f1这样的芯片内存空间仍然不是很足,但是tos系统可以轻松的裁剪,移植的体验也非常不错,如果你不需要其它的功能的话,你只需要把kernel和arch目录以及tos_config复制到你的工程就行了,当然你还得进行一些微调,比如系统时钟脉冲设置,但我花了一些时间也就顺利移植成功了。

可能到现在你有个疑问,stm32真的有必要使用操作系统吗?的确,裸机编程能满足大多数场景了,但是如果你想实现实时性很强,任务又非常多的话,使用操作系统的确非常便捷。tos系统采用了抢占式的任务调度。高优先级任务就绪的时候会立即打断低优先级的任务。相同优先级的就是使用时间片调度。这样能保证高优先的任务能优先被处理。tos系统还提供了任务之间通讯的机制,互斥锁,消息队列以及事件机制。这使得基于任务编程成为轻松的事情。否则的话你就只能一个循环加中断或者dma等。这样的cpu利用率以及响应速度都并不能让人非常满意。

tos系统如果仅仅有上述任务调度功能,它就不能称为物联网操作系统,而顶多叫做实时操作系统。tos系统提供了非常强大的网络工具,可以兼容esp8266等众多芯片,提供了一个统一的at指令框架以及各种协议栈的支持。都说抽象是软件的灵魂。tos系统实现了at指令的抽象框架到sal层的抽象框架,从而虚拟出socket接口。有人觉得这不是多此一举吗?这当然不是,实际上这个设计和“一切皆文件”是有异曲同工之妙的。不过由于tos系统是腾讯开发的,它当然要给很便捷的api给自家的iot平台,但这些工具的确方便了物联网的开发。下面引用一下腾讯知乎的一张图,它清晰地阐述了sal接口的好处。

不过说了这么多,sal接口对我的吸引力并没有那么大,实际上我们使用的通讯芯片一般都是确定的一种,并不太可能替换另外一种,但是移植这抽象层占用的flash空间又是真实存在的,因此我倒不如直接at指令来的方便。

物联网操作系统的产生屏蔽了纷繁复杂的物联网硬件,提供了一些较为统一的接口,据说鸿蒙还提供了分布式软总线,但我还没有看过。但是我感觉只靠这样的一个操作系统就能一统江湖,估计还是任重道远。不过我发现了一个有趣的现象,人们正打算在物联网操作系统中提供js引擎。我觉得这个技术未来确实有比较好的前途。我们下次再讨论一下js引擎对于物联网的影响。

最后来个传送门,祝大家元旦快乐。