OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 4120|回复: 0

完善自制OS--加入信号量机制!

[复制链接]

1158

主题

1170

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
4993
金钱
4993
注册时间
2019-5-8
在线时间
1260 小时
发表于 2021-8-18 12:28:50 | 显示全部楼层 |阅读模式
本帖最后由 正点原子运营 于 2021-11-1 10:50 编辑

以下文章摘自微信公众号——开源电子网《完善自制OS--加入信号量机制!》
更多技术文章,请扫下方二维码关注

开源电子网,扫码2222222.png




本文讨论内容

  在上一篇文章中,笔者在CM3内核中实现了多任务调度。那么一个OS仅仅有任务调度是不够的!我们还要加入一下基本的功能,对于大多数OS使用者来说,最常用的功能可能就是信号量了,那么本文就在我们之前搭建好的多任务调度基础上制作出一个信号量机制吧!本文搭建的信号量机制几乎适用于所有系列的处理器,因为信号量机制对底层的依赖并不强,我们只要引入一点任务调度的功能就可以完成信号量机制的创建了!下面开源我搭建出来的信号量机制源文件:

链接:https://pan.baidu.com/s/1gSjKok3uAM5vsGTD2uE3DA
提取码:8xtx

第一节 带信号量的OS快速体验

  读者下载了OS的文件源码后,里面会有一个“WenOS(多任务调度+信号量机制)_内核文件”文件夹,然后我们找来一份源码,一定要是CM3(Cortex㎝3)内核的处理器!此处找来“正点原子-战舰(STM32F103ZE)开发板-库函数版本-实验1跑马灯实验”进行移植。我们先创建一个文件夹“Middlewares”,然后将WenOS直接复制过去,如下图:

1.png
  然后我们创建两个文件组,分别存放多任务调度源码和功能源码,我这里命名为“Middlewares\WenOS”和“Middlewares/WenOS_fun”,将C文件和汇编文件添加进去,然后记得包含头文件!如下图:

2.png
  最后一步找到"stm32f10x_it.c”文件将PendSV和SysTick中断服务函数给屏蔽了(因为要多任务调度需要用这两个中断函数),如下图所示:
3.png

  进过前面的步骤你已经完全搭建好WenOS信号量机制的环境了!!!那么接下来在main.c里面写个程序吧,笔者的程序如下所示,这个程序可以让LED0和LED1同时闪烁,其中LED0慢闪烁,LED1快闪烁。但资源数目=1,而且任务刚刚开始时就被任务LED0_task先申请了,LED1_task也需要该资源才能运行。当LED0_task闪烁五次后,将释放信号量。此时任务LED1_task成功申请到信号量,开始运行。此后两个任务一起执行,也就是两个LED一起闪烁!
  1. <font size="4">#include "sys.h"
  2. #include "led.h"

  3. /* OS头文件 */
  4. #include "Wenos.h"
  5. #include "WenOS_sem.h"

  6. /* 堆栈大小 */
  7. #define LED0_task_stk_size  64
  8. #define LED1_task_stk_size  64

  9. unsigned int LED0_task_stk[LED0_task_stk_size];
  10. unsigned int LED1_task_stk[LED1_task_stk_size];

  11. /* 信号量结构体 */
  12. OS_sem task_sem;

  13. /* 该函数使得LED0闪烁,延时1000ms,慢闪烁 */
  14. void LED0_task(void *p_arg)
  15. {
  16.     char i;
  17.     OS_sem_Pass(&task_sem,1);
  18.     for(i=0;i<5;i++)
  19.     {
  20.         LED0=1;
  21.         OS_delay_ticks(1000);
  22.         LED0=0;
  23.         OS_delay_ticks(1000);
  24.     }
  25.     OS_sem_Vri(&task_sem);
  26.     while(1)
  27.     {
  28.         LED0=1;
  29.         OS_delay_ticks(1000);
  30.         LED0=0;
  31.         OS_delay_ticks(1000);
  32.     }
  33. }

  34. /* 该函数使得LED1闪烁,延时200ms,快闪烁 */
  35. void LED1_task(void *p_arg)
  36. {
  37.     OS_sem_Pass(&task_sem,1);
  38.     while(1)
  39.     {
  40.         LED1=1;
  41.         OS_delay_ticks(200);
  42.         LED1=0;
  43.         OS_delay_ticks(200);
  44.     }
  45. }

  46. int main(void)
  47. {
  48.     /* 初始化LED,配置NVIC */
  49.     LED_Init();
  50.     NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
  51.    
  52.     /* 创建任务 */
  53.     OS_create_task(LED0_task,&LED0_task_stk[LED0_task_stk_size-1],0,"123");
  54.     OS_create_task(LED1_task,&LED1_task_stk[LED1_task_stk_size-1],1,"123");
  55.    
  56.     /* 初始化信号量,资源数=1 */
  57.     OS_sem_init(&task_sem,1);
  58.    
  59.     /* 开始任务调度 */
  60.     OS_Start();
  61. </font><div><font size="4">}</font></div>
复制代码




第二节 如何实现信号量机制

  信号量的两个基本操作就是P操作和V操作了!这两个是最核心的操作。当初始化完信号量后,程序要使用信号量执行的函数都是P操作函数或者V操作函数!下面带初学OS的读者认识一下这两种操作,如果读者已经十分熟悉这两个操作,可以跳过本节的内容!

  信号量的基本操作为P操作和V操作(通俗来讲,P操作就是申请信号量,此时信号量的值会-1,而V操作恰好相反,V操作会让信号量的值+1),假如信号量的值为S。

  则P(S)的主要功能是:先执行S=S-1;若S>=0,则表示申请到资源,进程继续执行;若S<0则无资源可用,阻塞该进程,并将它插入该信号量的等待队列Q中。

  而V(S)的主要功能是:先执行S=S+1;若S>0,则表示资源先前不为0(资源足够),原进程继续执行;若S<=0则表示资源先前不足,但是现在释放了一个资源,那么等待队列Q中的第一个进程将会被移出,使得其变为就绪状态并插入就绪队列,然后再返回原进程继续执行。

  如下图所示,可以表示P操作和V操作的简示图。初学OS的读者应理解PV操作的核心思想,这样将有利于你更好的理解信号量的本质。
4.jpg


第三节 信号量机制的具体实现

1.管理一下内核的功能模块!
  一个完整的OS将会有十分多的内核操作。虽然笔者开发的OS现在没有这么多功能,但是为了以后我们能开发出更多的功能,我们必须要对这些内核操作进行管理。在这种管理体系下,我们就可以轻易剪裁内核的大小了,那么我们就制作出一个Config配置文件,它表示管理内核的功能模块开启与否!如果对OS有一定了解的读者对这种管理模式其实并不陌生。那么我的内核功能的管理文件名就叫“WenOSConfig.h”,当我们需要使用某一内核功能的时候,我们将相关宏定义设置成1,否则宏定义=1,这样就可以在编译前拦截下相关内核模块的编译。文件内容如下:
  1. /* WenOS Kernel V1.0.0 */

  2. #ifndef WENOS_CONFIG_H
  3. #define WENOS_CONFIG_H

  4. #define WenOS_sem_fun    1   /* 信号量功能 */

  5. #endif
复制代码




2.
描述一下信号量的原型!
  信号量是个抽象的概念,我们需要将其具体化!也就是构建一下信号量的数据类型,让它具有各种属性!

  那么信号量的数据类型有哪些?这个其实可以推导一下就知道了。首先信号量是管理资源的,那么它必定有一个数据是表示资源数目,这里我就设置为signedchar类型也就是最多拥有127个资源!相信这个资源数已经满足大部分的应用场景了。接下来我们是不是要知道一下信号量变量的内存位置啊?这样我们就可以对信号量进行删除了(这不是真正意义上的将信号量彻底删除)!最后想想多线程中用信号量会出现什么现象,那就是多个线程都申请这个信号量。假如只有少量资源,但又有大量线程进行申请同个信号量,我们是不是要记录一下哪些线程没有申请到信号量,以后有信号量时再通知线程它获得信号量了!我们现在脑海里已经构思出信号量的原型了,那么用代码描述一下它吧,笔者的源码如下:

  1. typedef struct  p_sem
  2. {
  3.     /* 初始化资源数目 */
  4.     signed char sem_soure_num;
  5.    
  6.     /* 自动记录信号量数值的内存位置 */
  7.     signed char men_list_sit;
  8.    
  9.     /* 可能会有多个任务请求信号量,这个表格记录这被挂起的任务 */
  10.     unsigned int  OS_sem_task;
  11.    
  12. }OS_sem; /* 信号量结构体 */
复制代码


3.管理一下内存池!
  这里我们把全部信号量放入一个容器进行管理,这个容器就是内存池了!

  有读者会问,每个信号量结构体都占据一定的空间了,为什么还需要建立内存池啊!我的答案是内存池其实非必要,但是为了统一管理所有的信号量,我们还是要建立这样一个容器存放所有的信号量的资源!我们的OS以后不仅只有信号量,可能还会拥有各种内核机制,如果都要分别去访问对应的信号量太麻烦了!所有我们将信号量统一管理一下!我们的内存池不需要存放整个信号量结构体,这样太占空间了,我们只需要记录一下各信号量下还有多少资源便可!如下是笔者写的信号量内存池机制,该内存池最多容纳32个信号量,估计也能满足大多的应用场合了。
  1. <font size="4">signed char membase[32];       /* 内存池 */
  2. unsigned int men_list = 0xff; /* 内存映射表格,bitx=1,表示未被使用 */

  3. /* 自动查找空闲的内存池,并且初始化内存池 */
  4. void m_malloc(signed char sem_source_num,signed char * men_list_sit)
  5. {
  6.     signed char free_men=0;
  7.     unsigned int x = men_list;
  8.    
  9.     /* 类似二分查找的算法,如果bitk=1,则x=k(min) */
  10.     if (0 == (x & 0X0000FFFF))
  11.     {
  12.         x >>= 16;
  13.         free_men += 16;
  14.     }
  15.     if (0 == (x & 0X000000FF))
  16.     {
  17.        x >>= 8;
  18.         free_men += 8;
  19.     }
  20.     if (0 == (x & 0X0000000F))
  21.     {
  22.         x >>= 4;
  23.         free_men += 4;
  24.      }
  25.      if (0 == (x & 0X00000003))
  26.     {
  27.         x >>= 2;
  28.         free_men += 2;
  29.     }
  30.     if (0 == (x & 0X00000001))
  31.     {
  32.         free_men += 1;
  33.     }
  34.    
  35.     *men_list_sit = free_men;
  36.     membase[free_men] = sem_source_num;   /* 初始化资源数 */
  37. }

  38. /* 释放内存池 */
  39. __inline void sem_free(signed char men_list_sit)
  40. {
  41.     men_list&=~(0x01<<men_list_sit); /* 从内存映射表格中标志当前为空闲内存块 */
  42. </font><div><font size="4">}</font></div>
复制代码

4.管实现创建信号量和删除信号量!
  上面建立了一张信号量的管理内存池。那么删除信号量就是将信号量占据的内存池位置空出来!也就是在信号量内存映射表格将对应的位置标志为1,表示这个位置可以容纳新的信号量!而创建信号量则是将内存映射表格将对应的位置标志为0,这部分并不难,笔者的源码如下:

  1. <font size="4">void OS_sem_init(OS_sem *sem,signed char soure_num)
  2. {
  3.     sem->sem_soure_num=soure_num;
  4.     m_malloc(sem->sem_soure_num,&sem->men_list_sit);
  5.    
  6.     /* 初始化OS_sem_task列表 */
  7.     sem->OS_sem_task = 0x00000000;
  8. }

  9. void OS_sem_del(OS_sem *sem)
  10. {
  11.     sem_free(sem->men_list_sit);
  12. </font><div><font size="4">}</font></div>
复制代码




5.P操作的实现!
  P操作也就是申请信号量,这部分尤其核心!笔者在第二节讲述了信号量的P操作和V操作的实现过程,现在就来实践一下。

  首先我们要注意一个问题,那就是并不是所有任务都希望无休止的等待信号量,特点场景下,线程就算获取不到信号量它也要继续执行特定任务!所有我们需要设计个接口给上层应用让它选择是否需要无休止的等待信号量的带来,这个接口的具体代码其实就是一个参数值,笔者设计当参数sem_option=1时,任务将会被挂起,此时任务将会处于休眠状态,无休止的等待信号量!当sem_option=0时,任务并不会无休止等待,此时函数的返回值=1就表示任务已经申请到了信号量,返回值=0则表示未获取到信号量!那么问题来了,如何让任务处于休眠状态呢?这就要引用到上一篇文章中创建多任务调度机制的就绪表了,回忆一下,任务就绪表就是管理着所有线程的状态,那么我们只要让任务就绪表中任务的对应位失能(=0)再调用任务切换函数就可以让任务休眠了!笔者设计的源码如下:
  1. <font size="4">extern unsigned int  OS_readly_task;
  2. extern unsigned char OS_present_prio; /* 记录当前运行的任务优先级 */

  3. char OS_sem_Pass(OS_sem *sem,unsigned char sem_option)
  4. {
  5.     membase[sem->men_list_sit]--;
  6.    
  7.     if(sem_option==1)  /* 任务可能会进入休眠 */
  8.     {
  9.         if(membase[sem->men_list_sit]>=0)
  10.         {
  11.             return 1;   /* 申请到信号量了 */
  12.         }
  13.         else
  14.         {
  15.             /* 记录当前被挂起的任务 */
  16.             sem->OS_sem_task|=(0x00000001<<OS_present_prio);
  17.             
  18.             /* 任务休眠 */
  19.             OS_readly_task&=~(0x00000001<<OS_present_prio);
  20.             OS_task_schedule();
  21.         }
  22.     }
  23.     else if(sem_option==0)  /* 表示任务不休眠等待信号量 */
  24.     {
  25.         /* 等到信号量了 */
  26.         if(membase[sem->men_list_sit]>=0)
  27.         {
  28.             return 1;
  29.         }
  30.         else
  31.         {
  32.             /* 未等到信号量了,需要手动恢复之前信号量的状态 */
  33.             membase[sem->men_list_sit]++;
  34.             return 0;
  35.         }
  36.     }
  37.     return 0;
  38. </font><div><font size="4">}</font></div>
复制代码




6.V操作的实现!
  V操作的实现也是信号量机制的核心函数!但这部分设计其实并不难,我们如果检测到信号量可用,且还有其它线程申请信号量时,主动将等待队列中的最高优先级任务恢复便可以了。笔者的源码如下:
  1. void OS_sem_Vri(OS_sem *sem)
  2. {
  3.     unsigned int  x=sem->OS_sem_task;
  4.     unsigned int sem_sleep_high_pro=0;
  5.    
  6.     membase[sem->men_list_sit]++;
  7.    
  8.     /* 如果有信号量可用,则从进程队列中恢复任务 */
  9.     if(membase[sem->men_list_sit]<=0)
  10.     {
  11.    
  12.         /* 类似二分查找的算法 */
  13.         if (0 == (x & 0X0000FFFF))
  14.         {
  15.             x >>= 16;
  16.             sem_sleep_high_pro += 16;
  17.         }
  18.         if (0 == (x & 0X000000FF))
  19.         {
  20.            x >>= 8;
  21.             sem_sleep_high_pro += 8;
  22.         }
  23.         if (0 == (x & 0X0000000F))
  24.         {
  25.             x >>= 4;
  26.             sem_sleep_high_pro += 4;
  27.          }
  28.          if (0 == (x & 0X00000003))
  29.         {
  30.             x >>= 2;
  31.             sem_sleep_high_pro += 2;
  32.         }
  33.         if (0 == (x & 0X00000001))
  34.         {
  35.             sem_sleep_high_pro += 1;
  36.         }
  37.         
  38.         /* 从任务优先级表格中,恢复任务 */
  39.         OS_readly_task|=0x01<<sem_sleep_high_pro;
  40.         OS_task_schedule();
  41.     }
  42. }
复制代码


  至此!本文结束,读者可以点赞+在看+留言想要搭建出来的功能,我后续会继续搭建读者希望拥有的OS功能!我们下期见!!!

正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2025-6-24 16:42

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表