2.6 信号量(semaphore)
2.6.1 概念
对于多任务甚至多核的操作系统,需要访问共同的系统资源。共同的系统资源包括软件共享资源和硬件共享资源。软件共享资源主要是共享内存,包括共享变量、共享队列等;硬件共享资源包括一些硬件设备的访问,如输入/输出设备、打印机等。为了避免软件访问共享资源的读写发生相互影响甚至冲突,一般在保护共享资源时,有下列几种处理方式:开关中断、信号量(semaphore)、互斥量(mutex)、锁(lock)。
(1)开关中断:一般用于单核内多任务之间的互斥,其途径在于关闭任务的调度切换,从而达到单任务访问共享资源的目的,其缺点是会影响实际的中断调度效率。
(2)信号量:多任务可以通过获取信号量来获取访问共享资源的“门禁”,可以配置信号量数目,让多个任务同时获取“门禁”,当信号量无法获取时,相关任务会按照优先级排序等待信号量释放,并让出CPU资源;其缺点是存在高低任务优先级反转的问题。这一内容我们将在本节后续讨论。
(3)互斥量:任务也可以通过获取mutex来获取访问共享资源的门禁,但是单次只有一个任务能获取到该互斥量。互斥量通过动态调整任务的优先级来解决高低优先级反转的问题。具体我们将在下一节讨论。
(4)锁:又分为自旋锁、读写锁等,目的也是通过实现同时只有一个任务或者单核在使用共享资源。它和信号量/互斥量最大的区别在于,CPU在加锁阶段一直处于休眠或者空操作阶段,不进行其他空闲任务的切换。
本节主要介绍信号量。
2.6.2 信号量与任务状态关系
任务(task)一节描述了任务的状态切换机制,此处着重描述信号量的操作对任务状态的影响。当调用krhino_sem_take()时,若信号量已被占用,任务将由RDY状态进入PEND状态;当krhino_sem_give/take超时情况下,任务将由PEND状态进入RDY状态;当任务为PEND状态时,若有其他任务调用krhino_task_suspend(),任务将由PEND状态进入PEND_SUSPENDED状态;当任务为PEND_SUSPENDED时,若有其他任务调用krhino_task_resume(),任务将由PEND_SUSPENDED状态进入PEND状态,如图2-8所示。
图2-8 信号量与任务状态关系
2.6.3 信号量的操作
1.sem创建
(1)静态创建:
kstat_t krhino_sem_create(ksem_t *sem, const name_t *name, sem_count_t count)
sem占用的内存由使用者直接传入,不在内部申请内存。
(2)动态创建:
kstat_t krhino_sem_dyn_create(ksem_t **sem, const name_t *name, sem_count_t count)
sem占用的内存在创建接口内部通过krhino_mm_alloc申请。
创建时设定sem的name来进行标识,并设置信号量的初始值。信号量的阻塞队列按照优先级排序管理。如果要修改信号量的count计数,可以通过krhino_sem_count_set来设置,通过krhino_sem_count_get来获取当前count值。
2.sem删除
(1)静态删除:
kstat_t krhino_sem_del(ksem_t *sem, const name_t *name, sem_count_t count)
(2)动态删除:
kstat_t krhino_sem_dyn_del(ksem_t **sem, const name_t *name, sem_count_t count)
两者的区别在于动态删除需要释放sem占用的内存,其他处理并无差别。
sem删除主要包含以下两个动作:
(1)从sem阻塞队列中恢复之前被pend的任务,涉及两种任务状态的切换:
PEND --> READY
PEND_SUSPENDED --> SUSPENDED
(2)将被pend的任务从tick延迟队列中删除,主要是针对获取信号量时设置了延迟时间的任务。
2.6.4 信号量的获取与释放
1.sem获取
函数原型:kstat_t krhino_sem_take(ksem_t *sem, tick_t ticks)
信号量的获取分为以下四种情况:
(1)成功获取:信号量的count计数大于0,表示未完全占用,返回take信号量成功。
(2)非等待获取:ticks设置为RHINO_NO_WAIT,表明当前如果无法获取信号量,直接返回错误RHINO_NO_PEND_WAIT。
(3)设置最大等待时间:ticks设置为非0、非全F有效值,当前任务会被加入tick的延时队列,当达到延时时间后,如果任务还被阻塞,返回超时RHINO_BLK_TIMEOUT。
(4)无限等待:ticks设置为全F,则该任务会永久等待此信号量。除非使用信号量删除接口krhino_sem_del,或者强制任务调度接口krhino_task_wait_abort。
2.sem释放
释放接口包括唤醒单个任务和唤醒所有任务:
kstat_t krhino_sem_give(ksem_t *sem) kstat_t krhino_sem_give_all(ksem_t *sem)
kstat_t krhino_sem_give只会将当前阻塞在此信号量的最高优先级任务恢复;
kstat_t krhino_sem_give_all会将阻塞在此信号量的所有任务恢复。
恢复过程分为以下几个步骤:
(1)如果当前sem没有任务阻塞,则信号量计数count++,并返回成功;
(2)否则选择唤醒单个或者多个优先级任务;
(3)将唤醒任务从tick延时队列中删除。
2.6.5 使用例程
信号量的创建跟销毁测试代码原型如下,这是一个功能性的测试用例:
CASE(test_task_comm, aos_1_015) { kstat_t ret=RHINO_SUCCESS; ret=krhino_sem_create(&g_sem, "g_sem",0); ASSERT_EQ(ret, RHINO_SUCCESS); ret=krhino_sem_is_valid(&g_sem); ASSERT_EQ(ret, RHINO_SUCCESS); ret=krhino_sem_take(&g_sem, RHINO_CONFIG_TICKS_PER_SECOND); ASSERT_EQ(ret, RHINO_BLK_TIMEOUT); ret=krhino_sem_give(&g_sem); ASSERT_EQ(ret, RHINO_SUCCESS); ret=krhino_sem_take(&g_sem, RHINO_CONFIG_NEXT_INTRPT_TICKS); ASSERT_EQ(ret, RHINO_SUCCESS); ret=krhino_sem_del(&g_sem); ASSERT_EQ(ret, RHINO_SUCCESS); }
使用信号量任务同步的测试代码原型如下,这是一个稳定性的测试用例:
CASE(test_task_comm, aos_1_016) { kstat_t ret=RHINO_SUCCESS; char task_name[10]={0}; uns igned int task_count=4; int i=0; ASSERT_TRUE(task_count%2 == 0); ret=krhino_sem_create(&g_sem, "g_sem",1); ASSERT_EQ(ret, RHINO_SUCCESS); g_var0=0; g_var1=0; for(i=0 ; i<task_count; i++){ sprintf(task_name, "task%d", i+1); if(i < (task_count>>1)){ ret=krhino_task_create(&g_task[i], task_name, NULL,10 ,50 , stack_buf[i], TEST_CONFIG_STACK_SIZE, task3 ,1); ASSERT_EQ(ret, RHINO_SUCCESS); } else { ret=krhino_task_create(&g_task[i], task_name, NULL,10 ,50 , stack_buf[i], TEST_CONFIG_STACK_SIZE, task4 ,1); ASSERT_EQ(ret, RHINO_SUCCESS); } krhino_task_sleep(1); } while(g_var1 < task_count){ krhino_task_sleep(RHINO_CONFIG_TICKS_PER_SECOND); } for(i=0 ; i<task_count; i++){ krhino_task_del(&g_task[i]); } krhino_sem_del(&g_sem); ASSERT_EQ(g_var0,0); } /* task:decrease g_var with sem */ static void task3(void *arg) { int i=0; for(i=0; i<TEST_CONFIG_SYNC_TIMES; i++){ krhino_sem_take(&g_sem, RHINO_WAIT_FOREVER); g_var0--; krhino_sem_give(&g_sem); } g_var1++; } /* task:decrease g_var with sem */ static void task4(void *arg) { int i=0; for(i=0; i<TEST_CONFIG_SYNC_TIMES; i++){ krhino_sem_take(&g_sem, RHINO_WAIT_FOREVER); g_var0++; krhino_sem_give(&g_sem); } g_var1++; }
这两个测试用例运行情况如下:
_[1 ;33mTEST [15/29 ]test_task_comm.aos_1_015 ... _[1 ;32m[OK] _[1 ;33mTEST [16/29 ]test_task_comm.aos_1_016... task name task1 :decrease task name task2:decrease task name task3 :increase task name task4 :increase g_var=0 _[1 ;32m[OK]
2.6.6 使用注意事项
1.中断禁止信号量获取检测
信号量的获取接口在中断上下文调用时很容易发生死锁问题。当被打断的上下文和打断的中断上下文要获取同一个信号量时,会发生互相等待的情况。有些内核将这种判断处理交由上层软件进行,本内核会在take信号量时进行检测,如果是中断上下文,则直接返回失败。
2.占用信号量非等待、永远等待、延时使用区别
上层应用在获取信号量时,需要按照实际的需求来安排信号量获取策略。krhino_sem_take传入延时ticks为0,获取不到信号量会立即报失败;ticks为全F时,会永远在此等待,直到获取到信号量,可能会造成该任务无法继续运行;其他值标识有最大延迟的时间上限,达到上限时,即使未获取到信号量,tick中断处理也会将任务唤醒,并返回状态为超时。
3.信号量优先级反转问题
优先级反转是在高、中、低三个优先级任务同时访问使用信号量互斥资源时,可能出现的问题。当高优先级的任务需要的信号量被低优先级任务占用时,CPU资源会调度给低优先级任务。此时,如果低优先级需要获取的另一个信号量被中优先级的pend任务所占用,那么低优先级的任务则需要等待中优先级的任务事件到来,并释放信号量,就出现了高、中优先级的任务并不是等待一个信号量,导致中优先级任务先运行的现象。
该优先级反转的缺陷,在互斥机制中得以解决,其途径在于动态提高低任务运行优先级来避免任务优先级的反转问题,详细内容参见下一节。