RT-Thread版本:4.0.5
MCU型号:STM32F103RCT6(ARM Cortex-M3 内核)
先补充几个概念:
当多个执行单元(线程、中断)同时执行临界区,操作临界资源,会导致竞态产生,此时可以采用同步或互斥的方式解决该问题。
线程互斥可以看成是一种特殊的线程同步。
对于裸机而言,一般采用全局标志实现对临界资源的同步与互斥访问,通常CPU会一直轮询查询该标志是否满足占用条件。在RTOS中,一般采用信号量、互斥量、事件集等方式保护临界区资源,就信号量来说,当信号量实例资源为空时,线程进入阻塞状态等待信号量到来,一但有信号量资源时会立刻唤醒该线程,这样可以减少CPU资源消耗,而且实时响应速度也是最快的。
信号量(Semaphore)是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。
RT-Thread中信号量对象的组成:
通常用一个信号量的计数值(sem->value
)表示可被占用的资源数,其值为只能为0和正整数最大(65535),成功释放信号量会让该值加1,成功获取信号量会让该值减1。当值为0时表示资源为空,想获取该信号量的线程会被阻塞挂在线程等待队列上。
信号量互斥与同步在使用方式上有如下不同:
注意:中断与线程间的互斥不能采用信号量的方式,而应采用开关中断的方式。
二值信号量类似一个标志位,其资源计数值只能为0和1,0表示该资源被获取,1表示被释放。
前面说过使用全局变量做标志位CPU需要一直查询,造成CPU资源浪费。使用二值信号量可以很好解决该问题,当二值信号量为空时,线程进入阻塞态等待即可,当该信号量被标记为1时唤醒该线程。但是使用二值信号量保护临界资源时会导致无界优先级翻转。
计数型信号量其资源计数值范围为0~65535,即可允许多个线程获取信号量访问资源,但会限制资源的最大数目,当访问的线程达到此数目时,会阻塞其他试图获取该信号量的线程,直至有线程释放了信号量(未获取信号量的线程也可以释放信号量)。一般用于:
信号量控制块是操作系统用于管理信号量的一个数据结构:
struct rt_ipc_object { struct rt_object parent; /**< 继承自 rt_object 类 */ rt_list_t suspend_thread; /**< 挂在此资源上的线程 */ }; struct rt_semaphore { struct rt_ipc_object parent; /* 继承自 ipc_object 类 */ rt_uint16_t value; /* 信号量的值 */ rt_uint16_t reserved; /* 保留字段 */ }; typedef struct rt_semaphore *rt_sem_t;
其中sem->value
即为资源计数值,sem->parent.suspend_thread
为因获取该信号量失败而被挂起的线程,利用双向循环链表存储,按线程优先级或FIFO顺序插入到链表中。
/** * @brief Creating a semaphore object. * @param name 信号量名称 * @param 资源计数值 * value 如果用于共享资源,则应该将该值初始化为可用资源的数量。 * 如果用于表示事件的发生,则应该将该值初始化为0。 * * @param flag 表示当信号量不可用时(value==0),想获取该信号量的线程等待的排队方式: * RT_IPC_FLAG_PRIO: 等待线程队列将按照线程优先级进行排队,优先级高的等待线程将先获得等待的信号量 * RT_IPC_FLAG_FIFO: 等待线程队列将按照先进先出的方式排队,先进入的线程将先获得等待的信号量 * * NOTE: RT_IPC_FLAG_FIFO 属于非实时调度方式,通常不建议使用,除非应用程序非常在意先来后到, * 并且用户清楚地明白所有涉及到该信号量的线程都将会变为非实时线程, * 否则建议采用 RT_IPC_FLAG_PRIO,即确保线程的实时性。 * * @return 返回一个指向信号量对象的指针。创建失败返回值为RT_NULL */ rt_sem_t rt_sem_create(const char *name, rt_uint32_t value, rt_uint8_t flag) { rt_sem_t sem; RT_DEBUG_NOT_IN_INTERRUPT; RT_ASSERT(value < 0x10000U); // 值必须小于2^16(65536) /* 分配内核对象 */ sem = (rt_sem_t)rt_object_allocate(RT_Object_Class_Semaphore, name); if (sem == RT_NULL) return sem; /* 初始化信号量对象 */ _ipc_object_init(&(sem->parent)); /* 设置可用信号量的值 */ sem->value = value; // 如果创建的是二值信号量,其取值范围为[0,1],如果是计数信号量,其取值范围为[0,65535] /* 设置信号量模式 */ sem->parent.parent.flag = flag; return sem; } }
rt_err_t rt_sem_delete(rt_sem_t sem) { RT_DEBUG_NOT_IN_INTERRUPT; /* 参数检查 */ RT_ASSERT(sem != RT_NULL); RT_ASSERT(rt_object_get_type(&sem->parent.parent) == RT_Object_Class_Semaphore); RT_ASSERT(rt_object_is_systemobject(&sem->parent.parent) == RT_FALSE); /* 唤醒所有阻塞挂着此信号量上的线程 */ _ipc_list_resume_all(&(sem->parent.suspend_thread)); /* 删除信号量对象 */ rt_object_delete(&(sem->parent.parent)); return RT_EOK; }
调用该函数时,系统将删除这个信号量(必须是动态创建的),如果删除该信号量时,有线程正在等待该信号量,那么删除操作会先唤醒等待在该信号量上的线程(等待线程的返回值是 -RT_ERROR),然后再释放信号量的内存资源。
静态初始化与脱离(静态对象不能被删除)对象,功能与上述函数一致,不再赘述。
rt_err_t rt_sem_take(rt_sem_t sem, rt_int32_t time) { register rt_base_t temp; struct rt_thread *thread; /* 参数检查 */ RT_ASSERT(sem != RT_NULL); RT_ASSERT(rt_object_get_type(&sem->parent.parent) == RT_Object_Class_Semaphore); RT_OBJECT_HOOK_CALL(rt_object_trytake_hook, (&(sem->parent.parent))); /* 关中断 */ temp = rt_hw_interrupt_disable(); if (sem->value > 0) { /* 有可用信号量 */ sem->value --; /* 开中断 */ rt_hw_interrupt_enable(temp); } else { /* 不等待,返回超时错误 */ if (time == 0) { rt_hw_interrupt_enable(temp); return -RT_ETIMEOUT; } else { /* 当前上下文检查 */ RT_DEBUG_IN_THREAD_CONTEXT; /* 信号量不可用,挂起当前线程 */ /* 获取当前线程 */ thread = rt_thread_self(); /* 设置线程错误码 */ thread->error = RT_EOK; /* 挂起线程 */ _ipc_list_suspend(&(sem->parent.suspend_thread), thread, sem->parent.parent.flag); /* 有等待时间则开始计时 */ if (time > 0) { /* 设置线程超时时间,并启动定时器 */ rt_timer_control(&(thread->thread_timer), RT_TIMER_CTRL_SET_TIME, &time); rt_timer_start(&(thread->thread_timer)); } /* 开中断 */ rt_hw_interrupt_enable(temp); /* 发起线程调度 */ rt_schedule(); if (thread->error != RT_EOK) { return thread->error; } } } RT_OBJECT_HOOK_CALL(rt_object_take_hook, (&(sem->parent.parent))); return RT_EOK; }
sem->vaule
大于0,线程将获取信号量,并将sem->vaule
减1sem->vaule
为0,表示当前信号量资源实例为空,申请信号量的线程根据time参数决定调用方式:
rt_err_t rt_sem_trytake(rt_sem_t sem) { return rt_sem_take(sem, RT_WAITING_NO); }
RT_WAITING_NO
宏值为0,即使用没有等待时间的方式获取信号量。
rt_err_t rt_sem_release(rt_sem_t sem) { register rt_base_t temp; register rt_bool_t need_schedule; /* parameter check */ RT_ASSERT(sem != RT_NULL); RT_ASSERT(rt_object_get_type(&sem->parent.parent) == RT_Object_Class_Semaphore); RT_OBJECT_HOOK_CALL(rt_object_put_hook, (&(sem->parent.parent))); need_schedule = RT_FALSE; /* disable interrupt */ temp = rt_hw_interrupt_disable(); if (!rt_list_isempty(&sem->parent.suspend_thread)) { /* 恢复阻塞线程 */ _ipc_list_resume(&(sem->parent.suspend_thread)); need_schedule = RT_TRUE; } else { if(sem->value < RT_SEM_VALUE_MAX) { sem->value ++; /* value值自增1 */ } else { rt_hw_interrupt_enable(temp); /* enable interrupt */ return -RT_EFULL; /* value值溢出 */ } } /* enable interrupt */ rt_hw_interrupt_enable(temp); /* 恢复当前线程,发起任务调度 */ if (need_schedule == RT_TRUE) rt_schedule(); return RT_EOK; }
sem->value
加1注:中断中可以调用此函数实现中断与线程的同步(中断中仅可以释放信号量,不能获取信号量)。
/* * Date Author * 2022-02-05 issac wan */ #include <rtthread.h> #define my_printf(fmt, ...) rt_kprintf(fmt"\n", ##__VA_ARGS__) #define THREAD_PRIORITY 20 #define THREAD_TIMESLICE 5 static uint16_t num1 = 0, num2 = 0; static rt_sem_t sem1; static rt_thread_t send_thread = RT_NULL; void send_thread_entry(void * param){ while(1){ rt_sem_take(sem1, RT_WAITING_FOREVER); num1++; rt_thread_delay(100); num2++; rt_sem_release(sem1); } } ALIGN(RT_ALIGN_SIZE) static struct rt_thread receive_thread; static rt_uint8_t receive_thread_stack[512] = {0}; void receive_thread_entry(void * param){ while(1){ rt_sem_take(sem1, RT_WAITING_FOREVER); if (num1 == num2) my_printf("[%u]Successful! num1:[%d], num2:[%d]", rt_tick_get(), num1, num2); else my_printf("[%u]Fail! num1:[%d], num2:[%d]", rt_tick_get(), num1, num2); rt_sem_release(sem1); rt_thread_delay(1000); } } int semaphore_sample(void){ rt_err_t receive_thread_ret = 0; sem1 = rt_sem_create("sem1", 1, RT_IPC_FLAG_FIFO); if (sem1 == RT_NULL) return -RT_ERROR; send_thread = rt_thread_create("send_th", send_thread_entry, RT_NULL, 512, THREAD_PRIORITY, THREAD_TIMESLICE); if (send_thread == RT_NULL) return -RT_ERROR; else rt_thread_startup(send_thread); receive_thread_ret = rt_thread_init(&receive_thread, "rec_th", receive_thread_entry, RT_NULL, receive_thread_stack, sizeof(receive_thread_stack), THREAD_PRIORITY - 1, THREAD_TIMESLICE); if (receive_thread_ret != RT_EOK) return receive_thread_ret; else rt_thread_startup(&receive_thread); return RT_EOK; } INIT_APP_EXPORT(semaphore_sample);
num1
+1,然后用延时函数模拟占用信号量的时间,延时结束后再让num2
+1,最后释放信号量;num1==num2
则表示同步成功,反之失败,最后释放信号量。串口打印信息如下:
注:互斥可以看成特殊的线程同步
/* * Date Author * 2022-02-05 issac wan */ #include <rtthread.h> #define my_printf(fmt, ...) rt_kprintf("[%u]"fmt"\n", rt_tick_get(), ##__VA_ARGS__) #define THREAD_PRIORITY 20 #define THREAD_TIMESLICE 5 #define MAX_TRUCK_SPACE (5U) // 最大5个车位 static rt_sem_t sem; static rt_thread_t park_thread = RT_NULL; static rt_thread_t pick_thread = RT_NULL; void park_thread_entry(void* param){ rt_err_t park_ret = RT_EOK; while(1){ park_ret = rt_sem_trytake(sem); // 无等待获取 if(RT_EOK == park_ret){ my_printf("成功获取1个车位, 当前还有%d个空车位", sem->value); }else{ my_printf("车位已满!"); } rt_thread_delay(1000); } } void pick_thread_entry(void* param){ while(1){ if(MAX_TRUCK_SPACE == sem->value){ my_printf("车位已全部空出!"); } else{ rt_sem_release(sem); my_printf("成功释放1个车位, 当前共有%d个空车位", sem->value); } rt_thread_delay(3000); } } int semaphore_sample(void){ sem = rt_sem_create("sem", MAX_TRUCK_SPACE, RT_IPC_FLAG_PRIO); if (sem == RT_NULL) return -RT_ERROR; park_thread = rt_thread_create("park_th", park_thread_entry, RT_NULL, 512, THREAD_PRIORITY - 1, THREAD_TIMESLICE); if (park_thread == RT_NULL) return -RT_ERROR; else rt_thread_startup(park_thread); pick_thread = rt_thread_create("pick_th", pick_thread_entry, RT_NULL, 512, THREAD_PRIORITY, THREAD_TIMESLICE); if (pick_thread == RT_NULL) return -RT_ERROR; else rt_thread_startup(pick_thread); return RT_EOK; } INIT_APP_EXPORT(semaphore_sample);
串口打印信息如下: