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

信号量(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请求信号量,收到信号量后打印一行输出。

/ 定义信号量结构体/

ksem_t sem_test;

/ 定义任务相关资源/

ktask_t test_task1_tcb;

cpu_stack_t test_task1_stack[TEST_TASK_STACKSIZE];

ktask_t test_task2_tcb;

cpu_stack_t test_task2_stack[TEST_TASK_STACKSIZE];

/ 前向声明任务入口函数/

static void test_task1(void *arg);

static void test_task2(void *arg);

/ 主入口 /

int application_start(int argc, char *argv[])

{

/ 静态创建信号量,初始个数为0 /

krhino_sem_create(&sem_test, "sem_test", 0);

/ 创建两个测试任务 /

krhino_task_create(&test_task1_tcb, TEST_TASK1_NAME, 0, TEST_TASK1_PRI, 50,

                  test_task1_stack, TEST_TASK_STACKSIZE, test_task1, 0);

krhino_task_create(&test_task2_tcb, TEST_TASK2_NAME, 0, TEST_TASK2_PRI, 50,

                  test_task2_stack, TEST_TASK_STACKSIZE, test_task2, 0);


}

/ 任务1的入口 /

static void test_task1_entry(void *arg)

{

kstat_t stat;

while (1) {

   /* 请求信号量*/

   stat = krhino_sem_take(&sem_test, RHINO_WAIT_FOREVER);

   if (stat == RHINO_SUCCESS) {

       printf("revc sem\r\n");

   }

}

}

/ 任务2的入口 /

static void test_task2(void *arg) {

while(1) {

   /* 睡眠1s */

   aos_msleep(1000);

   /* 释放信号量*/

   krhino_sem_give(&sem_test);

}

}

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

8、开发者技术支持
如需更多技术支持,可加入钉钉开发者群,或者关注微信公众号
高薪秘诀,跟着AliOS Things轻松入门操作系统:信号量

上一篇:智能网联车被远程攻击了该怎么办


下一篇:透过ALSA这层迷雾剖析AliOS Things音频框架设计