freertos内核--任务调度剖析
前言
在使用freertos的时候,我们都知道在创建了一系列任务之后,启用调度器,系统就可以帮我们管理任务,分配资源。本文主要对调度器的原理进行剖析,从vTaskStartScheduler()函数开始,一探究竟。
freertos版本:9.0.0
启动调度器
vTaskStartScheduler()
vTaskStartScheduler()用于开启调度器,具体代码如下:
void vTaskStartScheduler( void ){BaseType_t xReturn;xReturn = xTaskCreate( prvIdleTask,"IDLE",configMINIMAL_STACK_SIZE,( void * ) NULL,( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),&xIdleTaskHandle );if( xReturn == pdPASS ){ portDISABLE_INTERRUPTS();if( xPortStartScheduler() != pdFALSE ){/* Should not reach here as if the scheduler is running thefunction will not return. */} }
}
代码已做删减(只显示了核心部分,下同)。可以看到在开启调度器的时候,vTaskStartScheduler()主要做了三件事:创建空闲任务;关闭中断,确保后续工作不会被Systick打断;同时调用xPortStartScheduler()(此处以ARM_CM3内核为例)
xPortStartScheduler()
xPortStartScheduler()函数代码如下:
BaseType_t xPortStartScheduler( void )
{extern void vPortStartFirstTask( void );/* Make PendSV and SysTick the lowest priority interrupts. */portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;/* Start the timer that generates the tick ISR. Interrupts are disabledhere already. */vPortSetupTimerInterrupt();/* Initialise the critical nesting count ready for the first task. */uxCriticalNesting = 0;/* Start the first task. */prvPortStartFirstTask();/* Should never get here as the tasks will now be executing! Call the taskexit error function to prevent compiler warnings about a static functionnot being called in the case that the application writer overrides thisfunctionality by defining configTASK_RETURN_ADDRESS. */prvTaskExitError();/* Should not get here! */return 0;
}
}
第一步是对系统Systick中断和PendSV中断进行优先级设置,均设置为最低( 为什么要设置Systick中断和PendSV中断最低优先级),第二步使能Systick中断(但是中断不会发生,因为之前关闭了中断),最后调用vPortStartFirstTask()。
执行第一个任务
vPortStartFirstTask()
vPortStartFirstTask()用于执行第一个任务。
static void prvPortStartFirstTask( void )
{__asm volatile(" ldr r0, =0xE000ED08 \n" /* Use the NVIC offset register to locate the stack. */" ldr r0, [r0] \n"" ldr r0, [r0] \n"" msr msp, r0 \n" /* Set the msp back to the start of the stack. */" cpsie i \n" /* Globally enable interrupts. */" cpsie f \n"" dsb \n"" isb \n"" svc 0 \n" /* System call to start first task. */" nop \n");
}
代码分析:
ldr r0, =0xE000ED08
ldr r0, [r0]
ldr r0, [r0]
msr msp, r0
这4步的目的是给主堆栈的栈顶指针msp赋初值,具体操作步骤如图:
主堆栈指针就指向了栈顶0x200008DB。
cpsie i
cpsie f
之后开启中断和异常,让下面的SVC中断能够响应
svc 0
产生系统调用服务号为0的SVC中断
SVC中断服务函数
vPortSVCHandler()函数代码如下:
void vPortSVCHandler( void )
{__asm volatile (" ldr r3, pxCurrentTCBConst2 \n" /* Restore the context. */" ldr r1, [r3] \n" /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */" ldr r0, [r1] \n" /* The first item in pxCurrentTCB is the task top of stack. */" ldmia r0!, {r4-r11} \n" /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */" msr psp, r0 \n" /* Restore the task stack pointer. */" 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");
}
代码分析:
ldr r3, pxCurrentTCBConst2
ldr r1, [r3]
ldr r0, [r1]
此部分代码是将pxCurrentTCBConst2这个任务控制块指向的第一个成员的值赋值给r0。从下图任务控制块的结构体可以看到,第一个成员是栈顶指针。
ldmia r0!, {r4-r11}
msr psp, r0
以r0 为基地址,将栈中向上增长的8个字的内容加载到CPU寄存
器r4~r11,同时r0 也会跟着自增。并将自增后的r0赋值给psp,如下图所示:
mov r0, #0
msr basepri, r0
basepri寄存器置0,打开所有中断
orr r14, #0xd
bx r14
当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D,使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、返回用户级。在SVC中断服务里面,使用的是MSP堆栈指针,是处在特权级。关于msp和psp可参考这篇文章:双堆栈…的区别
退出中断,由于此时sp指针使用任务指针psp,所以在进行中断退出的出栈操作时,是以psp指针指向地址开始出栈。这一部分均由硬件完成,相应寄存器会被置位,比如PC指针会更新成新任务的入口地址。
在我们创建任务时,会有初始化任务控制块和任务堆栈的操作。这时候再看看任务堆栈的初始化过程,和上面对比,很多问题都会迎刃而解。
任务堆栈初始化:
StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{/* Simulate the stack frame as it would be created by a context switchinterrupt. */pxTopOfStack--; /* Offset added to account for the way the MCU uses the stack on entry/exit of interrupts. */*pxTopOfStack = portINITIAL_XPSR; /* xPSR */pxTopOfStack--;*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */pxTopOfStack--;*pxTopOfStack = ( StackType_t ) portTASK_RETURN_ADDRESS; /* LR */pxTopOfStack -= 5; /* R12, R3, R2 and R1. */*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */return pxTopOfStack;
}
至此,pc指针指向任务的函数地址,sp指针(此时为psp)指向任务栈的栈顶,第一个任务成功运行。
任务切换
创建完了第一个任务,我们看到之后没有代码了,那操作系统如何对其他任务调度呢?
不要忘了之前的Systick和PendSV两个中断,因为之前关中断以及SVC执行的缘故,他们一直没有起作用。SVC中断执行完之后,Systick开始执行。
Systick中断
Systick中断使能
在开启调度器的时候,我们调用了vPortSetupTimerInterrupt()来使能系统时钟Systick,代码如下:
__attribute__(( weak )) void vPortSetupTimerInterrupt( void )
{/* Configure SysTick to interrupt at the requested rate. */portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
}
主要工作是设置Systick定时器的中断周期和使能Systick中断
Systick中断服务函数
中断使能后,接下来的所有关于任务切换的工作都由Systick发起。
void xPortSysTickHandler( void )
{portDISABLE_INTERRUPTS();{/* Increment the RTOS tick. */if( xTaskIncrementTick() != pdFALSE ){portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;}}portENABLE_INTERRUPTS();
}
xPortSysTickHandler()函数首先进行关中断处理,然后调用xTaskIncrementTick()函数
xTaskIncrementTick()函数代码如下:
BaseType_t xTaskIncrementTick( void )
{TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;/*调度器没有挂起时执行*/if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ){/*系统时基+1*/const TickType_t xConstTickCount = xTickCount + 1;xTickCount = xConstTickCount;/*若系统时间溢出(比如stm32为32位),则交换延时列表*/if( xConstTickCount == ( TickType_t ) 0U ){taskSWITCH_DELAYED_LISTS();}/*有任务的阻塞时间到,解锁任务*/if( xConstTickCount >= xNextTaskUnblockTime ){/*遍历完所有阻塞时间到的任务*/for( ;; ){/*判断延时列表是否为空*/if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE ){/*设置解锁时间为最大值,不让进入if( xConstTickCount >= xNextTaskUnblockTime )*/xNextTaskUnblockTime = portMAX_DELAY; break;}else{/*获取延时列表头部的TCB。延时列表是按照各任务的解锁时间顺序排列的,所以头部任务最先解锁*/pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );/*获取解锁时间*/xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );/*因为延时列表可能存在多个解锁时间相同的任务,需要遍历完*/if( xConstTickCount < xItemValue ){xNextTaskUnblockTime = xItemValue;break;}/*将任务移出延时列表*/( void ) uxListRemove( &( pxTCB->xStateListItem ) );/*将任务移除事件列表*/if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL ){( void ) uxListRemove( &( pxTCB->xEventListItem ) );}/*将任务加入就绪列表*/prvAddTaskToReadyList( pxTCB );/* A task being unblocked cannot cause an immediatecontext switch if preemption is turned off. */#if ( configUSE_PREEMPTION == 1 ){/*判断当前任务的优先级,最大则进行任务切换,否则继续运行原有任务*/if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ){xSwitchRequired = pdTRUE;}}#endif /* configUSE_PREEMPTION */}}}}else{/*挂起时间自增*/++uxPendedTicks;}/*全局变量xYieldPending为任务切换标志,在其他函数中调用*/#if ( configUSE_PREEMPTION == 1 ){if( xYieldPending != pdFALSE ){xSwitchRequired = pdTRUE;}}#endif /* configUSE_PREEMPTION *//*返回的xSwitchRequired决定是否需要任务切换*/return xSwitchRequired;
}
代码分析:
const TickType_t xConstTickCount = xTickCount + 1;xTickCount = xConstTickCount;
这是代码的第一部分,负责系统时基计数。每进入Systick中断一次,xTickCount值+1。例如Systick中断1ms一次,则xTickCount的值就代表系统运行的毫秒数。当然xTickCount也会有溢出的时候,这视处理器而定。溢出了则从0计数,所以对于长时间的,精确的时间统计,我们会选择其他方案。
/*若系统时间溢出(比如stm32为32位),则交换延时列表*/
if( xConstTickCount == ( TickType_t ) 0U )
{taskSWITCH_DELAYED_LISTS();
}
这是代码第二部分。这里涉及到了任务的延时列表。freertos采用延时列表的机制来管理阻塞任务,通过给任务设置延时来让任务进入阻塞状态。例如:任务延时A:60ms,B:70ms,C:120ms,系统会按照当前时基值设置任务的解锁时间(假设xTickCount=40)。所以,任务的解锁时间为A:40+60=100ms,B:40+70=110ms,C:40+120=160ms。同时,系统将这些任务按照解锁时间将其插入延时列表,并按顺序排列,值最小的任务代表最先被解锁加入就绪列表,就会被插入到列表最前面。如下图所示:
现在再来谈上面代码的含义。因为Systick存在溢出问题,比如16位系统,会在xTickCount=65535时加一溢出。为了解决这一问题,freertos采用了延时列表和延时溢出列表,比如Systick的溢出值为140,A,B任务由于解锁时间均大于xTickCount,所以都会加入延时列表;而C任务由于40+120=20(溢出)<xTickCount,会被放进溢出延时列表。如下图:
延时列表中的A,B任务执行完后,延时列表为空。在xTickCount溢出后,xTickCount从0开始计数,并交换两列表(指针指向C所在的列表),等C任务解锁并执行。值得注意的是,此时C所在的列表变为延时列表,而刚刚A,B所在的变为溢出列表。这就是freertos对Systick溢出采用的解决办法。
/*有任务的阻塞时间到,解锁任务*/
if( xConstTickCount >= xNextTaskUnblockTime )
{/*遍历完所有阻塞时间到的任务*/for( ;; ){/*判断延时列表是否为空*/if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE ){/*设置解锁时间为最大值,不让进入if( xConstTickCount >= xNextTaskUnblockTime )*/xNextTaskUnblockTime = portMAX_DELAY; break;}else{/*获取延时列表头部的TCB。延时列表是按照各任务的解锁时间顺序排列的,所以头部任务最先解锁*/pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );/*获取解锁时间*/xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );/*因为延时列表可能存在多个解锁时间相同的任务,需要遍历完*/if( xConstTickCount < xItemValue ){xNextTaskUnblockTime = xItemValue;break;}/*将任务移出延时列表*/( void ) uxListRemove( &( pxTCB->xStateListItem ) );/*将任务移除事件列表*/if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL ){( void ) uxListRemove( &( pxTCB->xEventListItem ) );}/*将任务加入就绪列表*/prvAddTaskToReadyList( pxTCB );/* A task being unblocked cannot cause an immediatecontext switch if preemption is turned off. */#if ( configUSE_PREEMPTION == 1 ){/*判断当前任务的优先级,最大则进行任务切换,否则继续运行原有任务*/if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ){xSwitchRequired = pdTRUE;}}#endif /* configUSE_PREEMPTION */}}
}}
else
{/*挂起时间自增*/
++uxPendedTicks;
}
这是代码第三部分。它判断是否有任务解锁,其中xNextTaskUnblockTime代表最近的任务解锁时间。如果有任务需要解锁,就获取延时列表头部的任务TCB,同时获取其xItemValue值(就是任务解锁时间),之后将其移出延时列表,插入就绪列表,在判断它的优先级是否最高,最高则切换任务,否则不切换。通过这样的操作,任务才能再次被调度。
当然还有一点需要注意。以上步骤是在for循环里面实现的,它的退出条件如下:
/*因为延时列表可能存在多个解锁时间相同的任务,需要遍历完*/
if( xConstTickCount < xItemValue )
{xNextTaskUnblockTime = xItemValue;break;
}
之所以出现遍历多次的情况,是因为同一时间有可能有多个任务需要解锁,也就是说它们的解锁时间是相同的,这种情况不能忽略。
/*全局变量xYieldPending为任务切换标志,在其他函数中调用*/
#if ( configUSE_PREEMPTION == 1 )
{if( xYieldPending != pdFALSE ){xSwitchRequired = pdTRUE;}
}
#endif /* configUSE_PREEMPTION */
这是代码的最后部分。这里涉及到一个全局变量xYieldPending,它是任务切换的标志,会在其它函数中调用,比如vTaskNotifyGiveFromISR(),还有之后的vTaskSwitchContext()。因为有些任务不是在xTaskIncrementTick()中解除阻塞的,而是在其他函数中解除。他们将xYieldPending置位,以达到任务切换的目的。
至此,xTaskIncrementTick()函数结束。它的返回值会作为任务切换的依据,从上面代码的分析可知,任务只有在以下两种情况下才会自动切换:有任务解锁并且其优先级最高,或者xYieldPending置位。
PendSV中断
PendSV中断通过将PENDSV位置位来触发,此前已经在Systick中断服务函数中出现过。
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
PendSV中断服务函数
xPortPendSVHandler()代码如下:
__asm void xPortPendSVHandler( void )
{extern uxCriticalNesting;extern pxCurrentTCB;extern vTaskSwitchContext;PRESERVE8mrs r0, pspisbldr r3, =pxCurrentTCB /* Get the location of the current TCB. */ldr r2, [r3]stmdb r0!, {r4-r11} /* Save the remaining registers. */str r0, [r2] /* Save the new top of stack into the first member of the TCB. */stmdb sp!, {r3, r14}mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITYmsr basepri, r0dsbisbbl vTaskSwitchContextmov r0, #0msr basepri, r0ldmia sp!, {r3, r14}ldr r1, [r3]ldr r0, [r1] /* The first item in pxCurrentTCB is the task top of stack. */ldmia r0!, {r4-r11} /* Pop the registers and the critical nesting count. */msr psp, r0isbbx r14nop
}
整个代码分为两部分:保存上文和切换下文
保存上文部分:
mrs r0, psp
isbldr r3, =pxCurrentTCB /* Get the location of the current TCB. */
ldr r2, [r3]stmdb r0!, {r4-r11} /* Save the remaining registers. */
str r0, [r2] /* Save the new top of stack into the first member of the TCB. */
整个过程和SVC中断差不多。
R0=psp,R3=pxCurrentTCB,R2=pxCurrentTCB->TopOfStack(任务块第一个成员为栈顶地址),保存它们是为了之后使用。
因为在进入PendSV中断时,硬件已经自动将一些寄存器入栈了,所以此时psp的指向如下图所示,只需要将R4-R11手动入栈即可。
此时的R0指向栈顶,将其保存到R2中,更新当前任务栈的栈顶,方便以后任务切换时调用。
中间有一行代码值得注意:
stmdb sp!, {r3, r14}
这行代码的作用时保存R3,R14寄存器的值到堆栈中。在中断中执行时,需使用主堆栈,栈顶指针为msp。之所以要保存到主堆栈中,是因为中断退出的时候,使用的是msp进行出栈操作,而执行vTaskSwitchContext()会让R14的值发生改变,所以需要入栈保护。
至于R3,他在入栈前存的是pxCurrentTCB的地址,执行vTaskSwitchContext()不能确定会不会改变R3的值,所以也入栈保护。
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
关中断保护,因为之后要操作全局变量pxCurrentTCB。
切换下文部分:
bl vTaskSwitchContext
mov r0, #0
msr basepri, r0
ldmia sp!, {r3, r14}ldr r1, [r3]
ldr r0, [r1] /* The first item in pxCurrentTCB is the task top of stack. */
ldmia r0!, {r4-r11} /* Pop the registers and the critical nesting count. */
msr psp, r0
isb
bx r14
nop
这里首先执行vTaskSwitchContext()函数。
vTaskSwitchContext()函数代码如下:
void vTaskSwitchContext( void )
{if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ){/* The scheduler is currently suspended - do not allow a contextswitch. */xYieldPending = pdTRUE;}else{xYieldPending = pdFALSE;/* Select a new task to run using either the generic C or portoptimised asm code. */taskSELECT_HIGHEST_PRIORITY_TASK();}
}
首先判断调度器是否挂起,再决定是否对xYieldPending进行置位。如果挂起了,则置位xYieldPending,相当于下次开启时会强制进行一次任务切换。如果没挂起,则执行taskSELECT_HIGHEST_PRIORITY_TASK()函数。
taskSELECT_HIGHEST_PRIORITY_TASK()函数代码如下:
#define taskSELECT_HIGHEST_PRIORITY_TASK() \{ \UBaseType_t uxTopPriority = uxTopReadyPriority; \\/* Find the highest priority queue that contains ready tasks. */ \while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ) \{ \configASSERT( uxTopPriority ); \--uxTopPriority; \} \\/* listGET_OWNER_OF_NEXT_ENTRY indexes through the list, so the tasks of \the same priority get an equal share of the processor time. */ \listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \uxTopReadyPriority = uxTopPriority; \} /* taskSELECT_HIGHEST_PRIORITY_TASK */
此函数用于选择优先级最高的任务,全局变量pxCurrentTCB就是在这里修改的。因为优先级最高的任务列表不一定就有任务存在(任务阻塞被暂时移除),所以要遍历出此刻有任务存在列表的最高优先级,然后调用listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) )函数,注意此处传入的参数为pxCurrentTCB。
listGET_OWNER_OF_NEXT_ENTRY()代码如下:
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) \
{ \
List_t * const pxConstList = ( pxList ); \/* Increment the index to the next item and return the item, ensuring */ \/* we don't return the marker used at the end of the list. */ \( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \{ \( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \} \( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; \
}
因为同一优先级下可能存在多个任务,freertos的处理方法是使用时间片,从代码可以很容易看出,每进入一次这个函数,( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext都会执行一次,作用是切换到同优先级的下一个任务,让同优先级的每一个任务都能拥有相同的执行时间。
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;
之后就是最核心的修改全局变量pxCurrentTCB,它作为参数传递给了pxTCB。至此,pxCurrentTCB更新,指向优先级最高任务。
接下来回到汇编,回到切换下文的内容。
mov r0, #0
msr basepri, r0
ldmia sp!, {r3, r14}ldr r1, [r3]
ldr r0, [r1] /* The first item in pxCurrentTCB is the task top of stack. */
ldmia r0!, {r4-r11} /* Pop the registers and the critical nesting count. */
msr psp, r0
isb
bx r14
nop
首先是开中断,修改完了pxCurrentTCB要立即打开,不然会影响系统的实时性。之后的操作与入栈如出一辙,值得注意的是,之前R3,R14入栈的作用就在这里体现了,R3始终指向pxCurrentTCB的地址,R14保存的进入中断之前的处理器模式和堆栈指针。
到此,freertos的任务调度内核函数剖析结束。
freertos内核--任务调度剖析相关推荐
- 腾讯Elasticsearch海量规模背后的内核优化剖析
点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:黄华,腾讯 TEG 云架构平台部研发工程师 背景 Elast ...
- freertos内核 任务定义与切换 原理分析
freertos内核 任务定义与切换 原理分析 主程序 任务控制块 任务创建函数 任务栈初始化 就绪列表 调度器 总结任务切换 主程序 这个程序目的就是,使用freertos让两个任务不断切换.看两个 ...
- 【Elasticsearch】腾讯Elasticsearch海量规模背后的内核优化剖析
1.概述 转载:腾讯Elasticsearch海量规模背后的内核优化剖析
- linux0.11内核完全剖析 - ll_rw_blk.c
声明: 参考<linux内核完全剖析基于linux0.11>--赵炯 节选 1.功能描述 该程序主要用于执行低层块设备读 / 写操作,是本章所有块设备与系统其它部分的接口程序.其它程 ...
- 《Tomcat内核设计剖析》勘误表
##<Tomcat内核设计剖析>勘误表 书中第95页图request部分印成了reqiest. 书中第311页两个tomcat3,其中一个应为tomcat4. 书中第5页URL应为URI. ...
- 《Linux内核完全剖析-基于0.12内核》书评之陈莉君
<Linux内核完全剖析-基于0.12内核>书评之陈莉君 <Linux内核完全剖析-基于0.12内核>一书出版之后,机械工业出版社编辑希望我就此书抽空写一个书评.在我拿到这本书 ...
- FreeRTOS内核笔记(一):基本知识和命名规则
FreeRTOS内核笔记(一):基本知识和命名规则 FreeRTOS内核笔记 命名规则: 常用宏定义 Thread运行状态: RTOS Tick Context切换: 实时调度器Scheduler F ...
- 《Linux内核完全剖析》阅读笔记
我是通过阅读赵炯老师编的厚厚的 linux 内核完全剖析看完 LINUX0.11 的代码,不得不发自内心的说 Linus 真的是个天才.虽然我觉得很多 OS 设计的思想他是从 UNIX 学来的,但是他 ...
- freertos内核走读2——task任务调度机制(三)
本文为jorhai原创,转载请注明,谢谢! 继续任务操作相关函数走读. vTaskDelayUntil, vTaskDelay的可以实现当前任务阻塞一定的tick时间,然后唤醒任务.任务从vTaskD ...
最新文章
- POJ - 3160 Father Christmas flymouse tanjar缩点构图+dfs
- NOIP2012-摆花
- 教你如何创建类似QQ的android弹出菜单
- 我对对象和引用的理解
- 高效终端设备视觉系统开发与优化
- 第一章 统计学概论
- 一套标准的ASP.NET Core容器化应用日志收集分析方案
- 如何在Unity3d平台下低延迟播放RTMP或RTSP流
- 题库明细 C#语言和SQL Server
- PHP与前端协作模式的理解
- ORA-28056,安装Oracle出错
- CentOS下使用SVN实现多项目管理配置方案
- in use 大学英语4word_《新视野大学英语4网络测试题unit6++Microsoft+Word+文档》.doc
- 高效集成连接管理与平台运营 中琛物联赋能智慧城市建设
- Latch up 闩锁效应
- web前端设计与开发,css段落首行缩进2字符怎么设置
- 计算机组装流程是什么,组装电脑的步骤
- Workgroup 协议
- 【强化学习】什么是强化学习算法?
- 【区块链】开源社区发力区块链,超级账本会成就Linux一样的传奇吗?