前言

在使用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内核--任务调度剖析相关推荐

  1. 腾讯Elasticsearch海量规模背后的内核优化剖析

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:黄华,腾讯 TEG 云架构平台部研发工程师 背景 Elast ...

  2. freertos内核 任务定义与切换 原理分析

    freertos内核 任务定义与切换 原理分析 主程序 任务控制块 任务创建函数 任务栈初始化 就绪列表 调度器 总结任务切换 主程序 这个程序目的就是,使用freertos让两个任务不断切换.看两个 ...

  3. 【Elasticsearch】腾讯Elasticsearch海量规模背后的内核优化剖析

    1.概述 转载:腾讯Elasticsearch海量规模背后的内核优化剖析

  4. linux0.11内核完全剖析 - ll_rw_blk.c

    声明: 参考<linux内核完全剖析基于linux0.11>--赵炯    节选 1.功能描述 该程序主要用于执行低层块设备读 / 写操作,是本章所有块设备与系统其它部分的接口程序.其它程 ...

  5. 《Tomcat内核设计剖析》勘误表

    ##<Tomcat内核设计剖析>勘误表 书中第95页图request部分印成了reqiest. 书中第311页两个tomcat3,其中一个应为tomcat4. 书中第5页URL应为URI. ...

  6. 《Linux内核完全剖析-基于0.12内核》书评之陈莉君

    <Linux内核完全剖析-基于0.12内核>书评之陈莉君 <Linux内核完全剖析-基于0.12内核>一书出版之后,机械工业出版社编辑希望我就此书抽空写一个书评.在我拿到这本书 ...

  7. FreeRTOS内核笔记(一):基本知识和命名规则

    FreeRTOS内核笔记(一):基本知识和命名规则 FreeRTOS内核笔记 命名规则: 常用宏定义 Thread运行状态: RTOS Tick Context切换: 实时调度器Scheduler F ...

  8. 《Linux内核完全剖析》阅读笔记

    我是通过阅读赵炯老师编的厚厚的 linux 内核完全剖析看完 LINUX0.11 的代码,不得不发自内心的说 Linus 真的是个天才.虽然我觉得很多 OS 设计的思想他是从 UNIX 学来的,但是他 ...

  9. freertos内核走读2——task任务调度机制(三)

    本文为jorhai原创,转载请注明,谢谢! 继续任务操作相关函数走读. vTaskDelayUntil, vTaskDelay的可以实现当前任务阻塞一定的tick时间,然后唤醒任务.任务从vTaskD ...

最新文章

  1. POJ - 3160 Father Christmas flymouse tanjar缩点构图+dfs
  2. NOIP2012-摆花
  3. 教你如何创建类似QQ的android弹出菜单
  4. 我对对象和引用的理解
  5. 高效终端设备视觉系统开发与优化
  6. 第一章 统计学概论
  7. 一套标准的ASP.NET Core容器化应用日志收集分析方案
  8. 如何在Unity3d平台下低延迟播放RTMP或RTSP流
  9. 题库明细 C#语言和SQL Server
  10. PHP与前端协作模式的理解
  11. ORA-28056,安装Oracle出错
  12. CentOS下使用SVN实现多项目管理配置方案
  13. in use 大学英语4word_《新视野大学英语4网络测试题unit6++Microsoft+Word+文档》.doc
  14. 高效集成连接管理与平台运营 中琛物联赋能智慧城市建设
  15. Latch up 闩锁效应
  16. web前端设计与开发,css段落首行缩进2字符怎么设置
  17. 计算机组装流程是什么,组装电脑的步骤
  18. Workgroup 协议
  19. 【强化学习】什么是强化学习算法?
  20. 【区块链】开源社区发力区块链,超级账本会成就Linux一样的传奇吗?

热门文章

  1. python 灰度图转矩阵_图像转换矩阵
  2. 如何在Spring官网下载Spring源码包
  3. 从BAT到阿Q:百度真的要掉队了?
  4. 树莓派无线网络设置、WLAN0设置
  5. matlab中怎么仿真出bumps信号,显示仿真过程中生成的信号
  6. 索尼常务董事:绩效主义毁了索尼
  7. ecplise工具栏大小设置
  8. Jackson - @JsonInclude之NON_EMPTY
  9. css弹性布局和网格布局
  10. 相机sd卡删除的照片如何恢复?