zsky · 2022年02月08日

RT-Thread快速入门-了解内核启动流程

首发,公众号【一起学嵌入式】,RTOS、Linux、C

内核是操作系统最基础也是最重要的部分。从本文开始进入 RT-Thread 内核相关知识的学习。

首先了解内核的基础知识,对 RT-Thread 内核的设计有个初步的认识。

然后了解一下 RT-Thread 系统启动流程。

内核介绍

下图为 RT-Thread 的内核架构图:

image-20220110201443245.png

内核包括两部分:内核库、实时内核实现。

内核库

为了保证内核能够独立运行,RT-Thread 设计了一套小型的类似 C 库的函数实现子集,根据编译器的不同 C 库的情况会有些不同。

RT-Thread 内核服务库仅提供了内核用到的 C 库函数的实现,为了避免与标准 C 库重名,在这些函数前都会加上 “rt_” 前缀。文件 src/kservice.c 部分函数定义如下:

void *rt_memset(void *s, int c, rt_ubase_t count)

void *rt_memcpy(void *dst, const void *src, rt_ubase_t count)

rt_int32_t rt_memcmp(const void *cs, const void *ct, rt_ubase_t count)

char *rt_strstr(const char *s1, const char *s2)

rt_size_t rt_strlen(const char *s)

rt_int32_t rt_snprintf(char *buf, rt_size_t size, const char *fmt, ...)

rt_int32_t rt_sprintf(char *buf, const char *format, ...)

内核实现

实时内核的实现包括:对象管理、线程管理、调度器、线程间通信、时钟管理、内存管理等等。内核最小的资源占用情况是 3KB ROM,1.2KB RAM。

1. 线程

线程是 RT-Thread 操作系统中最小的调度单位,线程调度算法是基于优先级的全抢占式多线程调度算法。

RT-Thread 支持 256 个线程优先级,也可通过配置文件更改为最大支持 32 个或 8 个线程优先级。 0 优先级代表最高优先级,最低优先级留给空闲线程使用。

RT-Thread 支持创建多个具有相同优先级的线程。相同优先级的线程之间的调度,采用时间片轮转算法,使每个线程都运行设定的时间。

调度器在切换到最高就绪线程的时间是恒定的,系统也不限制线程数量的多少,线程数目只和硬件平台的具体内存相关 。

2.时钟管理

RT-Thread 的时钟管理以时钟节拍为基础,时钟节拍是 RT-Thread 操作系统中最小的时钟单位。

RT-Thread 提供了两类定时器机制:

  • 单次触发定时器。该定时器启动后只会触发一次定时器事件,然后自动停止。
  • 周期触发定时器。这类定时器会周期性地触发定时器事件,直到用户手动停止定时器。

3.线程间同步

RT-Thread 采用信号量、互斥量与事件集实现线程间同步。

线程通过对信号量、互斥量的获取与释放进行同步。线程同步机制支持线程按优先级等待或按先进先出方式获取信号量或互斥量。

线程通过对事件的发送与接收进行同步;事件集支持多事件的 “或触发” 和 “与触发”,适合于线程等待多个事件的情况。

4.线程间通信

RT-Thread 支持邮箱和消息队列等通信机制。

邮箱中一封邮件的长度固定为 4 字节大小;消息队列能够接收不固定长度的消息,并把消息缓存在自己的内存空间中。

5.内存管理

RT-Thread 支持静态内存池管理及动态内存堆管理。

动态内存堆管理模块在系统资源不同的情况下,分别提供了面向小内存系统的内存管理算法及面向大内存系统的 SLAB 内存管理算法。

还有一种动态内存堆管理叫做 memheap,适用于系统含有多个地址且不连续的内存堆。使用 memheap 可以将多个内存堆 “粘贴” 在一起,让用户操作起来像是在操作一个内存堆。

启动过程分析

RT-Thread 的启动流程与其他 RTOS 有所不同。许多 RTOS 启动入口函数为 main,而 RT-Thread 在 main 函数运行之前,系统已经完成了功能初始化。main 函数作为用户程序的入口。

系统先从启动文件开始运行,然后进入 RT-Thread 的启动入口 rtthread_startup() ,最后进入用户入口 main()

以 MDK-ARM 为例,RT-Thread 启动流程,如下图所示:

image-20220110220424242.png

系统启动后,先从汇编代码 startup_xx.s 开始运行,然后跳转到 C 代码,进行 RT-Thread 系统启动,最后进入用户程序入口 main()

1. 扩展 main()

RT-Thread 使用了 MDK 的扩展功能 $Sub$$` 和 `$Super$$,使得 RT-Thread 可以在进入 main() 之前完成系统功能初始化。关于 $Sub$$` 和 `$Super$$ 的扩展功能,可以查看 《Arm Compiler for Embedded Reference Guide Version 6.17》

https://developer.arm.com/documentation/101754/0617/armlink-Reference/Accessing-and-Managing-Symbols-with-armlink/Use-of--Super---and--Sub---to-patch-symbol-definitions?lang=en

指导文档内容原文如下:

image-20220110231409139.png

对于 RT-Thread 中的 main() 函数,给其添加 $Sub$$` 的前缀符号作为一个新功能函数 `$Sub$$main ,在这个函数中添加 RT-Thread 的系统启动,进行一系列的初始化操作。

接着在调用 $Super$$main 转到 main() 函数执行 。这样可以减少用户的工作量,不用去管 main() 之前的系统初始化操作。将主要精力用于完成用户需求的功能模块。

src/components.c 文件中可以看到相关的源码:

extern int $Super$$main(void);
/* re-define main function */
int $Sub$$main(void)
{
  rtthread_startup();
  return 0;
}

$Sub$$main 函数调用了 rtthread_startup() 函数,rtthread_startup() 完成 RT-Thread 的启动。

int rtthread_startup(void)
{
        /* 关闭全局中断 */
    rt_hw_interrupt_disable();

    /* 硬件配置初始化 */
    rt_hw_board_init();

    /* 打印 RT-Thread 版本信息 */
    rt_show_version();

    /* 定时器系统初始化 */
    rt_system_timer_init();

    /* 线程调度器初始化 */
    rt_system_scheduler_init();

#ifdef RT_USING_SIGNALS
    /* 信号初始化 */
    rt_system_signal_init();
#endif

    /* 创建用户主线程 main */
    rt_application_init();

    /* 定时器线程初始化 */
    rt_system_timer_thread_init();

    /* 空闲线程初始化 */
    rt_thread_idle_init();

#ifdef RT_USING_SMP
    rt_hw_spin_lock(&_cpus_lock);
#endif /*RT_USING_SMP*/

    /* 启动调度器 */
    rt_system_scheduler_start();

    /* never reach here */
    return 0;
}

这部分启动代码,大致可以分为四个部分:

  • 初始化与系统相关的硬件;
  • 初始化系统内核对象,例如定时器、调度器、信号;
  • 创建 main 线程,在 main 线程中对各类模块依次进行初始化;
  • 初始化定时器线程、空闲线程,并启动调度器。

2. 进入 main()

rtthread_startup() 函数中调用 rt_application_init() 函数,该函数会创建一个初始化线程,也就是用户线程。

该线程的入口函数为 main_thread_entry(),在这个函数中会调用 $Super$$main(), 进入 main()

/* 系统 main 线程入口函数 */
void main_thread_entry(void *parameter)
{
    extern int main(void);
    extern int $Super$$main(void);
    
#ifdef RT_USING_COMPONENTS_INIT
    /* RT-Thread 组件初始化 */
    rt_components_init();
#endif    
#ifdef RT_USING_SMP
    rt_hw_secondary_cpu_up();
#endif
    /* 调用系统主函数 main() */
#if defined(__CC_ARM) || defined(__CLANG_ARM)
    $Super$$main(); /* for ARMCC. */
#elif defined(__ICCARM__) || defined(__GNUC__)
    main();
#endif
}

自动初始化

RT-Thread 具备自动初始化机制:初始化函数不需要被显示调用,只需要在函数定义处,通过宏定义的方式进行声明,即可在系统启动过程中执行。

RT-Thread 的自动初始化机制使用了自定义 RTI 符号段,宏定义将需要在启动时进行初始化的函数指针放到该段中,形成一张初始化函数表。在系统启动过程中会遍历该表,并调用表中的函数,达到自动初始化的目的。

示例代码:

int rt_hw_usart_init(void) /* 串 口 初 始 化 函 数 */
{
  ... ...
  /* 注 册 串 口 1 设 备 */
  rt_hw_serial_register(&serial1, "uart1",
  RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX,
  uart);
  return 0;
}
INIT_BOARD_EXPORT(rt_hw_usart_init); /* 使 用 组 件 自 动 初 始 化 机 制 */

上述代码中 rt_hw_usart_init() 会被系统自动调用。

在系统启动框图中,有两个函数 rt_components_board_init()rt_components_init(),其后的带底色方框内部的函数表示被自动初始化的函数:

初始化序号函数函数声明的宏描述
1board init functionsINIT_BOARD_EXPORT(fn)非常早期的初始化,此时调度器还未启动
2pre-initialization functionsINIT_PREV_EXPORT(fn)主要是用于纯软件的初始化、没有太多依赖的函数
3device init functionsINIT_DEVICE_EXPORT(fn)外设驱动初始化相关,比如网卡设备
4components init functionsINIT_COMPONENT_EXPORT(fn)组件初始化,比如文件系统或者 LWIP
5enviroment init functionsINIT_ENV_EXPORT(fn)系统环境初始化,比如挂载文件系统
6application init functionsINIT_APP_EXPORT(fn)应用初始化,比如 GUI 应用

rt_components_board_init() 函数会遍历通过 INIT_BOARD_EXPORT(fn) 申明的初始化函数表,并调用各个函数,主要初始化硬件环境,其函数代码如下:

void rt_components_board_init(void)
{
  const init_fn_t *fn_ptr;
  for (fn_ptr = &__rt_init_rti_board_start; fn_ptr < &__rt_init_rti_board_end; fn_ptr++)
  {
      (*fn_ptr)();
  }
}

rt_components_init() 函数会系统运行起来之后,在 main 线程中被调用。此时硬件环境和操作系统都已经初始化完成,可以执行应用代码。rt_components_init() 会遍历剩下的几个宏声明的初始化函数表。

void rt_components_init(void)
{
  const init_fn_t *fn_ptr;

  for (fn_ptr = &__rt_init_rti_board_end; fn_ptr < &__rt_init_rti_end; fn_ptr ++)
  {
    (*fn_ptr)();
  }
}

上述两端代码中 __rt_init_rti_board_start__rt_init_rti_board_end、和 __rt_init_rti_end 是通过RT-Thread 内部宏定义实现的,各个宏定义汇总如下:

#define INIT_EXPORT(fn, level) \
            RT_USED const init_fn_t __rt_init_##fn SECTION(".rti_fn." level) = fn

/* board init routines will be called in board_init() function */
#define INIT_BOARD_EXPORT(fn)           INIT_EXPORT(fn, "1")

/* components pre-initialization (pure software initilization) */
#define INIT_PREV_EXPORT(fn)            INIT_EXPORT(fn, "2")
/* device initialization */
#define INIT_DEVICE_EXPORT(fn)          INIT_EXPORT(fn, "3")
/* components initialization (dfs, lwip, ...) */
#define INIT_COMPONENT_EXPORT(fn)       INIT_EXPORT(fn, "4")
/* environment initialization (mount disk, ...) */
#define INIT_ENV_EXPORT(fn)             INIT_EXPORT(fn, "5")
/* appliation initialization (rtgui application etc ...) */
#define INIT_APP_EXPORT(fn)             INIT_EXPORT(fn, "6")

static int rti_start(void)
{
    return 0;
}
INIT_EXPORT(rti_start, "0");

/* 实现 __rt_init_rti_board_start */
static int rti_board_start(void)
{
    return 0;
}
INIT_EXPORT(rti_board_start, "0.end");

/* 实现 __rt_init_rti_board_end */
static int rti_board_end(void)
{
    return 0;
}
INIT_EXPORT(rti_board_end, "1.end");

/* 实现 __rt_init_rti_end */
static int rti_end(void)
{
    return 0;
}
INIT_EXPORT(rti_end, "6.end");

自动初始化机制中宏定义的核心部分为 INIT_EXPORT,这个宏定义的涉及到的一些宏定义如下:

#define SECTION(x)                  __attribute__((section(x)))
/* 该宏的作用是向编译器说明这段代码有用 */
#define RT_USED                     __attribute__((used))

宏定义中,两个符号 ## 用于将两个字符串进行拼接。宏定义 INIT_EXPORT将 字符串__rt_init_ 和 函数名字符串 fn 进行拼接。

section 关键字可以将变量定义到指定的输入段中。宏定义会将__rt_init_fn 放到指定的段中。

OK,今天先到这,下次继续。加油~

公众号【一起学嵌入式】,专注分享 RTOS、Linux、C
推荐阅读
关注数
2392
内容数
31
公众号【一起学嵌入式】专注嵌入式软件技术分享,RTOS、Linux、C/C++,一起学习,一起进步。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息