FreeRTOS學習筆記——創建任務

主機環境:Windows

開發環境:MDK4.7.2

FreeRTOS版本:FreeRTOS8.1.2

目標環境:STM32F030C8T6

FreeRTOS中一個很重要的結構就是TCB任務控制塊了,來實現對任務的管理,TCB的結構定義在tasks.c文件中

typedef struct tskTaskControlBlock
{
	volatile StackType_t	*pxTopOfStack;	

	#if ( portUSING_MPU_WRAPPERS == 1 )
		xMPU_SETTINGS	xMPUSettings;	
	#endif

	ListItem_t			xGenericListItem;	
	ListItem_t			xEventListItem;		
	UBaseType_t			uxPriority;			
	StackType_t			*pxStack;		
	char				pcTaskName[ configMAX_TASK_NAME_LEN ];

	#if ( portSTACK_GROWTH > 0 )
		StackType_t		*pxEndOfStack;		
	#endif

	#if ( portCRITICAL_NESTING_IN_TCB == 1 )
		UBaseType_t 	uxCriticalNesting; 
	#endif

	#if ( configUSE_TRACE_FACILITY == 1 )
		UBaseType_t		uxTCBNumber;	
		UBaseType_t  	uxTaskNumber;		
	#endif

	#if ( configUSE_MUTEXES == 1 )
		UBaseType_t 	uxBasePriority;		
		UBaseType_t 	uxMutexesHeld;
	#endif

	#if ( configUSE_APPLICATION_TASK_TAG == 1 )
		TaskHookFunction_t pxTaskTag;
	#endif

	#if ( configGENERATE_RUN_TIME_STATS == 1 )
		uint32_t		ulRunTimeCounter;	
	#endif

	#if ( configUSE_NEWLIB_REENTRANT == 1 )
		/* Allocate a Newlib reent structure that is specific to this task.
		Note Newlib support has been included by popular demand, but is not
		used by the FreeRTOS maintainers themselves.  FreeRTOS is not
		responsible for resulting newlib operation.  User must be familiar with
		newlib and must provide system-wide implementations of the necessary
		stubs. Be warned that (at the time of writing) the current newlib design
		implements a system-wide malloc() that must be provided with locks. */
		struct 	_reent xNewLib_reent;
	#endif

} tskTCB;
由於FreeRTOS是用戶可配置的,一些不需要的功能我們可以不添加以便節省資源,在STM32F0C8T6中是沒有MPU(內存保護單元)的,因此這塊代碼完全不必去了解,另外一些應用程序調試、跟蹤以及程序運行狀態的一些參數我們也可以暫時不去管理,其中最基本的幾個屬性有棧頂指針pxTopOfStack、狀態項xGenericListItem、事件項xEventListItem、任務優先級uxPriority、用戶棧空間起始地址pxStack以及任務名稱pcTaskName,其中任務長度是有限制的,configMAX_TASK_NAME_LEN有用戶在FreeRTOSConfig.h文件中配置,根據MCU的棧增長方向可能需要棧底指針屬性。

FreeRTOS的任務創建是以宏定義形式調用的形式如下

xTaskCreate( pvTaskCode, pcName, usStackDepth, pvParameters, uxPriority, pxCreatedTask )
實際中是調用的下面函數

xTaskGenericCreate( ( pvTaskCode ), ( pcName ), ( usStackDepth ), ( pvParameters ), ( uxPriority ), ( pxCreatedTask ), ( NULL ), ( NULL ) )
在tasks.c中還有一些需要用的變量如下

/* Other file private variables. --------------------------------*/
PRIVILEGED_DATA static volatile UBaseType_t uxCurrentNumberOfTasks 	= ( UBaseType_t ) 0U;
PRIVILEGED_DATA static volatile TickType_t xTickCount 				= ( TickType_t ) 0U;
PRIVILEGED_DATA static volatile UBaseType_t uxTopReadyPriority 		= tskIDLE_PRIORITY;
PRIVILEGED_DATA static volatile BaseType_t xSchedulerRunning 		= pdFALSE;
PRIVILEGED_DATA static volatile UBaseType_t uxPendedTicks 			= ( UBaseType_t ) 0U;
PRIVILEGED_DATA static volatile BaseType_t xYieldPending 			= pdFALSE;
PRIVILEGED_DATA static volatile BaseType_t xNumOfOverflows 			= ( BaseType_t ) 0;
PRIVILEGED_DATA static UBaseType_t uxTaskNumber 					= ( UBaseType_t ) 0U;
PRIVILEGED_DATA static volatile TickType_t xNextTaskUnblockTime		= portMAX_DELAY;
任務創建代碼如下

BaseType_t xTaskGenericCreate( TaskFunction_t pxTaskCode, const char * const pcName, const uint16_t usStackDepth, 
	void * const pvParameters, UBaseType_t uxPriority, TaskHandle_t * const pxCreatedTask, StackType_t * const puxStackBuffer, 
	const MemoryRegion_t * const xRegions )
{
BaseType_t xReturn;
TCB_t * pxNewTCB;

	configASSERT( pxTaskCode );
	configASSERT( ( ( uxPriority & ( ~portPRIVILEGE_BIT ) ) < configMAX_PRIORITIES ) );

	/* Allocate the memory required by the TCB and stack for the new task,
	checking that the allocation was successful. */
	pxNewTCB = prvAllocateTCBAndStack( usStackDepth, puxStackBuffer );

	if( pxNewTCB != NULL )
	{
		StackType_t *pxTopOfStack;

		#if( portUSING_MPU_WRAPPERS == 1 )
			/* Should the task be created in privileged mode? */
			BaseType_t xRunPrivileged;
			if( ( uxPriority & portPRIVILEGE_BIT ) != 0U )
			{
				xRunPrivileged = pdTRUE;
			}
			else
			{
				xRunPrivileged = pdFALSE;
			}
			uxPriority &= ~portPRIVILEGE_BIT;
		#endif /* portUSING_MPU_WRAPPERS == 1 */

		/* Calculate the top of stack address.  This depends on whether the
		stack grows from high memory to low (as per the 80x86) or vice versa.
		portSTACK_GROWTH is used to make the result positive or negative as
		required by the port. */
		#if( portSTACK_GROWTH < 0 )
		{
			pxTopOfStack = pxNewTCB->pxStack + ( usStackDepth - ( uint16_t ) 1 );
			pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ( portPOINTER_SIZE_TYPE ) ~portBYTE_ALIGNMENT_MASK  ) );

			/* Check the alignment of the calculated top of stack is correct. */
			configASSERT( ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack & ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) == 0UL ) );
		}
		#else /* portSTACK_GROWTH */
		{
			pxTopOfStack = pxNewTCB->pxStack;

			/* Check the alignment of the stack buffer is correct. */
			configASSERT( ( ( ( portPOINTER_SIZE_TYPE ) pxNewTCB->pxStack & ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) == 0UL ) );

			/* If we want to use stack checking on architectures that use
			a positive stack growth direction then we also need to store the
			other extreme of the stack space. */
			pxNewTCB->pxEndOfStack = pxNewTCB->pxStack + ( usStackDepth - 1 );
		}
		#endif /* portSTACK_GROWTH */

		/* Setup the newly allocated TCB with the initial state of the task. */
		prvInitialiseTCBVariables( pxNewTCB, pcName, uxPriority, xRegions, usStackDepth );

		/* Initialize the TCB stack to look as if the task was already running,
		but had been interrupted by the scheduler.  The return address is set
		to the start of the task function. Once the stack has been initialised
		the	top of stack variable is updated. */
		#if( portUSING_MPU_WRAPPERS == 1 )
		{
			pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters, xRunPrivileged );
		}
		#else /* portUSING_MPU_WRAPPERS */
		{
			pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
		}
		#endif /* portUSING_MPU_WRAPPERS */

		if( ( void * ) pxCreatedTask != NULL )
		{
			/* Pass the TCB out - in an anonymous way.  The calling function/
			task can use this as a handle to delete the task later if
			required.*/
			*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}

		/* Ensure interrupts don't access the task lists while they are being
		updated. */
		taskENTER_CRITICAL();
		{
			uxCurrentNumberOfTasks++;
			if( pxCurrentTCB == NULL )
			{
				/* There are no other tasks, or all the other tasks are in
				the suspended state - make this the current task. */
				pxCurrentTCB =  pxNewTCB;

				if( uxCurrentNumberOfTasks == ( UBaseType_t ) 1 )
				{
					/* This is the first task to be created so do the preliminary
					initialisation required.  We will not recover if this call
					fails, but we will report the failure. */
					prvInitialiseTaskLists();
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				/* If the scheduler is not already running, make this task the
				current task if it is the highest priority task to be created
				so far. */
				if( xSchedulerRunning == pdFALSE )
				{
					if( pxCurrentTCB->uxPriority <= uxPriority )
					{
						pxCurrentTCB = pxNewTCB;
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}

			uxTaskNumber++;

			#if ( configUSE_TRACE_FACILITY == 1 )
			{
				/* Add a counter into the TCB for tracing only. */
				pxNewTCB->uxTCBNumber = uxTaskNumber;
			}
			#endif /* configUSE_TRACE_FACILITY */
			traceTASK_CREATE( pxNewTCB );

			prvAddTaskToReadyList( pxNewTCB );

			xReturn = pdPASS;
			portSETUP_TCB( pxNewTCB );
		}
		taskEXIT_CRITICAL();
	}
	else
	{
		xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
		traceTASK_CREATE_FAILED();
	}

	if( xReturn == pdPASS )
	{
		if( xSchedulerRunning != pdFALSE )
		{
			/* If the created task is of a higher priority than the current task
			then it should run now. */
			if( pxCurrentTCB->uxPriority < uxPriority )
			{
				taskYIELD_IF_USING_PREEMPTION();
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}

	return xReturn;
}
創建任務函數的第一步是創建該任務的TCB指針通過調用prvAllocateTCBAndStack()函數來實現

該函數實現如下

static TCB_t *prvAllocateTCBAndStack( const uint16_t usStackDepth, StackType_t * const puxStackBuffer )
{
TCB_t *pxNewTCB;

	/* Allocate space for the TCB.  Where the memory comes from depends on
	the implementation of the port malloc function. */
	pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );

	if( pxNewTCB != NULL )
	{
		/* Allocate space for the stack used by the task being created.
		The base of the stack memory stored in the TCB so the task can
		be deleted later if required. */
		pxNewTCB->pxStack = ( StackType_t * ) pvPortMallocAligned( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ), puxStackBuffer ); 

		if( pxNewTCB->pxStack == NULL )
		{
			/* Could not allocate the stack.  Delete the allocated TCB. */
			vPortFree( pxNewTCB );
			pxNewTCB = NULL;
		}
		else
		{
			/* Avoid dependency on memset() if it is not required. */
			#if( ( configCHECK_FOR_STACK_OVERFLOW > 1 ) || ( configUSE_TRACE_FACILITY == 1 ) || ( INCLUDE_uxTaskGetStackHighWaterMark == 1 ) )
			{
				/* Just to help debugging. */
				( void ) memset( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE, ( size_t ) usStackDepth * sizeof( StackType_t ) );
			}
			#endif /* ( ( configCHECK_FOR_STACK_OVERFLOW > 1 ) || ( ( configUSE_TRACE_FACILITY == 1 ) || ( INCLUDE_uxTaskGetStackHighWaterMark == 1 ) ) ) */
		}
	}

	return pxNewTCB;
}
該函數很簡單,申請一塊TCB空間,如果申請成功之後則根據用戶指定的棧深度來申請棧空間,如果棧空間申請成功則將該TCB指針返回上一級。棧空間申請的實現與FreeRTOS的內存管理有關,在portable/MemMang文件夾下有5個文件來實現內存分配管理,這裏就先不詳細瞭解了,後面再去深入瞭解吧


在TCB指針申請成功之後,第二步是初始化棧頂指針,一般而言棧的增長方向是向下的,即往地址小的方向增長,隨着對棧的操作,棧頂指針不斷地變化,pxStack是棧的起始地址,根據用戶指定的棧深度來得到棧頂指針的地址,棧的結構如下


同時根據portBYTE_ALIGNMENT_MASK宏來指定棧頂指針的字節對齊方式,STM32是32位MCU,因此棧頂指針是4字節對齊的。

有關TCB的空間申請完畢之後就要填充TCB裏面的一些變量屬性了,由prvInitialiseTCBVariables函數來實現,該函數實現代碼如下

static void prvInitialiseTCBVariables( TCB_t * const pxTCB, const char * const pcName, UBaseType_t uxPriority, 
	const MemoryRegion_t * const xRegions, const uint16_t usStackDepth ) 
{
UBaseType_t x;

	/* Store the task name in the TCB. */
	for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )
	{
		pxTCB->pcTaskName[ x ] = pcName[ x ];

		if( pcName[ x ] == 0x00 )
		{
			break;
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}

	/* Ensure the name string is terminated in the case that the string length
	was greater or equal to configMAX_TASK_NAME_LEN. */
	pxTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';

	/* This is used as an array index so must ensure it's not too large.  First
	remove the privilege bit if one is present. */
	if( uxPriority >= ( UBaseType_t ) configMAX_PRIORITIES )
	{
		uxPriority = ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t ) 1U;
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}

	pxTCB->uxPriority = uxPriority;
	#if ( configUSE_MUTEXES == 1 )
	{
		pxTCB->uxBasePriority = uxPriority;
		pxTCB->uxMutexesHeld = 0;
	}
	#endif /* configUSE_MUTEXES */

	vListInitialiseItem( &( pxTCB->xGenericListItem ) );
	vListInitialiseItem( &( pxTCB->xEventListItem ) );

	/* Set the pxTCB as a link back from the ListItem_t.  This is so we can get
	back to	the containing TCB from a generic item in a list. */
	listSET_LIST_ITEM_OWNER( &( pxTCB->xGenericListItem ), pxTCB );

	/* Event lists are always in priority order. */
	listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority );
	listSET_LIST_ITEM_OWNER( &( pxTCB->xEventListItem ), pxTCB );

	#if ( portCRITICAL_NESTING_IN_TCB == 1 )
	{
		pxTCB->uxCriticalNesting = ( UBaseType_t ) 0U;
	}
	#endif /* portCRITICAL_NESTING_IN_TCB */

	#if ( configUSE_APPLICATION_TASK_TAG == 1 )
	{
		pxTCB->pxTaskTag = NULL;
	}
	#endif /* configUSE_APPLICATION_TASK_TAG */

	#if ( configGENERATE_RUN_TIME_STATS == 1 )
	{
		pxTCB->ulRunTimeCounter = 0UL;
	}
	#endif /* configGENERATE_RUN_TIME_STATS */

	#if ( portUSING_MPU_WRAPPERS == 1 )
	{
		vPortStoreTaskMPUSettings( &( pxTCB->xMPUSettings ), xRegions, pxTCB->pxStack, usStackDepth );
	}
	#else /* portUSING_MPU_WRAPPERS */
	{
		( void ) xRegions;
		( void ) usStackDepth;
	}
	#endif /* portUSING_MPU_WRAPPERS */

	#if ( configUSE_NEWLIB_REENTRANT == 1 )
	{
		/* Initialise this task's Newlib reent structure. */
		_REENT_INIT_PTR( ( &( pxTCB->xNewLib_reent ) ) );
	}
	#endif /* configUSE_NEWLIB_REENTRANT */
}
是比較簡單的一個函數,首先是採用字節拷貝方式來存儲任務名稱,記錄任務的優先級且增加了優先級保護代碼,初始化了狀態項和事件項,同時設置了狀態項和事件項的所有者爲該任務的TCB指針,有關事件項賦值語句

listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority );
還不瞭解爲啥要用configMAX_PRIORITIES-uxPriority的值來存儲,後面想到了再看吧。回到創建任務的函數中接着是初始化棧空間

StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{
	/* Simulate the stack frame as it would be created by a context switch
	interrupt. */
	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;	/* PC */
	pxTopOfStack--;
	*pxTopOfStack = ( StackType_t ) prvTaskExitError;	/* LR */
	pxTopOfStack -= 5;	/* R12, R3, R2 and R1. */
	*pxTopOfStack = ( StackType_t ) pvParameters;	/* R0 */
	pxTopOfStack -= 8; /* R11..R4. */

	return pxTopOfStack;
}

即棧空間中高地址主要是存儲通用寄存器的值,低地址空間用來存儲用戶數據,如下


如果用戶需要用到任務句柄,在創建任務函數中將該任務的TCB指針賦給了任務句柄pxCreatedTask,之後進入臨界區中,進入和退出臨界區代碼如下

void vPortEnterCritical( void )
{
    portDISABLE_INTERRUPTS();
    uxCriticalNesting++;//臨界區嵌套標記加1
	__dsb( portSY_FULL_READ_WRITE );
	__isb( portSY_FULL_READ_WRITE );
}
/*-----------------------------------------------------------*/

void vPortExitCritical( void )
{
	configASSERT( uxCriticalNesting );
    uxCriticalNesting--;//臨界區嵌套標記減1
    if( uxCriticalNesting == 0 )
    {
        portENABLE_INTERRUPTS();
    }
}

uxCriticalNesting變量是臨界區嵌套標記,聲明如下

static UBaseType_t uxCriticalNesting = 0xaaaaaaaa;
在進入臨界區時關閉中斷,在退出臨界區時判斷是否有臨界區嵌套,如果沒有則打開中斷,在臨界區代碼中更新當前任務數uxCurrentNumberOfTasks,如果當前TCB指針pxCurrentTCB爲空,則將pxCurrentTCB指針指向新建任務的TCB,如果新建的任務是第一個任務則還需要初始化任務鏈表,進入prvInitialiseTaskLists()函數

static void prvInitialiseTaskLists( void )
{
UBaseType_t uxPriority;

	for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )
	{
		vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
	}

	vListInitialise( &xDelayedTaskList1 );
	vListInitialise( &xDelayedTaskList2 );
	vListInitialise( &xPendingReadyList );

	#if ( INCLUDE_vTaskDelete == 1 )
	{
		vListInitialise( &xTasksWaitingTermination );
	}
	#endif /* INCLUDE_vTaskDelete */

	#if ( INCLUDE_vTaskSuspend == 1 )
	{
		vListInitialise( &xSuspendedTaskList );
	}
	#endif /* INCLUDE_vTaskSuspend */

	/* Start with pxDelayedTaskList using list1 and the pxOverflowDelayedTaskList
	using list2. */
	pxDelayedTaskList = &xDelayedTaskList1;
	pxOverflowDelayedTaskList = &xDelayedTaskList2;
}
首先初始化的是就緒任務鏈表,初始化每一個優先級的就緒鏈表,此外還初始化兩個延時鏈表以及掛起的就緒鏈表,並將兩個延時鏈表指針指向了已初始化的兩個延時鏈表。

如果當前TCB指針pxCurrentTCB不爲空且調度器還沒有運行則根據當前任務優先級判斷是否需要更新pxCurrentTCB指針,pxCurrentTCB指針總是指向優先級最高的任務,更新任務數uxTaskNumber,完成之後將該任務插入相應的就緒鏈表中等待調度器調用,

#define prvAddTaskToReadyList( pxTCB )																\
	traceMOVED_TASK_TO_READY_STATE( pxTCB )															\
	taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority );												\
	vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xGenericListItem )

這裏有個疑問好想xGenericListItem還沒有包含有效值xItemValue

退出臨界區代碼後如果所有資源已準備就緒即xReturn=pdPass時,如果掉調度處於運行中則根據優先級判斷是否需要執行一次任務切換,該代碼最終調用vPortYield()函數,如下

void vPortYield( void )
{
	/* Set a PendSV to request a context switch. */
	*( portNVIC_INT_CTRL ) = portNVIC_PENDSVSET;

	/* Barriers are normally not required but do ensure the code is completely
	within the specified behaviour for the architecture. */
	__dsb( portSY_FULL_READ_WRITE );
	__isb( portSY_FULL_READ_WRITE );
}

進入PENDSV中斷執行任務切換代碼,

#define portNVIC_INT_CTRL			( ( volatile uint32_t *) 0xe000ed04 )
該寄存器的說明在ARMv6架構參考手冊中有說明

#define portNVIC_PENDSVSET			0x10000000


將PENDSVSET置1進入PENDSV中斷執行任務切換代碼,但是我們創建任務的代碼還沒有執行完畢,沒有返回,調度器就去執行新建任務的代碼了,總覺得應該創建任務代碼返回後再切換纔對,寫一個示例代碼驗證

static const char *taskName2="Task2\r\n";
void vTask1(void *pvParamters);
void vTask2(void *pvParamters);
void vTask1(void *pvParamters)
{
	char *TskName;
	TskName = (char *)pvParamters;
	if(xTaskCreate(vTask2,"Task2\r\n",128,(void *)taskName2,2,NULL)== pdPASS)
	{
		uart_puts("create task2 success\r\n");
	}
	while(1)
	{
		uart_puts(TskName);

		vTaskDelay(1000/portTICK_RATE_MS);
		
	}
}
void vTask2(void *pvParamters)
{
	char *TskName;
	TskName = (char *)pvParamters;
	while(1)
	{
		uart_puts(TskName);

		vTaskDelay(1000/portTICK_RATE_MS);
		
	}
}

int main(void)
{
	static const char *taskName1="Task1\r\n";
	

	uart_init (SET_UART (BAUD_115200,PAR_NONE));  //初始化串口

	xTaskCreate(vTask1,"Task1",128,(void *)taskName1,1,NULL);
	/*啓動調度器,任務開始執行*/
	vTaskStartScheduler();
	while(1);
	return 0;
}

在主函數中創建TASK1,在TASK1的代碼中創建TASK2,且TASK2的優先級要高於TASK1並接收創建TASK2的返回值,驗證結果如下


運行結果果然與代碼一致,創建完TASK2後,調度器發現TASK2的優先級要高於TASK1,執行了任務切換去執行TASK2的代碼,當TASK2掛起時,TASK1獲取了CPU繼續執行,獲取到了創建TASK2的返回值並輸出了“create task2 success”。創建任務的代碼就先分析到這,後面有新發現的話再補充吧。。。

發佈了98 篇原創文章 · 獲贊 101 · 訪問量 51萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章