5.3 协作式多任务操作系统

合集下载

51—TinyOS51嵌入式操作系统微小内核

51—TinyOS51嵌入式操作系统微小内核
5.1.2 <setjmp.h>头文件
与中止函数about()和退出函数exit()相比,初看 起来,goto语句处理异常更可行,但是,goto语句 只能在函数内部跳转,即不能从一个函数直接跳转 到另一个函数。
为此,标准C函数库提供了setjmp()和longjmp() 函数,setjmp()函数相当于非局部标号,longjmp() 函数相当于goto的作用,从而解决了从一个函数直 接跳转到另一个函数的问题,即非局部跳转。头文 件<setjmp.h>申明了这些函数及同时所需的jmp_buf 数据类型。
如下图所示:
8
第5章 TinyOS51嵌入式操作系统 时间轮询调度为每个任务提供同份额的cpu执行
时间。由于纯粹的时间轮询调度不能满足实时性系统 要求,取而代之的是基于优先级抢占式调度扩充时间 轮询调度,即对同样优先级的任务使用时间片获得相 等的cpu分配时间,不同优先级的具有抢占权。
9
第5章 TinyOS51嵌入式操作系统
就绪状态 ;运行状态; 阻塞状态
10
第5章 TinyOS51嵌入式操作系统
就绪状态:当一个任务创立并准备运行时,内核将其放入 就绪状态。但不能运行,因为有一个更高优先级的任务在 执行,内核调度器根据优先级决定哪个任务先迁移到运行 状态,但处于就绪状态的任务不能直接迁移到阻塞状态。
运行状态:操作系统可让处于运行状态的低优先级任务暂 停运行,转而执行另一个处于就绪状态的高优先级任务, 这样正运行的任务就从运行状态迁移到了就绪状态。
SP
变化。
addr15~addr8
addr7~addr0
图 5.7
上下文信息 23
第5章 TinyOS51嵌入式操作系统

计算机操作系统中的多任务处理与调度算法

计算机操作系统中的多任务处理与调度算法

计算机操作系统中的多任务处理与调度算法随着计算机应用场景的多样化,多任务处理成为操作系统中必不可少的一部分。

在计算机操作系统中,多任务处理涉及到如何有效地利用计算机资源,使多个任务并发执行,以提高系统的运行效率和资源利用率。

为了实现多任务处理,操作系统需要采用相应的调度算法来决定任务的执行顺序和时间片分配,以确保系统的公平性、高效性和稳定性。

一、多任务处理的概念与分类多任务处理是指操作系统能够同时运行多个任务,使得这些任务在用户看来就像在同时执行一样。

根据任务之间的关系,多任务处理可以分为协作式和抢占式两种模式。

协作式多任务处理模式要求各个任务之间主动地协调与合作,每个任务必须主动释放CPU资源,否则其他任务无法执行。

这种模式的优点是实现简单,但是一旦有任务崩溃或者无限循环,将会导致整个系统崩溃。

抢占式多任务处理模式则由操作系统来决定任务的执行优先级和时间片分配,可以主动抢占正在执行的任务,使其他任务有机会执行。

这种模式的优点是能够保证系统的稳定性和响应性,但实现相对复杂。

二、常见的调度算法1. 先来先服务(FCFS)先来先服务是一种简单的调度算法,按照任务到达的顺序进行调度,即先到达的任务优先执行,直到该任务执行完毕或者发生阻塞。

优点是公平,缺点是无法应对执行时间较长的任务,可能导致其他任务的等待时间较长。

2. 最短作业优先(SJF)最短作业优先调度算法是根据任务的执行时间来进行调度,执行时间最短的任务先执行。

优点是能够减少平均等待时间,提高系统的执行效率,缺点是对执行时间较长的任务不公平。

3. 时间片轮转(RR)时间片轮转调度算法将任务划分为等长的时间片,每个任务按照时间片的顺序轮流执行,每个任务执行一个时间片后切换至下一个任务,循环往复。

优点是公平,能够保证每个任务都能获得一定的执行时间,同时也能快速响应用户的操作。

缺点是对于执行时间较长的任务,会造成一定的延迟。

4. 优先级调度优先级调度算法根据任务的优先级来进行调度,优先级高的任务先执行。

操作系统中的多任务处理

操作系统中的多任务处理

操作系统中的多任务处理
在操作系统中,多任务处理是指操作系统能够同时管理和执行多个任务的能力。

这种能力使得计算机系统能够更高效地利用资源,提高系统的响应速度和性能。

多任务处理有两种主要的方式:并发和并行。

并发是指在同一时间段内执行多
个任务,而并行是指在同一时刻执行多个任务。

在操作系统中,通常会采用并发的方式来实现多任务处理。

为了实现多任务处理,操作系统会使用一些技术和机制来管理和调度任务。


中最常见的技术包括进程管理、线程管理和调度算法。

进程是程序的执行实例,每个进程都有自己的独立地址空间和资源。

操作系统
通过进程管理来创建、删除和调度进程,确保它们能够正确地运行。

线程是在进程内部执行的轻量级任务,多个线程可以共享进程的资源,提高系统的并发性能。

线程管理负责创建和调度线程,保证它们能够协同工作完成任务。

调度算法是操作系统用来选择下一个要执行的任务的规则。

常见的调度算法包
括先来先服务、最短作业优先、时间片轮转和优先级调度等。

这些算法可以根据任务的特性和系统的需求来选择合适的调度方式,确保系统能够高效地完成任务。

通过多任务处理,操作系统能够更好地响应用户的请求,提高系统的利用率和
性能。

同时,多任务处理也能够提高系统的稳定性和可靠性,确保系统能够持续运行。

总的来说,操作系统中的多任务处理是一种重要的技朧,它可以提高系统的效
率和性能,提升用户体验,是现代计算机系统不可或缺的一部分。

通过合理地管理和调度任务,操作系统能够更好地满足用户的需求,提高系统的运行效率和稳定性。

操作系统的多任务处理

操作系统的多任务处理

操作系统的多任务处理操作系统是计算机硬件与应用程序之间的关系管理者,它负责调度和管理计算机资源,为应用程序提供必要的支持和服务。

而多任务处理作为操作系统的一个重要特性,在提高计算机效率和资源利用率方面具有重要作用。

本文将探讨操作系统的多任务处理机制及其应用。

一、多任务处理概述多任务处理是指操作系统能够同时执行多个任务,使得用户感觉像是多个任务在同时进行。

它分为并行处理和时间片轮转两种方式。

1. 并行处理并行处理是指在多处理器或多核处理器系统中,多个任务能够同时执行。

每个处理器或核心负责一个任务,通过并行计算、并发执行提高了整个系统的计算速度和效率。

2. 时间片轮转时间片轮转是指操作系统按照时间片(一小段时间)轮流分配给各个任务,使得多个任务可以交替执行。

每个任务在一个时间片内执行一段时间,然后让出CPU资源给其他任务,通过快速切换任务的方式,让用户感觉多个任务在同时进行。

二、多任务处理的实现机制为了实现多任务处理,操作系统需要具备以下几个重要的机制:1. 进程管理进程管理是指操作系统对进程的创建、调度、状态转换和销毁等操作。

操作系统为每个任务分配一个独立的进程,并利用进程调度算法按照一定的优先级和策略进行调度,确保每个任务都能够得到公平的执行机会。

2. 任务切换多任务处理需要操作系统具备快速任务切换的能力,以实现任务间的流畅转换。

当一个任务的时间片用尽或发生阻塞时,操作系统会迅速切换到下一个任务,保证多个任务都能够得到执行。

3. 资源分配操作系统需要合理地分配和管理CPU、内存、外设等计算机资源,以满足多个任务对资源的需求。

通过资源分配策略,操作系统能够为每个任务提供所需的资源,并确保资源的公平分配和高效利用。

三、多任务处理的应用多任务处理在操作系统中广泛应用于各种场景,提供了更加灵活和高效的计算环境。

1. 多用户环境在多用户环境下,多任务处理允许多个用户同时进行各自的操作和任务。

每个用户可以独立地运行自己的应用程序,而不会干扰其他用户的操作。

操作系统的多任务与多线程支持

操作系统的多任务与多线程支持

操作系统的多任务与多线程支持操作系统是计算机系统中最为核心的软件之一,它负责管理和控制计算机中的硬件资源以及运行程序的执行。

多任务和多线程是操作系统的两个重要特性,它们在提高计算机系统性能、资源利用率和用户体验等方面发挥着重要作用。

一、多任务支持多任务是指操作系统能够同时运行多个程序,并且给用户的感觉是这些程序在同时进行。

操作系统通过轮询或者中断的方式在不同程序之间进行切换,为每个程序分配一定的执行时间片,给用户一种同时运行多个程序的错觉。

多任务支持使得用户能够方便地在计算机上同时运行多个应用程序,例如同时打开多个浏览器窗口、编辑文档和播放音乐等。

同时,多任务也提高了计算机系统的资源利用率,因为在一个时间片内,操作系统可以将执行权交给其他程序,使得系统中的计算资源得到充分利用。

在多任务系统中,操作系统通过调度算法来决定每个程序的执行顺序和时间片大小。

常见的调度算法有先来先服务(FCFS)、时间片轮转、优先级调度等。

这些算法根据不同的系统需求和优先级策略来进行选择。

二、多线程支持多线程是指在一个程序内部,能够同时执行多个子任务或者称之为线程的部分。

多线程在一个进程内共享同一块内存空间,各个线程之间可以共享数据和资源,使得程序的并发度增加,进而提高系统的吞吐量和响应速度。

多线程支持使得程序在执行过程中能够以更高效的方式处理并发任务,因为线程之间切换的开销要远远小于进程之间的切换。

此外,多线程也能够简化程序的编写,通过将程序拆分为多个线程来处理不同的任务,使得程序的结构更加清晰和模块化。

在多线程系统中,操作系统需要提供线程的管理和调度功能。

通过线程调度算法,操作系统能够决定哪些线程先被执行、如何切换线程以及如何调整不同线程之间的优先级。

常见的线程调度算法有抢占式调度、协同式调度和时间片轮转等。

三、多任务与多线程的关系多任务和多线程是操作系统中相关但又具有不同概念和作用的特性。

多任务是指操作系统能够同时运行多个程序,而多线程是指一个程序内部可以同时执行多个线程。

51单片机多任务的原理及其实现

51单片机多任务的原理及其实现

51单片机多任务操作系统的原理与实现51单片机多任务操作系统的原理与实现-- 一个超轻量级的操作系统前言想了很久,要不要写这篇文章最后觉得对操作系统感兴趣的人还是很多,写吧.我不一定能造出玉,但我可以抛出砖.包括我在内的很多人都对51使用操作系统呈悲观态度,因为51的片上资源太少.但对于很多要求不高的系统来说,使用操作系统可以使代码变得更直观,易于维护,所以在51上仍有操作系统的生存机会.流行的uCos,Tiny51等,其实都不适合在2051这样的片子上用,占资源较多,唯有自已动手,以不变应万变,才能让51也有操作系统可用.这篇贴子的目的,是教会大家如何现场写一个OS,而不是给大家提供一个OS版本.提供的所有代码,也都是示例代码,所以不要因为它没什么功能就说LAJI之类的话.如果把功能写全了,一来估计你也不想看了,二来也失去灵活性没有价值了.下面的贴一个示例出来,可以清楚的看到,OS本身只有不到10行源代码,编译后的目标代码60字节,任务切换消耗为20个机器周期.相比之下,KEIL内嵌的TINY51目标代码为800字节,切换消耗100~700周期.唯一不足之处是,每个任务要占用掉十几字节的堆栈,所以任务数不能太多,用在128B内存的51里有点难度,但对于52来说问题不大.这套代码在36M主频的STC12C4052上实测,切换任务仅需2uS.#include <>#define MAX_TASKS 2 须和实际任务数一至#define MAX_TASK_DEP 12 低不得少于2个,保守值为12.unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP];unsigned char task_id; 指定的函数(参数1)装入指定(参数2)的任务槽中.如果该槽中原来就有任务,则原任务丢失,但系统本身不会发生错误.void task_load(unsigned int fn, unsigned char tid){task_sp[tid] = task_stack[tid] + 1;task_stack[tid][0] = (unsigned int)fn & 0xff;task_stack[tid][1] = (unsigned int)fn >> 8;}用该宏后,将永不返回.#define os_start(tid) {task_id = tid,SP = task_sp[tid];return;}/*======================以下为测试代码======================*/void task1(){static unsigned char i;while(1){i++;task_switch(); 么是操作系统?人脑比较容易接受"类比"这种表达方式,我就用"公交系统"来类比"操作系统"吧.当我们要解决一个问题的时候,是用某种处理手段去完成它,这就是我们常说的"方法",计算机里叫"程序"(有时候也可以叫它"算法").以出行为例,当我们要从A地走到B地的时候,可以走着去,也可以飞着去,可以走直线,也可以绕弯路,只要能从A地到B地,都叫作方法.这种从A地到B的需求,相当于计算机里的"任务",而实现从A地到B地的方法,叫作"任务处理流程"很显然,这些走法中,并不是每种都合理,有些傻子都会采用的,有些是傻子都不采会用的.用计算机的话来说就是,有的任务处理流程好,有的任务处理流程好,有的处理流程差.可以归纳出这么几种真正算得上方法的方法:有些走法比较快速,适合于赶时间的人;有些走法比较省事,适合于懒人;有些走法比较便宜,适合于穷人.用计算机的话说就是,有些省CPU,有些流程简单,有些对系统资源要求低.现在我们可以看到一个问题:如果全世界所有的资源给你一个人用(单任务独占全部资源),那最适合你需求的方法就是好方法.但事实上要外出的人很多,例如10个人(10个任务),却只有1辆车(1套资源),这叫作"资源争用".如果每个人都要使用最适合他需求的方法,那司机就只好给他们一人跑一趟了,而在任一时刻里,车上只有一个乘客.这叫作"顺序执行",我们可以看到这种方法对系统资源的浪费是严重的.如果我们没有法力将1台车变成10台车来送这10个人,就只好制定一些机制和约定,让1台车看起来像10台车,来解决这个问题的办法想必大家都知道,那就是制定公交线路.最简单的办法是将所有旅客需要走的起点与终点串成一条线,车在这条线上开,乘客则自已决定上下车.这就是最简单的公交线路.它很差劲,但起码解决客人们对车争用.对应到计算机里,就是把所有任务的代码混在一起执行.这样做既不优异雅,也没效率,于是司机想了个办法,把这些客户叫到一起商量,将所有客人出行的起点与终点罗列出来,统计这些线路的使用频度,然后制定出公交线路:有些路线可以合并起来成为一条线路,而那些不能合并的路线,则另行开辟行车车次,这叫作"任务定义".另外,对于人多路线,车次排多点,时间上也优先安排,这叫作"任务优先级".经过这样的安排后,虽然仍只有一辆车,但运载能力却大多了.这套车次/路线的按排,就是一套"公交系统".哈,知道什么叫操作系统了吧它也就是这么样的一种约定.操作系统:我们先回过头归纳一下:汽车系统资源.主要指的是CPU,当然还有其它,比如内存,定时器,中断源等.客户出行任务正在走的路线进程一个一个的运送旅客顺序执行同时运送所有旅客多任务并行按不同的使用频度制定路线并优先跑较繁忙的路线任务优先级计算机内有各种资源,单从硬件上说,就有CPU,内存,定时器,中断源,I/O端口等.而且还会派生出来很多软件资源,例如消息池.操作系统的存在,就是为了让这些资源能被合理地分配.最后我们来总结一下,所谓操作系统,以我们目前权宜的理解就是:为"解决计算机资源争用而制定出的一种约定".二.51上的操作系统对于一个操作系统来说,最重要的莫过于并行多任务.在这里要澄清一下,不要拿当年的DOS来说事,时代不同了.况且当年IBM和小比尔着急将PC搬上市,所以才抄袭PLM(好象是叫这个名吧记不太清)搞了个今天看来很"粗制滥造"的DOS出来.看看当时真正的操作系统---UNIX,它还在纸上时就已经是多任务的了.对于我们PC来说,要实现多任务并不是什么问题,但换到MCU却很头痛:1.系统资源少在PC上,CPU主频以G为单位,内存以GB为单位,而MCU的主频通常只有十几M,内存则是Byts.在这么少的资源上同时运行多个任务,就意味着操作系统必须尽可能的少占用硬件资源.2.任务实时性要求高PC并不需要太关心实时性,因为PC上几乎所有的实时任务都被专门的硬件所接管,例如所有的声卡网卡显示上都内置有DSP以及大量的缓存.CPU只需坐在那里指手划脚告诉这些板卡如何应付实时信息就行了.而MCU不同,实时信息是靠CPU来处理的,缓存也非常有限,甚至没有缓存.一旦信息到达,CPU必须在极短的时间内响应,否则信息就会丢失.就拿串口通信来举例,在标准的PC架构里,巨大的内存允许将信息保存足够长的时间.而对于MCU来说内存有限,例如51仅有128字节内存,还要扣除掉寄存器组占用掉的8~32个字节,所以通常都仅用几个字节来缓冲.当然,你可以将数据的接收与处理的过程合并,但对于一个操作系统来说,不推荐这么做.假定以115200bps通信速率向MCU传数据,则每个字节的传送时间约为9uS,假定缓存为8字节,则串口处理任务必须在70uS内响应.这两个问题都指向了同一种解决思路:操作系统必须轻量轻量再轻量,最好是不占资源(那当然是做梦啦).可用于MCU的操作系统很多,但适合51(这里的51专指无扩展内存的51)几乎没有.前阵子见过一个"圈圈操作系统",那是我所见过的操作系统里最轻量的,但仍有改进的余地.很多人认为,51根本不适合使用操作系统.其实我对这种说法并不完全接受,否则也没有这篇文章了.我的看法是,51不适合采用"通用操作系统".所谓通用操作系统就是,不论你是什么样的应用需求,也不管你用什么芯片,只要你是51,通通用同一个操作系统.这种想法对于PC来说没问题,对于嵌入式来说也不错,对AVR来说还凑合,而对于51这种"贫穷型"的MCU来说,不行.怎样行量体裁衣,现场根据需求构建一个操作系统出来!看到这里,估计很多人要翻白眼了,大体上两种:1.操作系统那么复杂,说造就造,当自已是神了2.操作系统那么复杂,现场造一个会不会出BUG哈哈,看清楚了问题出在"复杂"上面,如果操作系统不复杂,问题不就解决了事实上,很多人对操作系统的理解是片面的,操作系统不一定要做得很复杂很全面,就算仅个多任务并行管理能力,你也可以称它操作系统.只要你对多任务并行的原理有所了解,就不难现场写一个出来,而一旦你做到了这一点,为各任务间安排通信约定,使之发展成一个为你的应用系统量身定做的操作系统也就不难了.为了加深对操作系统的理解,可以看一看<<演变>>这份PPT,让你充分了解一个并行多任务是如何一步步从顺序流程演变过来的.里面还提到了很多人都在用的"状态机",你会发现操作系统跟状态机从原理上其实是多么相似.会用状态机写程序,都能写出操作系统.三.我的第一个操作系统直接进入主题,先贴一个操作系统的示范出来.大家可以看到,原来操作系统可以做得么简单.当然,这里要申明一下,这玩意儿其实算不上真正的操作系统,它除了并行多任务并行外根本没有别的功能.但凡事都从简单开始,搞懂了它,就能根据应用需求,将它扩展成一个真正的操作系统.好了,代码来了.将下面的代码直接放到KEIL里编译,在每个task()函数的"task_switch();"那里打上断点,就可以看到它们的确是"同时"在执行的.#include <>#define MAX_TASKS 2 须和实际任务数一至#define MAX_TASK_DEP 12 低不得少于2个,保守值为12.unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP];unsigned char task_id; 指定的函数(参数1)装入指定(参数2)的任务槽中.如果该槽中原来就有任务,则原任务丢失,但系统本身不会发生错误.void task_load(unsigned int fn, unsigned char tid){task_sp[tid] = task_stack[tid] + 1;task_stack[tid][0] = (unsigned int)fn & 0xff;task_stack[tid][1] = (unsigned int)fn >> 8;}用该宏后,将永不返回.#define os_start(tid) {task_id = tid,SP = task_sp[tid];return;}/*==================以下为测试代码=====================*/void task1(){static unsigned char i;while(1){i++;task_switch();现在来看看这个多任务系统的原理:这个多任务系统准确来说,叫作"协同式多任务".所谓"协同式",指的是当一个任务持续运行而不释放资源时,其它任务是没有任何机会和方式获得运行机会,除非该任务主动释放CPU.在本例里,释放CPU是靠task_switch()来完成的.task_switch()函数是一个很特殊的函数,我们可以称它为"任务切换器".要清楚任务是如何切换的,首先要回顾一下堆栈的相关知识.有个很简单的问题,因为它太简单了,所以相信大家都没留意过:我们知道,不论是CALL还是JMP,都是将当前的程序流打断,请问CALL和JMP的区别是什么你会说:CALL可以RET,JMP不行.没错,但原因是啥呢为啥CALL过去的就可以用RET跳回来,JMP过去的就不能用RET来跳回呢很显然,CALL通过某种方法保存了打断前的某些信息,而在返回断点前执行的RET指令,就是用于取回这些信息.不用多说,大家都知道,"某些信息"就是PC指针,而"某种方法"就是压栈.很幸运,在51里,堆栈及堆栈指针都是可被任意修改的,只要你不怕死.那么假如在执行RET前将堆栈修改一下会如何往下看:当程序执行CALL后,在子程序里将堆栈刚才压入的断点地址清除掉,并将一个函数的地址压入,那么执行完RET后,程序就跳到这个函数去了.事实上,只要我们在RET前将堆栈改掉,就能将程序跳到任务地方去,而不限于CALL里压入的地址.重点来了......首先我们得为每个任务单独开一块内存,这块内存专用于作为对应的任务的堆栈,想将CPU交给哪个任务,只需将栈指针指向谁内存块就行了.接下来我们构造一个这样的函数:当任务调用该函数时,将当前的堆栈指针保存一个变量里,并换上另一个任务的堆栈指针.这就是任务调度器了.OK了,现在我们只要正确的填充好这几个堆栈的原始内容,再调用这个函数,这个任务调度就能运行起来了.那么这几个堆栈里的原始内容是哪里来的呢这就是"任务装载"函数要干的事了.在启动任务调度前将各个任务函数的入口地址放在上面所说的"任务专用的内存块"里就行了!对了,顺便说一下,这个"任务专用的内存块"叫作"私栈",私栈的意思就是说,每个任务的堆栈都是私有的,每个任务都有一个自已的堆栈.话都说到这份上了,相信大家也明白要怎么做了:1.分配若干个内存块,每个内存块为若干字节:这里所说的"若干个内存块"就是私栈,要想同时运行几少个任务就得分配多少块.而"每个子内存块若干字节"就是栈深.记住,每调一层子程序需要2字节.如果不考虑中断,4层调用深度,也就是8字节栈深应该差不多了.unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP]当然,还有件事不能忘,就是堆指针的保存处.不然光有堆栈怎么知道应该从哪个地址取数据啊unsigned char idata task_sp[MAX_TASKS]上面两项用于装任务信息的区域,我们给它个概念叫"任务槽".有些人叫它"任务堆",我觉得还是"槽"比较直观对了,还有任务号.不然怎么知道当前运行的是哪个任务呢unsigned char task_id当前运行存放在1号槽的任务时,这个值就是1,运行2号槽的任务时,这个值就是2....2.构造任务调度函函数:void task_switch(){task_sp[task_id] = SP;}3.装载任务:将各任务的函数地址的低字节和高字节分别入在task_stack[任务号][0]和task_stack[任务号][1]中:为了便于使用,写一个函数: task_load(函数名, 任务号)void task_load(unsigned int fn, unsigned char tid){task_sp[tid] = task_stack[tid] + 1;task_stack[tid][0] = (unsigned int)fn & 0xff;task_stack[tid][1] = (unsigned int)fn >> 8;}4.启动任务调度器:将栈指针指向任意一个任务的私栈,执行RET指令.注意,这可很有学问的哦,没玩过堆栈的人脑子有点转不弯:这一RET,RET到哪去了嘿嘿,别忘了在RET前已经将堆栈指针指向一个函数的入口了.你别把RET看成RET,你把它看成是另一种类型的JMP就好理解了.SP = task_sp[任务号];return;做完这4件事后,任务"并行"执行就开始了.你可以象写普通函数一个写任务函数,只需(目前可以这么说)注意在适当的时候(例如以前调延时的地方)调用一下task_switch(),以让出CPU控制权给别的任务就行了.最后说下效率问题.这个多任务系统的开销是每次切换消耗20个机器周期(CALL和RET都算在内了),贵吗不算贵,对于很多用状态机方式实现的多任务系统来说,其实效率还没这么高--- case switch和if()可不像你想像中那么便宜.关于内存的消耗我要说的是,当然不能否认这种多任务机制的确很占内存.但建议大家不要老盯着编译器下面的那行字"DATA = XXXbyte".那个值没意义,堆栈没算进去.关于比较省内存多任务机制,我将来会说到.概括来说,这个多任务系统适用于实时性要求较高而内存需求不大的应用场合,我在运行于36M主频的STC12C4052上实测了一把,切换一个任务不到3微秒.下回我们讲讲用KEIL写多任务函数时要注意的事项.下下回我们讲讲如何增强这个多任务系统,跑步进入操作系统时代.四.用KEIL写多任务系统的技巧与注意事项C51编译器很多,KEIL是其中比较流行的一种.我列出的所有例子都必须在KEIL 中使用.为何不是因为KEIL好所以用它(当然它的确很棒),而是因为这里面用到了KEIL的一些特性,如果换到其它编译器下,通过编译的倒不是问题,但运行起来可能是堆栈错位,上下文丢失等各种要命的错误,因为每种编译器的特性并不相同.所以在这里先说清楚这一点.但是,我开头已经说了,这套帖子的主要目的是阐述原理,只要你能把这几个例子消化掉,那么也能够自已动手写出适合其它编译器的OS.好了,说说KEIL的特性吧,先看下面的函数:sbit sigl = P1^7;void func1(){register char data i;i = 5;do{sigl = !sigl;}while(--i);}你会说,这个函数没什么特别的嘛!呵呵,别着急,你将它编译了,然后展开汇编代码再看看:193: void func1(){194: register char data i;195: i = 5;C:0x00C3 7F05 MOV R7,#0x05196: do{197: sigl = !sigl;C:0x00C5 B297 CPL sigl198: }while(--i);C:0x00C7 DFFC DJNZ R7,C:00C5199: }C:0x00C9 22 RET看清楚了没这个函数里用到了R7,却没有对R7进行保护!有人会跳起来了:这有什么值得奇怪的,因为上层函数里没用到R7啊.呵呵,你说的没错,但只说对了一半:事实上,KEIL编译器里作了约定,在调子函数前会尽可能释放掉所有寄存器.通常性况下,除了中断函数外,其它函数里都可以任意修改所有寄存器而无需先压栈保护(其实并不是这样,但现在暂时这样认为,饭要一口一口吃嘛,我很快会说到的).这个特性有什么用呢有!当我们调用任务切换函数时,要保护的对象里可以把所有的寄存器排除掉了,就是说,只需要保护堆栈即可!现在我们回过头来看看之前例子里的任务切换函数:void task_switch(){task_sp[task_id] = SP;}看到没,一个寄存器也没保护,展开汇编看看,的确没保护寄存器.好了,现在要给大家泼冷水了,看下面两个函数:void func1(){register char data i;i = 5;do{sigl = !sigl;}while(--i);}void func2(){register char data i;i = 5;do{func1();}while(--i);}父函数fun2()里调用func1(),展开汇编代码看看: 193: void func1(){194: register char data i;195: i = 5;C:0x00C3 7F05 MOV R7,#0x05 196: do{197: sigl = !sigl; C:0x00C5 B297 CPL sigl 198: }while(--i);C:0x00C7 DFFC DJNZ R7,C:00C5 199: }C:0x00C9 22 RET200: void func2(){201: register char data i;202: i = 5;C:0x00CA 7E05 MOV R6,#0x05 203: do{204: func1();C:0x00CC 11C3 ACALL func1(C:00C3) 205: }while(--i);C:0x00CE DEFC DJNZ R6,C:00CC206: }C:0x00D0 22 RET看清楚没函数func2()里的变量使用了寄存器R6,而在func1和func2里都没保护.听到这里,你可能又要跳一跳了:func1()里并没有用到R6,干嘛要保护没错,但编译器是怎么知道func1()没用到R6的呢是从调用关系里推测出来的.一点都没错,KEIL会根据函数间的直接调用关系为各函数分配寄存器,既不用保护,又不会冲突,KEIL好棒哦!!等一下,先别高兴,换到多任务的环境里再试试:void func1(){register char data i;i = 5;do{sigl = !sigl;}while(--i);}void func2(){register char data i;i = 5;do{sigl = !sigl;}while(--i);}展开汇编代码看看:193: void func1(){194: register char data i;195: i = 5;C:0x00C3 7F05 MOV R7,#0x05 196: do{197: sigl = !sigl; C:0x00C5 B297 CPL sigl 198: }while(--i);C:0x00C7 DFFC DJNZ R7,C:00C5 199: }C:0x00C9 22 RET200: void func2(){201: register char data i;202: i = 5;C:0x00CA 7F05 MOV R7,#0x05203: do{204: sigl = !sigl;C:0x00CC B297 CPL sigl205: }while(--i);C:0x00CE DFFC DJNZ R7,C:00CC206: }C:0x00D0 22 RET看到了吧哈哈,这回神仙也算不出来了.因为两个函数没有了直接调用的关系,所以编译器认为它们之间不会产生冲突,结果分配了一对互相冲突的寄存器,当任务从func1()切换到func2()时,func1()中的寄存器内容就给破坏掉了.大家可以试着去编译一下下面的程序:sbit sigl = P1^7;void func1(){register char data i;i = 5;do{sigl = !sigl;task_switch();} while (--i);}void func2(){register char data i;i = 5;do{sigl = !sigl;task_switch();}while(--i);}我们这里只是示例,所以仍可以通过手工分配不同的寄存器避免寄存器冲突,但在真实的应用中,由于任务间的切换是非常随机的,我们无法预知某个时刻哪个寄存器不会冲突,所以分配不同寄存器的方法不可取.那么,要怎么办呢这样就行了:sbit sigl = P1^7;void func1(){static char data i;while(1){i = 5;do{sigl = !sigl;task_switch();}while(--i);}}void func2(){static char data i;while(1){i = 5;do{sigl = !sigl;task_switch();}while(--i);}}将两个函数中的变量通通改成静态就行了.还可以这么做:sbit sigl = P1^7;void func1(){register char data i;while(1){i = 5;do{sigl = !sigl;}while(--i);task_switch();}}void func2(){register char data i;while(1){i = 5;do{sigl = !sigl;}while(--i);task_switch();}}即,在变量的作用域内不切换任务,等变量用完了,再切换任务.此时虽然两个任务仍然会互相破坏对方的寄存器内容,但对方已经不关心寄存器里的内容了.以上所说的,就是"变量覆盖"的问题.现在我们系统地说说关于"变量覆盖".变量分两种,一种是全局变量,一种是局部变量(在这里,寄存器变量算到局部变量里).对于全局变量,每个变量都会分配到单独的地址.而对于局部变量,KEIL会做一个"覆盖优化",即没有直接调用关系的函数的变量共用空间.由于不是同时使用,所以不会冲突,这对内存小的51来说,是好事.但现在我们进入多任务的世界了,这就意味着两个没有直接调用关系的函数其实是并列执行的,空间不能共用了.怎么办呢一种笨办法是关掉覆盖优化功能.呵呵,的确很笨.比较简单易行一个解决办法是,不关闭覆盖优化,但将那些在作用域内需要跨越任务(换句话说就是在变量用完前会调用task_switch()函数的)变量通通改成静态(static)即可.这里要对初学者提一下,"静态"你可以理解为"全局",因为它的地址空间一直保留,但它又不是全局,它只能在定义它的那个花括号对{}里访问.静态变量有个副作用,就是即使函数退出了,仍会占着内存.所以写任务函数的时候,尽量在变量作用域结束后才切换任务,除非这个变量的作用域很长(时间上长),会影响到其它任务的实时性.只有在这种情况下才考虑在变量作用域内跨越任务,并将变量申明为静态.事实上,只要编程思路比较清析,很少有变量需要跨越任务的.就是说,静态变量并不多.说完了"覆盖"我们再说说"重入".所谓重入,就是一个函数在同一时刻有两个不同的进程复本.对初学者来说可能不好理解,我举个例子吧:有一个函数在主程序会被调用,在中断里也会被调用,假如正当在主程序里调用时,中断发生了,会发生什么情况void func1(){static char data i;i = 5;do{sigl = !sigl;}while(--i);}假定func1()正执行到i=3时,中断发生,一旦中断调用到func1()时,i的值就被破坏了,当中断结束后,i == 0.以上说的是在传统的单任务系统中,所以重入的机率不是很大.但在多任务系统中,很容易发生重入,看下面的例子:void func1()....delay();....}void func2(){....delay();....}void delay(){static unsigned char i;申明为static会发生重入问题.麻烦啊for(i=0;i<10;i++)task_switch();两个并行执行的任务都调用了delay(),这就叫重入.问题在于重入后的两个复本都依赖变量i来控制循环,而该变量跨越了任务,这样,两个任务都会修改i值了.重入只能以防为主,就是说尽量不要让重入发生,比如将代码改成下面的样子:#define delay() {static unsigned char i; for(i=0;i<10;i++)task_switch();}void func1(){....delay();....}void func2(){....delay();....用宏来代替函数,就意味着每个调用处都是一个独立的代码复本,那么两个delay实际使用的内存地址也就不同了,重入问题消失.但这种方法带来的问题是,每调用一次delay(),都会产生一个delay的目标代码,如果delay的代码很多,那就会造成大量的rom空间占用.有其它办法没本人所知有限,只有最后一招了:void delay() reentrant{unsigned char i;for(i=0;i<10;i++)task_switch();}加入reentrant申明后,该函数就可以支持重入.但小心使用,申明为重入后,函数效率极低!最后附带说下中断.因为没太多可说的,就不单独开章了.中断跟普通的写法没什么区别,只不过在目前所示例的多任务系统里因为有堆栈的压力,所以要使用using来减少对堆栈的使用(顺便提下,也不要调用子函数,同样是为了减轻堆栈压力)用using,必须用#pragma NOAREGS关闭掉绝对寄存器访问,如果中断里非要调用函数,连同函数也要放在#pragma NOAREGS的作用域内.如例所示:#pragma SAVE#pragma NOAREGS 是说,如果你在不用中断时任务栈深定为8的话,现在就要定为8+4 = 12了.另外说句废话,中断里处理的事一定要少,做个标记就行了,剩下的事交给对应的任务去处理.现在小结一下:切换任务时要保证没有寄存器跨越任务,否则产生任务间寄存器覆盖. 使用静态变量解决切换任务时要保证没有变量跨越任务,否则产生任务间地址空间(变量)覆盖. 使用静态变量解决两个不同的任务不要调用同时调用同一个函数,否则产生重入覆盖. 使用重入申明解决。

什么是多任务处理?

什么是多任务处理?

什么是多任务处理?多任务处理是计算机科学领域的一个重要概念,它指的是在同一时间内处理多个任务的能力。

在现代计算机系统中,多任务处理是一项基本功能,它使得计算机可以同时执行多个程序,从而提高系统的效率和性能。

多任务处理技术是操作系统的核心组成部分,它允许用户在同一时间内进行多项工作,如同时打开多个应用程序、在浏览器中浏览网页、听音乐等等。

下面将依次介绍多任务处理的相关概念、分类和优势。

一、多任务处理的概念多任务处理是指计算机系统能够同时执行多个任务的能力。

在实际应用中,这些任务可以包括用户的各种操作、程序的运行以及系统的管理等。

通过多任务处理,计算机能够快速响应用户的操作并保持高效运行。

二、多任务处理的分类1. 抢占式多任务处理抢占式多任务处理是一种操作系统调度方式,它会根据一定的策略自动中断当前执行的任务,并切换到下一个待执行的任务。

这种方式可以保证任务的快速响应和高效运行。

2. 协作式多任务处理协作式多任务处理是指任务的切换需要由任务自身配合完成,而不是由操作系统主动调度。

任务在执行完成后主动将控制权交给其他任务,这种方式适用于处理简单任务和资源受限的环境。

三、多任务处理的优势1. 提高系统的效率和性能多任务处理能够充分利用计算机系统的资源,将多个任务分配到不同的处理器或者时间片中执行,从而提高系统的效率和性能。

2. 增加用户的工作效率多任务处理允许用户同时进行多项工作,如在处理文档的同时查看邮件,这样可以提高用户的工作效率,节省时间和精力。

3. 提高系统的稳定性和可靠性通过多任务处理,计算机系统可以更加灵活地分配资源和处理任务,当一个任务出现异常时,其他任务仍然可以正常运行,从而提高了系统的稳定性和可靠性。

尽管多任务处理技术带来了很多好处,但也存在一些挑战和限制。

例如,在多任务处理过程中,不同任务之间的资源竞争可能导致系统性能下降,需要合理分配和管理系统资源。

此外,任务的调度和切换也需要一定的时间和开销,可能会影响系统的响应速度。

操作系统的多任务处理

操作系统的多任务处理

操作系统的多任务处理多任务处理是现代操作系统的重要特性之一,它使得计算机可以同时执行多个任务。

通过合理的任务切换和资源分配,操作系统能够提高计算机的利用率和效率。

本文将介绍操作系统的多任务处理原理、策略和应用。

一、多任务处理原理多任务处理是指在一个计算机系统中同时执行多个任务的能力。

操作系统通过任务调度算法和进程管理来实现多任务处理。

在单核处理器系统中,操作系统通过时间片轮转等算法按照一定的时间片轮询切换任务,使得任务在人眼看来是同时执行的。

而在多核处理器系统中,操作系统可以将多个任务分配给多个处理器核心并行执行。

二、多任务处理策略1. 抢占式调度抢占式调度是指操作系统可以随时中断正在执行的任务,将处理器分配给其他高优先级任务的调度策略。

当有更高优先级的任务就绪时,操作系统可以立即切换到该任务并执行,以保证高优先级任务的及时响应。

2. 合作式调度合作式调度是指任务执行必须主动释放处理器资源,才能让其他任务执行的调度策略。

在合作式调度中,每个任务都需要遵守一定的规则,如不可长时间占用处理器资源,否则会影响其他任务的执行。

3. 多级反馈队列调度多级反馈队列调度算法将任务分为多个优先级队列,每个队列具有不同的时间片大小。

当任务执行完成后,如果没有新任务到达,则继续执行该队列的下一个任务;如果有新任务到达,则将该任务插入到更高优先级队列中。

这种调度策略既能保证高优先级任务优先执行,又能公平地分配处理器资源。

三、多任务处理的应用1. 多媒体播放操作系统的多任务处理能力使得计算机可以同时播放多个媒体文件,如音频、视频等。

用户可以在观看视频的同时听音乐,提高了用户体验。

2. 并行计算通过多任务处理和多核处理器,操作系统可以将大型计算任务分解为多个子任务,然后利用不同核心并行执行这些任务,提高计算速度和效率。

3. 虚拟化技术多任务处理为虚拟化技术的实现提供了基础。

操作系统可以将物理资源虚拟化为多个虚拟机,每个虚拟机可以独立运行不同的任务,实现资源的有效利用和管理。

  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

TinyOS51
tnOsTaskCreate() 任务创建
tnOsSched() tnOsTaskDel() tnOsTaskCreate() tnOsStart() tnOsInit()
作用 初始化TinyOS51 实现任务切换, 创建一个任务, 启动多任务环境, 删除一个任务, 执行下一个任务 内部变量 为任务创建资源 并执行第一个任 释放删除任务的 (内存) 务 资源(内存) 注意 调用此函数之前 必须在任务中调 该函数可在调用 必须在调用其他 可以删除其他任 tnOsInit()函数后 务或自身 用,而且每个任 函数之前调用, 必须创建至少一 并只能调用一次 务都必须周期性 任意时候调用, 个任务,该函数 地调用它 但在调用 不能重复调用, tnOsStart()函数之 且不返回 前必须调用一次
启动多任务环境
在操作系统中,运行第一个用户任务的过程被称为“启动多任务环境”,也 即运行句柄为0的任务。TinyOS51是通过调用tnOsStart()函数实现的,在内部由 longjmp()函数执行句柄为0的任务。
void tnOsStart (void) { longjmp(__GtcbTasks[0].jbTaskContext); }
tnOsTaskDel() 任务删除
名 称 名 称 函数原型 名 称 函数原型 函数原型 输入参数 输入参数 返回值
tnOsSched() 任务切换
tnOsTaskCreate() tnOsTaskDel() TN_OS_TASK_HANDLE tnOsTaskCreate (void( * tnOsSched() tnOsStart() pfuncTask)(void), idata unsigned char *pucStk) tnOsInit() void tnOsTaskDel (TN_OS_TASK_HANDLE thTask)
设置任务就绪状态 创建任务中不需要创建空闲任务。
创建任务失败
结束
创建任务时TCB填充
假设系统最大的任务数为2,系统刚完成初始化,现在要创建Task0,TCB的申请和 填充过程是:
TCB0 bp SP addr15~addr8 addr7~addr0 ucTaskStat xxxxxxxx bp pucStkTask0 xxxxxxxx xxxxxxxx Task0Addr高8位 xxxxxxxx Task0Addr低8位 00000001 00000000 TCB1 xxxxxxxx xxxxxxxx xxxxxxx xxxxxxxx 00000000
任务块结构体 __GtcbTasks[x]
上下文信息 jbTaskContext
上下文信息 jmp_buf [x] bp SP addr15~addr8
ucTaskStat 任务状态字
addr7~addr0
ucTaskStat 即任务状态字,是char型变量,对于V1.0版本的 TinyOS51,它有两个值,如下宏定义所示: #define __TN_TASK_FLG_DEL 0x00 // 任务被删除 #define __TN_TASK_FLG_RDY 0x01 // 任务就绪
TinyOS51
支持的任务 对最大任务数目 支持的任务数量有限 数量无限 没有限制
51单片机idata空间256字节 0xFF
Task3任务堆栈 Task2任务堆栈 Task1任务堆栈
Task0任务堆栈
1 2
单片机片内idata空间有限 任务堆栈占用idata资源
不能继续分配 任务堆栈 0x00
TinyOS51支持的任务数
RTOS使用TCB指针识别任务,对于 SDCC编译器来说,指针变量占3个字节空 间,当任务较多时,内存开销大。
pTCB2 … pTCBn
TCB2 …
TCBn
TinyOS51
TCB[1]
TinyOS51使用TCB结构体类型数组 下标来识别任务,类型是无符号char型, 仅占一个字节内存空间,适合内存资源 紧张的51单片机
调用 setjmp()
是否存在下一 个就绪任务? 是

实现系统 空闲任务
通过任务转换实现
保存当前任务TCB
等待当前任务就绪
调用 longjmp()
从下一个就绪任务 TCB恢复上下文
结束
任务切换程序说明1
tiny_os_51_core.c
Task0程序 入口地址
tnOsCreate(Task0Addr, pucStkTask0)
CPU
TCB填充:包括bp, TCB申请:由于 TCB0的状态字为 堆栈指针,函数入 口地址以及设置任 “删除状态”,所 有将其分配给Task0 务为“就绪状态”
Task0堆栈指针
创建任务的程序实现
tiny_os_51_core.c
thTask;
// 操作的任务
for (thTask = 0; thTask < TN_OS_MAX_TASKS; thTask++) { __GtcbTasks[thTask].ucTaskStat = TN_TASK_FLG_DEL; } __GthTaskCur } = 0; // 初始化任务号为0 // 使任务处于删除状态
0xFF
Task3任务堆栈 Task2任务堆栈 Task1任务堆栈 Task0任务堆栈
0xFF
Task7任务堆栈
0x00
至少32字节空间 • R0~R7 • 编译器用到的空间 • TinyOS51占用空间 • 用户代码占用空间 0x00

Task1任务堆栈 Task0任务堆栈
实际最大任务数以3~4个最为合适
企业员工
操作系统任务
TCB全称是Task Control Block,即任务控制块,它是一个结构体数据结构, 用于记录各个任务的各种信息,包括任务堆栈指针、任务状态、延时计数、 等待事件指针等。
RTOS和TinyOS51任务识别
RTOS(实时操作系统)
指向TCB 的指针 pTCB1 TCB 结构体变量 TCB1
5.3 协作式多任务操作系统


整体规划 任务控制块 内核API 小结
简介
任务切换的时机完全取决于正在运行的任务;
现代操作系统不再使用,适合于学习范例;
下面介绍 TinyOS51 V1.0版 操作系统。
内核 API
TinyOS51 V1.0是完全用C语言编写的协作式多任务操作系统,它的内核API (Application Interface Program:接口函数)包括5个函数。 tnOsInit() 系统初始化 tnOsStart() 系统启动
TCB的程序实现
tiny_os_51_core.c
#define __TN_TASK_FLG_DEL #define __TN_TASK_FLG_RDY struct tn_os_tcb { jmp_buf jbTaskContext; // 用于存储上下文信息 // 任务状态字 0x00 0x01 // 任务被删除 // 任务就绪
TCB[2] … TCB[n] TCB结构体 类型数组 数组下标, 俗称句柄 Handle
TCB的构成
TCB的类型是结构体,对于V1.0版本的TinyOS51操作系统,它主要由上下文信 息和任务状态字组成。
jbTaskContext是 jmp_buf 数组类 型变量,而 jmp_buf 数组类 型在<setjmp.h> 中定义
TCB1
xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx 00000000
TCBn
xxxxxxxx xxxxxxxx
……
xxxxxxx xxxxxxxx xxxxxxxx 00000000
#define __TN_TASK_FLG_DEL #define __TN_TASK_FLG_RDY
TN_OS_TASK_HANDLE tnOsTaskCreate (void (*pfuncTask)(void), idata unsigned char *pucStk) { TN_OS_TASK_HANDLE thRt; for (thRt = 0; thRt < TN_OS_MAX_TASKS; thRt++) { // 搜索是否有空闲的任务控制块 if (__GtcbTask[thRt].uctaskStat == __TN_TASK_FLG_DEL) { setTaskJmp(pfuncTask, pucStk, __GtcbTasks[thRt].jbTaskContext); // 保存上下文信息 __GtcbTasks[thRt].ucTaskStat = __TN_TASK_FLG_RDY; // 任务就绪 return thRt; } } return -1; } // 如果创建任务失败,则返回-1 // 如果创建任务成功,则返回该任务的句柄
创建任务
在操作系统中,任何一个程序只有成为了任务才能在内存中运行,使用创建任务 创建与之对应的任务。
开始
如果没有空闲TCB 则不能创建任务
关于空闲任务
是否有空闲 任务控制块
是 保存上下文信息

空闲任务是CPU闲置时候运行的任务,TinyOS51为了节省片内 填充TCB的过程 RAM,决定使用调度器来完成空闲任务的工作。
// 执行0号任务
SP
PC 高8位
Task0 堆栈指针 Task0 入口地址高8位 Task0 入口地址低8位 CPU
Task0
Task1
低8位
Task2
任务切换
任务切换是指保存当前任务的上下文,并恢复需要执行任务的上下文的过程。
任务在宏观上是并发运行的
开始
task0
相关文档
最新文档