文章目录
序言
本篇文章的主题是信号量和互斥信号量,学过操作系统原理的人对信号量这个词应该是不陌生的。在一个多任务系统中,有可能会有多个任务同时想要访问某个资源,如果允许这种行为的话,程序可能会出现难以预料的结果,所以我们不能让它们同时使用某个资源,信号量就是用来控制任务对资源的访问的一种机制。本篇文章主要分为三大部分:原理介绍、相关函数介绍、实验案例介绍。注:程序源码来自正点原子
原理介绍
信号量这个概念是荷兰计算机科学家E.W.Dijkstra于1965年提出的一个概念,其实这个可以去看我的另一篇文章进程间通信,看完之后我们就往下走,在UCOSIII中,信号量分为两种:二值信号量和计数信号量
二值信号量
什么是二值信号量?顾名思义,二值信号量就是只有两个值(0和1)的信号量,当它为1的时候,与它绑定的资源就可以被访问,当它为0的时候,与它绑定的资源不可以被访问。试图访问一个信号量为0的资源的任务会被放入到等待信号量的任务表中,在等待信号量的时候也可以设置超时处理,如果设定的时间任务没有等到信号量的话那么该任务就会进入就绪态。可以看出,一个信号量如果为二值信号量的话,一次只能有一个任务可以访问这个资源
计数型信号量
二值信号量使得我们一次只能有一个任务可以访问共享资源,但有时候,我们想要多个任务可以同时访问共享资源,比如进程间通信的时候。一个计数型信号量的值可以超过2,假如一个信号量的值为10,那么就有10个资源可以同时使用这个资源,每来一个任务,信号量的值就会减1,直到为0。每当一个任务离开资源的时候,信号量的值就会加1。
相关函数介绍
函数 | |
---|---|
OSSemCreate() | 创建一个信号量 |
OSSemDel() | 删除一个信号量 |
OSSemPend() | 等待一个信号量 |
OSSemPendAbort() | 取消等待 |
OSSemPost() | 释放一个信号量 |
OSSemSet() | 强制设置一个信号量的值 |
这些函数在os_sem.c这个文件中
OSSemCreate
这个函数是用来创建一个信号量的,函数原型如下
void OSSemCreate (OS_SEM *p_sem,
CPU_CHAR *p_name,
OS_SEM_CTR cnt,
OS_ERR *p_err)
- *p_sem:一个指向信号量的指针
- *p_name:信号量的名字
- *cnt:一个计数值,可以是0,或者是其他值,具体的含义,可以参见UCOSIII源码注释(一定要自己去看,学习没有捷径)
- *p_err:用来保存错误码
OSSemDel
这个函数是用来删除一个信号量的,函数原型如下
OS_OBJ_QTY OSSemDel (OS_SEM *p_sem,
OS_OPT opt,
OS_ERR *p_err)
- *p_sem:要删除的信号量的指针
- *opt:决定删除的方式,具体有哪些方式,可以看源码注释
- *p_err:用来保存错误码
OSSemPend
这个函数是用来请求一个信号量的,函数原型如下
OS_SEM_CTR OSSemPend (OS_SEM *p_sem,
OS_TICK timeout,
OS_OPT opt,
CPU_TS *p_ts,
OS_ERR *p_err)
- *p_sem:指向等待的信号量的指针
- timeout:请求信号量的时长,如果超过这个设定值,任务就不会再等待这个信号量被释放,如果为0,就会一直等待
- opt:设定等待方式,具体有哪些等待方式,可以看源码注释
- *p_ts:暂时不清楚功能
- *p_err:用来保存错误码
OSSemPendAbort
这个函数是用来终止任务等待的,函数原型如下
OS_OBJ_QTY OSSemPendAbort (OS_SEM *p_sem,
OS_OPT opt,
OS_ERR *p_err)
- *p_sem:指向信号量的指针
- opt:终止方式
- *p_err:用来保存错误码
OSSemPost
这个函数是用来释放一个信号量的,也就是任务离开信号量资源的时候,会调用这个函数来将信号量的值减一,函数原型如下
OS_SEM_CTR OSSemPost (OS_SEM *p_sem,
OS_OPT opt,
OS_ERR *p_err)
- *p_sem:指向信号量的指针
- opt:释放的方式,具体有哪些方式,看源码注释
- *p_err:用来保存错误码
OSSemSet
这个函数是用来设置信号量的默认值的,函数原型如下
void OSSemSet (OS_SEM *p_sem,
OS_SEM_CTR cnt,
OS_ERR *p_err)
- *p_sem:指向信号量的指针
- opt:设置的方式,具体有哪些方式,看源码注释
- *p_err:用来保存错误码
这个文件里面还有一些其它的函数,等到我后面整体学完UCOSIII的时候在看吧。
实验案例介绍
看了上面的原理介绍与函数介绍,是不是感觉很枯燥,根本学不进去,如果是这样,那是正常的。下面我们将进入实验环节,在本实验中,我们将切身地感受到信号量的作用并学会UCOSIII中信号量的基本使用方法,实验主要有两个:直接访问共享资源区实验、使用信号量访问共享资源区、使用信号量进行任务同步。
直接访问共享资源区
设置一个资源共享区,任务1和任务2都可以访问这片区域
先设置一个共享资源区
u8 share_resource[30];
然后就是在main函数中创建start_task任务,在start_task任务中创建task1和task2,具体的创建过程就不说了,下面是task1和task2的内容、
//任务1的任务函数
void task1_task(void *p_arg)
{
OS_ERR err;
u8 task1_str[]="First task Running!";
while(1)
{
printf("\r\n任务1:\r\n");
LCD_Fill(0,110,239,319,CYAN);
memcpy(share_resource,task1_str,sizeof(task1_str)); //向共享资源区拷贝数据
delay_ms(200);
printf("%s\r\n",share_resource); //串口输出共享资源区数据
LED0 = ~LED0;
OSTimeDlyHMSM(0,0,1,0,OS_OPT_TIME_PERIODIC,&err); //延时1s
}
}
//任务2的任务函数
void task2_task(void *p_arg)
{
OS_ERR err;
u8 task2_str[]="Second task Running!";
while(1)
{
printf("\r\n任务2:\r\n");
LCD_Fill(0,110,239,319,BROWN);
memcpy(share_resource,task2_str,sizeof(task2_str)); //向共享资源区拷贝数据
delay_ms(200);
printf("%s\r\n",share_resource); //串口输出共享资源区数据
LED1 = ~LED1;
OSTimeDlyHMSM(0,0,1,0,OS_OPT_TIME_PERIODIC,&err); //延时1s
}
}
把程序下到开发板里面,观察结果如下图
我们发现,程序并没有按照我们的预期运行。我们分析一下源码,在任务1向share_resource拷贝数据"First task Running"以后,delay_ms()函数被运行,这个函数会引发任务调度,任务2开始运行,这是任务2又向share_resource拷贝了数据“Second task Running”,然后delay_ms()函数又运行,这个函数再次引发任务调度,任务1开始运行,这时share_resource已经被修改为"Second task Running",从而导致非预期结果。所以我们需要对共享资源区进行保护
使用信号量访问共享资源区
这个实验是在上一个实验的基础上增加了信号量机制,具体如何增加,请看下面的程序分析
先定义一个信号量
OS_SEM MY_SEM;
然后在start_task()中调用OSSemCreate()函数创建一个信号量,代码如下
OSSemCreate ((OS_SEM* )&MY_SEM,
(CPU_CHAR* )"MY_SEM",
(OS_SEM_CTR)1,
(OS_ERR* )&err);
task1和task2的代码如下
//任务1的任务函数
void task1_task(void *p_arg)
{
OS_ERR err;
u8 task1_str[]="First task Running!";
while(1)
{
printf("\r\n任务1:\r\n");
LCD_Fill(0,110,239,319,CYAN);
OSSemPend(&MY_SEM,0,OS_OPT_PEND_BLOCKING,0,&err); //请求信号量
memcpy(share_resource,task1_str,sizeof(task1_str)); //向共享资源区拷贝数据
delay_ms(300);
printf("%s\r\n",share_resource); //串口输出共享资源区数据
OSSemPost (&MY_SEM,OS_OPT_POST_1,&err); //发送信号量
LED0 = ~LED0;
OSTimeDlyHMSM(0,0,1,0,OS_OPT_TIME_PERIODIC,&err); //延时1s
}
}
//任务2的任务函数
void task2_task(void *p_arg)
{
OS_ERR err;
u8 task2_str[]="Second task Running!";
while(1)
{
printf("\r\n任务2:\r\n");
LCD_Fill(0,110,239,319,BROWN);
OSSemPend(&MY_SEM,0,OS_OPT_PEND_BLOCKING,0,&err); //请求信号量
memcpy(share_resource,task2_str,sizeof(task2_str)); //向共享资源区拷贝数据
delay_ms(300);
printf("%s\r\n",share_resource); //串口输出共享资源区数据
OSSemPost (&MY_SEM,OS_OPT_POST_1,&err); //发送信号量
LED1 = ~LED1;
OSTimeDlyHMSM(0,0,1,0,OS_OPT_TIME_PERIODIC,&err); //延时1s
}
}
实验结果如下图,达到了我们的预期
使用信号量进行任务同步
首先我们要理解什么是任务同步。如下图所示,小旗子代表信号量,N代表信号量的数值,ISR每发生一次就会发送一次信号量,N的值就会加1,任务每申请成功一次资源信号量就会将N减1,直到减为0,如果任务请求超时,UCOSIII就会将任务放在任务就续表中,并返回错误码。原理如下图所示
下面我们来进行实验
定义一个信号量,用于任务同步
OS_SEM SYNC_SEM;
创建一个信号量
OSSemCreate ((OS_SEM* )&SYNC_SEM,
(CPU_CHAR* )"SYNC_SEM",
(OS_SEM_CTR)0,
(OS_ERR* )&err);
task1和task2函数实现
//任务1的任务函数
void task1_task(void *p_arg)
{
u8 key;
OS_ERR err;
while(1)
{
key = KEY_Scan(0); //扫描按键
if(key==WKUP_PRES)
{
OSSemPost(&SYNC_SEM,OS_OPT_POST_1,&err);//发送信号量
LCD_ShowxNum(150,111,SYNC_SEM.Ctr,3,16,0); //显示信号量值
}
OSTimeDlyHMSM(0,0,0,10,OS_OPT_TIME_PERIODIC,&err); //延时10ms
}
}
//任务2的任务函数
void task2_task(void *p_arg)
{
u8 num;
OS_ERR err;
while(1)
{
OSSemPend(&SYNC_SEM,0,OS_OPT_PEND_BLOCKING,0,&err); //请求信号量
num++;
LCD_ShowxNum(150,111,SYNC_SEM.Ctr,3,16,0); //显示信号量值
LCD_Fill(6,131,233,313,lcd_discolor[num%14]); //刷屏
LED1 = ~LED1;
OSTimeDlyHMSM(0,0,1,0,OS_OPT_TIME_PERIODIC,&err); //延时1s
}
}
task1会不断地扫描WKUP_PRES按键,如果WKUP_PRES被按下,task1就会post一个信号量,SYNC_SEM的值就会加task2会请求信号量,请求成功一次就将屏幕颜色刷新一次。
大家自己尝试一下,一定要自己把代码写一遍,不要只是看个例程观察个现象就结束了,别自欺欺人了,兄弟们。
总结
以前在看《现代操作系统原理》的时候一直思考到底什么是信号量,到现在才明白,它是一种思想载体,信号量的具体实现方式可以有多种。