参考官方文档:https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/basic/basic
1 目录 RT-Thread 简介 1.内核 2.RT-Thread 启动流程 3.程序内存分布 4.内核对象模型 5.线程管理 6.时钟管理 7.线程间同步 8.线程间通信 9.内存管理 10.中断管理 问题与思考
2 嵌入式操作系统分类(实时和分时) 2.1 实时操作系统 分为硬实时和软实时,区别在于实时的保证程度 硬实时:规定操作在有限时间内完成,不允许超出时限的错误。超时错误会带来损害甚至导致系统失效 软实时:可以容忍偶然的超时错误,超时造成的后果不严重
例如,有的应用要求系统在 95%的情况下都会确保在规定的时间内完成某个动作,而不一定要求 100%。在许多情况下,这样的“软性”正确率已经可以达到用户期望的水平。
实时操作系统一般基于优先级抢占式机制,可以确保高优先级的任务在任何情况下都能够优先执行,从而保证实时性 如 RT-Thread,VxWorks,FreeRTOS
2.2 非实时操作系统(分时) 分时操作系统基于时间片轮转调度算法 ,旨在为所有用户(任务)公平的提供时间片,所有任务按照既定的时间片轮流执行,无法响应突发的事件
3 RT-Thread 简介 具体包括以下部分:
+ 内核层 :RT-Thread 内核,是 RT-Thread 的核心部分,包括了内核系统中对象的实现,例如多线程及其调度、信号量、邮箱、消息队列、内存管理、定时器等;libcpu/BSP(芯片移植相关文件 / 板级支持包)与硬件密切相关,由外设驱动和 CPU 移植构成。 + 组件与服务层 :组件是基于 RT-Thread 内核之上的上层软件,例如虚拟文件系统、FinSH 命令行界面、 + 网络框架、设备框架等。采用模块化设计,做到组件内部高内聚,组件之间低耦合。
4 内核 - 内核的组成部分、系统如何启动、内存分布情况以及内核配置方法
+ 内核库 是为了保证内核能够独立运行的一套小型的类似 C 库的函数实现子集。 + 实时内核 的实现包括:对象管理、线程管理及调度器、线程间通信管理、时钟管理及内存管理等等,内核最小的资源占用情况是 3 KB ROM,1.2 KB RAM。
线程调度 :系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的,包括线程调度器自身。
时钟管理 :时钟节拍是 RT-Thread 操作系统中最小的时钟单位。通常使用定时器定时回调函数(即超时函数),完成定时服务。
线程间同步 :RT-Thread 采用信号量、互斥量与事件集实现线程间同步。
线程间通信 :支持邮箱和消息队列等通信机制。邮件的长度固定为 4 字节大小;消息队列能够接收不固定长度的消息,并把消息缓存在自己的内存空间中。邮箱效率较消息队列更为高效。 内存管理 :支持静态内存池管理及动态内存堆管理。 I/O 设备管理 :将 PIN、I 2 C、SPI、USB、UART 等作为外设设备,统一通过设备注册完成。当设备事件触发时,由驱动程序通知给上层的应用程序。
5 RT-Thread 启动流程
具体启动流程见 [[RTT启动过程]] rtthread_startup () 函数是 RT-Thread 规定的统一启动入口。
1 MDK 的扩展功能 $Sub$$ 和 $Super$$
1 2 3 startup_stm32f103xe.s 开始-> components.c 根据宏跳转到对应函数,如MDK: int $Sub$$main(void) ->int rtthread_startup(void) rtthread_startup:关闭中断 ->板级初始化 -> 打印版本信息-> 定时器初始化->调度器初始化 -> 信号初始化->创建初始化线程(main线程) -> 定时器线程初始化 -> 空闲线程初始化->启动调度器。
启动调度器之前,系统所创建的线程在执行 rt_thread_startup () 后并不会立马运行,它们会处于就绪状态等待系统调度
6 程序内存分布 MCU 包含的存储空间有:片内 Flash 与片内 RAM。Program Size 包含以下几个部分 1)Code :代码段,存放程序的代码部分; 2)RO-data :只读数据段,存放程序中定义的常量; 3)RW-data :读写数据段,存放初始化为非 0 值的全局变量; 4)ZI-data :0 数据段,存放未初始化的全局变量及初始化为 0 的变量 . map 的文件,说明了各个函数占用的尺寸和地址 1)RO Size 包含了 Code 及 RO-data,表示程序占用 Flash 空间的大小; 2)RW Size 包含了 RW-data 及 ZI-data,表示运行时占用的 RAM 的大小; 3)ROM Size 包含了 Code、RO-data 以及 RW-data,表示烧写程序所占用的 Flash 空间的大小;
6.1 烧写过程 STM 32 在上电启动之后默认从 Flash 启动,启动之后会将 RW 段中的 RW-data(初始化的全局变量)搬运到 RAM 中 ,但不会搬运 RO 段,即 CPU 的执行代码从 Flash 中读取,另外根据编译器给出的 ZI 地址和大小分配出 ZI 段 ,并将这块 RAM 区域清零。动态内存堆 为未使用的 RAM 空间,应用程序申请和释放的内存块都来自该空间。 一些全局变量则是存放于 RW 段和 ZI 段中,RW 段存放的是具有初始值的全局变量 (而常量形式的全局变量 则放置在 RO 段中,是只读属性的),ZI 段存放的系统未初始化的全局变量
总结 :程序烧录时,代码和部分数据都被烧写到 Flash 中。当 MCU 运行时,全局变量部分被移动到 RAM 中。其他的全局变量直接在 RAM 中生成,剩余的 RAM 空间用作动态内存堆,详细存储说明见[[ARM 存储机制]]
6.2 RT-Thread 自动初始化机制 自动初始化机制是指初始化函数不需要被显式调用 ,只需要在函数定义处通过宏定义 的方式进行声明 ,就会在系统启动过程中被执行 。 在哪里被调用的呢?
1 2 3 4 5 6 1 INIT_BOARD_EXPORT(fn) 非常早期的初始化,此时**调度器还未启动** 2 INIT_PREV_EXPORT(fn) 主要是用于纯软件的初始化、**没有太多依赖的函数** 3 INIT_DEVICE_EXPORT(fn) 外设驱动初始化相关,比如网卡设备 4 INIT_COMPONENT_EXPORT(fn) 组件初始化,比如文件系统或者 LWIP 5 INIT_ENV_EXPORT(fn) 系统环境初始化,比如挂载文件系统 6 INIT_APP_EXPORT(fn) 应用初始化,比如 GUI 应用
自动初始化机制详见[[自动初始化]]
7 内核对象模型 RT-Thread 内核采用面向对象的设计思想进行设计,系统级的基础设施都是一种内核对象,例如线程,信号量,互斥量,定时器等。内核对象分为两类:静态内核对象和动态内核对象,静态内核对象 通常放在 RW 段和 ZI 段 中,在系统启动后在程序中初始化 ;动态内核对象 则是从内存堆中创建的,而后手工做初始化 。如:静态线程对象 thread 1 ,线程控制块 thread 1 与栈空间 thread 1_stack 都是编译时决定的。thread 2 是一个动态线程对象运行中用到的空间都是动态分配的 静态对象 会占用 RAM 空间,不依赖于内存堆管理器,内存分配时间确定。动态对象 则依赖于内存堆管理器,运行时申请 RAM 空间,当对象被删除后,占用的 RAM 空间被释放。
7.1 内核对象管理架构 RT-Thread 采用内核对象管理系统来访问 / 管理所有内核对象。不依赖于具体的内存分配方式。 RT-Thread 内核对象包括:线程,信号量,互斥量,事件,邮箱,消息队列和定时器,内存池,设备驱动等。对象容器中包含了每类内核对象的信息,包括对象类型,大小等。对象容器给每类内核对象分配了一个链表,所有的内核对象都被链接到该链表上 RT-Thread 中各类内核对象的派生和继承关系。对于每一种具体内核对象和对象控制块,除了基本结构外,还有自己的扩展属性(私有属性)。每一种具体对象是抽象对象的派生,继承了基本对象的属性并在此基础上扩展了与自己相关的属性。 在对象管理模块 中,定义了通用的数据结构,用来保存各种对象的共同属性,各种具体对象只需要在此基础上加上自己的某些特别的属性,就可以清楚的表示自己的特征。由对象控制块 rt_object 派生出来的有:线程对象、内存池对象、定时器对象、设备对象和 IPC 对象(IPC:进程间通信。此处作用是进行线程间同步与通信);由 IPC 对象派生出信号量、互斥量、事件、邮箱与消息队列、信号等对象。
7.2 对象控制块 1 2 3 4 5 6 7 8 9 10 11 12 struct rt_object //内核对象控制块的数据结构: { char name[RT_NAME_MAX]; rt_uint8_t type; rt_uint8_t flag; rt_list_t list ; }; type ->>>>> enum rt_object_class_type
如果是静态对象,那么对象类型的最高位将是 1,否则就是动态对象,系统最多能够容纳的对象类别数目是 127 个。
7.3 内核对象管理方式 1 2 3 4 5 6 7 8 9 struct rt_object_information { enum rt_object_class_type type ; rt_list_t object_list; rt_size_t object_size; };
一类对象由一个 rt_object_information 结构体来管理,每一个这类对象的具体实例都通过链表的形式挂接在 object_list 上。而这一类对象的内存块尺寸由 object_size 标识出来, 每一类对象的具体实例,他们占有的内存块大小都是相同的
7.3.1 初始化(静态)对象 1 2 3 4 5 6 void rt_object_init (struct rt_object* object ,enum rt_object_class_type type ,const char * name)
7.3.2 脱离(静态)对象 调用该接口,可使得一个静态内核对象从内核对象容器中脱离出来,即从内核对象容器链表上删除相应的对象节点。对象脱离后,对象占用的内存并不会被释放。
1 void rt_object_detach (rt_object_t object) ;
7.3.3 分配(动态)对象 动态的对象则可以在需要时申请,不需要时释放出内存空间给其他应用使用。
1 2 3 rt_object_t rt_object_allocate (enum rt_object_class_type type ,const char * name)
7.3.4 删除(动态)对象 1 void rt_object_delete (rt_object_t object) ;
7.3.5 辨别 (静态/动态)对象 1 rt_err_t rt_object_is_systemobject (rt_object_t object) ;
7.4 RT-Thread 内核配置示例 通过修改工程目录下的 rtconfig. h 文件来进行,在实际应用中,系统配置文件 rtconfig. h 是由配置工具自动生成的,无需手动更改。
7.4.1 . RT-Thread 内核部分: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #define RT_NAME_MAX 8 #define RT_ALIGN_SIZE 4 #define RT_THREAD_PRIORITY_MAX 32 #define RT_TICK_PER_SECOND 100 #define RT_USING_OVERFLOW_CHECK #define RT_DEBUG #define RT_DEBUG_INIT 0 #define RT_DEBUG_THREAD 0 #define RT_USING_HOOK #define IDLE_THREAD_STACK_SIZE 256
7.4.2 . 线程间同步与通信部分 1 2 3 4 5 6 7 8 9 10 11 12 #define RT_USING_SEMAPHORE #define RT_USING_MUTEX #define RT_USING_EVENT #define RT_USING_MAILBOX #define RT_USING_MESSAGEQUEUE #define RT_USING_SIGNALS
7.4.3 . 内存管理部分 1 2 3 4 5 6 7 8 9 10 #define RT_USING_MEMPOOL #define RT_USING_MEMHEAP #define RT_USING_SMALL_MEM #define RT_USING_HEAP
7.4.4 . 内核设备对象 1 2 3 4 5 6 7 8 #define RT_USING_DEVICE #define RT_USING_CONSOLE #define RT_CONSOLEBUF_SIZE 128 #define RT_CONSOLE_DEVICE_NAME "uart1"
7.4.5 . 自动初始化方式 1 2 3 4 5 6 #define RT_USING_COMPONENTS_INIT #define RT_USING_USER_MAIN #define RT_MAIN_THREAD_STACK_SIZE 2048
7.4.6 . FinSH MSH: 指令描述功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #define RT_USING_FINSH #define FINSH_THREAD_NAME "tshell" #define FINSH_USING_HISTORY #define FINSH_HISTORY_LINES 5 #define FINSH_USING_SYMTAB #define FINSH_THREAD_PRIORITY 20 #define FINSH_THREAD_STACK_SIZE 4096 #define FINSH_CMD_SIZE 80 #define FINSH_USING_MSH #define FINSH_USING_MSH_DEFAULT #define FINSH_USING_MSH_ONLY
7.4.7 . 关于 MCU 1 2 3 4 5 6 #define STM32F103ZE #define RT_HSE_VALUE 8000000 #define RT_USING_UART1
7.4.8 -8. 常见宏定义说明 1 2 3 4 5 6 7 8 9 10 11 12 #define rt_inline static __inline #define RT_USED __attribute__((used)) #define RT_UNUSED __attribute__((unused)) #define RT_WEAK __weak #define ALIGN(n) __attribute__((aligned(n))) #define RT_ALIGN(size, align) (((size) + (align) - 1) & ~((align) - 1))
8 线程管理 ^3b547f
例如让嵌入式系统执行这样的任务,系统通过传感器采集数据,并通过显示屏将数据显示出来,在多线程实时系统中,可以将这个任务分解成两个子任务,如下图所示,一个子任务不间断地读取传感器数据,并将数据写到共享内存中,另外一个子任务周期性的从共享内存中读取数据,并将传感器数据输出到显示屏上。 线程是实现任务的载体,它是 RT-Thread 中最基本的调度单位,它描述了一个任务执行的运行环境,也描述了这个任务所处的优先等级. 线程运行时,它会认为自己是以独占 CPU 的方式在运行,线程执行时的运行环境称为上下文,具体来说就是各个变量和数据,包括所有的寄存器变量、堆栈、内存信息等。
8.1 线程管理的功能特点 线程管理的主要功能是对线程进行管理和调度,系统中总共存在两类线程,分别是系统线程 和用户线程 ,系统线程是由 RT-Thread 内核创建的线程,用户线程是由应用程序创建的线程 每个线程都有重要的属性,如线程控制块、线程栈、入口函数等。 RT-Thread 的线程调度器是抢占式的, 当一个运行着的线程使一个比它优先级高的线程满足运行条件,高优先级的线程立刻得到了 CPU 的使用权。 如果是中断服务程序使一个高优先级的线程满足运行条件,中断完成时,被中断的线程挂起,优先级高的线程开始运行。 当调度器调度线程切换时,先将当前线程上下文保存起来,当再切回到这个线程时,线程调度器将该线程的上下文信息恢复。
8.2 线程的工作机制 8.2.1 . 线程控制块 线程控制块由结构体 struct rt_thread 表示,线程控制块是操作系统用于管理线程的一个数据结构,它会存放线程的一些信息,例如优先级、线程名称、线程状态等,也包含线程与线程之间连接用的链表结构,线程等待事件集合等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 struct rt_thread { char name[RT_NAME_MAX]; rt_uint8_t type; rt_uint8_t flags; rt_list_t list ; rt_list_t tlist; void *sp; void *entry; void *parameter; void *stack_addr; rt_uint32_t stack_size; rt_err_t error; rt_uint8_t stat; rt_uint8_t current_priority; rt_uint8_t init_priority; rt_uint32_t number_mask; rt_ubase_t init_tick; rt_ubase_t remaining_tick; struct rt_timer thread_timer ; void (*cleanup)(struct rt_thread *tid); rt_uint32_t user_data; };
8.2.2 . 线程重要属性 线程栈 : RT-Thread 线程具有独立的栈,当进行线程切换时,会将当前线程的上下文存在栈中 还用来存放函数中的局部变量 ,函数中局部变量初始时从寄存器中分配(ARM 架构),当这个函数再调用另一个函数时,这些局部变量将放入栈中。 对于线程第一次运行,可以以手工的方式构造这个上下文来设置一些初始的环境:入口函数(PC 寄存器)、入口参数(R0 寄存器)、返回位置(LR 寄存器)、当前机器运行状态(CPSR 寄存器)。 线程栈大小设定 :对于资源相对较大的 MCU,可以适当设计较大的线程栈;也可以在初始时设置较大的栈,例如指定大小为 1 K 或 2 K 字节,然后在 FinSH 中用 list_thread 命令查看线程运行的过程中线程所使用的栈的大小,通过此命令,能够看到从线程启动运行时,到当前时刻点,线程使用的最大栈深度,而后加上适当的余量形成最终的线程栈大小,最后对栈空间大小加以修改。 线程状态 :
1 2 3 4 5 **1. 初始状态** 当线程刚开始创建还没开始运行时就处于初始状态;在初始状态下,线程不参与调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_INIT **2. 就绪状态** 在就绪状态下,线程按照优先级排队,等待被执行;一旦当前线程运行完毕让出处理器,操作系统会马上寻找最高优先级的就绪态线程运行。此状态在 RT-Thread 中的宏定义为 RT_THREAD_READY **3. 运行状态** 线程当前正在运行。在单核系统中,只有 rt_thread_self() 函数返回的线程处于运行状态;在多核系统中,可能就不止这一个线程处于运行状态。此状态在 RT-Thread 中的宏定义为 RT_THREAD_RUNNING **4. 挂起状态** 也称阻塞态。它可能因为资源不可用而挂起等待,或线程主动延时一段时间而挂起。在挂起状态下,线程不参与调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_SUSPEND **5. 关闭状态** 当线程运行结束时将处于关闭状态。关闭状态的线程不参与线程的调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_CLOSE
RT-Thread 线程的优先级是表示线程被调度的优先程度。数值越小的优先级越高 时间片 : 系统对优先级相同的就绪态线程采用时间片轮转的调度方式进行调度时,时间片起到约束线程单次运行时长的作用,其单位是一个系统节拍(OS Tick) 线程的入口函数 : entry 是线程的入口函数,它是线程实现预期功能的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 般有以下两种代码形式: -无限循环模式: void thread_entry (void * paramenter) { while (1 ) { 线程中不能陷入死循环操作,必须要有让出 CPU 使用权的动作,如循环中调用延时函数或者主动挂起。用户设计这种无限循环的线程的目的,就是为了让这个线程一直被系统循环调度运行,永不删除。 } } -顺序执行或有限次循环模式: 在执行完毕后,线程将被系统自动删除。 static void thread_entry (void * parameter) {…… }
线程错误码 :
1 2 3 4 5 6 7 8 9 10 11 #define RT_EOK 0 #define RT_ERROR 1 #define RT_ETIMEOUT 2 #define RT_EFULL 3 #define RT_EEMPTY 4 #define RT_ENOMEM 5 #define RT_ENOSYS 6 #define RT_EBUSY 7 #define RT_EIO 8 #define RT_EINTR 9 #define RT_EINVAL 10
8.2.3 . 线程状态切换 线程通过调用函数 rt_thread_create/init () 进入到初始状态(RT_THREAD_INIT);初始状态的线程通过调用函数 rt_thread_startup () 进入到就绪状态(RT_THREAD_READY);就绪状态的线程被调度器调度后进入运行状态(RT_THREAD_RUNNING);当处于运行状态的线程调用 rt_thread_delay (),rt_sem_take (),rt_mutex_take (),rt_mb_recv () 等函数或者获取不到资源时,将进入到挂起状态 (RT_THREAD_SUSPEND);处于挂起状态的线程,如果等待超时依然未能获得资源或由于其他线程释放了资源,那么它将返回到就绪状态。挂起状态的线程,如果调用 rt_thread_delete/detach () 函数,将更改为关闭状态(RT_THREAD_CLOSE);而运行状态的线程,如果运行结束,就会在线程的最后部分执行 rt_thread_exit () 函数,将状态更改为关闭状态。 RT-Thread 中,实际上线程并不存在运行状态 ,就绪状态和运行状态是等同的。
8.2.4 .系统线程 (空闲线程、主线程) 系统线程是指由系统创建的线程,用户线程是由用户程序调用线程管理接口创建的线程,在 RT-Thread 内核中的系统线程有空闲线程和主线程。 空闲线程 系统创建的最低优先级的线程,线程状态永远为就绪态。当系统中无其他就绪线程存在时,调度器将调度到空闲线程,它通常是一个死循环,且永远不能被挂起。 rt_thread_defunct 僵尸队列(资源未回收、处于关闭状态的线程队列)中,最后空闲线程会回收被删除线程的资源。 空闲线程也提供了接口来运行用户设置的钩子函数,在空闲线程运行时会调用该钩子函数,适合钩入功耗管理、看门狗喂狗等 工作。 主线程 在系统启动时,系统会创建 main 线程,它的入口函数为 main_thread_entry ()
8.3 线程的管理方式 线程的相关操作 ,包含:创建 / 初始化线程、启动线程、运行线程、删除 / 脱离线程。动态线程与静态线程的区别是:动态线程是系统自动从动态内存堆上分配栈空间与线程句柄(初始化 heap 之后才能使用 create 创建动态线程),静态线程是由用户分配栈空间与线程句柄。
8.3.1 . 创建和删除线程 动态线程:仅在使能了系统动态堆时才有效(即 RT_USING_HEAP 宏定义已经定义了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 rt_thread_t rt_thread_create (const char * name,void (*entry)(void * parameter),void * parameter,rt_uint32_t stack_size,rt_uint8_t priority,rt_uint32_t tick) ; rt_err_t rt_thread_delete (rt_thread_t thread) ;
静态线程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 rt_err_t rt_thread_init (struct rt_thread* thread,const char * name,void (*entry)(void * parameter), void * parameter,void * stack_start, rt_uint32_t stack_size,rt_uint8_t priority, rt_uint32_t tick) ; rt_err_t rt_thread_detach (rt_thread_t thread) ;
8.3.2 启动线程 1 2 3 4 5 6 7 rt_err_t rt_thread_startup (rt_thread_t thread) ;
8.3.3 . 获得当前线程 1 2 3 4 5 6 rt_thread_t rt_thread_self (void ) ;
8.3.4 . 使线程让出处理器资源 1 2 3 4 5 rt_err_t rt_thread_yield (void ) ;
8.3.5 . 使线程睡眠 1 2 3 4 5 6 7 8 9 10 rt_err_t rt_thread_sleep (rt_tick_t tick) ; rt_err_t rt_thread_delay (rt_tick_t tick) ; rt_err_t rt_thread_mdelay (rt_int32_t ms) ;
8.3.6 . 挂起和恢复线程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 rt_err_t rt_thread_suspend (rt_thread_t thread) ; 只能使用本函数来挂起线程自己,不可以在线程A中尝试挂起线程B, 而且在挂起线程自己后,需要立刻调用 rt_schedule() 函数进行手动的线程上下文切换。 不推荐使用该接口。该接口可视为内核内部接口。 rt_err_t rt_thread_resume (rt_thread_t thread) ;
8.3.7 . 控制线程 1 2 3 4 5 6 7 8 9 rt_err_t rt_thread_control (rt_thread_t thread, rt_uint 8 _t cmd, void * arg) ;
1 2 3 4 指示控制命令 cmd 当前支持的命令包括: •RT_THREAD_CTRL_CHANGE_PRIORITY:动态更改线程的优先级; •RT_THREAD_CTRL_STARTUP:开始运行一个线程,等同于 rt_thread_startup () 函数调用; •RT_THREAD_CTRL_CLOSE:关闭一个线程,等同于 rt_thread_delete () 或 rt_thread_detach () 函数调用。
8.3.8 -8. 设置和删除空闲钩子 空闲钩子函数是空闲线程的钩子函数,如果设置了空闲钩子函数,就可以在系统执行空闲线程时,自动执行空闲钩子函数来做一些其他事情,比如系统指示灯。
1 2 3 4 5 6 7 8 9 rt_err_t rt_thread_idle_sethook (void (*hook)(void )) ; rt_err_t rt_thread_idle_delhook (void (*hook)(void )) ; hook 设置的钩子函数 返回 —— RT_EOK 设置/删除 成功 -RT_EFULL 设置失败 -RT_ENOSYS 删除失败
8.3.9 -9. 设置调度器钩子 在整个系统的运行时,系统都处于线程运行、中断触发 - 响应中断、切换到其他线程,甚至是线程间的切换过程中,或者说系统的上下文切换是系统中最普遍的事件。有时用户可能会想知道在一个时刻发生了什么样的线程切换,可以通过调用下面的函数接口设置一个相应的钩子函数。在系统线程切换时,这个钩子函数将被调用:
1 2 3 4 void rt_scheduler_sethook (void (*hook)(struct rt_thread* from, struct rt_thread* to)) ; void hook (struct rt_thread* from, struct rt_thread* to) ; *请仔细编写你的钩子函数,稍有不慎将很可能导致整个系统运行不正常(在这个钩子函数中,基本上不允许调用系统 API,更不应该导致当前运行的上下文挂起)。
8.4 线程应用示例 Keil 模拟器环境下的应用示例: 创建一个动态线程初始化一个静态线程,一个线程在运行完毕后自动被系统删除,另一个线程一直打印计数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <rtthread.h> #define THREAD_PRIORITY 25 #define THREAD_STACK_SIZE 512 #define THREAD_TIMESLICE 5 static rt_thread_t tid 1 = RT_NULL; static void thread 1 _entry (void *parameter) { rt_uint 32_t count = 0 ; while (1 ) { rt_kprintf ("thread 1 count: %d\n" , count ++); rt_thread_mdelay (500 ); } } ALIGN (RT_ALIGN_SIZE) static char thread 2 _stack[1024 ]; static struct rt_thread thread 2; static void thread 2 _entry (void *param) { rt_uint 32_t count = 0 ; for (count = 0 ; count < 10 ; count++) { rt_kprintf ("thread 2 count: %d\n" , count); } rt_kprintf ("thread 2 exit\n" ); } int thread_sample (void ) { tid 1 = rt_thread_create ("thread 1" , thread 1 _entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE); if (tid 1 != RT_NULL) rt_thread_startup (tid 1 ); rt_thread_init (&thread 2 ,"thread 2" , thread 2 _entry, RT_NULL,&thread 2 _stack[0 ], sizeof (thread 2 _stack), THREAD_PRIORITY - 1 , THREAD_TIMESLICE); rt_thread_startup (&thread 2 ); return 0 ; } MSH_CMD_EXPORT (thread_sample, thread sample);
关于删除线程 :大多数线程是循环执行的,无需删除;而能运行完毕的线程,RT-Thread 在线程运行完毕后,自动删除线程,在 rt_thread_exit () 里完成删除动作。用户只需要了解该接口的作用,不推荐使用该接口(可以由其他线程调用此接口或在定时器超时函数中调用此接口删除一个线程,但是这种使用非常少)。 线程调度器钩子示例 : 在线程进行调度切换时,会执行调度,我们可以设置一个调度器钩子,这样可以在线程切换时,做一些额外的事情,这个例子是在调度器钩子函数中打印线程间的切换信息,如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <rtthread.h> #define THREAD_STACK_SIZE 1024 #define THREAD_PRIORITY 20 #define THREAD_TIMESLICE 10 volatile rt_uint 32_t count[2 ]; static void thread_entry (void * parameter) {rt_uint 32_t value; value = (rt_uint 32_t ) parameter; while (1 ){rt_kprintf ("thread %d is running\n" , value); rt_thread_mdelay (1000 ); } static rt_thread_t tid 1 = RT_NULL; static rt_thread_t tid 2 = RT_NULL; static void hook_of_scheduler (struct rt_thread* from, struct rt_thread* to) {rt_kprintf ("from: %s --> to: %s \n" , from->name , to->name); } int scheduler_hook (void ) {rt_scheduler_sethook (hook_of_scheduler);tid 1 = rt_thread_create ("thread 1" , thread_entry, (void *) 1 , THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE); if (tid 1 != RT_NULL) rt_thread_startup (tid 1 );tid 2 = rt_thread_create ("thread 2" , thread_entry, (void *) 2 , THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE - 5 ); if (tid 2 != RT_NULL) rt_thread_startup (tid 2 ); return 0 ; } MSH_CMD_EXPORT (scheduler_hook, scheduler_hook sample);
由仿真的结果可以看出,对线程进行切换时,设置的调度器钩子函数是在正常工作的,一直在打印线程切换的信息,包含切换到空闲线程。
9 时钟管理 9.1 时钟节拍 任何操作系统都需要提供一个时钟节拍,以供系统处理所有和时间有关的事件,如线程的延时、线程的时间片轮转调度以及定时器超时等。时钟节拍是特定的周期性中断,这个中断可以看做是系统心跳,中断之间的时间间隔取决于不同的应用,一般是 1 ms–100 ms,时钟节拍率越快,系统的实时响应越快 ,但是系统的额外开销就越大 ,从系统启动开始计数的时钟节拍数称为系统时间。 RT-Thread 中,时钟节拍的长度可以根据 RT_TICK_PER_SECOND 的定义来调整,等于 1/RT_TICK_PER_SECOND 秒。
9.1.1 . 时钟节拍的实现方式 时钟节拍由配置为中断触发模式的硬件定时器产生,当中断到来时,将调用一次:void rt_tick_increase (void),通知操作系统已经过去一个系统时钟;不同硬件定时器中断实现都不同
1 2 3 4 5 6 7 8 9 void SysTick_Handler (void ) { rt_interrupt_enter (); ……rt_tick_increase (); rt_interrupt_leave (); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void rt_tick_increase (void ) { struct rt_thread *thread ; ++ rt_tick; thread = rt_thread_self (); -- thread->remaining_tick; if (thread->remaining_tick == 0 ) { thread->remaining_tick = thread->init_tick; rt_thread_yield (); } rt_timer_check (); }
中断中的 rt_timer_check () 用于检查系统硬件定时器链表,如果有定时器超时,将调用相应的超时函数。且所有定时器在定时超时后都会从定时器链表中被移除,而周期性定时器会在它再次启动时被加入定时器链表。
9.1.2 . 获取时钟节拍 由于全局变量 rt_tick 在每经过一个时钟节拍时,值就会加 1,通过调用 rt_tick_get 会返回当前 rt_tick 的值,即可以获取到当前的时钟节拍值。此接口可用于记录系统的运行时间长短,或者测量某任务运行的时间。
1 rt_tick_t rt_tick_get (void ) ;
9.2 定时器管理 定时器有硬件定时器和软件定时器之分: 1)硬件定时器 是芯片本身提供的定时功能。一般是由外部晶振提供给芯片输入时钟,芯片向软件模块提供一组配置寄存器,接受控制输入,到达设定时间值后芯片中断控制器产生时钟中断。硬件定时器的精度一般很高,可以达到纳秒级别,并且是中断触发方式。 2)软件定时器 是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受数目限制的定时器服务。 RT-Thread 操作系统提供软件实现的定时器,以时钟节拍(OS Tick)的时间长度为单位,即定时数值必须是 OS Tick 的整数倍,例如一个 OS Tick 是 10 ms,那么上层软件定时器只能是 10 ms,20 ms,100 ms 等,而不能定时为 15 ms。RT-Thread 的定时器也基于系统的节拍,提供了基于节拍整数倍的定时能力。
9.2.1 . RT-Thread 定时器介绍 RT-Thread 的定时器提供两类定时器机制:第一类是单次触发定时器,这类定时器在启动后只会触发一次定时器事件,然后定时器自动停止。第二类是周期触发定时器,这类定时器会周期性的触发定时器事件,直到用户手动的停止 根据超时函数执行时所处的上下文环境,RT-Thread 的定时器可以分为 HARD_TIMER 模式与 SOFT_TIMER 模式
9.2.2 . HARD_TIMER 模式 (定时器默认的方式) 超时函数在中断上下文环境中执行,可以在初始化 / 创建定时器时使用参数 RT_TIMER_FLAG_HARD_TIMER 来指定。 与中断服务例程的要求相同:执行时间应该尽量短,执行时不应导致当前上下文挂起、等待。例如在中断上下文中执行的超时函数它不应该试图去申请动态内存、释放动态内存等。 不能够执行非常长的时间,否则会导致其他中断的响应时间加长或抢占了其他线程执行的时间。
9.2.3 . SOFT_TIMER 模式 (可配置,创建 timer 线程) 通过宏定义 RT_USING_TIMER_SOFT 来决定是否启用该模式。该模式被启用后,系统会在初始化时创建一个 timer 线程,然后 SOFT_TIMER 模式的定时器超时函数在都会在 timer 线程的上下文环境中执行。可以在初始化 / 创建定时器时使用参数 RT_TIMER_FLAG_SOFT_TIMER 来指定设置 SOFT_TIMER 模式。
9.2.4 . 定时器工作机制 在 RT-Thread 定时器模块中维护着两个重要的全局变量 : (1)当前系统经过的 tick 时间 rt_tick(当硬件定时器中断来临时,它将加 1); (2)定时器链表 rt_timer_list。系统新创建并激活的定时器都会按照以超时时间排序的方式插入到 rt_timer_list 链表中。
9.2.5 . 定时器控制块 定时器控制块由结构体 struct rt_timer 定义并形成定时器内核对象,再链接到内核对象容器中进行管理。它是操作系统用于管理定时器的一个数据结构,会存储定时器的一些信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct rt_timer { struct rt_object parent ; rt_list_t row[RT_TIMER_SKIP_LIST_LEVEL]; void (*timeout_func)(void *parameter); void *parameter; rt_tick_t init_tick; rt_tick_t timeout_tick; }; typedef struct rt_timer *rt_timer_t ;
9.2.6 . 定时器跳表 (Skip List) 算法 rt_timer_list 链表是一个有序链表,RT-Thread 中使用了跳表算法来加快搜索链表元素的速度。 跳表是一种基于并联链表的数据结构,实现简单,插入、删除、查找的时间复杂度均为 O (log n)。跳表是链表的一种,但它在链表的基础上增加了 “跳跃” 功能
9.2.7 .定时器的管理方式 (接口函数) ^b6b8c7
1 2 3 4 void rt_system_timer_init (void ) ; void rt_system_timer_thread_init (void ) ;
定时器控制块中含有定时器相关的重要参数,在定时器各种状态间起到纽带的作用。定时器的相关操作如下图所示: 创建和删除定时器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 rt_timer_t rt_timer_create (const char * name, void (*timeout)(void * parameter), void * parameter, rt_tick_t time, rt_uint 8 _t flag) ; name 定时器的名称 void (timeout) (void parameter) 定时器超时函数指针(当定时器超时时,系统会调用这个函数) parameter 定时器超时函数的入口参数(当定时器超时时,调用超时回调函数会把这个参数做为入口参数传递给超时函数) time 定时器的超时时间,单位是时钟节拍 flag 定时器创建时的参数,支持的值包括单次定时、周期定时、硬件定时器、软件定时器等(可以用 “或” 关系取多个值) 返回 —— RT_NULL 创建失败(通常会由于系统内存不够用而返回 RT_NULL) 定时器的句柄 定时器创建成功 "include/rtdef. h" 定时器相关的宏 #define RT_TIMER_FLAG_ONE_SHOT 0 x 0 #define RT_TIMER_FLAG_PERIODIC 0 x 2 #define RT_TIMER_FLAG_HARD_TIMER 0 x 0 #define RT_TIMER_FLAG_SOFT_TIMER 0 x 4 rt_err_t rt_timer_delete (rt_timer_t timer) ; timer 定时器句柄,指向要删除的定时器 返回 —— RT_EOK 删除成功(如果参数 timer 句柄是一个 RT_NULL,将会导致一个 ASSERT 断言)
初始化和脱离定时器
1 2 3 4 5 6 7 8 9 10 void rt_timer_init (rt_timer_t timer, const char * name, void (*timeout)(void * parameter), void * parameter, rt_tick_t time, rt_uint 8 _t flag) ; timer 定时器句柄,指向要初始化的定时器控制块 name 定时器的名称 void (timeout) (void parameter) 定时器超时函数指针(当定时器超时时,系统会调用这个函数) parameter 定时器超时函数的入口参数(当定时器超时时,调用超时回调函数会把这个参数做为入口参数传递给超时函数) time 定时器的超时时间,单位是时钟节拍 flag 定时器创建时的参数 rt_err_t rt_timer_detach (rt_timer_t timer) ;
启动和停止定时器
1 2 3 4 5 6 7 8 rt_err_t rt_timer_start (rt_timer_t timer) ; 调用定时器启动函数接口后,定时器的状态将更改为激活状态(RT_TIMER_FLAG_ACTIVATED), 并按照超时顺序插入到 rt_timer_list 队列链表中 rt_err_t rt_timer_stop (rt_timer_t timer) ; 调用定时器停止函数接口后,定时器状态将更改为停止状态,并从 rt_timer_list 链表中脱离出来不参与定时器超时检查。 当一个(周期性)定时器超时时,也可以调用这个函数接口停止这个(周期性)定时器本身
控制定时器
1 2 3 4 5 6 7 rt_err_t rt_timer_control (rt_timer_t timer, rt_uint 8 _t cmd, void * arg) ; #define RT_TIMER_CTRL_SET_TIME 0 x 0 #define RT_TIMER_CTRL_GET_TIME 0 x 1 #define RT_TIMER_CTRL_SET_ONESHOT 0 x 2 #define RT_TIMER_CTRL_SET_PERIODIC 0 x 3
9.3 定时器应用示例 创建两个动态定时器,一个是单次定时,一个是周期性定时并让周期定时器运行一段时间后停止运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <rtthread.h> static rt_timer_t timer 1 ; static rt_timer_t timer 2 ; static int cnt = 0 ; static void timeout 1 (void *parameter) {rt_kprintf ("periodic timer is timeout %d\n" , cnt);if (cnt++>= 9 ){rt_timer_stop (timer 1 ); rt_kprintf ("periodic timer was stopped! \n" );} } static void timeout 2 (void *parameter) {rt_kprintf ("one shot timer is timeout\n" ); } int timer_sample (void ) {timer 1 = rt_timer_create ("timer 1" , timeout 1 , RT_NULL, 10 , RT_TIMER_FLAG_PERIODIC);if (timer 1 != RT_NULL) rt_timer_start (timer 1 );timer 2 = rt_timer_create ("timer 2" , timeout 2 , RT_NULL, 30 , RT_TIMER_FLAG_ONE_SHOT);if (timer 2 != RT_NULL) rt_timer_start (timer 2 ); return 0 ; } MSH_CMD_EXPORT (timer_sample, timer sample);
初始化定时器的例子与创建定时器的例子类似,这个程序会初始化 2 个静态定时器,一个是单次定时,一个是周期性的定时
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <rtthread.h> static struct rt_timer timer 1; static struct rt_timer timer 2; static int cnt = 0 ; static void timeout 1 (void * parameter) {rt_kprintf ("periodic timer is timeout\n" );if (cnt++>= 9 ){rt_timer_stop (&timer 1 );} } static void timeout 2 (void * parameter) {rt_kprintf ("one shot timer is timeout\n" ); } int timer_static_sample (void ) {rt_timer_init (&timer 1 , "timer 1" , timeout 1 , RT_NULL, 10 , RT_TIMER_FLAG_PERIODIC); rt_timer_init (&timer 2 , "timer 2" , timeout 2 , RT_NULL, 30 , RT_TIMER_FLAG_ONE_SHOT); rt_timer_start (&timer 1 ); rt_timer_start (&timer 2 ); return 0 ; } MSH_CMD_EXPORT (timer_static_sample, timer_static sample);
9.4 高精度延时 定时器的最小精度是由系统时钟节拍所决定的, 当需要实现更短时间长度的系统定时时,例如 OS Tick 是 10 ms,而程序需要实现 1 ms 的定时或延时,这种时候操作系统定时器将不能够满足要求,只能通过读取系统某个硬件定时器的计数器或直接使用硬件定时器的方式。 在 Cortex-M 系列中,SysTick 已经被 RT-Thread 用于作为 OS Tick 使用,它被配置成 1/RT_TICK_PER_SECOND 秒后触发一次中断的方式,中断处理函数使用 Cortex-M 3 默认的 SysTick_Handler 名字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <board.h> void rt_hw_us_delay (rt_uint 32 _t us) { rt_uint 32_t ticks; rt_uint 32_t told, tnow, tcnt = 0 ; rt_uint 32_t reload = SysTick->LOAD; ticks = us * reload / (1000000 / RT_TICK_PER_SECOND); told = SysTick->VAL; while (1 ){ tnow = SysTick->VAL; if (tnow != told){if (tnow < told){tcnt += told - tnow; } else { tcnt += reload - tnow + told;}told = tnow;if (tcnt >= ticks){break ;}}} } 入口参数 us 指示出需要延时的微秒数目,这个函数只能支持低于 1 OS Tick 的延时
10 线程间同步 例如一项工作中的两个线程:一个线程从传感器中接收数据并且将数据写到共享内存中,同时另一个线程周期性的从共享内存中读取数据并发送去显示,下图描述了两个线程间的数据传递:
+ 如果对共享内存的访问不是排他性的,那么各个线程间可能同时访问它,这将引起数据一致性的问题。例如,在显示线程试图显示数据之前,接收线程还未完成数据的写入,那么显示将包含不同时间采样的数据,造成显示数据的错乱。 + 将传感器数据写入到共享内存块的接收线程 #1 和将传感器数据从共享内存块中读出的线程 #2 都会访问同一块内存 。为了防止出现数据的差错,两个线程访问的动作必须是互斥进行的,应该是在一个线程对共享内存块操作完成后,才允许另一个线程去操作,这样,接收线程 #1 与显示线程 #2 才能正常配合,使此项工作正确地执行。 + 同步 是指按预定的先后次序进行运行,线程同步是指多个线程通过特定的机制(如互斥量,事件对象,临界区)来控制线程之间的执行顺序,也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间将是无序的。 + 多个线程操作 /访问同一块区域(代码),这块代码就称为临界区 ,上述例子中的共享内存块就是临界区。线程互斥是指对于临界区资源访问的排它性。当多个线程都要使用临界区资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
核心思想都是:在访问临界区的时候只允许一个 (或一类) 线程运行。
进入 / 退出临界区的方式有很多种:
1 2 3 4 调用 rt_hw_interrupt_disable () 进入临界区, 调用 rt_hw_interrupt_enable () 退出临界区;详见《中断管理》的全局中断开关内容。 调用 rt_enter_critical () 进入临界区, 调用 rt_exit_critical () 退出临界区。
使用信号量、互斥量、事件集这些对象进行线程间的同步。
10.1 信号量
信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。
如下图所示,每个信号量对象都有一个信号量值和一个线程等待队列,信号量的值对应了信号量对象的实例数目、资源数目,假如信号量值为 5,则表示共有 5 个信号量实例(资源)可以被使用,当信号量实例数目为零时,再申请该信号量的线程就会被挂起在该信号量的等待队列上,等待可用的信号量实例(资源)。
10.1.1 . 信号量控制块
信号量控制块是操作系统用于管理信号量的一个数据结构,由结构体 struct rt_semaphore 表示。另外一种 C 表达方式 rt_sem_t ,表示的是信号量的句柄,在 C 语言中的实现是指向信号量控制块的指针。
1 2 3 4 5 6 7 8 9 struct rt_semaphore { struct rt_ipc_object parent ; rt_uint 16_t value; }; typedef struct rt_semaphore * rt_sem_t ; rt_semaphore 对象从 rt_ipc_object 中派生,由 IPC 容器所管理, 信号量的最大值是 65535 。
10.1.2 . 信号量的管理方式 信号量相关接口如下图所示,对一个信号量的操作包含:创建 / 初始化信号量、获取信号量、释放信号量、删除 / 脱离信号量。 创建和删除信号量
1 2 3 4 5 6 7 name 信号量名称 value 信号量初始值,表示信号量可用资源数 flag 信号量标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
RT_IPC_FLAG_FIFO 属于非实时调度方式,除非应用程序非常在意先来后到,并且你清楚地明白所有涉及到该信号量的线程都将会变为非实时线程,方可使用 RT_IPC_FLAG_FIFO,否则建议采用 RT_IPC_FLAG_PRIO,即确保线程的实时性。
1 2 3 系统不再使用信号量时,可通过删除信号量以释放系统资源,适用于动态创建的信号量。 rt_err_t rt_sem_delete (rt_sem_t sem) ; 如果删除该信号量时,有线程正在等待该信号量,那么删除操作会先唤醒等待在该信号量上的线程(等待线程的返回值是 - RT_ERROR),然后再释放信号量的内存资源。
初始化和脱离信号量
1 2 3 4 5 6 对于静态信号量对象,它的内存空间在编译时期就被编译器分配出来,放在读写数据段或未初始化数据段上,此时使用信号量就不再需要使用 rt_sem_create 接口来创建它,而只需在使用前对它进行初始化即可。 rt_err_t rt_sem_init (rt_sem_t sem, const char *name, rt_uint 32 _t value, rt_uint 8 _t flag) 当调用这个函数时,系统将对这个 semaphore 对象进行初始化,然后初始化 IPC 对象以及与 semaphore 相关的部分。信号量标志可用上面创建信号量函数里提到的标志,value为信号量初始值,表示信号量可用资源数 rt_err_t rt_sem_detach (rt_sem_t sem) ; 使用该函数后,内核先唤醒所有挂在该信号量等待队列上的线程,然后将该信号量从内核对象管理器中脱离。原来挂起在信号量上的等待线程将获得 - RT_ERROR 的返回值。
获取信号量
1 2 3 4 5 6 7 8 9 10 11 12 rt_err_t rt_sem_take (rt_sem_t sem, rt_int 32 _t time) ; 调用这个函数时,如果信号量的值等于零,那么说明当前信号量资源实例不可用,申请该信号量的线程将根据 time 参数的情况选择直接返回、或挂起等待一段时间、或永久等待, 直到其他线程或中断释放该信号量。如果在参数 time 指定的时间内依然得不到信号量,线程将超时返回,返回值是 - RT_ETIMEOUT。
无等待获取信号量 当用户不想在申请的信号量上挂起线程进行等待时,可以使用无等待方式获取信号量
1 2 rt_err_t rt_sem_trytake (rt_sem_t sem) ; 信号量资源实例不可用的时候,它不会等待在该信号量上,而是直接返回 - RT_ETIMEOUT
释放信号量
1 2 rt_err_t rt_sem_release (rt_sem_t sem) ; 当信号量的值等于零时,并且有线程等待这个信号量时,释放信号量将唤醒等待在该信号量线程队列中的第一个线程,由它获取信号量;否则将把信号量的值加 1 。
10.1.3 . 信号量应用示例 该例程创建了一个动态信号量,初始化两个线程,一个线程发送信号量,一个线程接收到信号量后,执行相应的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 #include <rtthread.h> #define THREAD_STACK_SIZE 512 #define THREAD_PRIORITY 20 static rt_sem_t sem;static int shared_resource = 0 ;static void thread1_entry (void *parameter) { while (1 ) { rt_sem_take(sem, RT_WAITING_FOREVER); shared_resource++; rt_kprintf("Thread 1: Shared resource value: %d\n" , shared_resource); rt_sem_release(sem); rt_thread_mdelay(1000 ); } } static void thread2_entry (void *parameter) { while (1 ) { rt_sem_take(sem, RT_WAITING_FOREVER); shared_resource--; rt_kprintf("Thread 2: Shared resource value: %d\n" , shared_resource); rt_sem_release(sem); rt_thread_mdelay(1500 ); } } int main (void ) { sem = rt_sem_create("my_semaphore" , 1 , RT_IPC_FLAG_FIFO); rt_thread_t thread1 = rt_thread_create("thread1" , thread1_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, 10 , 0 ); if (thread1 != RT_NULL) rt_thread_startup(thread1); rt_thread_t thread2 = rt_thread_create("thread2" , thread2_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, 10 , 0 ); if (thread2 != RT_NULL) rt_thread_startup(thread2); rt_thread_startup(rt_thread_self()); return 0 ; }
信号量的另一个应用例程如下所示,本例程将使用 2 个线程、3 个信号量实现生产者与消费者的例子。 3 个信号量分别为:①lock:信号量锁的作用,因为 2 个线程都会对同一个数组 array 进行操作,所以该数组是一个共享资源,锁用来保护这个共享资源。②empty:空位个数,初始化为 5 个空位。③full:满位个数,初始化为 0 个满位。 2 个线程分别为:①生产者线程:获取到空位后,产生一个数字,循环放入数组中,然后释放一个满位。②消费者线程:获取到满位后,读取数组内容并相加,然后释放一个空位。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 #include <rtthread.h> #define THREAD_PRIORITY 6 #define THREAD_STACK_SIZE 512 #define THREAD_TIMESLICE 5 #define MAXSEM 5 rt_uint32_t array [MAXSEM]; static rt_uint32_t set = 0 , get = 0 ; struct rt_semaphore sem_lock ; struct rt_semaphore sem_empty ; struct rt_semaphore sem_full ; void producer_thread_entry (void *parameter) { int cnt = 0 ; while (cnt < 10 ) { rt_sem_take(&sem_empty, RT_WAITING_FOREVER); rt_sem_take(&sem_lock, RT_WAITING_FOREVER); array [set % MAXSEM] = cnt + 1 ; rt_kprintf("the producer generates a number: %d\n" , array [set % MAXSEM]); set ++; rt_sem_release(&sem_lock); rt_sem_release(&sem_full); cnt++; rt_thread_mdelay(20 ); } rt_kprintf("the producer exit!\n" ); } void consumer_thread_entry (void *parameter) { rt_uint32_t sum = 0 ; while (1 ) { rt_sem_take(&sem_full, RT_WAITING_FOREVER); rt_sem_take(&sem_lock, RT_WAITING_FOREVER); sum += array [get % MAXSEM]; rt_kprintf("the consumer[%d] get a number: %d\n" , (get % MAXSEM), array [get % MAXSEM]); get++; rt_sem_release(&sem_lock); rt_sem_release(&sem_empty); if (get == 10 ) break ; rt_thread_mdelay(50 ); } rt_kprintf("the consumer sum is: %d\n" , sum); rt_kprintf("the consumer exit!\n" ); } int producer_consumer (void ) { set = 0 ; get = 0 ; rt_sem_init(&sem_lock, "lock" , 1 , RT_IPC_FLAG_PRIO); rt_sem_init(&sem_empty, "empty" , MAXSEM, RT_IPC_FLAG_PRIO); rt_sem_init(&sem_full, "full" , 0 , RT_IPC_FLAG_PRIO); producer_tid = rt_thread_create("producer" , producer_thread_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY - 1 , THREAD_TIMESLICE); if (producer_tid != RT_NULL) { rt_thread_startup(producer_tid); } else { rt_kprintf("create thread producer failed\n" ); return -1 ; } consumer_tid = rt_thread_create("consumer" , consumer_thread_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY + 1 , THREAD_TIMESLICE); if (consumer_tid != RT_NULL) { rt_thread_startup(consumer_tid); } else { rt_kprintf("create thread consumer failed\n" ); return -1 ; } return 0 ; } MSH_CMD_EXPORT(producer_consumer, producer_consumer sample);
本例程可以理解为生产者生产产品放入仓库,消费者从仓库中取走产品。
(1)生产者线程:
1)获取 1 个空位(放产品 number),此时空位减 1;
2)上锁保护;本次的产生的 number 值为 cnt+1,把值循环存入数组 array 中;再开锁;
3)释放 1 个满位(给仓库中放置一个产品,仓库就多一个满位),满位加 1;
(2)消费者线程:
1)获取 1 个满位(取产品 number),此时满位减 1;
2)上锁保护;将本次生产者生产的 number 值从 array 中读出来,并与上次的 number 值相加;再开锁;
3)释放 1 个空位(从仓库上取走一个产品,仓库就多一个空位),空位加 1。
生产者依次产生 10 个 number,消费者依次取走,并将 10 个 number 的值求和。信号量锁 lock 保护 array 临界区资源:保证了消费者每次取 number 值的排他性,实现了线程间同步。
10.1.4 信号量的使用场合 ^7829c8
可以运用在多种场合中。形成锁、同步、资源计数等关系,也能方便的用于线程与线程、中断与线程间的同步中。
线程同步 线程同步是信号量最简单的一类应用。例如,使用信号量进行两个线程之间的同步,信号量的值初始化成 0,表示具备 0 个信号量资源实例;而尝试获得该信号量的线程,将直接在这个信号量上进行等待。 当持有信号量的线程完成它处理的工作时,释放这个信号量,可以把等待在这个信号量上的线程唤醒,让它执行下一部分工作。这类场合也可以看成把信号量用于工作完成标志:持有信号量的线程完成它自己的工作,然后通知等待该信号量的线程继续下一部分工作
锁(该功能仅做了解) 在计算机操作系统发展历史上,人们早期使用二值信号量来保护临界区,但是在 1990 年,研究人员发现了使用信号量保护临界区 (共享资源)会导致无界优先级反转的问题,因此提出了互斥量的概念。如今,我们已经不使用二值信号量来保护临界区,互斥量取而代之。
中断与线程的同步 例如一个中断触发,中断服务例程需要通知线程进行相应的数据处理。这个时候可以设置信号量的初始值是 0,线程在试图持有这个信号量时,由于信号量的初始值是 0,线程直接在这个信号量上挂起直到信号量被释放。当中断触发时,先进行与硬件相关的动作,例如从硬件的 I/O 口中读取相应的数据,并确认中断以清除中断源,而后释放一个信号量来唤醒相应的线程以做后续的数据处理。例如 FinSH 线程的处理方式 信号量的值初始为 0,当 FinSH 线程试图取得信号量时,因为信号量值是 0,所以它会被挂起。当 console 设备有数据输入时,产生中断,从而进入中断服务例程。在中断服务例程中,它会读取 console 设备的数据,并把读得的数据放入 UART buffer 中进行缓冲,而后释放信号量,释放信号量的操作将唤醒 shell 线程。在中断服务例程运行完毕后,如果系统中没有比 shell 线程优先级更高的就绪线程存在时,shell 线程将持有信号量并运行,从 UART buffer 缓冲区中获取输入的数据。
中断与线程间的互斥不能采用信号量(锁)的方式,而应采用开关中断的方式。
资源计数 信号量也可以认为是一个递增或递减的计数器,需要注意的是信号量的值非负。例如:初始化一个信号量的值为 5,则这个信号量可最大连续减少 5 次,直到计数器减为 0。资源计数适合于线程间工作处理速度不匹配的场合,这个时候信号量可以做为前一线程工作完成个数的计数,而当调度到后一线程时,它也可以以一种连续的方式一次处理多个事件。 一般资源计数类型多是混合方式的线程间同步,因为对于单个的资源处理依然存在线程的多重访问,这就需要对一个单独的资源进行访问、处理,并进行锁方式的互斥操作。
10.2 互斥量 又叫相互排斥的信号量,是一种特殊的二值信号量。 和信号量不同的是:拥有互斥量的线程拥有互斥量的所有权 ,互斥量支持递归访问且能防止线程优先级翻转;并且互斥量只能由持有线程释放 ,而信号量则可以由任何线程释放。 互斥量的状态只有两种,开锁或闭锁(两种状态值) 。当有线程持有它时,互斥量处于闭锁状态,由这个线程获得它的所有权。相反,当这个线程释放它时,将对互斥量进行开锁,失去它的所有权。当一个线程持有互斥量时,其他线程将不能够对它进行开锁或持有它,持有该互斥量的线程也能够再次获得这个锁而不被挂起 与一般的二值信号量有很大的不同:在信号量中,因为已经不存在实例,线程递归持有会发生主动挂起(最终形成死锁) 使用信号量会导致的另一个潜在问题是线程优先级翻转 问题。所谓优先级翻转,即当一个高优先级线程试图通过信号量机制访问共享资源时,如果该信号量已被一低优先级线程持有,而这个低优先级线程在运行过程中可能又被其它一些中等优先级的线程抢占,因此造成高优先级线程被许多具有较低优先级的线程阻塞,实时性难以得到保证。
有优先级为 A、B 和 C 的三个线程,优先级 A> B > C。线程 A,B 处于挂起状态,等待某一事件触发,线程 C 正在运行,此时线程 C 开始使用某一共享资源 M。在使用过程中,线程 A 等待的事件到来,线程 A 转为就绪态,因为它比线程 C 优先级高,所以立即执行。但是当线程 A 要使用共享资源 M 时,由于其正在被线程 C 使用,因此线程 A 被挂起切换到线程 C 运行。如果此时线程 B 等待的事件到来,则线程 B 转为就绪态。由于线程 B 的优先级比线程 C 高,且线程 B 没有用到共享资源 M ,因此线程 B 开始运行,直到其运行完毕,线程 C 才开始运行。只有当线程 C 释放共享资源 M 后,线程 A 才得以执行。在这种情况下,优先级发生了翻转:线程 B 先于线程 A 运行。这样便不能保证高优先级线程的响应时间。 互斥量可以解决优先级翻转问题,实现的是优先级继承协议 (Sha, 1990)。优先级继承是通过在线程 A 尝试获取共享资源而被挂起的期间内,将线程 C 的优先级提升到线程 A 的优先级别,从而解决优先级翻转引起的问题。这样能够防止 C(间接地防止 A)被 B 抢占
在获得互斥量后,请尽快释放互斥量,并且在持有互斥量的过程中,不得再行更改持有互斥量线程的优先级,否则可能人为引入无界优先级反转的问题。
10.2.1 . 互斥量控制块 操作系统用于管理互斥量的一个数据结构,由结构体 struct rt_mutex 表示。rt_mutex_t,表示的是互斥量的句柄
1 2 3 4 5 6 7 8 9 10 11 12 struct rt_ipc_object ; struct rt_mutex { struct rt_ipc_object parent ; rt_uint16_t value; rt_uint8_t original_priority; rt_uint8_t hold; struct rt_thread *owner ; }; typedef struct rt_mutex *rt_mutex_t ;
10.2.2 . 互斥量的管理方式 包含:创建 / 初始化互斥量、获取互斥量、释放互斥量、删除 / 脱离互斥量。
1 2 3 4 5 6 rt_mutex_t rt_mutex_create (const char * name, rt_uint 8 _t flag) ; 互斥量的 flag 标志已经作废,无论用户选择 RT_IPC_FLAG_PRIO 还是 RT_IPC_FLAG_FIFO,内核均按照 RT_IPC_FLAG_PRIO 处理。 rt_err_t rt_mutex_delete (rt_mutex_t mutex) ;
1 2 3 4 5 rt_err_t rt_mutex_init (rt_mutex_t mutex, const char * name, rt_uint 8 _t flag) ; 使用该函数接口时,需指定互斥量对象的句柄(即指向互斥量控制块的指针),互斥量名称以及互斥量标志。 rt_err_t rt_mutex_detach (rt_mutex_t mutex) ; 内核先唤醒所有挂在该互斥量上的线程(线程的返回值是 -RT_ERROR),然后系统将该互斥量从内核对象管理器中脱离。
1 2 3 4 rt_err_t rt_mutex_take (rt_mutex_t mutex, rt_int 32 _t time) ; 如果互斥量没有被其他线程控制,那么申请该互斥量的线程将成功获得该互斥量。如果互斥量已经被当前线程线程控制,则该互斥量的持有计数加 1 ,当前线程也不会挂起等待。 如果互斥量已经被其他线程占有,则当前线程在该互斥量上挂起等待,直到其他线程释放它或者等待时间超过指定的超时时间。
1 2 3 rt_err_t rt_mutex_trytake (rt_mutex_t mutex) ; 当线程申请的互斥量资源实例不可用的时候,它不会等待在该互斥量上,而是直接返回 - RT_ETIMEOUT。
1 2 rt_err_t rt_mutex_release (rt_mutex_t mutex) ;
10.2.3 . 互斥量应用示例 互斥量不能在中断服务例程中使用 。 互斥锁是一种保护共享资源的方法。当一个线程拥有互斥锁的时候,可以保护共享资源不被其他线程破坏。下面用一个例子来说明,有两个线程:线程 1 和线程 2,线程 1 对 2 个 number 分别进行加 1 操作;线程 2 也对 2 个 number 分别进行加 1 操作,使用互斥量保证线程改变 2 个 number 值的操作不被打断。如下代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 #include <rtthread.h> #define THREAD_PRIORITY 8 #define THREAD_TIMESLICE 5 static rt_mutex_t dynamic_mutex = RT_NULL; static rt_uint8_t number1, number2; ALIGN(RT_ALIGN_SIZE) static char thread1_stack[1024 ]; static struct rt_thread thread1 ; static void rt_thread_entry1 (void *parameter) { while (1 ) { rt_mutex_take(dynamic_mutex, RT_WAITING_FOREVER); number1++; rt_thread_mdelay(10 ); number2++; rt_mutex_release(dynamic_mutex); } } ALIGN(RT_ALIGN_SIZE) static char thread2_stack[1024 ]; static struct rt_thread thread2 ; static void rt_thread_entry2 (void *parameter) { while (1 ) { rt_mutex_take(dynamic_mutex, RT_WAITING_FOREVER); if (number1 != number2) { rt_kprintf("not protected. number1 = %d, number2 = %d\n" , number1, number2); } else { rt_kprintf("mutex protected, number1 = number2 is %d\n" , number1); } number1++; number2++; rt_mutex_release(dynamic_mutex); if (number1 >= 50 ) { return ; } } } int mutex_sample (void ) { dynamic_mutex = rt_mutex_create("dmutex" , RT_IPC_FLAG_PRIO); if (dynamic_mutex == RT_NULL) { rt_kprintf("create dynamic mutex failed.\n" ); return -RT_ERROR; } rt_thread_init(&thread1, "thread1" , rt_thread_entry1, RT_NULL, &thread1_stack[0 ], sizeof (thread1_stack), THREAD_PRIORITY, THREAD_TIMESLICE); rt_thread_startup(&thread1); rt_thread_init(&thread2, "thread2" , rt_thread_entry2, RT_NULL, &thread2_stack[0 ], sizeof (thread2_stack), THREAD_PRIORITY, THREAD_TIMESLICE); rt_thread_startup(&thread2); return 0 ; } MSH_CMD_EXPORT(mutex_sample, mutex sample);
10.2.4 . 互斥量的使用场合 它是信号量的一种,并且它是以锁的形式存在。在初始化的时候,互斥量永远都处于开锁的状态,而被线程持有的时候则立刻转为闭锁的状态。互斥量更适合于: (1)线程多次持有互斥量的情况下。这样可以避免同一线程多次递归持有而造成死锁的问题。 (2)可能会由于多线程同步而造成优先级翻转的情况。
10.3 事件集 线程间同步的机制之一,一个事件集可以包含多个事件,利用事件集可以完成一对多,多对多的线程间同步。 一个线程与多个事件的关系可设置为:其中任意一个事件唤醒线程,或几个事件都到达后才唤醒线程进行后续的处理;同样,事件也可以是多个线程同步多个事件。这种多个事件的集合可以用一个 32 位无符号整型变量来表示,变量的每一位代表一个事件,线程通过 “逻辑与” 或“逻辑或”将一个或多个事件关联起来,形成事件组合。事件的 “逻辑或” 也称为是独立型同步,指的是线程与任何事件之一发生同步;事件 “逻辑与” 也称为是关联型同步,指的是线程与若干事件都发生同步。
RT-Thread 定义的事件集有以下特点: 1)事件只与线程相关,事件间相互独立:每个线程可拥有 32 个事件标志,采用一个 32 bit 无符号整型数进行记录,每一个 bit 代表一个事件; 2)事件仅用于同步,不提供数据传输功能; 3)事件无排队性,即多次向线程发送同一事件 (如果线程还未来得及读走),其效果等同于只发送一次。 在 RT-Thread 中,每个线程都拥有一个事件信息标记,它有三个属性,分别是 RT_EVENT_FLAG_AND (逻辑与), RT_EVENT_FLAG_OR (逻辑或)以及 RT_EVENT_FLAG_CLEAR (清除标记)。当线程等待事件同步时,可以通过 32 个事件标志和这个事件信息标记来判断当前接收的事件是否满足同步条件。 如上图所示,线程 #1 的事件标志中第 1 位和第 30 位被置位,如果事件信息标记位设为逻辑与,则表示线程 #1 只有在事件 1 和事件 30 都发生以后才会被触发唤醒,如果事件信息标记位设为逻辑或,则事件 1 或事件 30 中的任意一个发生都会触发唤醒线程 #1 。如果信息标记同时设置了清除标记位,则当线程 #1 唤醒后将主动把事件 1 和事件 30 清为零,否则事件标志将依然存在(即置 1)。
10.3.1 . 事件集控制块 由结构体 struct rt_event 表示。另外一种 C 表达方式 rt_event_t,表示的是事件集的句柄,在 C 语言中的实现是事件集控制块的指针. 由 IPC 容器所管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct rt_ipc_object { int status; }; struct rt_event { struct rt_ipc_object parent ; rt_uint32_t set ; }; typedef struct rt_event * rt_event_t ;
10.3.2 . 事件集的管理方式
1 2 3 4 5 6 7 8 9 10 11 12 13 rt_event_t rt_event_create (const char * name, rt_uint 8 _t flag) ; flag 事件集的标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO RT_IPC_FLAG_FIFO 属于非实时调度方式,除非应用程序非常在意先来后到,并且你清楚地明白所有涉及到该事件集的线程都将会变为非实时线程, 方可使用 RT_IPC_FLAG_FIFO,否则建议采用 RT_IPC_FLAG_PRIO,即确保线程的实时性。 rt_err_t rt_event_delete (rt_event_t event) ; 调用 rt_event_delete 函数删除一个事件集对象时,应该确保该事件集不再被使用。在删除前会唤醒所有挂起在该事件集上的线程(线程的返回值是 - RT_ERROR) rt_err_t rt_event_init (rt_event_t event, const char * name, rt_uint 8 _t flag) ; 调用该接口时,需指定静态事件集对象的句柄(即指向事件集控制块的指针),然后系统会初始化事件集对象,并加入到系统对象容器中进行管理。 rt_err_t rt_event_detach (rt_event_t event) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 rt_err_t rt_event_send (rt_event_t event, rt_uint32_t set ) ; rt_err_t rt_event_recv (rt_event_t event, rt_uint32_t set , rt_uint8_t option, rt_int32_t timeout, rt_uint32_t * recved) ; #define RT_EVENT_FLAG_OR (1 << 0) #define RT_EVENT_FLAG_AND (1 << 1) #define RT_EVENT_FLAG_CLEAR (1 << 2)
10.3.3 . 事件集应用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 #include <rtthread.h> #define THREAD_PRIORITY 9 #define THREAD_TIMESLICE 5 #define EVENT_FLAG_3 (1 << 3) #define EVENT_FLAG_5 (1 << 5) static struct rt_event event ; ALIGN(RT_ALIGN_SIZE) static char thread1_stack[1024 ]; static struct rt_thread thread1 ; static void thread1_recv_event (void *param) { rt_uint32_t e; if (rt_event_recv(&event, (EVENT_FLAG_3 | EVENT_FLAG_5), RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR, RT_WAITING_FOREVER, &e) == RT_EOK) { rt_kprintf("thread 1: OR recv event 0x%x\n" , e); } rt_kprintf("thread 1: delay 1 s to prepare the second event\n" ); rt_thread_mdelay(1000 ); if (rt_event_recv(&event, (EVENT_FLAG_3 | EVENT_FLAG_5), RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR, RT_WAITING_FOREVER, &e) == RT_EOK) { rt_kprintf("thread 1: AND recv event 0x%x\n" , e); } rt_kprintf("thread 1 leave.\n" ); } ALIGN(RT_ALIGN_SIZE) static char thread2_stack[1024 ]; static struct rt_thread thread2 ; static void thread2_send_event (void *param) { rt_kprintf("thread 2: send event 3\n" ); rt_event_send(&event, EVENT_FLAG_3); rt_thread_mdelay(200 ); rt_kprintf("thread 2: send event 5\n" ); rt_event_send(&event, EVENT_FLAG_5); rt_thread_mdelay(200 ); rt_kprintf("thread 2: send event 3\n" ); rt_event_send(&event, EVENT_FLAG_3); rt_kprintf("thread 2 leave.\n" ); } int event_sample (void ) { rt_err_t result; result = rt_event_init(&event, "event" , RT_IPC_FLAG_PRIO); if (result != RT_EOK) { rt_kprintf("init event failed.\n" ); return -RT_ERROR; } rt_thread_init(&thread1, "thread1" , thread1_recv_event, RT_NULL, &thread1_stack[0 ], sizeof (thread1_stack), THREAD_PRIORITY - 1 , THREAD_TIMESLICE); rt_thread_startup(&thread1); rt_thread_init(&thread2, "thread2" , thread2_send_event, RT_NULL, &thread2_stack[0 ], sizeof (thread2_stack), THREAD_PRIORITY, THREAD_TIMESLICE); rt_thread_startup(&thread2); return 0 ; }
10.3.4 . 事件集的使用场合
可使用于多种场合,它能够在一定程度上替代信号量,用于线程间同步。一个线程或中断服务例程发送一个事件给事件集对象,而后等待的线程被唤醒并对相应的事件进行处理。但是它与信号量不同的是,事件的发送操作在事件未清除前,是不可累计的,而信号量的释放动作是累计的。事件的另一个特性是,接收线程可等待多种事件,即多个事件对应一个线程或多个线程。同时按照线程等待的参数,可选择是 “逻辑或” 触发还是 “逻辑与” 触发。这个特性也是信号量等所不具备的
11 线程间通信 在裸机编程中,经常会使用全局变量 进行功能间的通信,如某些功能可能由于一些操作而改变全局变量的值,另一个功能对此全局变量进行读取,根据读取到的全局变量值执行相应的动作,达到通信协作的目的。RT-Thread 中则提供了更多的工具帮助在不同的线程中间传递信息
11.1 邮箱 实时操作系统中一种典型的线程间通信方法。
举一个简单的例子,有两个线程,线程 1 检测按键状态并发送,线程 2 读取按键状态并根据按键的状态相应地改变 LED 的亮灭。这里就可以使用邮箱的方式进行通信,线程 1 将按键的状态作为邮件发送到邮箱,线程 2 在邮箱中读取邮件获得按键状态并对 LED 执行亮灭操作。 线程 1 也可以扩展为多个线程。例如,共有三个线程,线程 1 检测并发送按键状态,线程 2 检测并发送 ADC 采样信息,线程 3 则根据接收的信息类型不同,执行不同的操作。
11.1.1 -0. 邮箱的工作机制 用于线程间通信,特点是开销比较低,效率较高。邮箱中的每一封邮件只能容纳固定的 4 字节内容(针对 32 位处理系统,指针的大小即为 4 个字节,所以一封邮件恰好能够容纳一个指针)
典型的邮箱也称作交换消息,如下图所示,线程或中断服务例程把一封 4 字节长度的邮件发送到邮箱中,而一个或多个线程可以从邮箱中接收这些邮件并进行处理。 非阻塞方式的邮件发送过程能够安全的应用于中断服务中,是线程、中断服务、定时器向线程发送消息的有效手段。通常来说,邮件收取过程可能是阻塞的,这取决于邮箱中是否有邮件,以及收取邮件时设置的超时时间。当邮箱中不存在邮件且超时时间不为 0 时,邮件收取过程将变成阻塞方式。在这类情况下,只能由线程进行邮件的收取。
当一个线程向邮箱发送邮件时,如果邮箱没满,将把邮件复制到邮箱中。如果邮箱已经满了,发送线程可以设置超时时间,选择等待挂起或直接返回 - RT_EFULL。如果发送线程选择挂起等待,那么当邮箱中的邮件被收取而空出空间来时,等待挂起的发送线程将被唤醒继续发送。 当一个线程从邮箱中接收邮件时,如果邮箱是空的,接收线程可以选择是否等待挂起直到收到新的邮件而唤醒,或可以设置超时时间。当达到设置的超时时间,邮箱依然未收到邮件时,这个选择超时等待的线程将被唤醒并返回 - RT_ETIMEOUT。如果邮箱中存在邮件,那么接收线程将复制邮箱中的 4 个字节邮件到接收缓存中。
11.1.2 . 邮箱控制块 管理邮箱的一个数据结构,由结构体 struct rt_mailbox 表示。另外一种 C 表达方式 rt_mailbox_t,表示的是邮箱的句柄
1 2 3 4 5 6 7 8 9 10 11 12 struct rt_mailbox { struct rt_ipc_object parent ; rt_uint32_t * msg_pool; rt_uint16_t size; rt_uint16_t entry; rt_uint16_t in_offset; rt_uint16_t out_offset; rt_list_t suspend_sender_thread; }; typedef struct rt_mailbox * rt_mailbox_t ;
11.1.3 . 邮箱的管理方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 rt_mailbox_t * rt_mb_create (const char * name, rt_uint16_t msg_num, rt_uint16_t msg_size, rt_uint8_t flag) ; rt_err_t rt_mb_delete (rt_mailbox_t * mb) ; rt_err_t rt_mb_init (rt_mailbox_t * mb, const char * name, void * msgpool, rt_uint16_t msg_num, rt_uint16_t msg_size, rt_uint8_t flag) ; rt_err_t rt_mb_detach (rt_mailbox_t * mb) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 rt_err_t rt_mb_send (rt_mailbox_t mb, rt_uint32_t value) ; rt_err_t rt_mb_send_wait (rt_mailbox_t mb, rt_uint32_t value, rt_int32_t timeout) ; rt_err_t rt_mb_urgent (rt_mailbox_t mb, rt_ubase_t value) ; rt_err_t rt_mb_recv (rt_mailbox_t mb, rt_uint32_t * value, rt_int32_t timeout) ;
11.1.4 . 邮箱使用示例 初始化 2 个静态线程,一个静态的邮箱对象,其中一个线程往邮箱中发送邮件,一个线程往邮箱中收取邮件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 #include <rtthread.h> #define THREAD_PRIORITY 25 #define THREAD_TIMESLICE 5 static rt_mailbox_t mb; static rt_uint8_t mb_pool[128 ]; static char mb_str1[] = "Hello from mb_str1" ; static char mb_str2[] = "Hello from mb_str2" ; static char mb_str3[] = "Thread 2 finished" ; static void thread1_entry (void *parameter) { char *str; while (1 ) { rt_kprintf("thread 1: try to recv a mail\n" ); if (rt_mb_recv(&mb, (rt_uint32_t *)&str, RT_WAITING_FOREVER) == RT_EOK) { rt_kprintf("thread 1: get a mail from mailbox, the content: %s\n" , str); if (str == mb_str3) break ; rt_thread_mdelay(100 ); } } rt_mb_detach(&mb); } ALIGN(RT_ALIGN_SIZE) static rt_uint8_t thread2_stack[1024 ]; static struct rt_thread thread2 ; static void thread2_entry (void *parameter) { rt_uint8_t count; count = 0 ; while (count < 10 ) { count++; if (count & 0x1 ) { rt_mb_send(&mb, (rt_uint32_t )&mb_str1); } else { rt_mb_send(&mb, (rt_uint32_t )&mb_str2); } rt_thread_mdelay(200 ); } rt_mb_send(&mb, (rt_uint32_t )&mb_str3); } int mailbox_sample (void ) { rt_err_t result; result = rt_mb_init(&mb, "mbt" , &mb_pool[0 ], sizeof (mb_pool) / sizeof (rt_uint32_t ), RT_IPC_FLAG_FIFO); if (result != RT_EOK) { rt_kprintf("init mailbox failed.\n" ); return -1 ; } rt_thread_init(&thread2, "thread 2" , thread2_entry, RT_NULL, &thread2_stack[0 ], sizeof (thread2_stack), THREAD_PRIORITY, THREAD_TIMESLICE); rt_thread_startup(&thread2); rt_thread_init(&thread1, "thread 1" , thread1_entry, RT_NULL, RT_NULL, 0 , THREAD_PRIORITY, THREAD_TIMESLICE); rt_thread_startup(&thread1); return 0 ; }
11.1.5 . 邮箱的使用场合 简单的线程间消息传递方式,特点是开销比较低,效率较高。 能够一次传递一个 4 字节大小的邮件,并且邮箱具备一定的存储功能,能够缓存一定数量的邮件数; 当需要在线程间传递比较大的消息时,可以把指向一个缓冲区的指针作为邮件发送到邮箱中,即邮箱也可以传递指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct msg { rt_uint 8_t *data_ptr; rt_uint 32_t data_size; }; struct msg * msg_ptr ; msg_ptr = (struct msg*) rt_malloc (sizeof (struct msg)); msg_ptr->data_ptr = ...; msg_ptr->data_size = len; rt_mb_send (mb, (rt_uint 32_t ) msg_ptr); struct msg * msg_ptr ; if (rt_mb_recv (mb, (rt_uint 32_t *)&msg_ptr) == RT_EOK) { rt_free (msg_ptr); }
11.2 消息队列(邮箱的扩展) 另一种常用的线程间通讯方式,是邮箱的扩展。可以应用在多种场合:线程间的消息交换、使用串口接收不定长数据等。
11.2.1 消息队列的工作机制 能够接收来自线程或中断服务例程中不固定长度的消息,并把消息缓存在自己的内存空间中。其他线程也能够从消息队列中读取相应的消息,而当消息队列是空的时候,可以挂起读取线程。是一种异步的通信方式 。
消息队列对象由多个元素组成,当消息队列被创建时,它就被分配了消息队列控制块:消息队列名称、内存缓冲区、消息大小以及队列长度等。同时每个消息队列对象中包含着多个消息框,每个消息框可以存放一条消息;消息队列中的第一个和最后一个消息框被分别称为消息链表头和消息链表尾,对应于消息队列控制块中的 msg_queue_head 和 msg_queue_tail;有些消息框可能是空的,它们通过 msg_queue_free 形成一个空闲消息框链表。所有消息队列中的消息框总数即是消息队列的长度,这个长度可在消息队列创建时指定。
11.2.2 消息队列控制块 管理消息队列的一个数据结构,由结构体 struct rt_messagequeue 表示。另外一种 C 表达方式 rt_mq_t
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct rt_messagequeue { struct rt_ipc_object parent ; void * msg_pool; rt_uint16_t msg_size; rt_uint16_t max_msgs; rt_uint16_t entry; void * msg_queue_head; void * msg_queue_tail; void * msg_queue_free; rt_list_t suspend_sender_thread; }; typedef struct rt_messagequeue * rt_mq_t ;
11.2.3 消息队列的管理方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 rt_mq_t rt_mq_create (const char * name, rt_size_t msg_size, rt_size_t max_msgs, rt_uint8_t flag) ; rt_err_t rt_mq_delete (rt_mq_t mq) ; rt_err_t rt_mq_init (rt_mq_t mq, const char * name, void * msgpool, rt_size_t msg_size, rt_size_t pool_size, rt_uint8_t flag) ; rt_err_t rt_mq_detach (rt_mq_t mq) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 rt_err_t rt_mq_send (rt_mq_t mq, void * buffer, rt_size_t size) ; rt_err_t rt_mq_send_wait (rt_mq_t mq, const void * buffer, rt_size_t size, rt_int32_t timeout) ; rt_err_t rt_mq_urgent (rt_mq_t mq, void * buffer, rt_size_t size) ; rt_err_t rt_mq_recv (rt_mq_t mq, void * buffer, rt_size_t size, rt_int32_t timeout) ;
11.2.4 消息队列应用示例 初始化了 2 个静态线程,一个线程会从消息队列中收取消息;另一个线程会定时给消息队列发送普通消息和紧急消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 #include <rtthread.h> static rt_uint8_t msg_pool[2048 ]; ALIGN(RT_ALIGN_SIZE) static char thread1_stack[1024 ]; static struct rt_thread thread1 ; static char thread2_stack[1024 ]; static struct rt_thread thread2 ; static struct rt_messagequeue mq ; static void thread1_entry (void *parameter) { char buf = 0 ; rt_uint8_t cnt = 0 ; while (1 ) { if (rt_mq_recv(&mq, &buf, sizeof (buf), RT_WAITING_FOREVER) == RT_EOK) { rt_kprintf("thread 1: recv msg from msg queue, the content: %c\n" , buf); if (cnt == 19 ) { break ; } } cnt++; rt_thread_mdelay(50 ); } rt_kprintf("thread 1: quit\n" ); } static void thread2_entry (void *parameter) { int result; char buf = 'A' ; rt_uint8_t cnt = 0 ; while (1 ) { if (cnt == 8 ) { result = rt_mq_urgent(&mq, &buf, 1 ); if (result != RT_EOK) { rt_kprintf("rt_mq_urgent ERR\n" ); } else { rt_kprintf("thread 2: send urgent message - %c\n" , buf); } } else if (cnt >= 20 ) { rt_kprintf("message queue stop send, thread 2 quit\n" ); break ; } else { result = rt_mq_send(&mq, &buf, 1 ); if (result != RT_EOK) { rt_kprintf("rt_mq_send ERR\n" ); } rt_kprintf("thread 2: send message - %c\n" , buf); buf++; } cnt++; rt_thread_mdelay(5 ); } } int msgq_sample (void ) { rt_err_t result; result = rt_mq_init(&mq, "mqt" , &msg_pool[0 ], 1 , sizeof (msg_pool) / 1 , RT_IPC_FLAG_PRIO); if (result != RT_EOK) { rt_kprintf("init message queue failed.\n" ); return -1 ; } rt_thread_init(&thread1, "thread1" , thread1_entry, RT_NULL, &thread1_stack[0 ], sizeof (thread1_stack), 25 , 5 ); rt_thread_startup(&thread1); rt_thread_init(&thread2, "thread2" , thread2_entry, RT_NULL, &thread2_stack[0 ], sizeof (thread2_stack), 25 , 5 ); rt_thread_startup(&thread2); return 0 ; }
11.2.5 消息队列的使用场合 应用于发送不定长消息的场合,包括线程与线程间的消息交换,以及中断服务例程中给线程发送消息(中断服务例程不能接收消息) 发送消息 消息队列和邮箱的明显不同是消息的长度并不限定在 4 个字节以内;另外,消息队列也包括了一个发送紧急消息的函数接口。但是当创建的是一个所有消息的最大长度是 4 字节的消息队列时,消息队列对象将蜕化成邮箱 。
1 2 3 4 5 struct msg { rt_uint 8_t *data_ptr; rt_uint 32_t data_size; };
和邮箱例子相同的消息结构定义,假设依然需要发送这样一个消息给接收线程。在邮箱例子中,这个结构只能够发送指向这个结构的指针(在函数指针被发送过去后,接收线程能够正确的访问指向这个地址的内容,通常这块数据需要留给接收线程来释放)。而使用消息队列的方式则大不相同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <rtthread.h> struct msg { void * data_ptr; rt_size_t data_size; }; extern rt_mq_t mq; void send_op (void * data, rt_size_t length) { struct msg msg_to_send ; msg_to_send.data_ptr = data; msg_to_send.data_size = length; if (rt_mq_send(mq, &msg_to_send, sizeof (struct msg)) != RT_EOK) { rt_kprintf("send message failed.\n" ); } } void message_handler (void ) { struct msg msg_received ; if (rt_mq_recv(mq, &msg_received, sizeof (struct msg), RT_WAITING_FOREVER) == RT_EOK) { rt_kprintf("Received message with size: %d\n" , msg_received.data_size); } }
11.2.6 同步消息 一般的系统设计中会经常遇到要发送同步消息的问题,这个时候就可以根据当时状态的不同选择相应的实现:两个线程间可以采用 [消息队列 + 信号量或邮箱] 的形式实现。发送线程通过消息发送的形式发送相应的消息给消息队列,发送完毕后希望获得接收线程的收到确认,工作示意图如下图所示:
1 2 3 4 5 6 7 8 9 根据消息确认的不同,可以把消息结构体定义成: struct msg { struct rt_mailbox ack ; }; struct msg { struct rt_semaphore ack ; }; 第一种类型的消息使用了邮箱来作为确认标志,而第二种类型的消息采用了信号量来作为确认标志。邮箱作为确认标志,代表着接收线程能够通知一些状态值给发送线程;而信号量作为确认标志只能够单一的通知发送线程,消息已经确认接收。
11.3 信号(软中断信号) 软件层次上是对中断机制的一种模拟,在原理上,一个线程收到一个信号与处理器收到一个中断请求可以说是类似的。
11.3.1 信号的工作机制
信号在 RT-Thread 中用作异步通信,POSIX 标准定义了 sigset_t 类型来定义一个信号集,然而 sigset_t 类型在不同的系统可能有不同的定义方式,在 RT-Thread 中,将 sigset_t 定义成了 unsigned long 型,并命名为 rt_sigset_t,应用程序能够使用的信号为 SIGUSR 1(10)和 SIGUSR 2(12)
信号本质是软中断,用来通知线程发生了异步事件,用做线程之间的异常通知、应急处理。一个线程不必通过任何操作来等待信号的到达,事实上,线程也不知道信号到底什么时候到达,线程之间可以互相通过调用 rt_thread_kill () 发送软中断信号。
收到信号的线程对各种信号有不同的处理方法,处理方法可以分为三类: 第一种是类似中断的处理程序,对于需要处理的信号,线程可以指定处理函数,由该函数来处理。 第二种方法是,忽略某个信号,对该信号不做任何处理,就像未发生过一样。 第三种方法是,对该信号的处理保留系统的默认值。
如下图所示,假设线程 1 需要对信号进行处理,首先线程 1 安装一个信号并解除阻塞,并在安装的同时设定了对信号的异常处理方式;然后其他线程可以给线程 1 发送信号,触发线程 1 对该信号的处理。 当信号被传递给线程 1 时,如果它正处于挂起状态,那会把状态改为就绪状态去处理对应的信号 (即唤醒)。如果它正处于运行状态,那么会在它当前的线程栈基础上建立新栈帧空间去处理对应的信号,需要注意的是使用的线程栈大小也会相应增加
11.3.2 信号的管理方式
1 2 3 4 5 6 7 8 9 如果线程要处理某一信号,那么就要在线程中安装该信号。安装信号主要用来确定信号值及线程针对该信号值的动作之间的映射关系, 即线程将要处理哪个信号,该信号被传递给线程时,将执行何种操作。 rt_sighandler_t rt_signal_install (int signo, rt_sighandler_t [] handler) ; signo 信号值(只有 SIGUSR 1 和 SIGUSR 2 是开放给用户使用的,下同) handler 设置对信号值的处理方式, 决定了该信号的不同的处理方法。处理方法可以分为三种: 1 )类似中断的处理方式,参数指向当信号发生时用户自定义的处理函数,由该函数来处理。 2 )参数设为 SIG_IGN,忽略某个信号,对该信号不做任何处理,就像未发生过一样。 3 )参数设为 SIG_DFL,系统会调用默认的处理函数_signal_default_handler ()。
1 2 3 4 5 6 7 8 9 10 11 12 13 也可以理解为屏蔽信号。如果该信号被阻塞,则该信号将不会递达给安装此信号的线程,也不会引发软中断处理。 void rt_signal_mask (int signo) ; 线程中可以安装好几个信号,使用此函数可以对其中一些信号给予 “关注”,那么发送这些信号都会引发该线程的软中断。 void rt_signal_unmask (int signo) ; 需要进行异常处理时,可以给设定了处理异常的线程发送信号,调用 rt_thread_kill () 可以用来向任何线程发送信号 int rt_thread_kill (rt_thread_t tid, int sig) ; 等待 set 信号的到来,如果没有等到这个信号,则将线程挂起,直到等到这个信号或者等待时间超过指定的超时时间 timeout。 如果等到了该信号,则将指向该信号体的指针存入 si int rt_signal_wait (const rt_sigset_t *set , rt_siginfo_t [] *si, rt_int 32 _t timeout) ;
11.3.3 信号应用示例 创建了 1 个线程,在安装信号时,信号处理方式设为自定义处理,定义的信号的处理函数为 thread 1_signal_handler ()。待此线程运行起来安装好信号之后,给此线程发送信号。此线程将接收到信号,并打印信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #include <rtthread.h> #define THREAD_PRIORITY 25 #define THREAD_STACK_SIZE 512 #define THREAD_TIMESLICE 5 static rt_thread_t tid1 = RT_NULL; void thread1_signal_handler (int sig) { rt_kprintf("thread 1 received signal %d\n" , sig); } static void thread1_entry (void *parameter) { int cnt = 0 ; rt_signal_install(SIGUSR1, thread1_signal_handler); rt_signal_unmask(SIGUSR1); while (cnt < 10 ) { rt_kprintf("thread 1 count : %d\n" , cnt); cnt++; rt_thread_mdelay(100 ); } } int signal_sample (void ) { tid1 = rt_thread_create("thread 1" , thread1_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE); if (tid1 != RT_NULL) rt_thread_startup(tid1); rt_thread_mdelay(300 ); return 0 ; }
12 内存管理 RAM(随机存储器) ROM(只读存储器)
12.1 内存管理的功能特点 实时系统中对时间的要求非常严格,内存管理往往要比通用操作系统要求苛刻得多:
1)分配内存的时间必须是确定的。一般内存管理算法是根据需要存储的数据的长度在内存中去寻找一个与这段数据相适应的空闲内存块,然后将数据存储在里面。而寻找这样一个空闲内存块所耗费的时间是不确定的,因此对于实时系统来说,这就是不可接受的,实时系统必须要保证内存块的分配过程在可预测的确定时间内完成,否则实时任务对外部事件的响应也将变得不可确定。 2)随着内存不断被分配和释放,整个内存区域会产生越来越多的碎片(因为在使用过程中,申请了一些内存,其中一些释放了,导致内存空间中存在一些小的内存块,它们地址不连续,不能够作为一整块的大内存分配出去),系统中还有足够的空闲内存,但因为它们地址并非连续,不能组成一块连续的完整内存块,会使得程序不能申请到大的内存。对于通用系统而言,这种不恰当的内存分配算法可以通过重新启动系统来解决 (每个月或者数个月进行一次),但是对于那些需要常年不间断地工作于野外的嵌入式系统来说,就变得让人无法接受了。 3)嵌入式系统的资源环境也是不尽相同,有些系统的资源比较紧张,只有数十 KB 的内存可供分配,而有些系统则存在数 MB 的内存,如何为这些不同的系统,选择适合它们的高效率的内存分配算法,就将变得复杂化
RT-Thread 操作系统在内存管理上可分为两类:内存堆管理 与内存池管理 内存堆管理又根据具体内存设备划分为三种情况:小内存管理算法,slab 大内存管理算法,memheap 多内存管理算法
12.2 内存堆管理 RT-Thread 将 “ZI 段结尾处” 到内存尾部的空间用作内存堆 内存堆可以在当前资源满足的情况下,根据用户的需求分配任意大小的内存块。而当用户不需要再使用这些内存块时,又可以释放回堆中供其他应用分配使用。
小内存管理算法主要针对系统资源比较少,一般用于小于 2 MB 内存空间的系统; slab 内存管理算法则主要是在系统资源比较丰富时,提供了一种近似多内存池管理算法的快速算法。 memheap 方法适用于系统存在多个内存堆的情况,它可以将多个内存 “粘贴” 在一起,形成一个大的内存堆 系统运行时只能选择其中之一或者完全不使用内存堆管理器,他们提供给应用程序的 API 接口完全相同。
因为内存堆管理器要满足多线程情况下的安全分配,会考虑多线程间的互斥问题,所以请不要在中断服务例程中分配或释放动态内存块 。因为它可能会引起当前上下文被挂起等待。
12.2.1 小内存管理算法 ^a97576
初始时,它是一块大的内存。当需要分配内存块时,将从这个大的内存块上分割出相匹配的内存块,然后把分割出来的空闲内存块还回给堆管理系统中。每个内存块都包含一个管理用的数据头,通过这个头把使用块与空闲块用双向链表的方式链接起来
每个内存块(不管是已分配的内存块还是空闲的内存块)都包含一个数据头,其中包括: 1)magic:变数(或称为幻数),它会被初始化成 0x1ea0(即英文单词 heap),用于标记这个内存块是一个内存管理用的内存数据块;变数不仅仅用于标识这个数据块是一个内存管理用的内存数据块,实质也是一个内存保护字:如果这个区域被改写,那么也就意味着这块内存块被非法改写(正常情况下只有内存管理器才会去碰这块内存)。 2)used:指示出当前内存块是否已经分配。
内存管理的表现主要体现在内存的分配与释放上,小型内存管理算法可以用以下例子体现出来。 如下图所示的内存分配情况,空闲链表指针 lfree 初始指向 32 字节的内存块。当用户线程要再分配一个 64 字节的内存块时,但此 lfree 指针指向的内存块只有 32 字节并不能满足要求,内存管理器会继续寻找下一内存块,当找到再下一块内存块,128 字节时,它满足分配的要求。因为这个内存块比较大,分配器将把此内存块进行拆分,余下的内存块(52 字节)继续留在 lfree 链表中,如下图分配 64 字节后的链表结构所示。 每次分配内存块前,都会留出 12 字节数据头用于 magic、used 信息及链表节点使用。返回给应用的地址实际上是这块内存块 12 字节以后的地址,前面的 12 字节数据头是用户永远不应该碰的部分。(注:12 字节数据头长度会与系统对齐差异而有所不同)释放时则是相反的过程,但分配器会查看前后相邻的内存块是否空闲,如果空闲则合并成一个大的空闲内存块。
12.2.2 slab 管理算法(大内存) slab 分配器会根据对象的大小分成多个区(zone),也可以看成每类对象有一个内存池,如下图所示:
一个 zone 的大小在 32 K 到 128 K 字节之间,分配器会在堆初始化时根据堆的大小自动调整。系统中的 zone 最多包括 72 种对象,一次最大能够分配 16 K 的内存空间,如果超出了 16 K 那么直接从页分配器中分配。每个 zone 上分配的内存块大小是固定的,能够分配相同大小内存块的 zone 会链接在一个链表中,而 72 种对象的 zone 链表则放在一个数组(zone_array[])中统一管理。 下面是内存分配器主要的两种操作: (1)内存分配 : 假设分配一个 32 字节的内存,slab 内存分配器会先按照 32 字节的值,从 zone array 链表表头数组中找到相应的 zone 链表。如果这个链表是空的,则向页分配器分配一个新的 zone,然后从 zone 中返回第一个空闲内存块。如果链表非空,则这个 zone 链表中的第一个 zone 节点必然有空闲块存在(否则它就不应该放在这个链表中),那么就取相应的空闲块。如果分配完成后,zone 中所有空闲内存块都使用完毕,那么分配器需要把这个 zone 节点从链表中删除。 (2)内存释放 : 分配器需要找到内存块所在的 zone 节点,然后把内存块链接到 zone 的空闲内存块链表中。如果此时 zone 的空闲链表指示出 zone 的所有内存块都已经释放,即 zone 是完全空闲的,那么当 zone 链表中全空闲 zone 达到一定数目后,系统就会把这个全空闲的 zone 释放到页面分配器中去。
12.2.3 memheap 管理算法 适用于系统含有多个地址可不连续的内存堆。使用 memheap 内存管理可以简化系统存在多个内存堆时的使用:当系统中存在多个内存堆的时候,用户只需要在系统初始化时将多个所需的 memheap 初始化,并开启 memheap 功能就可以很方便地把多个 memheap(地址可不连续)粘合起来用于系统的 heap 分配 在开启 memheap 之后原来的 heap 功能将被关闭,两者只可以通过打开或关闭 RT_USING_MEMHEAP_AS_HEAP 来选择其一
memheap 工作机制如下图所示,首先将多块内存加入 memheap_item 链表进行粘合。当分配内存块时,会先从默认内存堆去分配内存,当分配不到时会查找 memheap_item 链表,尝试从其他的内存堆上分配内存块。应用程序不用关心当前分配的内存块位于哪个内存堆上,就像是在操作一个内存堆。
12.2.4 内存堆配置和初始化 1 2 3 4 5 6 void rt_system_heap_init (void * begin_addr, void * end_addr) ; 这个函数会把参数 begin_addr,end_addr 区域的内存空间作为内存堆来使用。 rt_err_t rt_memheap_init (struct rt_memheap *memheap, const char *name, void *start_addr, rt_uint 32 _t size) 如果有多个不连续的 memheap 可以多次调用该函数将其初始化并加入 memheap_item 链表。
12.2.5 内存堆的管理方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void *rt_malloc (rt_size_t nbytes) ; 返回分配的内存块地址 void rt_free (void *ptr) ; void *rt_realloc (void *rmem, rt_size_t newsize) ; 在已分配内存块的基础上重新分配内存块的大小(增加或缩小) 在进行重新分配内存块时,原来的内存块数据保持不变(缩小的情况下,后面的数据被自动截断)。 从内存堆中分配连续内存地址的多个内存块 void *rt_calloc (rt_size_t count, rt_size_t size) ; count 内存块数量 size 内存块容量 在分配内存块过程中,用户可设置一个钩子函数 void rt_malloc_sethook (void (*hook)(void *ptr, rt_size_t size)) ; hook 钩子函数指针 设置的钩子函数会在内存分配完成后进行回调。回调时,会把分配到的内存块地址和大小做为入口参数传递进去。 void rt_free_sethook (void (*hook)(void *ptr)) ; 设置的钩子函数会在调用内存释放完成前进行回调。回调时,释放的内存块地址会做为入口参数传递进去
12.2.6 内存堆管理应用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include <rtthread.h> #define THREAD_PRIORITY 25 #define THREAD_STACK_SIZE 512 #define THREAD_TIMESLICE 5 void thread_entry (void *parameter) { int i; char *ptr = RT_NULL; for (i = 0 ; ; i++) { ptr = rt_malloc(1 << i); if (ptr != RT_NULL) { rt_kprintf("get memory :%d byte\n" , (1 << i)); rt_free(ptr); rt_kprintf("free memory :%d byte\n" , (1 << i)); ptr = RT_NULL; } else { rt_kprintf("try to get %d byte memory failed!\n" , (1 << i)); return ; } } } int dynmem_sample (void ) { rt_thread_t tid = RT_NULL; tid = rt_thread_create("thread 1" , thread_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE); if (tid != RT_NULL) rt_thread_startup(tid); return 0 ; }
12.3 内存池 内存堆管理器可以分配任意大小的内存块,非常灵活和方便。但其也存在明显的缺点:一是分配效率不高,在每次分配时,都要空闲内存块查找;二是容易产生内存碎片。 RT-Thread 提供了另外一种内存管理方法:内存池(Memory Pool)。 内存池是一种内存分配方式,用于分配大量大小相同的小内存块,它可以极大地加快内存分配与释放的速度,且能尽量避免内存碎片化,RT-Thread 的内存池支持线程挂起功能,非常适合需要通过内存资源进行同步的场景
例如播放音乐时,播放器线程会对音乐文件进行解码,然后发送到声卡驱动,从而驱动硬件播放音乐。
如上图所示,当播放器线程需要解码数据时,就会向内存池请求内存块,如果内存块已经用完,线程将被挂起,否则它将获得内存块以放置解码的数据; 而后播放器线程把包含解码数据的内存块写入到声卡抽象设备中 (线程会立刻返回,继续解码出更多的数据); 当声卡设备写入完成后,将调用播放器线程设置的回调函数,释放写入的内存块,如果在此之前,播放器线程因为把内存池里的内存块都用完而被挂起的话,那么这时它将被将唤醒,并继续进行解码。
12.3.1 内存池控制块 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct rt_mempool { struct rt_object parent ; void *start_address; rt_size_t size; rt_size_t block_size; rt_uint8_t *block_list; rt_size_t block_total_count; rt_size_t block_free_count; rt_list_t suspend_thread; rt_size_t suspend_thread_count; }; typedef struct rt_mempool * rt_mp_t ;
12.3.2 内存块分配机制 内存池在创建时先向系统申请一大块内存,然后分成同样大小的多个小内存块,小内存块直接通过链表连接起来(此链表也称为空闲链表)。每次分配的时候,从空闲链表中取出链头上第一个内存块,提供给申请者。 物理内存中允许存在多个大小不同的内存池,每一个内存池又由多个空闲内存块组成,内核用它们来进行内存管理。当一个内存池对象被创建时,内存池对象就被分配给了一个内存池控制块。 内存池一旦初始化完成,内部的内存块大小将不能再做调整。 每一个内存池对象由上述结构组成,其中 suspend_thread 形成了一个申请线程等待列表,即当内存池中无可用内存块,并且申请线程允许等待时,申请线程将挂起在 suspend_thread 链表上。
12.3.3 内存池的管理方式
1 2 3 4 5 6 7 创建内存池后,线程便可以从内存池中执行申请、释放等操作。 rt_mp_t rt_mp_create (const char * name, rt_size_t block_count, rt_size_t block_size) ; rt_err_t rt_mp_delete (rt_mp_t mp) ; 删除内存池时,会首先唤醒等待在该内存池对象上的所有线程(返回 -RT_ERROR), 然后再释放已从内存堆上分配的内存池数据存放区域,然后删除内存池对象。
1 2 3 4 5 6 与创建内存池不同的是,此处内存池对象所使用的内存空间是由用户指定的一个缓冲区空间,用户把缓冲区的指针传递给内存池控制块 rt_err_t rt_mp_init (rt_mp_t mp, const char * name, void *start, rt_size_t size, rt_size_t block_size) ; 内存池块个数 = size / (block_size + 4 链表指针大小),计算结果取整数 rt_err_t rt_mp_detach (rt_mp_t mp);
1 2 3 4 5 分配和释放内存块 void *rt_mp_alloc (rt_mp_t mp, rt_int 32 _t time) ; void rt_mp_free (void *block) ;
12.3.4 内存池应用示例 静态内内存池的应用例程: 创建一个静态的内存池对象,2 个动态线程。一个线程会试图从内存池中获得内存块,另一个线程释放内存块内存块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #include <rtthread.h> #define THREAD_PRIORITY 25 #define THREAD_STACK_SIZE 512 #define THREAD_TIMESLICE 5 static rt_uint8_t *ptr[50 ]; static rt_uint8_t mempool[4096 ]; static struct rt_mempool mp ; static void thread1_mp_alloc (void *parameter) { int i; for (i = 0 ; i < 50 ; i++) { if (ptr[i] == RT_NULL) { ptr[i] = rt_mp_alloc(&mp, RT_WAITING_FOREVER); if (ptr[i] != RT_NULL) rt_kprintf("allocate No.%d\n" , i); } } } static void thread2_mp_release (void *parameter) { int i; rt_kprintf("thread 2 try to release block\n" ); for (i = 0 ; i < 50 ; i++) { if (ptr[i] != RT_NULL) { rt_kprintf("release block %d\n" , i); rt_mp_free(ptr[i]); ptr[i] = RT_NULL; } } } int mempool_sample (void ) { int i; for (i = 0 ; i < 50 ; i++) ptr[i] = RT_NULL; rt_mp_init(&mp, "mp_1" , &mempool[0 ], sizeof (mempool), 80 ); rt_thread_t tid1 = rt_thread_create("thread_1" , thread1_mp_alloc, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE); if (tid1 != RT_NULL) rt_thread_startup(tid1); rt_thread_t tid2 = rt_thread_create("thread_2" , thread2_mp_release, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY + 1 , THREAD_TIMESLICE); if (tid2 != RT_NULL) rt_thread_startup(tid2); return 0 ; }
13 中断管理 中断 :
当 CPU 正在处理内部数据时,外界发生了紧急情况,要求 CPU 暂停当前的工作转去处理这个异步事件。处理完毕后,再回到原来被中断的地址,继续原来的工作 实现这一功能的系统称为 中断系统 ,申请 CPU 中断的请求源称为 中断源 中断处理与 CPU 架构密切相关
13.1 ARM Cortex-M 的 CPU 架构 13.1.1 寄存器简介 Cortex-M 系列 CPU 的寄存器组里有 R0~R15 共 16 个通用寄存器组和若干特殊功能寄存器
通用寄存器组里的 R13 作为堆栈指针寄存器 (Stack Pointer,SP); R14 作为连接寄存器 (Link Register,LR),用于在调用子程序时,存储返回地址; R15 作为程序计数器 (Program Counter,PC),其中堆栈指针寄存器可以是主堆栈指针(MSP),也可以是进程堆栈指针(PSP)。
1 2 3 4 5 6 特殊功能寄存器包括程序状态字寄存器组(PSRs)、中断屏蔽寄存器组(PRIMASK, FAULTMASK, BASEPRI) 、控制寄存器(CONTROL),可以通过 MSR/MRS 指令来访问特殊功能寄存器 MRS R 0, CONTROL ; 读取 CONTROL 到 R 0 中 MSR CONTROL, R 0 ; 写入 R 0 到 CONTROL 寄存器中程序状态字寄存器里保存算术与逻辑标志,例如负数标志,零结果标志,溢出标志等等。 中断屏蔽寄存器组控制 Cortex-M 的中断除能。控制寄存器用来定义特权级别和当前使用哪个堆栈指针。如果是具有浮点单元的 Cortex-M 4 或者 Cortex-M 7,控制寄存器也用来指示浮点单元当前是否在使用, 浮点单元包含了 32 个浮点通用寄存器 S 0~S 31 和特殊 FPSCR 寄存器(Floating point status and control register)。
13.1.2 操作模式和特权级别 Cortex-M 引入了操作模式和特权级别的概念,分别为线程模式和处理模式,如果进入异常或中断 处理则进入处理模式 ,其他情况则为线程模式。 Cortex-M 有两个运行级别,分别为特权级和用户级,线程模式可以工作在特权级或者用户级,而处理模式总工作在特权级 ,可通过 CONTROL 特殊寄存器控制。
13.1.3 嵌套向量中断控制器 NVIC(嵌套向量中断控制器),支持中断嵌套功能。当一个中断触发并且系统进行响应时,处理器硬件会将当前运行位置的上下文寄存器自动压入中断栈中,这部分的寄存器包括 PSR、PC、LR、R12、R3-R0 寄存器。 当系统正在服务一个中断时,如果有一个更高优先级的中断触发,那么处理器同样会打断当前运行的中断服务程序,然后把这个中断服务程序上下文的 PSR、PC、LR、R12、R3-R0 寄存器自动保存到中断栈中。
13.1.4 PendSV 系统调用 PendSV 也称为可悬起的系统调用,它是一种异常,可以像普通的中断一样被挂起,它是专门用来辅助操作系统进行上下文切换的。PendSV 异常会被初始化为最低优先级的异常。每次需要进行上下文切换的时候,会手动触发 PendSV 异常,在 PendSV 异常处理函数中进行上下文切换。
13.2 RT-Thread 中断工作机制 13.2.1 中断向量表 中断向量表是所有中断处理程序的入口,如下图所示是 Cortex-M 系列的中断处理过程:把一个函数(用户中断服务程序)同一个虚拟中断向量表中的中断向量联系在一起。当中断向量对应中断发生的时候,被连接的用户中断服务程序就会被调用执行 在 Cortex-M 内核上,所有中断都采用中断向量表的方式进行处理,即当一个中断触发时,处理器将直接判定是哪个中断源,然后直接跳转到相应的固定位置进行处理,每个中断服务程序必须排列在一起放在统一的地址上。每一条中断服务程序的地址按顺序分配到 Flash 代码区上(从 0x0800 0000 开始)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 __Vectors DCD __initial_sp ; 栈顶指针 DCD Reset_Handler ; 复位处理函数 DCD NMI_Handler ; NMI(非屏蔽中断)处理函数 DCD HardFault_Handler ; 硬故障处理函数 DCD MemManage_Handler ; 内存管理故障处理函数 DCD BusFault_Handler ; 总线故障处理函数 DCD UsageFault_Handler ; 使用故障处理函数 DCD 0 ; 保留(SVCall未使用) DCD 0 ; 保留 DCD 0 ; 保留 DCD 0 ; 保留 DCD SVC_Handler ; SVC(系统服务调用)处理函数 DCD DebugMon_Handler ; 调试监视器处理函数 DCD 0 ; 保留 DCD PendSV_Handler ; 可挂起的系统服务调用处理函数 DCD SysTick_Handler ; SysTick定时器处理函数 ; ... 其他中断处理函数 ... ; NMI_Handler 弱定义 NMI_Handler PROC EXPORT NMI_Handler [WEAK] B . ; 无限循环或跳转到某处 ENDP ; HardFault_Handler 弱定义 HardFault_Handler PROC EXPORT HardFault_Handler [WEAK] B . ; 无限循环或跳转到某处(通常是调试代码) ENDP ; ... 其他中断处理函数的弱定义 ...
[WEAK] 标识,它是符号弱化标识,在 [WEAK] 前面的符号 (如 NMI_Handler、HardFault_Handler)将被执行弱化处理,如果整个代码在链接时遇到了名称相同的符号(例如与 NMI_Handler 相同名称的函数),那么代码将使用未被弱化定义的符号(与 NMI_Handler 相同名称的函数),而与弱化符号相关的代码将被自动丢弃。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void SysTick_Handler (void ) { rt_interrupt_enter(); rt_tick_increase(); rt_interrupt_leave(); }
13.2.2 中断处理过程 RT-Thread 中断管理中,将中断处理程序分为中断前导程序、用户中断服务程序、中断后续程序三部分 中断前导程序 1)保存 CPU 中断现场,这部分跟 CPU 架构相关,不同 CPU 架构的实现方式有差异。 对于 Cortex-M 来说,该工作由硬件自动完成。当一个中断触发并且系统进行响应时,处理器硬件会将当前运行部分的上下文寄存器自动压入中断栈中,这部分的寄存器包括 PSR、PC、LR、R12、R3-R0 寄存器。 2)通知内核进入中断状态,调用 rt_interrupt_enter () 函数,作用是把全局变量 rt_interrupt_nest 加 1,用它来记录中断嵌套的层数,代码如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void rt_interrupt_enter (void ) { rt_base_t level; level = rt_hw_interrupt_disable(); rt_interrupt_nest++; rt_hw_interrupt_enable(level); }
用户中断服务程序 分为两种情况,第一种情况是不进行线程切换,这种情况下用户中断服务程序和中断后续程序运行完毕后退出中断模式,返回被中断的线程。 另一种情况是,在中断处理过程中需要进行线程切换,这种情况会调用 rt_hw_context_switch_interrupt () 函数进行上下文切换,该函数跟 CPU 架构相关,不同 CPU 架构的实现方式有差异。 Cortex-M 架构中,rt_hw_context_switch_interrupt () 的函数实现流程如下图所示 中断后续程序 1 通知内核离开中断状态,通过调用 rt_interrupt_leave () 函数,将全局变量 rt_interrupt_nest 减 1 2 恢复中断前的 CPU 上下文,如果在中断处理过程中未进行线程切换,那么恢复 from 线程的 CPU 上下文,如果在中断中进行了线程切换,那么恢复 to 线程的 CPU 上下文。
13.2.3 中断嵌套 在允许中断嵌套的情况下,在执行中断服务程序的过程中,如果出现高优先级的中断,当前中断服务程序的执行将被打断,以执行高优先级中断的中断服务程序,当高优先级中断的处理完成后,被打断的中断服务程序才又得到继续执行,如果需要进行线程调度,线程的上下文切换将在所有中断处理程序都运行结束时才发生
13.2.4 中断栈 在中断处理过程中,在系统响应中断前,软件代码(或处理器)需要把当前线程的上下文保存下来(通常保存在当前线程的线程栈中),再调用中断服务程序进行中断响应、处理。在进行中断处理时(实质是调用用户的中断服务程序函数),中断处理函数中很可能会有自己的局部变量,这些都需要相应的栈空间来保存,所以中断响应依然需要一个栈空间来做为上下文,运行中断处理函数。中断栈可以保存在打断线程的栈中,当从中断中退出时,返回相应的线程继续执行。 中断栈也可以与线程栈完全分离开来,即每次进入中断时,在保存完打断线程上下文后,切换到新的中断栈中独立运行。在中断退出时,再做相应的上下文恢复。使用独立中断栈相对来说更容易实现,并且对于线程栈使用情况也比较容易了解和掌握(否则必须要为中断栈预留空间,如果系统支持中断嵌套,还需要考虑应该为嵌套中断预留多大的空间)。 RT-Thread 采用的方式是提供独立的中断栈 ,即中断发生时,中断的前期处理程序会将用户的栈指针更换到系统事先留出的中断栈空间中,等中断退出时再恢复用户的栈指针。这样中断就不会占用线程的栈空间,从而提高了内存空间的利用率,且随着线程的增加,这种减少内存占用的效果也越明显。
13.2.5 中断的底半处理 RT-Thread 不对中断服务程序所需要的处理时间做任何假设、限制,但如同其他实时操作系统或非实时操作系统一样,用户需要保证所有的中断服务程序在尽可能短的时间内完成(中断服务程序在系统中相当于拥有最高的优先级,会抢占所有线程优先执行)。这样在发生中断嵌套,或屏蔽了相应中断源的过程中,不会耽误嵌套的其他中断处理过程,或自身中断源的下一次中断信号。 当一个中断发生时,中断服务程序需要取得相应的硬件状态或者数据。如果中断服务程序接下来要对状态或者数据进行简单处理,比如 CPU 时钟中断,中断服务程序只需对一个系统时钟变量进行加一操作,然后就结束中断服务程序。这类中断需要的运行时间往往都比较短。但对于另外一些中断,中断服务程序在取得硬件状态或数据以后,还需要进行一系列更耗时的处理过程,通常需要将该中断分割为两部分,即上半部分(Top Half)和底半部分(Bottom Half)。在上半部分中,取得硬件状态和数据后,打开被屏蔽的中断,给相关线程发送一条通知(可以是 RT-Thread 所提供的信号量、事件、邮箱或消息队列等方式),然后结束中断服务程序;而接下来,相关的线程在接收到通知后,接着对状态或数据进行进一步的处理,这一过程称之为底半处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include <rtthread.h> rt_sem_t nw_bh_sem = RT_NULL; uint8_t * packet_buffer = RT_NULL; void nw_packet_parser (uint8_t * buffer) { } void nw_packet_process (uint8_t * buffer) { } void demo_nw_thread (void *param) { device_init_setting(); nw_bh_sem = rt_sem_create("bh_sem" , 0 , RT_IPC_FLAG_PRIO); if (nw_bh_sem == RT_NULL) { return ; } while (1 ) { rt_sem_take(nw_bh_sem, RT_WAITING_FOREVER); nw_packet_parser(packet_buffer); nw_packet_process(packet_buffer); } } void demo_nw_isr (int vector , void *param) { nw_device_status_read(); rt_sem_release(nw_bh_sem); } int main (void ) { rt_thread_t thread; thread = rt_thread_create("nwt" , demo_nw_thread, RT_NULL, 1024 , 20 , 5 ); if (thread != RT_NULL) { rt_thread_startup(thread); } return 0 ; }
中断服务程序通过对一个信号量对象的等待和释放,来完成中断 Bottom Half 的起始和终结。由于将中断处理划分为 Top 和 Bottom 两个部分后,使得中断处理过程变为异步过程。这部分系统开销需要用户在使用 RT-Thread 时,必须认真考虑中断服务的处理时间是否大于给 Bottom Half 发送通知并处理的时间 。
13.2.6 RT-Thread 中断管理接口 为了把操作系统和系统底层的异常、中断硬件隔离开来,RT-Thread 把中断和异常封装为一组抽象接口 中断服务程序挂接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 rt_isr_handler_t rt_hw_interrupt_install (int vector , rt_isr_handler_t handler, void *param, char *name) ;
中断服务程序是一种需要特别注意的运行环境,它运行在非线程的执行环境下(一般为芯片的一种特殊运行模式(特权模式)),在这个运行环境中不能使用挂起当前线程的操作,因为当前线程并不存在
中断源管理 通常在 ISR 准备处理某个中断信号之前,我们需要先屏蔽该中断源,在 ISR 处理完状态或数据以后,及时的打开之前被屏蔽的中断源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 void rt_hw_interrupt_mask (int irqn) { uint32_t irq_bit = 1UL << (irqn & NVIC_IRQ_NUMBER_MASK); NVIC_DisableIRQ(irqn); } void rt_hw_interrupt_umask (int irqn) { NVIC_EnableIRQ(irqn); }
全局中断开关 也称为中断锁,是禁止多线程访问临界区最简单的一种方式,即通过关闭中断的方式,来保证当前线程不会被其他事件打断
1 2 3 4 5 6 7 8 9 10 rt_base_t rt_hw_interrupt_disable (void ) ;void rt_hw_interrupt_enable (rt_base_t level) ;
使用中断锁来操作临界区的方法可以应用于任何场合,且其他几类同步方式都是依赖于中断锁而实现的,可以说中断锁是最强大的和最高效的同步方法。只是使用中断锁最主要的问题在于,在中断关闭期间系统将不再响应任何中断,也就不能响应外部的事件。所以中断锁对系统的实时性影响非常巨大
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 rt_base_t level = rt_hw_interrupt_disable(); a = a + value; rt_hw_interrupt_enable(level); rt_err_t result = rt_sem_take(sem_lock, RT_WAITING_FOREVER); if (result == RT_EOK) { a = a + value; rt_sem_release(sem_lock); } else { }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <rthw.h> void global_interrupt_demo (void ) { rt_base_t level0; rt_base_t level1; level0 = rt_hw_interrupt_disable(); level1 = rt_hw_interrupt_disable(); do_something(); rt_hw_interrupt_enable(level1); rt_hw_interrupt_enable(level0); } void do_something (void ) { }
中断通知 当整个系统被中断打断,进入中断处理函数时,需要通知内核当前已经进入到中断状态。针对这种情况,可通过以下接口:
1 2 void rt_interrupt_enter (void ) ; void rt_interrupt_leave (void ) ;
这两个接口分别用在中断前导程序和中断后续程序中,均会对 rt_interrupt_nest(中断嵌套深度)的值进行修改: 每当进入中断时,可以调用 rt_interrupt_enter () 函数,用于通知内核,当前已经进入了中断状态,并增加中断嵌套深度(执行 rt_interrupt_nest++); 每当退出中断时,可以调用 rt_interrupt_leave () 函数,用于通知内核,当前已经离开了中断状态,并减少中断嵌套深度(执行 rt_interrupt_nest –)。注意不要在应用程序中调用这两个接口函数。
使用 rt_interrupt_enter/leave () 的作用是,在中断服务程序中,如果调用了内核相关的函数(如释放信号量等操作),则可以通过判断当前中断状态,让内核及时调整相应的行为。例如:在中断中释放了一个信号量,唤醒了某线程,但通过判断发现当前系统处于中断上下文环境中,那么在进行线程切换时应该采取中断中线程切换的策略,而不是立即进行切换。 如果中断服务程序不会调用内核相关的函数(释放信号量等操作),这个时候,也可以不调用 rt_interrupt_enter/leave () 函数
1 2 3 4 5 6 7 8 9 10 11 12 rt_uint8_t rt_interrupt_get_nest (void ) ;
13.3 中断与轮询 当驱动外设工作时,其编程模式到底采用中断模式触发还是轮询模式触发往往是驱动开发人员首先要考虑的问题,并且这个问题在实时操作系统与分时操作系统中差异还非常大。因为轮询模式本身采用顺序执行的方式:查询到相应的事件然后进行对应的处理。所以轮询模式从实现上来说,相对简单清晰。例如往串口中写入数据,仅当串口控制器写完一个数据时,程序代码才写入下一个数据(否则这个数据丢弃掉)。
1 2 3 4 5 6 7 8 9 10 11 while (size) { while (!(uart->uart_device->SR & USART_FLAG_TXE)); uart->uart_device->DR = (*ptr & 0x1FF ); ++ptr; --size; }
在实时系统中轮询模式可能会出现非常大问题,因为在实时操作系统中,当一个程序持续地执行时(轮询时),它所在的线程会一直运行,比它优先级低的线程都不会得到运行 。而分时系统中,这点恰恰相反,几乎没有优先级之分,可以在一个时间片运行这个程序,然后在另外一段时间片上运行另外一段程序。 通常情况下,实时系统中更多采用的是中断模式来驱动外设。当数据达到时,由中断唤醒相关的处理线程,再继续进行后续的动作。
例如一些携带 FIFO(包含一定数据量的先进先出队列)的串口外设,其写入过程可以是这样的(核心是异步处理,写 FIFO 快速动作完成后线程挂起,CPU 执行其他线程,UART 发送慢速动作由控制器外设完成,将二者异步,可使 CPU 不再等待低速设备)如下图所示:
线程先向串口的 FIFO 中写入数据,当 FIFO 满时,线程主动挂起。串口控制器持续地从 FIFO 中取出数据并以配置的波特率(例如 115200 bps)发送出去。当 FIFO 中所有数据都发送完成时,将向处理器触发一个中断;当中断服务程序得到执行时,可以唤醒这个线程。这里举例的是 FIFO 类型的设备,在现实中也有 DMA 类型的设备,原理类似。
对于低速设备来说,运用这种模式非常好,因为在串口外设把 FIFO 中的数据发送出去前,处理器可以运行其他的线程,这样就提高了系统的整体运行效率(甚至对于分时系统来说,这样的设计也是非常必要)。 但是对于一些高速设备,例如传输速度达到 10 Mbps 的时候,假设一次发送的数据量是 32 字节,我们可以计算出发送这样一段数据量需要的时间是:(32 * 8) * 1/10 Mbps = 25 us。当数据需要持续传输时,系统将在 25 us 后触发一个中断以唤醒上层线程继续下次传递。假设系统的线程切换时间是 8 us(通常实时操作系统的线程上下文切换时间只有几个 us ),那么当整个系统运行时,对于数据带宽利用率将只有 25/(25+8) =75.8%。但是采用轮询模式,数据带宽的利用率则可能达到 100%。这个也是大家普遍认为实时系统中数据吞吐量不足的缘故,系统开销消耗在了线程切换上(有些实时系统甚至会如本章前面说的,采用底半处理,分级的中断处理方式,相当于又拉长中断到发送线程的时间开销,效率会更进一步下降)。 当一个实时系统想要提升数据吞吐量时,可以考虑的几种方式: 1)增加每次数据量发送的长度,每次尽量让外设尽量多地发送数据(多量少次); 2)必要情况下更改中断模式为轮询模式。同时为了解决轮询方式一直抢占处理机,其他低优先级线程得不到运行的情况,可以把轮询线程的优先级适当降低。
13.4 全局中断开关使用示例 在多线程访问同一个变量时,使用开关全局中断对该变量进行保护,如下代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #include <rthw.h> #include <rtthread.h> #define THREAD_PRIORITY 20 #define THREAD_STACK_SIZE 512 #define THREAD_TIMESLICE 5 static rt_uint32_t cnt = 0 ; void thread_entry (void *parameter) { rt_uint32_t no = (rt_uint32_t )parameter; rt_uint32_t level; while (1 ) { level = rt_hw_interrupt_disable(); cnt += no; rt_hw_interrupt_enable(level); rt_kprintf("protect thread[%d]'s counter is %d\n" , no, cnt); rt_thread_mdelay(no * 10 ); } } int interrupt_sample (void ) { rt_thread_t thread; thread = rt_thread_create("thread 1" , thread_entry, (void *)10 , THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE); if (thread != RT_NULL) rt_thread_startup(thread); thread = rt_thread_create("thread 2" , thread_entry, (void *)20 , THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE); if (thread != RT_NULL) rt_thread_startup(thread); return 0 ; }
注:由于关闭全局中断会导致整个系统不能响应中断,所以在使用关闭全局中断做为互斥访问临界区的手段时,必须需要保证关闭全局中断的时间非常短,例如运行数条机器指令的时间。
#嵌入式 #操作系统 #rtthread