飞道的博客

高薪秘诀,跟着AliOS Things轻松入门操作系统:信号量

408人阅读  评论(0)

信号量(Semaphore)是操作系统上极为常用的一种同步机制。本篇文章通过解析AliOS Things内核源码来学习信号量机制。

在AliOS Things中信号量的源码路径如下

信号量源码位置:core/rhino/k_sem.c

信号量头文件位置:core/rhino/include/k_sem.h

 

1、信号量结构体ksem_t

k_sem.h头文件定义了信号量结构体ksem_t。信号量相关的函数都基于该结构体,所以我们首先分析一下该结构体,其具体定义如下:

 

typedef struct sem_s {

  blk_obj_t   blk_obj;

  sem_count_t count;

  sem_count_t peak_count;

#if (RHINO_CONFIG_KOBJ_LIST > 0)

  klist_t     sem_item;   /**< kobj list for statistics */

#endif

  uint8_t     mm_alloc_flag;  /**< buffer from internal malloc or caller input */

} ksem_t;

 

成员说明:

(1)blk_obj 这是内核的一个基础结构体,用于管理内核结构体的基本信息。用面向对象的思想来看,它相当于ksem_t的父类。它的主要域有:blk_list阻塞队列,name对象名字,blk_policy阻塞队列等待策略(主要有优先级(PRI)和先入先出(FIO)两种),obj_type结构体类型;

(2)count记录了信号的个数;

(3)peak_count是一个统计值,记录了系统运行过程中该信号量的最高信号个数。

(4)sem_item是一个链表节点,用来把信号量插入到全局链表,主要用作调试、统计。

(5)mm_alloc_flag是一个内存标记,用来表示该结构体的内存是静态分配的还是动态分配的。

 

2、创建信号量函数sem_create

创建信号量的核心函数是sem_create,它的原型如下:

static kstat_t sem_create(ksem_t *sem, const name_t *name, sem_count_t count,

                         uint8_t mm_alloc_flag);

参数含义:

sem:信号量结构体指针;

name:信号量名字,用户可以为自己的信号量指定名字,以便于调试区分;

count:初始信号个数;

mm_alloc_flag:内存类型,即sem指向的内存是静态分配的还是动态分配的。若为动态分配,则在删除信号量时需要释放sem指向的结构体内存。

 

在该函数内:

(1)首先用语句CPSR_ALLOC()定义一个内部变量,该变量被临界区语句RHINO_CRITICAL_ENTER()/RHINO_CRITICAL_EXIT()使用。在单核上临界区用关中断保护,因此CPSR_ALLOC()其实就是定义一个保存中断状态的变量。RHINO_CRITICAL_ENTER()将读取当前中断状态并保存下来,然后关中断。RHINO_CRITICAL_EXIT()将恢复中断状态。所以按如下方式进入/退出临界区:

CPSR_ALLOC();

RHINO_CRITICAL_ENTER();   //进入临界区

……   //临界区

RHINO_CRITICAL_EXIT();    //退出临界区

 

在sem_create函数中,用来保护对全局链表的访问。

klist_insert(&(g_kobj_list.sem_head), &sem->sem_item);  //把信号量结构体插入全局链表

(2)NULL_PARA_CHK()宏用来做指针非空检查。若发现传入的sem或name指针为NULL,将直接返回。

(3)接下来将初始化信号量结构体。阻塞策略被初始化为BLK_POLICY_PRI,意思是当有多个任务阻塞在信号量上时,高优先级任务优先获得信号量。另外一种策略是BLK_POLICY_FIFO,即先阻塞的任务优先获得信号量。ksem_t被初始化的类型为RHINO_SEM_OBJ_TYPE。

 

函数krhino_sem_create()和krhino_sem_dyn_create()是创建信号量的对外接口,两者的差别是前者是静态创建(K_OBJ_STATIC_ALLOC),即ksem_t结构体的内存由外部传入。后者是动态创建(K_OBJ_DYN_ALLOC),该函数内将调用krhino_mm_alloc动态分配ksem_t结构体的内存,并通过入参sem把创建的结构体对象传给调用者,所以入参sem的类型是ksem_t **。

 

分析信号量创建函数的源码,我们可以得出信号量的第一个特点:创建信号量时可以指定初始信号量个数。

 

3、请求信号量函数krhino_sem_take

函数krhino_sem_take用于请求信号量。该函数原型为:

kstat_t krhino_sem_take(ksem_t *sem, tick_t ticks);

参数说明:

(1)sem 指向信号量结构体的指针;

(2)ticks 阻塞时间。如果当前没有信号量,任务最多阻塞ticks个系统时钟。两个特殊值是:(a)RHINO_NO_WAIT,若当前没有信号量则直接返回;(2) RHINO_WAIT_FOREVER,一直阻塞任务直到获得信号量为止。

 

在该函数内:

(1)NULL_PARA_CHK(sem);检查入参sem是否为NULL,如果为NULL直接退出函数;

(2)RHINO_CRITICAL_ENTER();用于进入临界区,因为多个任务可能同时访问sem结构体,所以需要临界区保护;

(3)调用cpu_cur_get()用来获得当前核号,这是为了支持多核架构。单核处理器上,这个函数返回0;

(4)TASK_CANCEL_CHK(sem)用来检查当前任务是否已经被终止。INTRPT_NESTED_LEVEL_CHK()用来检查是否在中断上下文。中断处理函数不允许被阻塞,所以不能调用krhino_sem_take();

(5)条件判断语句if (sem->blk_obj.obj_type != RHINO_SEM_OBJ_TYPE)用来检查sem类型是否为RHINO_SEM_OBJ_TYPE。这可以避免信号量结构体被删除后误用;

(6)现在正式进入到申请信号量的逻辑。如果sem->count大于0,说明当前有信号量,那么sem->count减1后返回即可,申请成功。当然,返回前要用语句RHINO_CRITICAL_EXIT();退出临界区;

(7)如果当前没有信号量,且等待时间为RHINO_NO_WAIT,那么直接返回,申请失败;

(8)剩下的情况是:当前没有信号量,调用者想等待一段时间,直到超时或者获得信号量。条件判断g_sched_lock[cur_cpu_num] > 0u用来检查当前是否允许调度。由于后续代码将挂起当前任务,并调度到其他任务。因此若g_sched_lock[cur_cpu_num] > 0u(不允许调度)那么就不能执行下面的代码了,直接退出临界区并返回;

(9)语句pend_to_blk_obj将置当前任务为非就绪态;

(10)语句RHINO_CRITICAL_EXIT_SCHED();将退出临界区并触发调度。这里将切换到其他任务,直到超时或者有任务释放信号量并唤醒当前任务;

(11)语句pend_state_end_proc用来做唤醒后的处理,主要是判断因为什么原因被唤醒:获得了信号量或超时到期或信号量被删除等。调用krhino_sem_take的地方可以根据返回值决定后续操作;

 

4、释放信号量函数sem_give

这个接口也被称为发送信号量。函数原型如下:

static kstat_t sem_give(ksem_t *sem, uint8_t opt_wake_all)

参数说明:

(1)sem 指向信号量结构体的指针;

(2)opt_wake_all 唤醒一个等待任务(WAKE_ONE_SEM)还是唤醒所有任务(WAKE_ALL_SEM)。

 

在该函数内:

(1)RHINO_CRITICAL_ENTER();用于进入临界区,因为多个任务可能同时访问sem结构体,所以需要临界区保护。

(2)条件判断语句if (sem->blk_obj.obj_type != RHINO_SEM_OBJ_TYPE)用来检查sem类型是否为RHINO_SEM_OBJ_TYPE。这可以避免信号量结构体被删除后误用。

(3)函数cpu_cur_get()用来获得当前核号,这是为了支持多核架构。单核处理器上,这个函数返回0。

(4)条件语句if (is_klist_empty(blk_list_head))用来判断阻塞队列是否为空,即当前没有任务阻塞在该信号量上。这种情况下,处理比较简单,信号量个数加1即可。

这里先做了一个错误检查,如果sem->count == (sem_count_t)-1说明系统出现问题了。如果没有问题,则信号量个数加1:

sem->count++;

如果sem->count大于sem->peak_count将更新sem->peak_count,以记录历史最高信号量个数。

(5)如果有任务正在等待该信号量,那么不用对sem->count加1了,直接唤醒任务消耗掉该信号量即可。这里又分为两种情况:如果opt_wake_all不为0(WAKE_ALL_SEM),则调用pend_task_wakeup唤醒所有任务。否则只唤醒一个任务。

 

对外接口函数krhino_sem_give/krhino_sem_give_all都调用sem_give释放信号量,两者的差别是,前者只唤醒一个阻塞任务,后者将唤醒所有阻塞任务。

 

分析信号量释放函数的源码,我们可以得出信号量的另外两个特点:

第二个特点:信号量请求和释放不需要成对出现,没有获得信号量也可以释放信号量。所以中断处理函数可以释放信号量,这常用在中断与任务之间的同步。

第三个特点:可以唤醒所有阻塞的在该信号量上的任务。

 

5、删除信号量函数krhino_sem_del/krhino_sem_dyn_del

krhino_sem_dyn_del用来删除krhino_sem_dyn_create创建的信号量。krhino_sem_del用来删除krhino_sem_create创建的信号量。这两组函数必须配套使用,否则将出现严重问题。

krhino_sem_dyn_del相比krhino_sem_del多了一步释放信号量结构体的操作,这里我们分析krhino_sem_dyn_del函数:

(1)函数入口处和前面的函数处理类似,主要是做一些正确性检查;

(2)sem->blk_obj.obj_type = RHINO_OBJ_TYPE_NONE设置结构体的类型为NONE。该语句的好处是,若信号量释放后被误用,能被检测出来。

(3)循环语句调用pend_task_rm函数唤醒所有阻塞在该信号量上的任务。如果没有这步操作,一旦信号量被删除,等待该信号量的任务将永远都不会被唤醒了。

(4)语句klist_rm(&sem->sem_item);用于把信号量结构体从全局g_kobj_list.sem_head链表删除;

(5)最后退出临界区,并调用krhino_mm_free释放信号量结构体的内存空间。

 

分析信号量删除函数的源码,我们可以看到使用信号量的一个注意点:

若任务调用krhino_sem_take()被阻塞了,那么当删除信号量时将唤醒该任务,所以krhino_sem_take()返回时不代表一定获得了信号量,应判断返回值是否为RHINO_SUCCESS。

 

6、示例

在该示例中,任务2每隔1秒发送一个信号量。任务1请求信号量,收到信号量后打印一行输出。


  
  1. /* 定义信号量结构体*/
  2. ksem_t sem_test;  
  3. /* 定义任务相关资源*/
  4. ktask_t     test_task1_tcb;
  5. cpu_stack_t test_task1_stack[TEST_TASK_STACKSIZE];
  6. ktask_t     test_task2_tcb;
  7. cpu_stack_t test_task2_stack[TEST_TASK_STACKSIZE];
  8. /* 前向声明任务入口函数*/
  9. static void test_task1(void *arg);
  10. static void test_task2(void *arg);
  11. /* 主入口 */
  12. int application_start(int argc, char *argv[])
  13. {
  14.     /* 静态创建信号量,初始个数为0 */
  15.    krhino_sem_create(&sem_test, "sem_test", 0);
  16.     /* 创建两个测试任务 */
  17.    krhino_task_create(&test_task1_tcb, TEST_TASK1_NAME, 0, TEST_TASK1_PRI, 50,
  18.                       test_task1_stack, TEST_TASK_STACKSIZE, test_task1, 0);
  19.    krhino_task_create(&test_task2_tcb, TEST_TASK2_NAME, 0, TEST_TASK2_PRI, 50,
  20.                       test_task2_stack, TEST_TASK_STACKSIZE, test_task2, 0);
  21. }
  22. /* 任务1的入口 */
  23. static void test_task1_entry(void *arg)
  24. {
  25.     kstat_t stat;
  26.     while ( 1) {
  27.         /* 请求信号量*/
  28.        stat = krhino_sem_take(&sem_test, RHINO_WAIT_FOREVER);
  29.         if (stat == RHINO_SUCCESS) {
  30.             printf( "revc sem\r\n");
  31.        }
  32.    }
  33. }
  34. /* 任务2的入口 */
  35. static void test_task2(void *arg) {
  36.     while( 1) {
  37.         /* 睡眠1s */
  38.        aos_msleep( 1000);
  39.         /* 释放信号量*/
  40.        krhino_sem_give(&sem_test);
  41.    }
  42. }

 

7、总结

本文分析了AliOS Things信号量源码,并总结出三个特点和一个使用注意事项。留了krhino_sem_count_get和krhino_sem_count_set两个接口未分析,有了上面的分析基础,读者可以试着自己分析一下。也希望大家以此文为契机开始阅读AliOS Things内核源码,并欢迎投稿。

 

8、开发者技术支持

如需更多技术支持,可加入钉钉开发者群,或者关注微信公众号

更多技术与解决方案介绍,请访问阿里云AIoT首页https://iot.aliyun.com/


转载:https://blog.csdn.net/HaaSTech/article/details/113919776
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场