FreeRTOS --(1)鏈表

Based On FreeRTOS Kernel V10.3.1

1、相關文件

鏈表結構是 OS 內部經常使用到的,FreeRTOS 自然也不例外,在深入分析各個模塊的工作原理之前,首先來分析 FreeRTOS 的鏈表結構,和鏈表相關的代碼被定義在:

list.h

list.c

 

2、數據結構

不得不說,FreeRTOS 另一個成功的因素,在於他的代碼註釋,非常的完備,有的時候,代碼、結構等的定義,和具體的場景相關性很強,也就是說,沒有分析到更後面的使用場景,那麼可能便很難理解當前看到的結構定義;

FreeRTOS 這一點做得非常的棒,它有很多很棒的註釋,能夠幫助大家在初期能夠把握一些全局性的東西;

FreeRTOS 使用雙向鏈表來描述鏈表結構,FreeRTOS 中定義了 3 個相關的結構:

ListItem_t:用來表示鏈表中的一個元素;

MiniListItem_t:用來表示鏈表中初始的那個元素;

List_t:用來表示一個鏈表;

2.1、ListItem_t

ListItem_t 用於描述鏈表中的一個元素,它的定義爲:

/*
 * Definition of the only type of object that a list can contain.
 */
struct xLIST;
struct xLIST_ITEM
{
	listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE			/*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
	configLIST_VOLATILE TickType_t xItemValue;			/*< The value being listed.  In most cases this is used to sort the list in descending order. */
	struct xLIST_ITEM * configLIST_VOLATILE pxNext;		/*< Pointer to the next ListItem_t in the list. */
	struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;	/*< Pointer to the previous ListItem_t in the list. */
	void * pvOwner;										/*< Pointer to the object (normally a TCB) that contains the list item.  There is therefore a two way link between the object containing the list item and the list item itself. */
	struct xLIST * configLIST_VOLATILE pxContainer;		/*< Pointer to the list in which this list item is placed (if any). */
	listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE			/*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
};
typedef struct xLIST_ITEM ListItem_t;	

結構體中的各個成員的描述爲:

listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE :

這個是新版本加上的,用於鏈表是否有效的判斷,當定義了 configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES 這個爲 1 的時候,鏈表的這個成員便有效,否則是空定義;初始化的時候,這個佔位的 Tag 被設置成爲固定的 0x5a5a5a5aUL,作用是,在使用鏈表的時候,判斷這個成員是否可能被踩;

TickType_t xItemValue

這個成員用於排序,看得出來,被定義成爲了 TickType_t 類型,也就是按照時間的值來排序;

struct xLIST_ITEM * configLIST_VOLATILE pxNext

指向下一個成員的指針;

struct xLIST_ITEM * configLIST_VOLATILE pxPrevious

指向上一個成員的指針;

void * pvOwner

指向擁有這個 Item 成員的結構體,通常是描述進程 TCB 的指針;

struct xLIST * configLIST_VOLATILE pxContainer

指向這個 Item 所在的鏈表的指針;

listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE:

含義和第一個成員一樣

 

2.2、MiniListItem_t

從名字就看得出來,是一個迷你型的 Item,它的定義爲:

struct xMINI_LIST_ITEM
{
	listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE			/*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
	configLIST_VOLATILE TickType_t xItemValue;
	struct xLIST_ITEM * configLIST_VOLATILE pxNext;
	struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;

看得出來,它和 ListItem_t 的定義非常類似,關鍵成員少了 pvOwner、pxContainer;爲什麼少定義這兩個呢?因爲這個成員根本用不到這兩個,後面我們在談原因;

 

2.3、List_t

主角登場,List_t 用於描述一個鏈表,它的定義如下:

/*
 * Definition of the type of queue used by the scheduler.
 */
typedef struct xLIST
{
	listFIRST_LIST_INTEGRITY_CHECK_VALUE				/*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
	volatile UBaseType_t uxNumberOfItems;
	ListItem_t * configLIST_VOLATILE pxIndex;			/*< Used to walk through the list.  Points to the last item returned by a call to listGET_OWNER_OF_NEXT_ENTRY (). */
	MiniListItem_t xListEnd;							/*< List item that contains the maximum possible item value meaning it is always at the end of the list and is therefore used as a marker. */
	listSECOND_LIST_INTEGRITY_CHECK_VALUE				/*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
} List_t;

可以看到,FreeRTOS 對它的註釋是:

Definition of the type of queue used by the scheduler

被調度器使用的隊列,也就是說進程調度要用到它;

結構體一前一後兩個定義不再多說,和前面的一樣,爲了檢測結構是否被踩的可能性;

volatile UBaseType_t uxNumberOfItems

定義了當前這個鏈表中有多少個 Item ,增加一個鏈表元素,這個值加1,反之,減1;

ListItem_t * configLIST_VOLATILE pxIndex

用於鏈表遍歷的節點,怎麼個遍歷法,後面馬上獻上;

MiniListItem_t xListEnd

用於鏈表的最後的元素,相當於一個標記;

 

3、函數

既然數據結構介紹完畢(雖然看上去比較抽象,包括幾個不容易理解的數據結構),那麼接下來一邊分析函數,一邊分析數據結構的使用方式;

3.1、vListInitialise

一個鏈表的初始化函數爲:

void vListInitialise( List_t * const pxList )
{
	/* The list structure contains a list item which is used to mark the
	end of the list.  To initialise the list the list end is inserted
	as the only list entry. */
	pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );			/*lint !e826 !e740 !e9087 The mini list structure is used as the list end to save RAM.  This is checked and valid. */

	/* The list end value is the highest possible value in the list to
	ensure it remains at the end of the list. */
	pxList->xListEnd.xItemValue = portMAX_DELAY;

	/* The list end next and previous pointers point to itself so we know
	when the list is empty. */
	pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd );	/*lint !e826 !e740 !e9087 The mini list structure is used as the list end to save RAM.  This is checked and valid. */
	pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );/*lint !e826 !e740 !e9087 The mini list structure is used as the list end to save RAM.  This is checked and valid. */

	pxList->uxNumberOfItems = ( UBaseType_t ) 0U;

	/* Write known values into the list if
	configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
	listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList );
	listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList );
}

傳入一個表徵鏈表的結構體指針 List_t * const pxList

請注意一點,在 List_t 結構中,用於標記鏈表最後的 xListEnd 結構是一個定義,而不是指針,這裏首先將傳入鏈表的

pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );  // MiniListItem_t 結構強轉

強轉爲 ListItem_t 結構,並賦值給了 pxIndex,也就是給 pxIndex 內容爲這個 List 的 xListEnd 的地址;

接下來便將 xListEnd 的 xItemValue 寫入最大值 0xFFFFFFFF(32位CPU);

然後便將 xListEnd 的 next 和 prev 指針全部指向它自己,已達到初始化的目的;

最後初始化該鏈表中有效元素的個數爲 0 個,即 uxNumberOfItems = ( UBaseType_t ) 0U;

最後是,如果使能了 Check 鏈表有效性的那個宏,那麼這裏給鏈表結構的前後兩個 TAG 位賦值成爲固定的 0x5a5a5a5aUL;

初始化過程爲:

 

3.2、vListInitialiseItem

初始化一個鏈表元素:

void vListInitialiseItem( ListItem_t * const pxItem )

它的實現非常簡單:

void vListInitialiseItem( ListItem_t * const pxItem )
{
	/* Make sure the list item is not recorded as being on a list. */
	pxItem->pxContainer = NULL;

	/* Write known values into the list item if
	configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
	listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
	listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
}

僅僅是將元素的容器指針給賦值成爲 NULL;

 

3.3、vListInsertEnd

這個 API 是往指定的鏈表的後部插入一個 Item:

void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )
{
ListItem_t * const pxIndex = pxList->pxIndex;

	/* Only effective when configASSERT() is also defined, these tests may catch
	the list data structures being overwritten in memory.  They will not catch
	data errors caused by incorrect configuration or use of FreeRTOS. */
	listTEST_LIST_INTEGRITY( pxList );
	listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );

	/* Insert a new list item into pxList, but rather than sort the list,
	makes the new list item the last item to be removed by a call to
	listGET_OWNER_OF_NEXT_ENTRY(). */
	pxNewListItem->pxNext = pxIndex;
	pxNewListItem->pxPrevious = pxIndex->pxPrevious;

	/* Only used during decision coverage testing. */
	mtCOVERAGE_TEST_DELAY();

	pxIndex->pxPrevious->pxNext = pxNewListItem;
	pxIndex->pxPrevious = pxNewListItem;

	/* Remember which list the item is in. */
	pxNewListItem->pxContainer = pxList;

	( pxList->uxNumberOfItems )++;
}

入參 pxList 是指定的鏈表指針,pxNewListItem 是待插入的 Item;

前面兩行不多說了,配置用於檢查鏈表和 Item 被踩的 Tag;

首先獲取鏈表的 pxIndex 結構指針,此指針在鏈表初始化的時候,是指向了 xListEnd;

所以對於一個新的鏈表創建後,依次插入兩個元素 NewItem_1 和 NewItem_2 的過程如下所示:

可以看到,pxIndex 始終處於最開始的那個 xListEnd 結構,而 xListEnd 結構在 List 中有定義成爲只有 next 和 prev 的結構,這就凸顯出 FreeRTOS 在設計的時候,一點點空間都在節約的精益求精的準則;

 

3.4、vListInsert

這個 API 和前一個不同的是,前一個是插入到尾部,這個 API 按照 Item 中的 xItemValue 排序插入,xItemValue 越大,越靠近 xListEnd:

void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem )
{
ListItem_t *pxIterator;
const TickType_t xValueOfInsertion = pxNewListItem->xItemValue;

	/* Only effective when configASSERT() is also defined, these tests may catch
	the list data structures being overwritten in memory.  They will not catch
	data errors caused by incorrect configuration or use of FreeRTOS. */
	listTEST_LIST_INTEGRITY( pxList );
	listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );

	/* Insert the new list item into the list, sorted in xItemValue order.

	If the list already contains a list item with the same item value then the
	new list item should be placed after it.  This ensures that TCBs which are
	stored in ready lists (all of which have the same xItemValue value) get a
	share of the CPU.  However, if the xItemValue is the same as the back marker
	the iteration loop below will not end.  Therefore the value is checked
	first, and the algorithm slightly modified if necessary. */
	if( xValueOfInsertion == portMAX_DELAY )
	{
		pxIterator = pxList->xListEnd.pxPrevious;
	}
	else
	{
		/* *** NOTE ***********************************************************
		If you find your application is crashing here then likely causes are
		listed below.  In addition see https://www.freertos.org/FAQHelp.html for
		more tips, and ensure configASSERT() is defined!
		https://www.freertos.org/a00110.html#configASSERT

			1) Stack overflow -
			   see https://www.freertos.org/Stacks-and-stack-overflow-checking.html
			2) Incorrect interrupt priority assignment, especially on Cortex-M
			   parts where numerically high priority values denote low actual
			   interrupt priorities, which can seem counter intuitive.  See
			   https://www.freertos.org/RTOS-Cortex-M3-M4.html and the definition
			   of configMAX_SYSCALL_INTERRUPT_PRIORITY on
			   https://www.freertos.org/a00110.html
			3) Calling an API function from within a critical section or when
			   the scheduler is suspended, or calling an API function that does
			   not end in "FromISR" from an interrupt.
			4) Using a queue or semaphore before it has been initialised or
			   before the scheduler has been started (are interrupts firing
			   before vTaskStartScheduler() has been called?).
		**********************************************************************/

		for( pxIterator = ( ListItem_t * ) &( pxList->xListEnd ); pxIterator->pxNext->xItemValue <= xValueOfInsertion; pxIterator = pxIterator->pxNext ) /*lint !e826 !e740 !e9087 The mini list structure is used as the list end to save RAM.  This is checked and valid. *//*lint !e440 The iterator moves to a different value, not xValueOfInsertion. */
		{
			/* There is nothing to do here, just iterating to the wanted
			insertion position. */
		}
	}

	pxNewListItem->pxNext = pxIterator->pxNext;
	pxNewListItem->pxNext->pxPrevious = pxNewListItem;
	pxNewListItem->pxPrevious = pxIterator;
	pxIterator->pxNext = pxNewListItem;

	/* Remember which list the item is in.  This allows fast removal of the
	item later. */
	pxNewListItem->pxContainer = pxList;

	( pxList->uxNumberOfItems )++;
}

如果 xItemValue 爲 portMAX_DELAY(32bit CPU 中,這個值是 0xFFFFFFFF),那麼直接插入到 xListEnd 右邊:

否則,按照從 xListEnd 開始,從最右邊遍歷,從最右往左,依次排列 xItemValue 最小的,比如;

 

3.5、uxListRemove

該接口用於將鏈表中的特定元素摘除:

UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )
{
/* The list item knows which list it is in.  Obtain the list from the list
item. */
List_t * const pxList = pxItemToRemove->pxContainer;

	pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious;
	pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;

	/* Only used during decision coverage testing. */
	mtCOVERAGE_TEST_DELAY();

	/* Make sure the index is left pointing to a valid item. */
	if( pxList->pxIndex == pxItemToRemove )
	{
		pxList->pxIndex = pxItemToRemove->pxPrevious;
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}

	pxItemToRemove->pxContainer = NULL;
	( pxList->uxNumberOfItems )--;

	return pxList->uxNumberOfItems;
}

首先從指定元素中的 pxContainer 獲取到該元素所屬的鏈表結構;再將元素從鏈表中摘除;

 

4、宏

除了上面幾個常用到的 API,在 list.h 中還以宏的方式提供了一些常用的宏以及宏函數,下面我們依次看下:

4.1、listSET_LIST_ITEM_OWNER、listGET_LIST_ITEM_OWNER

listSET_LIST_ITEM_OWNER 用於設置一個 Item 的 Owner:

/*
 * Access macro to set the owner of a list item.  The owner of a list item
 * is the object (usually a TCB) that contains the list item.
 *
 * \page listSET_LIST_ITEM_OWNER listSET_LIST_ITEM_OWNER
 * \ingroup LinkedList
 */
#define listSET_LIST_ITEM_OWNER( pxListItem, pxOwner )		( ( pxListItem )->pvOwner = ( void * ) ( pxOwner ) )

listGET_LIST_ITEM_OWNER 用於獲取一個 Item 的 Owner:

/*
 * Access macro to get the owner of a list item.  The owner of a list item
 * is the object (usually a TCB) that contains the list item.
 *
 * \page listGET_LIST_ITEM_OWNER listSET_LIST_ITEM_OWNER
 * \ingroup LinkedList
 */
#define listGET_LIST_ITEM_OWNER( pxListItem )	( ( pxListItem )->pvOwner )

4.2、listSET_LIST_ITEM_VALUE、listGET_LIST_ITEM_VALUE

listSET_LIST_ITEM_VALUE 用於設置 Item 的 xItemValue 值:

/*
 * Access macro to set the value of the list item.  In most cases the value is
 * used to sort the list in descending order.
 *
 * \page listSET_LIST_ITEM_VALUE listSET_LIST_ITEM_VALUE
 * \ingroup LinkedList
 */
#define listSET_LIST_ITEM_VALUE( pxListItem, xValue )	( ( pxListItem )->xItemValue = ( xValue ) )

listGET_LIST_ITEM_VALUE 用於獲取 Item 的 xItemValue 值:

/*
 * Access macro to retrieve the value of the list item.  The value can
 * represent anything - for example the priority of a task, or the time at
 * which a task should be unblocked.
 *
 * \page listGET_LIST_ITEM_VALUE listGET_LIST_ITEM_VALUE
 * \ingroup LinkedList
 */
#define listGET_LIST_ITEM_VALUE( pxListItem )	( ( pxListItem )->xItemValue )

 

4.3、listGET_ITEM_VALUE_OF_HEAD_ENTRY

listGET_ITEM_VALUE_OF_HEAD_ENTRY 用於獲取指定鏈表的 Entry 的 xItemValue,這裏我們需要知道 Entry 的定義,其實就是 xListEnd->pxNext 的那個元素:

/*
 * Access macro to retrieve the value of the list item at the head of a given
 * list.
 *
 * \page listGET_LIST_ITEM_VALUE listGET_LIST_ITEM_VALUE
 * \ingroup LinkedList
 */
#define listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxList )	( ( ( pxList )->xListEnd ).pxNext->xItemValue )

 

4.4、listGET_HEAD_ENTRY

listGET_HEAD_ENTRY 用於獲取指定鏈表的 Entry 的 Item 指針:

/*
 * Return the list item at the head of the list.
 *
 * \page listGET_HEAD_ENTRY listGET_HEAD_ENTRY
 * \ingroup LinkedList
 */
#define listGET_HEAD_ENTRY( pxList )	( ( ( pxList )->xListEnd ).pxNext )

 

4.5、listGET_NEXT

listGET_NEXT 用戶獲取傳入 Item 的 next 指針:

/*
 * Return the next list item.
 *
 * \page listGET_NEXT listGET_NEXT
 * \ingroup LinkedList
 */
#define listGET_NEXT( pxListItem )	( ( pxListItem )->pxNext )

 

4.6、listGET_END_MARKER

listGET_END_MARKER 用來獲取指定鏈表的 xListEnd 標記位置:

/*
 * Return the list item that marks the end of the list
 *
 * \page listGET_END_MARKER listGET_END_MARKER
 * \ingroup LinkedList
 */
#define listGET_END_MARKER( pxList )	( ( ListItem_t const * ) ( &( ( pxList )->xListEnd ) ) )

 

4.6、listLIST_IS_EMPTY

listLIST_IS_EMPTY 用來獲取指定鏈表中是否有 Item,主要是通過查看鏈表的數據統計的變量來獲取:

/*
 * Access macro to determine if a list contains any items.  The macro will
 * only have the value true if the list is empty.
 *
 * \page listLIST_IS_EMPTY listLIST_IS_EMPTY
 * \ingroup LinkedList
 */
#define listLIST_IS_EMPTY( pxList )	( ( ( pxList )->uxNumberOfItems == ( UBaseType_t ) 0 ) ? pdTRUE : pdFALSE )

 

4.7、listCURRENT_LIST_LENGTH

listCURRENT_LIST_LENGTH 獲取鏈表元素的個數:

/*
 * Access macro to return the number of items in the list.
 */
#define listCURRENT_LIST_LENGTH( pxList )	( ( pxList )->uxNumberOfItems )

 

4.8、listGET_OWNER_OF_NEXT_ENTRY

listGET_OWNER_OF_NEXT_ENTRY 用於獲取指定鏈表的下一個 TCB 結構:

#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;											\
}

 

4.9、listGET_OWNER_OF_HEAD_ENTRY

listGET_OWNER_OF_HEAD_ENTRY 用於獲取 Entry 節點的 Owner:

/*
 * Access function to obtain the owner of the first entry in a list.  Lists
 * are normally sorted in ascending item value order.
 *
 * This function returns the pxOwner member of the first item in the list.
 * The pxOwner parameter of a list item is a pointer to the object that owns
 * the list item.  In the scheduler this is normally a task control block.
 * The pxOwner parameter effectively creates a two way link between the list
 * item and its owner.
 *
 * @param pxList The list from which the owner of the head item is to be
 * returned.
 *
 * \page listGET_OWNER_OF_HEAD_ENTRY listGET_OWNER_OF_HEAD_ENTRY
 * \ingroup LinkedList
 */
#define listGET_OWNER_OF_HEAD_ENTRY( pxList )  ( (&( ( pxList )->xListEnd ))->pxNext->pvOwner )

 

4.10、listIS_CONTAINED_WITHIN

listIS_CONTAINED_WITHIN 用於判斷給定的 Item 是否屬於一個指定的鏈表:

/*
 * Check to see if a list item is within a list.  The list item maintains a
 * "container" pointer that points to the list it is in.  All this macro does
 * is check to see if the container and the list match.
 *
 * @param pxList The list we want to know if the list item is within.
 * @param pxListItem The list item we want to know if is in the list.
 * @return pdTRUE if the list item is in the list, otherwise pdFALSE.
 */
#define listIS_CONTAINED_WITHIN( pxList, pxListItem ) ( ( ( pxListItem )->pxContainer == ( pxList ) ) ? ( pdTRUE ) : ( pdFALSE ) )

 

4.11、listLIST_ITEM_CONTAINER

listLIST_ITEM_CONTAINER 用於獲取 Item 屬於的鏈表結構:

/*
 * Return the list a list item is contained within (referenced from).
 *
 * @param pxListItem The list item being queried.
 * @return A pointer to the List_t object that references the pxListItem
 */
#define listLIST_ITEM_CONTAINER( pxListItem ) ( ( pxListItem )->pxContainer )

 

4.12、listLIST_IS_INITIALISED

listLIST_IS_INITIALISED 用於判斷一個指定的鏈表是否被初始化過:

/*
 * This provides a crude means of knowing if a list has been initialised, as
 * pxList->xListEnd.xItemValue is set to portMAX_DELAY by the vListInitialise()
 * function.
 */
#define listLIST_IS_INITIALISED( pxList ) ( ( pxList )->xListEnd.xItemValue == portMAX_DELAY )

 

到這裏,鏈表相關的函數和結構,以及常用到的宏也就分析完畢了,更多的在後面分析任務以及任務調度的時候,會經常涉及這些;

 

值得注意的一點是,List 中的 xListEnd 作爲一個 marker,是不動的,而 pxIndex 成員,用來做鏈表遍歷,初始化的時候,指向 xListEnd 位置,但是每次調用 listGET_OWNER_OF_NEXT_ENTRY 後,pxIndex 都會往當前的 pxIndex->pxNext 索引一次;這也是爲後面的進程相關的邏輯做準備,提高效率;

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章