SYSBIOS读书笔记

 

基本要求

再读这篇文档前,默认大家已经对C语言、linux的线程有所了解,如果不是太熟悉建议先了解一下。可以看看《C程序设计语言》《linux系统编程手册》,

TI和liunx线程不同点

LINUX

Linux 有三种调度策略:分时调度策略(linux默认的调度策略平时常用)、实时调度策略(先到先服务)、实时调度策略(时间片轮转)

分时调度策略:每个线程一视同仁,没有等级高低概念,大家平分时间。

实时调度策略:先到先得,一旦占用cpu则一直运行。一直运行直到有更高优先级任务到达或自己放弃。

实时调度策略:当进程的时间片用完,系统将重新分配时间片,并置于就绪队列尾。放在队列尾保证了所有具有相同优先级的RR任务的调度公平 Linux线程优先级设置。

TI

TI只有一个调度策略:那就是 实时调度策略(先到先服务),先到先得,一旦占用cpu则一直运行。一直运行直到有更高优先级任务到达或自己放弃。俗称抢占式。

 

小结

之所以开篇就讲区别也是为了后续看代码示例,有个准备,方便快速理解,TI线程使用方式,也可以以避免踩一些坑

TI线程分类

分类简介

TI系统里面有三种线程,分别是:硬中断,软中断,任务。大家目前可以理解为这三个都是线程唯一区别就是他们的优先级不同(三个线程差距比较大,为了快速学习,初学阶段可以认为就是优先级不一样)。

硬中断(HWI)

硬件中断(Hwi)线程。Hwi(也被称为中断服务例程或ISRs)线程是SYS/BIOS中优先级最高的线程,Hwi 线程被用于执行有硬性严格时间要求的任务。这个线程一般不会让上层软件调用的,所以咱们看看就好,基本也用不上

软中断(SWI)

软件中断(Swi)线程。位于Hwi之后的类型,软中断线程产生了Hwi线程和任务线程之间的新的优先级层级。这个软中断线程基本就是上层软件能用到的最高等级线程了。SWI有多达 32个可用的优先级层级(16是默认优先级层级,0-31 ,所以程序里面是15)。

软中断线程一般用于定时器和线程控制。定时器会在规定时间后产生一个线程调用函数,这个线程等级就是软中断等级,平时最好少用定时器做长时间运算,尤其死循环,否则其它线程永远也运行不了,只能等一辈子!!!

任务线程(Task)

任务线程(TASK)就是平时用到的线程。任务线程的优先级高于后台(空闲)线程而低于软件中断。任务不同于软件中断的是它们在执行的过程中可以等待直到必要的资源可被利用。任务为每个线程需要独立 的栈。 SYS/BIOS 提供了用于任务内通信和同步的机制。这些机制包括信号量, 事件,消息队列和邮箱。

       TASK也是32个等级(0-31)所以在程序里面等级的数值是15(代表第16级)。

空闲循环(Idle)线程

       空闲线程在SYS/BIOS应用中以最低优先级执行并且在一个循环(空闲循环)中一个接一个地被连续被执行,最没有存在感的线程,基本用不上。

线程抢占示意图

 

uploading.4e448015.gif转存失败重新上传取消

HOOK

HOOK并不是线程。Hwi, Swi 和任务线程提供了在一个线程的生命周期中插入用实现仪表,监视或统计数据收集目的的用户代码的可选控制点。每个这样的代码点被称为“ hook ”并且为 hook 提供的用户函数被称为“ hook 函数。

每个线程都有各种状态,注册、创建、就绪、切换、退出和删除,那么钩子函数就可以在这些状态之间运行,以达到检测线程的目的。

hook 函数只能被静态声明(在配置脚本)以便当提供 hook 函数时可被有效的调用。目前看HOOK上层软件也基本用不上。

小结

本章简单介绍了HWI,SWI, TASK 线程等,主要先让大家有个整体概念,有了这些概念,在接下来几章里,看例子就能理解快一些,注意一定要看明白线程抢占示意图!

如果想深入了解这些可以看《SYS BIOS (TI-RTOS 内核) v6.41 用户手册》这里面讲的比较细。

 

TASK

创建TASK函数

创建一个任务

uploading.4e448015.gif转存失败重新上传取消

上图就是个创建一个TASK的基本流程,下面讲一下每行的基本意思和函数的用法。

Task_Params taskParams; Task_Params 里面定义要创建的TASK的属性,比如优先级、堆栈大小、arg0(线程函数第一个参数)、arg1(线程函数第二个参数)。

Task_Handle task0,Task_Handle 存储创建后的TASK的信息,它将作为其它 Task 函数的参数

       Error_Block eb,Error_Block用于处理在任务创建期间产生的错误,主要用到eb.msg,eb.msg是个char* 指针里面存储着错误日志,如果出错了可以打印出错的具体信息

Error_init(&eb)初始化eb.

Task_Params_init(&taskParams), 初始化taskParams。

taskParams.stackSize = 512; 堆栈设置为512(字节 8位一字节)

taskParams.priority = 15设置优先级为16级(0-31,从0开始算第一级),不高不低,默认也是这个级数

task0 = Task_create((Task_FuncPtr)hiPriTask, &taskParams, &eb);创建一个task线程,这个线程要执行hiPriTask这个函数,并且线程属性需要满足taskParams里面的信息,出错信息存放在eb里面

当程序执行完上面这些语句线程只是被创建,但是不会执行,只有调用BIOS_start();后各种线程才会执行。

删除TASK函数

Void Task_delete(Task_Handle *task); 任务对象使用的内存和栈可以通过调用 Task_delete() 来回收。 Task_delete() 将从所有内部队列中移除任务并释放任务对象和栈空间

 

SWI

创建SWI

uploading.4e448015.gif转存失败重新上传取消

 

大家可以看到上面的创建和TASK的创建基本一模一样,就是函数名字有点不一样,所以代码分析就留给读者做练习,可以参考TASK创建过程来分析SWI创建过程。

SWI特有函数

Void Swi_post(Swi_Handle handle);:软中断不会主动执行,只有通过这个函数调用才会主动执行

UInt trigger;是句柄里面的一个计数器(U32)。

UInt Swi_disable(); 关闭所有软中断,这时候调用任何软中断都不好使,但是在这期间如果改变swiParams.trigger (以前是0 ,现在加一 变成1了)在调用时候trigger变化的所有软中断都会重新来一遍(即便trigger = 0    trigger++  trigger--  最后还是0 内核还是能看出来变化,这个不错!!!!)

Void Swi_or(Swi_Handle handle, UInt mask);会改变 trigger ,与 trigger做或运算并且每次必会执行软中断

Void Swi_andn(Swi_Handle handle, UInt mask);会改变 trigger   ,与 trigger做与运算,如果把trigger变成0 就会执行软中断

Void Swi_inc(Swi_Handle handle);会改变 trigger 每次加一  ,必发送。

Void Swi_dec(Swi_Handle handle);会改变 trigger 每次减一, 当trigger减到0时候会调用软中断。

Void Swi_delete(Swi_Handle *handleP); 与Swi相关联的内存被释放。Swi_delete()只能在任务线程中被调用。

注意以上任何函数,一旦成功调用回调函数,函数调用完后会把trigger重置,重置成初始化时候的那个数,这个一定要注意!

 

信号量、定时器、事件、邮箱

信号量(Semaphore)

信号量类型

信号量有两种,二进制和计数信号量。

信号量对象能被声明为计数或二进制信号量,它们能被用于任务的同步和互斥。计数和二进制信号量使用相同的APIs 。对于二进制信号量,它要么是可用的,要么是不可用的。它的值不会因增长而超过1。因此,它们用于协调最多两 个任务的共享资源的访问。二进制信号量的性能比计数信号量的性能好。

计数信号量持有一个对应可用资源数量的计数。当计数大于 0 时,请求信号量时任务不会阻塞。信号量的最大计数的值加1就是该信号量可协调的任务的数量。

要配置信号量的类型,可以再配置文件配置(在工程里面都有一个xxx.cfg的文件)。

config Mode mode = Mode_COUNTING;

信号量用法

Semaphore_Handle sem0;

Semaphore_Params attrs

Error_Block eb;

sem0 = Semaphore_create(0, &attrs, &eb);

sem1 = Semaphore_create(0, &attrs, &eb);

 

 

Semaphore_Handle Semaphore_create(Int count, const Semaphore_Params *params, Error_Block *eb); ,第一个参数是信号量出事 COUNT值,第二个参数是信号量的属性设置,第三个参数 存储错误信息。

Void Semaphore_delete(Semaphore_Handle *handleP);删除对应的信号量

Void Semaphore_post(Semaphore_Handle handle);发送一个信号量

Bool Semaphore_pend(Semaphore_Handle handle, UInt32 timeout);等待一个信号量,并可以设置一个超时值 blocked

定时器(Clock

定时器用法

uploading.4e448015.gif转存失败重新上传取消

上图就是创建过程,这里面比较主要的是clockParams.period和clockParams.startFlag。clockParams.period默认是0也就是定时器就运行一次,如果不是零,像上图没5个时钟重复运行一次。

myClock = Clock_create(myHandler1, 5, &clockParams, &eb); 第二个参数是指,从创建成功后5个时钟运行函数,注意第二个参数和period 不是一个意思,第二个参数是说创建后多少个时钟后第一次运行函数,period是指第一次运行以后,每个多少个时钟运行函数。

clockParams.startFlag = TRUE,代表调用BIOS_start()后5个时钟后调用函数,如果是FALSE需要手动调用Void Clock_start(Clock_Handle handle);才能启动

Void Clock_delete(Clock_Handle *handleP);删除定时器

注意定时创建的线程是SWI级别的,所以定时器会抢占所有TASK线程!

单点和连续时钟的时间线

uploading.4e448015.gif转存失败重新上传取消

事件(event)

事件用法

       事件提供了一种线程间通信和同步的方式。它们类似于信号量,除了在等待中的线程返回之前允许你指定必须发生的多个条件(“事件”)。事件实例与信号量相似,通过条用“pend ” 和“post ” 来使用。然而,可以在调用 Event_pend() 时额外指定等待的事件,然后调用 Event_post() 指定发出的事件。

       注意一次只能有一个任务挂起在一个事件对象上

一个事件实例可管理多达32个事件,每个事件用事件ID表示。事件IDs是由一个事件对象管理的对应每个唯一事件的简单的位掩码。每个事件的表现像一个二进制信号量

uploading.4e448015.gif转存失败重新上传取消

上图就是事件创建非常简单。

UInt Event_pend(Event_Handle handle, UInt andMask, UInt orMask, UInt32 timeout);

andMask :andMask是一堆数的组合如 andMask = Event_Id_06+ Event_Id_07,Event_pend只有收到Event_Id_06和Event_Id_07的时候才会运行,少一个都会一直挂起

orMask:orMask也是一堆数的组合orMask = Event_Id_08+ Event_Id_09,Event_pend收到任何一个(Event_Id_08或者Event_Id_09)都会运行

Void Event_post(Event_Handle handle, UInt eventMask);每次发送带个事件(就是个数字),呼应Event_pend

邮箱

邮箱用法

这个邮箱就是个固定大小的队列(不是用来发送E-mail的),

邮箱实例被多个Reader和Writer使用。邮箱模块拷贝要传递的buffer到固定大小的内部buffer中。内部buffer的大小和数量在创建 ( 或构建 ) 邮箱实例时指定。在通过Mailbox_post()发生buffer时完成拷贝。另一个拷贝发生在当buffer通过Mailbox_pend()取出时

邮箱能被用于保证进入的buffer不会超过系统对这些buffer的处理能力,当你创建邮箱时你指定邮箱的内部buffer的个数和每个buffer的大小。由于大小是在创建邮箱时指定,邮箱实例中发送和接收的所有buffer的大小必须和这个大小相同

Mailbox_Handle Mailbox_create(SizeT msgSize, UInt numMsgs, const Mailbox_Params *params, Error_Block *eb);       msgSize 存储的数据大小,numMsgs 邮箱一共能存储多少个数据

Bool Mailbox_pend(Mailbox_Handle handle, Ptr msg, UInt32 timeout); 被用于从邮箱读取一个buffer。如果可用buffer(即邮箱是空的),那么Mailbox_pend()会阻塞。Timeout参数用于等待超时,一直等待,使用BIOS_WAIT_FOREVER,不等待,使用BIOS_NO_WAIT, 时间单位为系统时钟节拍

Bool Mailbox_post(Mailbox_Handle handle, Ptr msg, UInt32 timeout); Mailbox_post() 被用于发送一个buffer到邮箱。如果buffer插槽可用(即邮箱是满的),那么Mailbox_post()阻塞。Timeout参数用于等待超时,一直等待,使用BIOS_WAIT_FOREVER,不等待,使用BIOS_NO_WAIT

邮箱事件

邮箱提供配置参数来让你的事件和邮箱相关联。这样你能同时等待邮箱消息和其它事件。

邮箱提供两个配置参数来支持邮箱的Reader(s)事件—readerEvent和readerEventId。这允许邮箱读者使用事件对象来等待邮箱信息。邮箱也为Writer(s)提供两个配置参数—writerEvent 和writerEventId。这允许邮箱Writer(s)使用事件对象来等待邮箱中有空闲空间。

这些事件句柄的名字可能会产生误导。 readerEvent 是邮箱的reader会挂起的事件,它在 Mailbox_post() 调用内由邮箱writer发出。 writerEvent 是邮箱writer会挂起的用于等待邮箱有空闲空间的事件,这样不会因邮箱满而在调用Mailbox_post()时被挂起。但是,writerEvent会在任何时候从Mailbox成功读取(即,Mailbox_pend() 返回 true)消息时被发出。

当使用事件时,线程调用 Event_pend() 并等待几个事件。因为已经从Event_pend()返回,线程应以BIOS_NO_WAIT作为超时参数 调 用Mailbox_pend()或Mailbox_post()(取决于是reader还是writer)。

 

队列和门

队列(Queue)

队列用法

队列基于双向链表实现,所以元素可在列表任意位置 插入或删除元素,并且队列元素个数没有最大值。

要在队列中增加一个结构体元素,这个元素的第一个字段必须是 Queue_Elem 类型的。接下来的示例展示了能被增加到队列中的结构体是怎样的。队列有一个 "head" ,它是列表的头部, Queue_enqueue() 添加元素到列表的尾部, Queue_dequeue()移除并返回列表头部的元素。这些函数结合起来就实现了一个FIFO队列。

uploading.4e448015.gif正在上传…重新上传取消

结果:rec: 100

rec: 200

 

队列操作

队列模块也提供多个 APIs 来遍历队列。 Queue_head() 返回队头元素(不移除它)并且Queue_next()和Queue_prev()分别返回队列中当前元素的下一个和前一个的元素。

运行时示例:下列示例演示了从头到尾遍历一个队列的方法。在这个例子中, "myQ" 是一个Queue_Handle。

uploading.4e448015.gif正在上传…重新上传取消

使用Queue_insert()和Queue_remove()可以从队列中间的任何地方移除和插入元素。Queue_insert()在指定元素的前面插入一个元素, Queue_remove() 从队列中任意位置移除指定的元素。注意,Queue没有提供用给定的索引插入或移除队列元素的APIs.

运行时示例:下列示例演示了Queue_insert()a和Queue_remove()的使用

uploading.4e448015.gif转存失败重新上传取消

原子性

       队列经常在系统中被多个线程共享,这可能导致队列被不同的线程同时修改,这会破坏队列。上面讨论的队列APIs不能防止这个问题 。但是,队列提供两个“原子”APIs,它在操作队列前禁止中断。 这些APIs 是: Queue_get(),它是 Queue_dequeue()的原子版本,而 Queue_put()是 Queue_enqueue()的原子版本

门(Gate)

门的基本用法

这里的门可以理解为linux的互斥锁。门用于防止对临界区代码的并发访问。不同的实现的门尝试锁定临界区的方式不同。线程可以被其它高优先级的线程抢占,而且一些代码段必须在一个线程完整执行完后才能被其他线程执行。修改全局变量的代码是一个需要用门保护临界区的常见的例子。

门通常是通过禁用一些抢占”层”,例如禁用任务切换甚至硬件中断,或是通过使用一个二进制信号量来工作的。所有的门的实现通过 "key" 的使用来实现嵌套。

对于禁用抢占的门函数,多个线程将可能会调用 Gate_enter() ,但是抢占不应该被恢复直到所有的线程都调用了Gate_leave()。这个功能是通过key的使用来提供的。调用Gate_enter()返回的key必须被传递回 Gate_leave()。只有使用最外面的Gate_enter()返回的key才能恢复抢占

优先级翻转

接下来的示例展示了优先级反转的问题。系统有三个任务—Low,Med,和High—每个任务的优先级如它们的名字所示。任务 Low首先运行并持有门。任务 High被调度并抢占 Low。任务 High试图持有这个门而等待它。 接着,任务 Med被调度并抢占任务 Low。现在任务 High必须等待任务 Med和任务 Low完成才可继 续。在这种情况下,在效果上,任务 Low 将任务 High 的优先级降为了 Low 。解决方案:优先级继承

为了防止优先级反转,GateMutexPri实现了优先级继承。当任务High试图获取任务Low拥有的门时,在High等待门期间,任务Low的优先级临时的被提高到High。因此,任务 High “捐赠”它的优先 级给任务Low。当多个任务等待门时,这个门的拥有者从所有在等待门的任务中获得最高的优先级。

注意事项优先级继承并不能完全防止优先级反转。任务仅在调用门的enter函数后捐赠它的优先级,因此如果任务在等待门时 优先级被提升,该优先级不会被传递给门的拥有者。

这可能发生在涉及多个门的情况下。例如系统有四个任务:VeryLow, Low, Med和High,每个任务的优先级如它们的名称所示。任务 VeryLow首先运行并获得门 A。任务 Low接着运行并获得门 B,然后等待门 A。任务 High 运行并等待门 B。任务 High 捐赠它的优先级给任务 Low,但 Low 因VeryLow被阻塞 ,因此优先级反转依然在使用门时发生。这个问题的解决方案在于它的设计。如果门A 可能需要被高优先级,时间关键的任务持有,那么其他任不能长时间持有这个门或在持有门时被阻

小结

       由于TI是抢占式调度,所以用到门锁比较少基本都是信号量。

       队列的调用最好都用原子操作,保证线程同步。

其它

BIOS 模块

BIOS_start()

BIOS_start():—应用程序的main()调用这个函数后,会将控制权交给SYS/BIOS线程调度器。 这个调用应该在由该应用程序所需的所有初始化已被执行之后才执行。这个 函数执行所有剩余的SYS/BIOS初始化然后转移控制权到准备运行的最高优先级的任务。如果任 务没被使能,那么控制直接被转移到空闲循环。除非BIOS_exit()被调 用,BIOS_start()函数并不返回

BIOS_exit()

BIOS_exit() :—当你想要SYS/BIOS应用正常终止并返回时调用这个函数。你可 以使用这个函数来传递一个整数状态值给调用BIOS_start()的函数,它通常是main()。对于SYS/BIOS应用程序,相较于System_exit(),推荐使用BIOS_exit()进行退出,因为BIOS_exit()在调用System_exit()之前会执行内部清理工作。

System模块

System_printf

System_printf(): 提供几个与printf类似的函数。这包括System_putch(),System_sprintf(),System_vprintf(),System_aprintf()(IArg类型的参数)和System_vsnprintf()(使用一个可变参数列表来传递的参数,打印指定数量的字符到一个字符缓冲区)。

强烈的推荐SYS/BIOS调用System_printf()和与它相关的函数以取代标准printf()函数。System模块提供相似的类printf功能,它们使用更少的参数。内存占用比传统的printf()小的多。System_printf()允许用户指定处理字符输出的System provider。

相较于Log模块打印函数,依然推荐System模块打印函数。因为System模块函数在很多情况下更容易使用。Log 模块用于主机仪器工具,并且它有更多选项以禁用/使能日志记录。

System_flush()

System_flush():这个函数发送所有缓冲区的输出字符到输出设备。它也发出一个断点到 IDE 。因为这个调用会暂停设备,它将影响程序的实时性

System_exit()

System_exit()—它是推荐的用于在SYS/BIOS应用中代替System_exit()的函数。BIOS_exit()函数在调用System_exit()之前执行内部清理。System_exit()能从任务中被调用,但不能从Swi或Hwi中调用。

错误处理

很多 SYS/BIOS API—尤其是那些创建对象和分配内存—都有一个参数 Error_Block。这个类型由xdc.runtime 定义。错误模块由 XDCtools 提供

 

用法

uploading.4e448015.gif转存失败重新上传取消

你可能看过一些在其他的文档中通过用 NULL 替代 Error_Block 的示例。如果一个错误发生在创建对象或者分配内存时并且使用 NULL 替代 Error_Block , 这个应用程序中止并且一个错误的原因就是使用System_printf() 输出。这可能还是系统的致命错误中最好的情况, 并且你还不想做任何的错误检查。通过和测试一个 Error_Block 的好处是当你的程序中止时完全可控。比如说,当内存无法分配为了防止中止 , 你可能想要去尝试释放其它的内存并尝试再次分配或者切换到另一个限制内存需求的模式。

 

  • 4
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值