精华内容
下载资源
问答
  • 轮询任务调度与抢占式任务调度的区别在于抢占式调度中的优先级可以抢占CPU,而轮询的不能。具体而言,轮询调度的原理是每一次把来自用户的请求轮流的分配给内部服务器,从1开始,直到N(内部服务器的个数),然

    在多任务系统中,在同一时刻通常会有多个任务处于活动状态,操作系统此时就需要对资源进行管理,在任务实现资源(CPU、内存等)的共享。任务调度是指基于给定时间点、给定时间间隔或者给定制执行次数自动执行任务。轮询任务调度与抢占式任务调度的区别在于抢占式调度中的优先级可以抢占CPU,而轮询的不能。

    具体而言,轮询调度的原理是每一次把来自用户的请求轮流的分配给内部服务器,从1开始,直到N(内部服务器的个数),然后重新开始循环。只有当前任务主动放弃CPU控制权的情况下(比如任务挂器),才允许其他任务(包括高优先级的任务)控制CPU。其优点是简洁性,无需记录当前所有连接的状态,所以,它是一种无状态的调度。但缺点是不利于后面的请求及时得到响应。抢占式调度允许高优先级的任务打断当前执行的任务,抢占CPU的控制权。这有利于后面的高优先级的任务也能及时得到响应。但是实现相对较复杂,并且可能出现低优先级的任务长期打得不到调度的情况。

    展开全文
  • #region Scheduler StdSchedulerFactory factory = new
  • 1、说说轮巡任务调度与抢占式任务调度的区别? 答:轮询任务调度与抢占式任务调度的区别在于抢占式调度可以因为优先级高的任务抢占cpu,而轮询的不能。  2当软件线程个数超过硬件线程个数的时候,支持抢占式多任务...

    1、说说轮巡任务调度与抢占式任务调度的区别?
    答:轮询任务调度与抢占式任务调度的区别在于抢占式调度可以因为优先级高的任务抢占cpu,而轮询的不能。 
    2当软件线程个数超过硬件线程个数的时候,支持抢占式多任务处理的操作系统一般会采用时间片轮转调度的方案。
    3 对于RTOS中,理解这两个概念是很重要的。实时系统对于响应时间是有非常严格的要求,尤其是在硬实时系统中,没有满足响应时间的上限将视为系统失败。影响RTOS响应时间的一个重要方面就是任务调度算法。在 RTOS中,主要的调度算法是基于任务优先级的抢占式调度。在这种调度算法中,系统总是选择优先级别的最高的算法进行调度,并且 一旦高优先级别的任务准备就绪之后,它就会马上被调度而不等待低优先级的任务主动放弃CPU。这和通用OS的时间片轮转调度算法是 不一样的,在时间片轮转调度算法中,只有等任务主动放弃CPU,高优先级的任务才有调度的优先权。在基于任务优先级抢占式调度算法中,会产生一个优先级反转问题。解决这个问题的方式主要包括继承优先级和天花板策略。继承优先级策略是一旦高优先级的任务所需要的竞争资源被低优先级的任务使用,就提高低优先级的任务的优先级别,从而使得能使竞争资源尽快释放。天花板策略是在创建信号量的时候,根据可能使用该信号量所有的任务的最高优先级别来设置当前任务的优先级,这个优先级是由创建资源的用户来决定。
    4很多编程人员都认为,使用多线程能够提升程序的性能,如果少量的线程能够提升程序的性能,他们就会认为更多的线程能够更好。但实际上,多线程只是为不同的程序比较合理地安排运行时间,更加充分的利用系统资源,这当中存在着一个线程数和程序性能的平衡,过多的线程可能会严重影响程序的性能。这种影响主要有以下两个方面:首先,将给定的工作量划分给过多的线程会造成每个线程的工作量过少,因此可能导致线程启动和终止时的开销比程序实际工作的开销还要多;其次,过多并发线程的存在将导致共享有限硬件资源的开销增大。 

    操作系统使用进程将它们正在执行的不同应用程序分开。线程是操作系统分配处理器时间的基本单元,并且进程中可以有多个线程同时执行代码。每个线程都维护异常处理程序、调度优先级和一组系统用于在调度该线程前保存线程上下文的结构。线程上下文包括为使线程在线程的宿主进程地址空间中无缝地继续执行所需的所有信息,包括线程的 CPU 寄存器组和堆栈。 
    当软件线程个数超过硬件线程个数的时候,支持抢占式多任务处理的操作系统一般会采用时间片轮转调度的方案。它通过以下方式实现这一点:在需要硬件线程时间的软件线程之间分割可用硬件线程时间,并轮流为每个线程分配硬件线程时间片(time slice)。当前执行的软件线程在其时间片结束时被挂起,而另一个软件线程继续运行。当系统从一个软件线程切换到另一个软件线程时,它将保存被抢占的软件线程的线程上下文,并重新加载线程队列中下一个软件线程的已保存线程上下文。 
    时间片的长度取决于操作系统和处理器。由于每个时间片都很小,因此即使只有一个处理器,多个线程看起来似乎也是在同时执行。这实际上就是多处理器系统中发生的情形,在此类系统中,可执行线程分布在多个可用处理器中。 
    时间片机制保证了所有的软件线程都能够被执行,这样也就避免了某些软件线程长期占据硬件线程,而导致其他的软件线程出现饥饿(starvation)的情况。但是,操作系统的时间片轮转调度方案也将引入额外的开销,随着软件线程的增多,这种开销将会急剧增加,进而降低系统性能。这种开销主要有以下几种: 
    首先是线程间切换时保存和恢复进程寄存器的开销。在系统要进行线程间切换的时候,需要保存当前线程的寄存器状态,以便在下次回调到当前线程的时候,能够恢复线程寄存器继续运行。当存在少量线程的时候,进程调度程序会给每一个线程分配足够长的时间片,相比较之下,保存和恢复线程寄存器的开销变得不是很显著。但是随着线程数目的增加,线程调度程序分给每个线程的时间片也会相应减少,而保存和恢复线程寄存器的开销不变,这样,在每个时间片当中,系统将更多的时间用于保存和恢复线程寄存器,就会显著地降低系统性能。 
    另一方面,在使用时间片机制的时候,保存和恢复线程使用的cache的开销则是更敏感的一种开销。现代体系结构中,cache通常比主存快10到100倍,CPU访问cache并命中对于系统性能的提高具有非常重要的作用。而当存在线程切换的时候,新的线程要访问的数据尚未装入cache,CPU需要从主存中读取信息的同时,cache替换策略把该地址所在的那块存储内容从主存拷贝到cache中。一般情况下,cache替换策略是使用最近最少使用策略(LRU)选择的,LRU策略是把当前近期cache中使用次数最少的那块数据块替换出去,而这些数据块中的数据很可能就是上几个时间片的线程使用的。这样,各个线程就在不断地竞争cache,相互淘汰彼此使用的cache数据,不断造成cache扑空,最终降低了系统性能。 
    此外,在更底层的存储器结构上也存在相似的问题,那就是线程对主存的争夺。现代大部分操作系统都实现了虚拟内存。一台机器上同时运行着很多程序,这些程序所需的存储空间可能比存储器的实际容量要大得多,但是在每个时间点,存储器只有一部分被激活。主存只需存放众多程序中的活跃部分,就像cache中只存放一个程序的活跃部分一样,而其他的部分则存储到磁盘上。由于每个线程都有自己的栈空间和私有数据结构。类似于对cache的争夺,随着线程数的增加,线程之间就会争夺主存,导致性能上的损失。 
    以上几种问题是由于线程对共享资源的争夺产生的,另外还存在一个性质不同,但是后果可能更加严重的问题,称为护航(convoying),它是指线程聚集在一起,等待获取某一个锁。当某个线程持有一个锁的时候,用完了自己的时间片,这时所有等待锁的线程都必须等待这个线程被唤醒并且释放锁。 
    最好的解决方法是依据实际情况,使用尽可能少的线程,这样可以最大限度地减少操作系统资源的使用,并可提高性能。千万不要硬性规定线程的个数,而是将其作为一个可调节的参数。 
    操作系统中存在两种类型的线程,I/O阻塞线程和计算非阻塞线程。许多应用程序,例如终端机模拟程序,都需要在同一时间处理对一个以上的文件的读写操作,我们不可能依次地等待每个请求完成才继续处理下一个请求,而是让所有这些I/O操作并行处理,并且当任何一个I/O完成时,应用程序会收到一个事件通告,这种处理I/O的线程称为I/O阻塞线程。而另外一些进程,例如进行大量计算,处理图形的线程则称为计算非阻塞线程。I/O阻塞线程不会引起时间片切换开销,而计算非阻塞线程则会引起时间片切换的开销。所以,将I/O阻塞线程和计算非阻塞线程分离开是一种非常好的组织方法。计算非阻塞线程在大多数时间内都是被调度函数调度到的,应该和处理器资源相匹配,而I/O阻塞线程在大多数时间内都在等待外部事件。 
    由于构造良好的多线程应用程序要多方面地考虑资源要求和潜在冲突,需要相当的专业技术,所以一般最好采用现成的软件来完成,下面的一些经验是很有帮助的: 
    建议您尽量使用OpenMP。OpenMP提供了一种简单的方法,供程序员描述要并行的循环迭代,而不用创建具体数目的线程,OpenMP可以根据目标系统尽量使用最优数量的线程个数。 
    建议您使用线程池。每个传入的请求都将分配给线程池中的一个线程,因此可以异步处理请求,而不会占用主线程,也不会延迟后续请求的处理。一旦池中的某个线程完成任务,它将返回到等待线程队列中,等待被再次使用,这种重用使应用程序可以避免为每个线程创建新进程的开销。线程池通常具有最大线程数限制,如果所有线程都繁忙,而额外的任务将放入队列中,直到有线程可用时才能够得到处理。当您不需要给一个任务设定特定的优先级,当不会有可能会运行很长时间的任务(并因此阻塞了其他任务),我们建议您使用线程池,技术高超的软件设计专家可能希望编写自己的任务调度程序,一般都采用任务窃取(work stealing)的方法,它是指每个线程都有自己私有的任务集合,当一个线程执行完自己的任务以后,它就从其他线程的任务集合中窃取任务来执行。任务窃取实现了较好的cache利用率和负载平衡。当一个线程运行自己的任务时,它往往会重用自己cache中的数据,当它完成自己的任务,就需要窃取任务来运行,这实际上就是一种负载平衡。高效的任务策略在于窃取目标的选择,选择较大的任务能够使窃取者忙碌相当长一段时间。早期的Clik调度程序(Blumofe,1995年)就是一个关于如何编写高效的任务窃取调度程序的范例。

    展开全文
  • FreeRTOS 任务调度 任务切换

    万次阅读 2016-10-15 17:07:34
    @(嵌入式) 简述 ...一般来说, 我们会在程序开始先创建若干个任务, 而此时任务调度器还没又开始运行,因此每一次任务创建后都会依据其优先级插入到就绪链表,同时保证全局变量 pxCurrentTCB 指向当

    @(嵌入式)

    Freertos
    FreeRtos

    简述

    前面文章 < FreeRTOS 任务调度 任务创建 > 介绍了 FreeRTOS 中如何创建任务以及其具体实现。
    一般来说, 我们会在程序开始先创建若干个任务, 而此时任务调度器还没又开始运行,因此每一次任务创建后都会依据其优先级插入到就绪链表,同时保证全局变量 pxCurrentTCB 指向当前创建的所有任务中优先级最高的一个,但是任务还没开始运行。
    当初始化完毕后,调用函数 vTaskStartScheduler启动任务调度器开始开始调度,此时,pxCurrentTCB所指的任务才开始运行。
    所以, 本章,介绍任务调度器启动以及如何进行任务切换。

    调度器涉及平台底层硬件操作,本文以Cotex-M3 架构为例, 具体可以参考 《Cortex-M3权威指南》(文末附)

    分析的源码版本是 v9.0.0
    (为了方便查看,github 上保留了一份源码Source目录下的拷贝)

    启动调度器

    创建任务后,系统不会自动启动任务调度器,需要用户调用函数 vTaskStartScheduler 启动调度器。 该函数被调用后,会先创建系统自己需要用到的任务,比如空闲任务 prvIdleTask,定时器管理的任务等。 之后, 调用移植层提供的函数 xPortStartScheduler
    代码解析如下,

    void vTaskStartScheduler( void )
    {
        BaseType_t xReturn;
        #if( configSUPPORT_STATIC_ALLOCATION == 1 )
        {
            // 采用静态内存创建空闲任务
            StaticTask_t *pxIdleTaskTCBBuffer = NULL;
            StackType_t *pxIdleTaskStackBuffer = NULL;
            uint32_t ulIdleTaskStackSize;
            // 获取静态内存地址/参数
            vApplicationGetIdleTaskMemory(
                &pxIdleTaskTCBBuffer, 
                &pxIdleTaskStackBuffer, 
                &ulIdleTaskStackSize );
            // 创建任务
            // 空闲任务优先级为 0, 也就是其优先级最低
            // !! 但是, 设置了特权位, 所以其运行在 特权模式
            xIdleTaskHandle = xTaskCreateStatic(prvIdleTask, "IDLE", 
                ulIdleTaskStackSize, (void *) NULL, 
                (tskIDLE_PRIORITY | portPRIVILEGE_BIT), 
                pxIdleTaskStackBuffer,
                pxIdleTaskTCBBuffer); 
    
            if( xIdleTaskHandle != NULL )
            {
                xReturn = pdPASS;
            }
            else
            {
                xReturn = pdFAIL;
            }
        }
        #else
        {
            // 动态申请内存创建任务
            xReturn = xTaskCreate(prvIdleTask,
                "IDLE", configMINIMAL_STACK_SIZE,
                (void *)NULL,
                (tskIDLE_PRIORITY | portPRIVILEGE_BIT),
                &xIdleTaskHandle );     
        }
        #endif
    
        // 如果工程使用了软件定时器, 需要创建定时器任务进行管理
        #if ( configUSE_TIMERS == 1 )
        {
            if( xReturn == pdPASS )
            {
                xReturn = xTimerCreateTimerTask();
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        #endif
    
        if( xReturn == pdPASS )
        {
    
            // 关闭中断, 避免调度器运行前节拍定时器产生中断
            // 中断在第一个任务启动时恢复
            portDISABLE_INTERRUPTS();
    
            #if ( configUSE_NEWLIB_REENTRANT == 1 )
            {
                // 如果使用了这个库
                // 更新第一个任务的的指针到全局变量
                _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
            }
            #endif
    
            // 初始化变量
            xNextTaskUnblockTime = portMAX_DELAY;
            xSchedulerRunning = pdTRUE;
            xTickCount = ( TickType_t ) 0U;
    
            // 如果启动统计任务运行时间, 宏 configGENERATE_RUN_TIME_STATS = 1
            // 需要定义以下宏, 初始化一个定时器用于该功能 
            portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
    
            // 设置系统节拍计数器, 启动任务
            // 硬件相关, 由系统移植层提供, 下面介绍
            if( xPortStartScheduler() != pdFALSE )
            {
                // 不会运行到这里, 如果调度器运行正常
            }
            else
            {
                // 当调用 xTaskEndScheduler()才会来到这里
            }
        }
        else
        {
            // 内存不足,创建空闲任务/定时任务失败, 调度器启动失败
            configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
        }
    
        // 预防编译器警告
        ( void ) xIdleTaskHandle;
    }

    移植层调度器

    上面提到, 创建系统所需任务和初始化相关静态变量后, 系统调用了 xPortStartScheduler设置节拍定时器和启动第一个任务,开始系统正常运行调度。 而对于不同架构平台,该函数的实现可能存在不同,以下, 拿比较常用的 Cotex-M3 架构举例。
    对于 M3, 可以在源码目录下 /Source/portable/GCC/ARM_CM3/port.c 看到该函数的实现。

    与 FreeRTOS 任务优先级相反, Cotex-M3 优先级值越小, 优先级越高。 Cotex-M3的优先级配置寄存器考虑器件移植而向高位对齐,实际可用的 CPU 会裁掉表达优先级低端的有效位,以减少优先级数。 举例子说, 加入平台支持3bit 表示优先级,则其优先级配置寄存器的高三位可以编程写入,其他位被屏蔽,不管写入何值,重新读回都是0。
    另外提供抢占优先级和子优先级分段配置相关,详细阅读 《Cortex-M3权威指南》

    在系统调度过程中,主要涉及到的三个异常:
    * SVC 系统服务调用
    操作系统通常不让用户程序直接访问硬件,而是通过提供一些系统服务函数。 这里主要触发后,在异常服务中启动第一个任务
    * PendSV 可悬起系统调用
    相比 SVC, PenndSV 异常后可能不会马上响应, 等到其他高优先级中断处理后才响应。 用于上下文切换,同时保证其他中断可以被及时响应处理。
    * SysTick 节拍定时器
    在没有高优先级任务强制下,同优先级任务按时间片轮流执行,每次SysTick中断,下一个任务将获得一个时间片。

    BaseType_t xPortStartScheduler( void )
    {
        configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );
        #if( configASSERT_DEFINED == 1 )
        {
            volatile uint32_t ulOriginalPriority;
            // 取出中断优先级寄存器
            volatile uint8_t * const pucFirstUserPriorityRegister = 
                (volatile uint8_t * const) (portNVIC_IP_REGISTERS_OFFSET_16 +
                            portFIRST_USER_INTERRUPT_NUMBER);
            volatile uint8_t ucMaxPriorityValue;
    
            // 保存原有优先级寄存器值
            ulOriginalPriority = *pucFirstUserPriorityRegister;
    
            // 判断平台支持优先级位数
            // 先全写 1
            *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
            // 重新读回, 不能设置的位依然是 0
            ucMaxPriorityValue = *pucFirstUserPriorityRegister;
            // 确保用户设置优先级不会超出范围
            ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;
    
            // 判断有几个1, 得到对应优先级数最大值
            ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
            while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
            {
                ulMaxPRIGROUPValue--;
                ucMaxPriorityValue <<= ( uint8_t ) 0x01;
            }
            ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
            ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;
    
            // 恢复优先级配置寄存器值
            *pucFirstUserPriorityRegister = ulOriginalPriority;
        }
        #endif /* conifgASSERT_DEFINED */
    
        // 设置 PendSV 和 SysTIck 异常优先级最低
        // 保证系统会话切换不会阻塞系统其他中断的响应
        portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
        portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
    
        // 初始化系统节拍定时器
        vPortSetupTimerInterrupt();
        // 初始化边界嵌套计数器
        uxCriticalNesting = 0;
    
        // 触发 svc 异常 启动第一个任务
        prvPortStartFirstTask();
    
        /* Should not get here! */
        prvTaskExitError();
        return 0;
    }

    启动第一个任务

    函数中调用了 prvPortStartFirstTask 来启动第一个任务, 该函数重新初始化了系统的栈指针,表示 FreeRtos 开始接手平台的控制, 同时通过触发 SVC 系统调用,运行第一个任务。具体实现如下

    static void prvPortStartFirstTask( void )
    {
        __asm volatile(
        " ldr r0, =0xE000ED08   \n" /*向量表偏移寄存器地址 CotexM3*/
        " ldr r0, [r0]          \n" /*取向量表地址*/
        " ldr r0, [r0]          \n" /*取 MSP 初始值*/
        /*重置msp指针 宣示 系统接管*/
        " msr msp, r0           \n"
        " cpsie i               \n" /*开中断*/
        " cpsie f               \n" /*开异常*/
        /*流水线相关*/
        " dsb                   \n" /*数据同步隔离*/
        " isb                   \n" /*指令同步隔离*/
        /*触发异常 启动第一个任务*/
        " svc 0                 \n"
        " nop                   \n"
        );
    }

    前面创建任务的文章介绍过, 任务创建后, 对其栈进行了初始化,使其看起来和任务运行过后被系统中断切换了一样。 所以,为了启动第一个任务,触发 SVC 异常后,异常处理函数中直接执行现场恢复, 把 pxCurrentTCB “恢复”到运行状态。

    (另外,Cotex-M3 具有三级流水线,所以切换任务的时候需要清除预取的指令,避免错误。)

    对于 Cotex-M3 , 其代码实现如下,

    void vPortSVCHandler( void )
    {
        __asm volatile (
        /*取 pxCurrentTCB 的地址*/
        "ldr r3, pxCurrentTCBConst2      \n"
        /*取出 pxCurrentTCB 的值 : TCB 地址*/
        "ldr r1, [r3]                    \n" 
        /*取出 TCB 第一项 : 任务的栈顶 */
        "ldr r0, [r1]                   \n"
        /*恢复寄存器数据*/
        "ldmia r0!, {r4-r11}            \n" 
        /*设置线程指针: 任务的栈指针*/
        "msr psp, r0                    \n" 
        /*流水线清洗*/
        "isb                            \n"
        "mov r0, #0                     \n"
        "msr    basepri, r0             \n"
        /*设置返回后进入线程模式*/
        "orr r14, #0xd                  \n"
        "bx r14                         \n"
        "                               \n"
        ".align 4               \n"
        "pxCurrentTCBConst2: .word pxCurrentTCB     \n"
        );
    }

    异常返回后, 系统进入线程模式, 自动从堆栈恢复PC等寄存器,而由于此时栈指针已经更新指向对应准备运行任务的栈,所以,程序会从该任务入口函数开始执行。
    到此, 第一个任务启动。

    前面提到, 第一个任务启动通过 SVC 异常, 而后续的任务切换, 使用的是 PendSV 异常, 而其对应的服务函数是 xPortPendSVHandler。 后续介绍任务切换再分析。

    任务切换

    FreeRTOS 支持时间片轮序和优先级抢占。系统调度器通过调度算法确定当前需要获得CPU 使用权的任务并让其处于运行状态。对于嵌入式系统,某些任务需要获得快速的响应,如果使用时间片,该任务可能无法及时被运行,因此抢占调度是必须的,高优先级的任务一旦就绪就能及时运行;而对于同优先级任务,系统根据时间片调度,给予每个任务相同的运行时间片,保证每个任务都能获得CPU 。
    1. 最高优先级任务 Task 1 运行,直到其被阻塞或者挂起释放CPU
    2. 就绪链表中最高优先级任务Task 2 开始运行, 直到…
    1. 调用接口进入阻塞或者挂起状态
    2. 任务 Task 1 恢复并抢占 CPU 使用权
    3. 同优先级任务TASK 3 就绪,时间片调度
    3. 没有用户任务执行,运行系统空闲任务。

    FreeRTOS 在两种情况下执行任务切换:
    1. 同等级任务时间片用完,提前挂起触发切换
    在 SysTick 节拍计数器中断中触发异常
    2. 高优先任务恢复就绪(如信号量,队列等阻塞、挂起状态下退出)时抢占
    最终都是通过调用移植层提供的 portYIELD() 宏悬起 PendSV 异常

    但是无论何种情况下,都是通过触发系统 PendSV 异常,在该服务程序中完成切换。
    使用该异常切换上下文的原因是保证切换不会影响到其他中断的及时响应(切换上下文抢占了 ISR 的执行,延时时间不可预知,对于实时系统是无法容忍的),在SysTick 中或其他需要进行任务切换的地方悬起一个 PendSV 异常,系统会直到其他所有 ISR 都完成处理后才执行该异常的服务程序,进行上下文切换。

    系统响应 PendSV 异常,在该中断服务程序中,保存当前任务现场, 选择切换的下一个任务,进行任务切换,退出异常恢复线程模式运行新任务,完成任务切换。

    以下是 Cotex-M3 的服务程序,
    首先先要明确的是,系统进入异常处理程序的时候,使用的是主堆栈指针 MSP, 而一般情况下运行任务使用的线程模式使用的是进程堆栈指针 PSP。后者使用是系统设置的,前者是硬件强制设置的。
    对应这两个指针,系统有两种堆栈,系统内核和异常程序处理使用的是主堆栈,MSP 指向其栈顶。而对应而不同任务,我们在创建时为其分配了空间,作为该任务的堆栈,在该任务运行时,由系统设置进程堆栈 PSP 指向该栈顶。
    如下分析该服务函数的执行:

    void xPortPendSVHandler( void )
    {
        /* This is a naked function. */
        __asm volatile
        (
        /*取出当前任务的栈顶指针 也就是 psp -> R0*/
        "   mrs r0, psp                         \n"
        "   isb                                 \n"
        "                                       \n"
        /*取出当前任务控制块指针 -> R2*/
        "   ldr r3, pxCurrentTCBConst           \n"
        "   ldr r2, [r3]                        \n"
        "                                       \n"
        /*R4-R11 这些系统不会自动入栈,需要手动推到当前任务的堆栈*/
        "   stmdb r0!, {r4-r11}                 \n"
        /*最后,保存当前的栈顶指针 
        R0 保存当前任务栈顶地址
        [R2] 是 TCB 首地址,也就是 pxTopOfStack
        下次,任务激活可以重新取出恢复栈顶,并取出其他数据
        */
        "   str r0, [r2]                        \n"
        "                                       \n"
        /*保护现场,调用函数更新下一个准备运行的新任务*/
        "   stmdb sp!, {r3, r14}                \n"
        /*设置优先级 第一个参数,
        即:configMAX_SYSCALL_INTERRUPT_PRIORITY
        进入临界区*/
        "   mov r0, %0                          \n"
        "   msr basepri, r0                     \n"
        "   bl vTaskSwitchContext               \n"
        "   mov r0, #0                          \n"
        "   msr basepri, r0                     \n"
        "   ldmia sp!, {r3, r14}                \n"
        "                                       \n"
        /*函数返回 退出临界区
        pxCurrentTCB 指向新任务
        取出新的 pxCurrentTCB 保存到 R1
        */
        "   ldr r1, [r3]                        \n"
        /*取出新任务的栈顶*/
        "   ldr r0, [r1]                        \n"
        /*恢复手动保存的寄存器*/
        "   ldmia r0!, {r4-r11}                 \n"
        /*设置线程指针 psp 指向新任务栈顶*/
        "   msr psp, r0                         \n"
        "   isb                                 \n"
        /*返回, 硬件执行现场恢复
        开始执行任务
        */
        "   bx r14                              \n"
        "                                       \n"
        "   .align 4                            \n"
        "pxCurrentTCBConst: .word pxCurrentTCB  \n"
        ::"i"(configMAX_SYSCALL_INTERRUPT_PRIORITY)
        );
    }

    在服务程序中,调用了函数 vTaskSwitchContext 获取新的运行任务, 该函数会更新当前任务运行时间,检查任务堆栈使用是是否溢出,然后调用宏 taskSELECT_HIGHEST_PRIORITY_TASK()设置新的任务。该宏实现分两种情况,普通情况下使用的定义如下

    UBaseType_t uxTopPriority = uxTopReadyPriority;
    while(listLIST_IS_EMPTY(&(pxReadyTasksLists[uxTopPriority])))
    {
        --uxTopPriority;
    }
    
    listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, 
        &(pxReadyTasksLists[ uxTopPriority]));
    
    uxTopReadyPriority = uxTopPriority;

    通过 while 查找当前存在就绪任务的最高优先级链表,获取链表项设置任务指针。(通一个链表内多个项目通过指针循环,实现同优先级任务获得相同时间片执行)。

    而另外一种方式,需要平台支持,主要差别是查找最高任务优先级,平台支持利用平台特性,效率会更高,但是移植性就不好说了。

    发生异常跳转到异常处理服务前,自动执行的现场保护会保留返回模式(线程模式),使用堆栈指针等信息,所以,结束任务切换, 通过执行 bx r14返回,系统会自动恢复现场(From stack),开始运行任务。

    至此,任务切换完成。

    参考

    展开全文
  • 轮询任务调度与抢占式任务调度

    千次阅读 2016-09-20 12:19:08
    1、说说轮巡任务调度与抢占式任务调度的区别? 答:轮询任务调度与抢占式任务调度的区别在于抢占式调度可以因为优先级高的任务抢占cpu,而轮询的不能。  2当软件线程个数超过硬件线程个数的时候,支持抢占式多...
    1、说说轮巡任务调度与抢占式任务调度的区别?
    答:轮询任务调度与抢占式任务调度的区别在于抢占式调度可以因为优先级高的任务抢占cpu,而轮询的不能。 
    2当软件线程个数超过硬件线程个数的时候,支持抢占式多任务处理的操作系统一般会采用时间片轮转调度的方案。
    3 对于RTOS中,理解这两个概念是很重要的。实时系统对于响应时间是有非常严格的要求,尤其是在硬实时系统中,没有满足响应时间的上限将视为系统失败。影响RTOS响应时间的一个重要方面就是任务调度算法。在 RTOS中,主要的调度算法是基于任务优先级的抢占式调度。在这种调度算法中,系统总是选择优先级别的最高的算法进行调度,并且 一旦高优先级别的任务准备就绪之后,它就会马上被调度而不等待低优先级的任务主动放弃CPU。这和通用OS的时间片轮转调度算法是 不一样的,在时间片轮转调度算法中,只有等任务主动放弃CPU,高优先级的任务才有调度的优先权。在基于任务优先级抢占式调度算法中,会产生一个优先级反转问题。解决这个问题的方式主要包括继承优先级和天花板策略。继承优先级策略是一旦高优先级的任务所需要的竞争资源被低优先级的任务使用,就提高低优先级的任务的优先级别,从而使得能使竞争资源尽快释放。天花板策略是在创建信号量的时候,根据可能使用该信号量所有的任务的最高优先级别来设置当前任务的优先级,这个优先级是由创建资源的用户来决定。
    4很多编程人员都认为,使用多线程能够提升程序的性能,如果少量的线程能够提升程序的性能,他们就会认为更多的线程能够更好。但实际上,多线程只是为不同的程序比较合理地安排运行时间,更加充分的利用系统资源,这当中存在着一个线程数和程序性能的平衡,过多的线程可能会严重影响程序的性能。这种影响主要有以下两个方面:首先,将给定的工作量划分给过多的线程会造成每个线程的工作量过少,因此可能导致线程启动和终止时的开销比程序实际工作的开销还要多;其次,过多并发线程的存在将导致共享有限硬件资源的开销增大。 

    操作系统使用进程将它们正在执行的不同应用程序分开。线程是操作系统分配处理器时间的基本单元,并且进程中可以有多个线程同时执行代码。每个线程都维护异常处理程序、调度优先级和一组系统用于在调度该线程前保存线程上下文的结构。线程上下文包括为使线程在线程的宿主进程地址空间中无缝地继续执行所需的所有信息,包括线程的 CPU 寄存器组和堆栈。 
    当软件线程个数超过硬件线程个数的时候,支持抢占式多任务处理的操作系统一般会采用时间片轮转调度的方案。它通过以下方式实现这一点:在需要硬件线程时间的软件线程之间分割可用硬件线程时间,并轮流为每个线程分配硬件线程时间片(time slice)。当前执行的软件线程在其时间片结束时被挂起,而另一个软件线程继续运行。当系统从一个软件线程切换到另一个软件线程时,它将保存被抢占的软件线程的线程上下文,并重新加载线程队列中下一个软件线程的已保存线程上下文。 
    时间片的长度取决于操作系统和处理器。由于每个时间片都很小,因此即使只有一个处理器,多个线程看起来似乎也是在同时执行。这实际上就是多处理器系统中发生的情形,在此类系统中,可执行线程分布在多个可用处理器中。 
    时间片机制保证了所有的软件线程都能够被执行,这样也就避免了某些软件线程长期占据硬件线程,而导致其他的软件线程出现饥饿(starvation)的情况。但是,操作系统的时间片轮转调度方案也将引入额外的开销,随着软件线程的增多,这种开销将会急剧增加,进而降低系统性能。这种开销主要有以下几种: 
    首先是线程间切换时保存和恢复进程寄存器的开销。在系统要进行线程间切换的时候,需要保存当前线程的寄存器状态,以便在下次回调到当前线程的时候,能够恢复线程寄存器继续运行。当存在少量线程的时候,进程调度程序会给每一个线程分配足够长的时间片,相比较之下,保存和恢复线程寄存器的开销变得不是很显著。但是随着线程数目的增加,线程调度程序分给每个线程的时间片也会相应减少,而保存和恢复线程寄存器的开销不变,这样,在每个时间片当中,系统将更多的时间用于保存和恢复线程寄存器,就会显著地降低系统性能。 
    另一方面,在使用时间片机制的时候,保存和恢复线程使用的cache的开销则是更敏感的一种开销。现代体系结构中,cache通常比主存快10到100倍,CPU访问cache并命中对于系统性能的提高具有非常重要的作用。而当存在线程切换的时候,新的线程要访问的数据尚未装入cache,CPU需要从主存中读取信息的同时,cache替换策略把该地址所在的那块存储内容从主存拷贝到cache中。一般情况下,cache替换策略是使用最近最少使用策略(LRU)选择的,LRU策略是把当前近期cache中使用次数最少的那块数据块替换出去,而这些数据块中的数据很可能就是上几个时间片的线程使用的。这样,各个线程就在不断地竞争cache,相互淘汰彼此使用的cache数据,不断造成cache扑空,最终降低了系统性能。 
    此外,在更底层的存储器结构上也存在相似的问题,那就是线程对主存的争夺。现代大部分操作系统都实现了虚拟内存。一台机器上同时运行着很多程序,这些程序所需的存储空间可能比存储器的实际容量要大得多,但是在每个时间点,存储器只有一部分被激活。主存只需存放众多程序中的活跃部分,就像cache中只存放一个程序的活跃部分一样,而其他的部分则存储到磁盘上。由于每个线程都有自己的栈空间和私有数据结构。类似于对cache的争夺,随着线程数的增加,线程之间就会争夺主存,导致性能上的损失。 
    以上几种问题是由于线程对共享资源的争夺产生的,另外还存在一个性质不同,但是后果可能更加严重的问题,称为护航(convoying),它是指线程聚集在一起,等待获取某一个锁。当某个线程持有一个锁的时候,用完了自己的时间片,这时所有等待锁的线程都必须等待这个线程被唤醒并且释放锁。 
    最好的解决方法是依据实际情况,使用尽可能少的线程,这样可以最大限度地减少操作系统资源的使用,并可提高性能。千万不要硬性规定线程的个数,而是将其作为一个可调节的参数。 
    操作系统中存在两种类型的线程,I/O阻塞线程和计算非阻塞线程。许多应用程序,例如终端机模拟程序,都需要在同一时间处理对一个以上的文件的读写操作,我们不可能依次地等待每个请求完成才继续处理下一个请求,而是让所有这些I/O操作并行处理,并且当任何一个I/O完成时,应用程序会收到一个事件通告,这种处理I/O的线程称为I/O阻塞线程。而另外一些进程,例如进行大量计算,处理图形的线程则称为计算非阻塞线程。I/O阻塞线程不会引起时间片切换开销,而计算非阻塞线程则会引起时间片切换的开销。所以,将I/O阻塞线程和计算非阻塞线程分离开是一种非常好的组织方法。计算非阻塞线程在大多数时间内都是被调度函数调度到的,应该和处理器资源相匹配,而I/O阻塞线程在大多数时间内都在等待外部事件。 
    由于构造良好的多线程应用程序要多方面地考虑资源要求和潜在冲突,需要相当的专业技术,所以一般最好采用现成的软件来完成,下面的一些经验是很有帮助的: 
    建议您尽量使用OpenMP。OpenMP提供了一种简单的方法,供程序员描述要并行的循环迭代,而不用创建具体数目的线程,OpenMP可以根据目标系统尽量使用最优数量的线程个数。 
    建议您使用线程池。每个传入的请求都将分配给线程池中的一个线程,因此可以异步处理请求,而不会占用主线程,也不会延迟后续请求的处理。一旦池中的某个线程完成任务,它将返回到等待线程队列中,等待被再次使用,这种重用使应用程序可以避免为每个线程创建新进程的开销。线程池通常具有最大线程数限制,如果所有线程都繁忙,而额外的任务将放入队列中,直到有线程可用时才能够得到处理。当您不需要给一个任务设定特定的优先级,当不会有可能会运行很长时间的任务(并因此阻塞了其他任务),我们建议您使用线程池,技术高超的软件设计专家可能希望编写自己的任务调度程序,一般都采用任务窃取(work stealing)的方法,它是指每个线程都有自己私有的任务集合,当一个线程执行完自己的任务以后,它就从其他线程的任务集合中窃取任务来执行。任务窃取实现了较好的cache利用率和负载平衡。当一个线程运行自己的任务时,它往往会重用自己cache中的数据,当它完成自己的任务,就需要窃取任务来运行,这实际上就是一种负载平衡。高效的任务策略在于窃取目标的选择,选择较大的任务能够使窃取者忙碌相当长一段时间。早期的Clik调度程序(Blumofe,1995年)就是一个关于如何编写高效的任务窃取调度程序的范例。
    展开全文
  • 任务调度(定时任务)

    千次阅读 2020-01-28 22:31:07
    任务调度、Java常见的任务调度方式、任务调度的问题、分布式任务调度平台
  • Quartz,水晶、石英,一个简单朴素有美丽的名字,在Java程序界,Quartz大名鼎鼎,很多Java应用几乎都集成或构建了一个定时任务调度系统,Quartz是一个定时任务调度框架。 何为定时任务调度框架?简而言之,它可以...
  • FreeRTOS 任务调度 任务创建

    千次阅读 2016-10-13 00:30:16
    FreeRtos 简述FreeRTOS 的任务调度在 Source/include/task.c 中实现,包含了任务的创建、切换、挂起、延时和删除等所有功能。涉及到的链表组织见文章 任务状态系统运行过程,任务可能处于以下各种状态,各个状态之间...
  • Spark 任务调度

    千次阅读 2018-03-06 19:07:03
    Spark 任务调度1. 任务调度流程图各个RDD之间存在着依赖关系,这些依赖关系就形成有向无环图DAG,DAGScheduler对这些依赖关系形成的DAG进行Stage划分,划分的规则很简单,从后往前回溯,遇到窄依赖加入本stage,遇见...
  • FreeRTOS任务调度研究

    千次阅读 2017-08-17 22:51:32
    所以不涉及对FreeRTOS整体的介绍,而只是分析任务调度这一块的机制。对应的Demo参考自CORTEX_A9_Zynq_ZC702。 一、触发任务调度的两种机制 taskYIELD() 这种机制其实是通过ARM swi 异常触发task context switch...
  • 什么是Oozie——大数据任务调度框架

    万次阅读 2018-08-21 22:33:15
    Oozie是大数据四大协作框架之一——任务调度框架,另外三个分别为数据转换工具Sqoop,文件收集库框架Flume,大数据WEB工具Hue。 它能够提供对Hadoop MapReduce和Pig Jobs的任务调度与协调。 Oozie需要部署到Java ...
  • 上篇博文《任务调度(三)——Timer的替代品ScheduledExecutorService简介》已经对ScheduledExecutorService做了简单介绍,其实使用ScheduledExecutorService来替代Timer也是迫不得已的事情。主要原因如下: Timer不...
  • ETL任务调度

    千次阅读 2020-06-16 15:25:45
    而ETL任务调度(简称ETL调度)用于控制ETL任务的启动运行(启动时间、运行周期及触发条件),实现数据的传输转换操作。 ETL调度按照功能复杂度分为简单定时调度和工作流调度二种方式。 定时调度用于控制ETL任务...
  • os任务调度实现原理

    千次阅读 2019-07-06 21:07:17
    文章目录为什么要做任务调度-why任务调度需要做什么?-what怎样实现任务调度?-how 为什么要做任务调度-why 操作系统中最为显著的特性就是任务调度任务调度主要来自于以下几种需求: 程序并发(multiprogram...
  • 分布式任务调度

    万次阅读 2018-10-01 22:31:50
    什么是定时任务? 指定时间去执行任务 Java实现定时任务方式 1.Thread public class Demo01 { static long count = 0; public static void main(String[] args) { Runnable runnable = new Runnable() { ...
  • 任务调度算法汇总

    万次阅读 2016-10-05 13:54:54
    任务调度算法汇总
  • Quartz任务调度框架

    万次阅读 2016-08-19 17:07:21
    Quartz 任务调度框架
  • 分布式任务调度相关介绍

    千次阅读 2019-02-21 16:04:36
    分布式任务调度先介绍下:Spring&amp;amp;amp;SpringBoot任务调度工具传统定时任务存在的问题分布式环境里, 任务调度的困难及解决思想高可用任务调度原理 先介绍下:Spring&amp;amp;amp;SpringBoot任务调度...
  • 一、简介 crond是Linux下用来周期性的执行某种任务或等待处理某些事件的一个守护进程,与windows... crond进程定期(每分钟)检查是否有要执行的任务,如果有要执行的任务,则自动执行该任务。用户在cron表 (也被称...
  • 由于开发使用公司电脑,不能使用Mac,如果要和生产环境同步,就得使用虚拟机,Docker , Vargrant 等其他 第三方软件。内存又有限,运行起来... Linux 的定时任务在windows 不能使用,只能使用 windows 的 计划任务...
  • Springboot 定时任务调度

    千次阅读 2018-04-02 14:45:56
    Springboot 定时任务调度器 1.创建任务调度组件 @Component /** * 启用定时器 */ @EnableScheduling @Slf4j public class TimerConfig { /** * 使用cron 创建 */ @Scheduled(cron = "0 0/1 * * *...
  • Celery任务调度示例

    万次阅读 2016-02-29 17:09:24
    Celery任务调度,定时任务Demon
  • 定时任务调度

    千次阅读 2017-06-02 18:00:26
    cron4j特点:最大的特点就是小巧,简单,功能说实话没什么可说的,就是模仿unix的crontab,门槛非常低,编程非常简单. 可以执行一些简单的定时调度功能,太复杂的还是用quartz比较好。定时任务:public class CronJob ...
  • Laravel 任务调度

    千次阅读 2019-12-27 11:39:51
    计划任务定期地在系统后台自动运行;即定时任务 2.学习/操作 1.介绍 TBD 2.操作 TBD 后续整理 ... 3....

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 404,184
精华内容 161,673
关键字:

任务调度什么意思