源碼獲取
https://github.com/icoty/nachos-3.4-Lab
內容一:總體概述
本實習希望通過修改Nachos系統平台的底層源代碼,達到“擴展調度算法”的目標。本次實驗主要是要理解Timer、Scheduler和Interrupt之間的關系,從而理解線程之間是如何進行調度的。
內容二:任務完成情況
任務完成列表(Y/N)
| Exercise1 | Exercise2 | Exercise3 | Challenge1 | |
|---|---|---|---|---|
| 第一部分 | Y | Y | Y | Y |
具體Exercise的完成情況
Exercise1 調研
調研Linux或Windows中采用的進程/線程調度算法。具體內容見課堂要求。
-
linux-4.19.23進程調度策略:SCHED_OTHER分時調度策略,SCHED_FIFO實時調度策略(先到先服務),SCHED_RR實時調度策略(時間片輪轉)。
- RR調度和FIFO調度的進程屬於實時進程,以分時調度的進程是非實時進程。
- 當實時進程准備就緒后,如果當前cpu正在運行非實時進程,則實時進程立即搶占非實時進程。
- RR進程和FIFO進程都采用實時優先級做為調度的權值標准,RR是FIFO的一個延伸。FIFO時,如果兩個進程的優先級一樣,則這兩個優先級一樣的進程具體執行哪一個是由其在隊列中的位置決定的,這樣導致一些不公正性(優先級是一樣的,為什么要讓你一直運行?),如果將兩個優先級一樣的任務的調度策略都設為RR,則保證了這兩個任務可以循環執行,保證了公平。
-
內核代碼:內核為每個cpu維護一個進程就緒隊列,cpu只調度由其維護的隊列上的進程:
vi linux-4.19.23/kernel/sched/core.c:
……
#define CREATE_TRACE_POINTS
#include <trace/events/sched.h>
DEFINE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);
……
vi linux-4.19.23/kernel/sched/sched.h:
/*
* This is the main, per-CPU runqueue data structure.
*
* Locking rule: those places that want to lock multiple runqueues
* (such as the load balancing or the thread migration code), lock
* acquire operations must be ordered by ascending &runqueue.
*/
struct rq {
/* runqueue lock: */
raw_spinlock_t lock; // 鎖保證互斥訪問runqueue
……
struct cfs_rq cfs; // 所有普通進程的集合,采用cfs調度策略
struct rt_rq rt; // 所有實時進程的集合,采用實時調度策略
struct dl_rq dl; // struct dl_rq空閑進程集合
……
};
// cfs_rq就緒隊列是一棵紅黑樹。
/* CFS-related fields in a runqueue */
struct cfs_rq {
……
struct rb_root_cached tasks_timeline; // 紅黑樹的樹根
/*
* 'curr' points to currently running entity on this cfs_rq.
* It is set to NULL otherwise (i.e when none are currently running).
*/
struct sched_entity *curr; // 指向當前正運行的進程
struct sched_entity *next; // 指向將被喚醒的進程
struct sched_entity *last; // 指向喚醒next進程的進程
struct sched_entity *skip;
……
};
vi linux-4.19.23/include/linux/sched.h:實時進程調度實體struct sched_rt_entity,雙向鏈表組織形式;空閑進程調度實體struct sched_dl_entity,紅黑樹組織形式;普通進程的調度實體sched_entity,每個進程描述符中均包含一個該結構體變量,該結構體有兩個作用:
- 包含有進程調度的信息(比如進程的運行時間,睡眠時間等等,調度程序參考這些信息決定是否調度進程);
- 使用該結構體來組織進程,struct rb_node類型結構體變量run_node是紅黑樹節點,struct sched_entity調度實體將被組織成紅黑樹的形式,同時意味着普通進程也被組織成紅黑樹的形式。parent指向了當前實體的上一級實體,cfs_rq指向了該調度實體所在的就緒隊列。my_q指向了本實體擁有的就緒隊列(調度組),該調度組(包括組員實體)屬於下一個級別,和本實體不在同一個級別,該調度組中所有成員實體的parent域指向了本實體,depth代表了此隊列(調度組)的深度,每個調度組都比其parent調度組深度大1。內核依賴my_q域實現組調度。
……
// 普通進程的調度實體sched_entity,使用紅黑樹組織
struct sched_entity {
/* For load-balancing: */
struct load_weight load;
unsigned long runnable_weight;
struct rb_node run_node; // 紅黑樹節點
struct list_head group_node;
unsigned int on_rq;
……
#ifdef CONFIG_FAIR_GROUP_SCHED
int depth;
struct sched_entity *parent; // 當前節點的父節點
/* rq on which this entity is (to be) queued: */
struct cfs_rq *cfs_rq; // 當前節點所在的就緒隊列
/* rq "owned" by this entity/group: */
struct cfs_rq *my_q;
#endif
……
};
// 實時進程調度實體,采用雙向鏈表組織
struct sched_rt_entity {
struct list_head run_list; // 鏈表組織
unsigned long timeout;
unsigned long watchdog_stamp;
unsigned int time_slice;
unsigned short on_rq;
unsigned short on_list;
struct sched_rt_entity *back;
#ifdef CONFIG_RT_GROUP_SCHED
struct sched_rt_entity *parent;
/* rq on which this entity is (to be) queued: */
struct rt_rq *rt_rq; // 當前節點所在的就緒隊列
/* rq "owned" by this entity/group: */
struct rt_rq *my_q;
#endif
} __randomize_layout;
// 空閑進程調度實體,采用紅黑樹組織
struct sched_dl_entity {
struct rb_node rb_node;
……
};
……
vi linux-4.19.23/kernel/sched/sched.h:內核聲明了一個調度類sched_class的結構體類型,用來實現不同的調度策略,可以看到該結構體成員都是函數指針,這些指針指向的函數就是調度策略的具體實現,所有和進程調度有關的函數都直接或者間接調用了這些成員函數,來實現進程調度。此外,每個進程描述符中都包含一個指向該結構體類型的指針sched_class,指向了所采用的調度類。
……
struct sched_class {
const struct sched_class *next;
void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
void (*yield_task) (struct rq *rq);
bool (*yield_to_task)(struct rq *rq, struct task_struct *p, bool preempt);
void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);
……
};
……
Exercise2 源代碼閱讀
code/threads/scheduler.h和code/threads/scheduler.cc:scheduler類是nachos中的進程調度器,維護了一個掛起的中斷隊列,通過FIFO進行調度。
- void ReadyToRun(Thread* thread);設置線程狀態為READY,並放入就緒隊列readyList。
- Thread* FindNextToRun(int source); 從就緒隊列中取出下一個上CPU的線程,實現基於優先級的搶占式調度和FIFO調度。
- void Run(Thread* nextThread); 把下CPU的線程的寄存器和堆棧信息從CPU保存到線程本身的寄存器數據結構中, 執行線程切換,把上CPU的線程的寄存器和堆棧信息從線程本身的寄存器中拷貝到CPU的寄存器中,運行新線程。
code/threads/switch.s:switch.s模擬內容是匯編代碼,負責CPU上進程的切換。切換過程中,首先保存當前進程的狀態,然后恢復新運行進程的狀態,之后切換到新進程的棧空間,開始運行新進程。
code/machine/timer.h和code/machine/timer.cc:Timer類用以模擬硬件的時間中斷。在TimerExired中,會調用TimeOfNextInterrupt,計算出下次時間中斷的時間,並將中斷插入中斷隊列中。初始化時會調用TimerExired,然后每次中斷處理函數中都會調用一次TimerExired,從而時間系統時間一步步向前走。需要說明的是,在運行nachos時加入-rs選項,會初始化一個隨機中斷的Timer。當然你也可以自己聲明一個非隨機的Timer,每隔固定的時間片執行中斷。時間片大小的定義位於ststs.h中,每次開關中斷會調用OneTick(),當Ticks數目達到時間片大小時,會出發一次時鍾中斷。
Exercise3 線程調度算法擴展
擴展線程調度算法,實現基於優先級的搶占式調度算法。
思路:更改Thread類,加入priority成員變量,同時更改初始化函數對其初始化,並完成對應的set和get函數。scheduler中的FindNextToRun負責找到下一個運行的進程,默認是FIFO,找到隊列最開始時的線程返回。我們現在要實現的是根據優先級來返回,僅需將插入readyList隊列的方法按照優先級從高到低順序插入SortedInsert,那么插入時會維護隊列中的Thread按照優先級排序,每次依舊從頭取出第一個,即為優先級最高的隊列。搶占式調度則需要在每次中斷發生時嘗試進行進程切換,如果有優先級更高的進程,則運行高優先級進程。
// 基於優先級的可搶占式調度策略和FIFO調度策略
Thread * Scheduler::FindNextToRun (bool bySleep)
{
// called by threadsleep,直接調度,不用判斷時間片
if(bySleep){
lastSwitchTick = stats->systemTicks;
return (Thread *)readyList->SortedRemove(NULL); // 與Remove()等價,都是從隊頭取
}else{
int ticks = stats->systemTicks - lastSwitchTick;
// 這里設置了運行的最短時間TimerSlice,防止頻繁切換消耗CPU資源
// 測試優先級搶占調度時需要屏蔽這句,因為調用Yield()的線程運行時間很短
// 會直接返回NULL
/*if(ticks < TimerSlice){
// 不用切換
return NULL;
}else*/{
if(readyList->IsEmpty()){
return NULL;
}
Thread * next = (Thread *)readyList->SortedRemove(NULL);
// 基於優先級可搶占調度策略,自己添加的宏,Makefile編譯添加: -DSCHED_PRIORITY
#ifdef SCHED_PRIORITY
// nextThread優先級高於當前線程則切換,否則不切換
if(next->getPriority() < currentThread->getPriority()){
lastSwitchTick = stats->systemTicks;
return next;
}else{
readyList->SortedInsert(next, next->getPriority());
return NULL;
}
#else // FIFO策略需要取消Makefile編譯選項:-DSCHED_PRIORITY
lastSwitchTick = stats->systemTicks;
return next;
#endif
}
}
}
// 線程主動讓出cpu,在FIFO調度策略下能夠看到多個線程按順序運行
void SimpleThread(int which)
{
for (int num = 0; num < 5; num++) {
int ticks = stats->systemTicks - scheduler->getLastSwitchTick();
printf("userId=%d,threadId=%d,prio=%d,loop:%d,lastSwitchTick=%d,systemTicks=%d,usedTicks=%d,TimerSlice=%d\n",currentThread->getUserId(),currentThread->getThreadId(),currentThread->getPriority(),num,scheduler->getLastSwitchTick(),stats->systemTicks,ticks,TimerSlice);
// 時間片輪轉算法,判斷時間片是否用完,
// 如果用完主動讓出cpu,針對nachos內核線程算法
/*if(ticks >= TimerSlice){
//printf("threadId=%d Yield\n",currentThread->getThreadId());
currentThread->Yield();
}*/
// 非搶占模式下,多個線程同時執行該接口的話,會交替執行,交替讓出cpu
// 基於優先級搶占模式下,優先級高的線程運行結束后才調度低優先級線程
currentThread->Yield();
}
}
threadtest.cc:
// 創建四個線程,加上主線程共五個,優先值越小優先級越高
void ThreadPriorityTest()
{
Thread* t1 = new Thread("forkThread1", 1);
printf("-->name=%s,threadId=%d\n",t1->getName(),t1->getThreadId());
t1->Fork(SimpleThread, (void*)1);
Thread* t2 = new Thread("forkThread2", 2);
printf("-->name=%s,threadId=%d\n",t2->getName(),t2->getThreadId());
t2->Fork(SimpleThread, (void*)2);
Thread* t3 = new Thread("forkThread3", 3);
printf("-->name=%s,threadId=%d\n",t3->getName(),t3->getThreadId());
t3->Fork(SimpleThread, (void*)3);
Thread* t4 = new Thread("forkThread4", 4);
printf("-->name=%s,threadId=%d\n",t4->getName(),t4->getThreadId());
t4->Fork(SimpleThread, (void*)4);
currentThread->Yield();
SimpleThread(0);
}
// 運行結果,優先級1最高,最先執行完,其次是優先為2的線程,直到所有線程結束
root@yangyu-ubuntu-32:/mnt/nachos-3.4-Lab/nachos-3.4/threads# ./nachos -q 3
-->name=forkThread1,threadId=1
-->name=forkThread2,threadId=2
-->name=forkThread3,threadId=3
-->name=forkThread4,threadId=4
userId=0,threadId=1,prio=1,loop:0,lastSwitchTick=50,systemTicks=60,usedTicks=10,TimerSlice=30
userId=0,threadId=1,prio=1,loop:1,lastSwitchTick=50,systemTicks=70,usedTicks=20,TimerSlice=30
userId=0,threadId=1,prio=1,loop:2,lastSwitchTick=50,systemTicks=80,usedTicks=30,TimerSlice=30
userId=0,threadId=1,prio=1,loop:3,lastSwitchTick=50,systemTicks=90,usedTicks=40,TimerSlice=30
userId=0,threadId=1,prio=1,loop:4,lastSwitchTick=50,systemTicks=100,usedTicks=50,TimerSlice=30
userId=0,threadId=2,prio=2,loop:0,lastSwitchTick=110,systemTicks=120,usedTicks=10,TimerSlice=30
userId=0,threadId=2,prio=2,loop:1,lastSwitchTick=110,systemTicks=130,usedTicks=20,TimerSlice=30
userId=0,threadId=2,prio=2,loop:2,lastSwitchTick=110,systemTicks=140,usedTicks=30,TimerSlice=30
userId=0,threadId=2,prio=2,loop:3,lastSwitchTick=110,systemTicks=150,usedTicks=40,TimerSlice=30
userId=0,threadId=2,prio=2,loop:4,lastSwitchTick=110,systemTicks=160,usedTicks=50,TimerSlice=30
userId=0,threadId=3,prio=3,loop:0,lastSwitchTick=170,systemTicks=180,usedTicks=10,TimerSlice=30
userId=0,threadId=3,prio=3,loop:1,lastSwitchTick=170,systemTicks=190,usedTicks=20,TimerSlice=30
userId=0,threadId=3,prio=3,loop:2,lastSwitchTick=170,systemTicks=200,usedTicks=30,TimerSlice=30
userId=0,threadId=3,prio=3,loop:3,lastSwitchTick=170,systemTicks=210,usedTicks=40,TimerSlice=30
userId=0,threadId=3,prio=3,loop:4,lastSwitchTick=170,systemTicks=220,usedTicks=50,TimerSlice=30
userId=0,threadId=4,prio=4,loop:0,lastSwitchTick=230,systemTicks=240,usedTicks=10,TimerSlice=30
userId=0,threadId=4,prio=4,loop:1,lastSwitchTick=230,systemTicks=250,usedTicks=20,TimerSlice=30
userId=0,threadId=4,prio=4,loop:2,lastSwitchTick=230,systemTicks=260,usedTicks=30,TimerSlice=30
userId=0,threadId=4,prio=4,loop:3,lastSwitchTick=230,systemTicks=270,usedTicks=40,TimerSlice=30
userId=0,threadId=4,prio=4,loop:4,lastSwitchTick=230,systemTicks=280,usedTicks=50,TimerSlice=30
userId=0,threadId=0,prio=6,loop:0,lastSwitchTick=290,systemTicks=300,usedTicks=10,TimerSlice=30
userId=0,threadId=0,prio=6,loop:1,lastSwitchTick=290,systemTicks=310,usedTicks=20,TimerSlice=30
userId=0,threadId=0,prio=6,loop:2,lastSwitchTick=290,systemTicks=320,usedTicks=30,TimerSlice=30
userId=0,threadId=0,prio=6,loop:3,lastSwitchTick=290,systemTicks=330,usedTicks=40,TimerSlice=30
userId=0,threadId=0,prio=6,loop:4,lastSwitchTick=290,systemTicks=340,usedTicks=50,TimerSlice=30
No threads ready or runnable, and no pending interrupts.
Assuming the program completed.
Machine halting!
Ticks: total 350, idle 0, system 350, user 0
Disk I/O: reads 0, writes 0
Console I/O: reads 0, writes 0
Paging: faults 0
Network I/O: packets received 0, sent 0
Cleaning up...
root@yangyu-ubuntu-32:/mnt/nachos-3.4-Lab/nachos-3.4/threads#
Challenge 線程調度算法擴展(至少實現一種算法)
可實現“時間片輪轉算法”、“多級隊列反饋調度算法”,或將Linux或Windows采用的調度算法應用到Nachos上。
思路:nachos啟動時在system.cc中會new一個timer類,每隔一個TimerTicks大小觸發時鍾中斷,從而讓時鍾向前走,時間片的大下定義在stats.h中。同時在stats.h中定義一個時間片大小變量TimerSlice,每個線程運行時間只要大於等於TimerSlice,立即放棄CPU。
stats.h:
……
// nachos執行每條用戶指令的時間為1Tick
#define UserTick 1
// 系統態無法進行指令計算,
// 所以nachos系統態的一次中斷調用或其他需要進行時間計算的單位設置為10Tick
#define SystemTick 10
// 磁頭尋找超過一個扇區的時間
#define RotationTime 500
// 磁頭尋找超過一個磁道的時間
#define SeekTime 500
#define ConsoleTime 100 // time to read or write one character
#define NetworkTime 100 // time to send or receive one packet
// 時鍾中斷間隔
#define TimerTicks 5 // (average) time between timer interrupts
// 時間片輪轉算法一個時間片大小
#define TimerSlice 10
……
threadtest.cc:
void SimpleThread(int which)
{
for (int num = 0; num < 5; num++) {
int ticks = stats->systemTicks - scheduler->getLastSwitchTick();
printf("userId=%d,threadId=%d,prio=%d,loop:%d,lastSwitchTick=%d,systemTicks=%d,usedTicks=%d,TimerSlice=%d\n",currentThread->getUserId(),currentThread->getThreadId(),currentThread->getPriority(),num,scheduler->getLastSwitchTick(),stats->systemTicks,ticks,TimerSlice);
// 時間片輪轉算法,判斷時間片是否用完
// 如果用完主動讓出cpu,針對nachos內核線程算法
if(ticks >= TimerSlice){
printf("threadId=%d Yield\n",currentThread->getThreadId());
currentThread->Yield();
}
// 非搶占模式下,多個線程同時執行該接口的話,會交替執行,交替讓出cpu
// currentThread->Yield();
}
}
threadtest.cc:
// 創建四個線程,加上主線程共五個,時間片輪轉調度策略,不可搶占
void ThreadPriorityTest()
{
Thread* t1 = new Thread("forkThread1", 1);
printf("-->name=%s,threadId=%d\n",t1->getName(),t1->getThreadId());
t1->Fork(SimpleThread, (void*)1);
Thread* t2 = new Thread("forkThread2", 2);
printf("-->name=%s,threadId=%d\n",t2->getName(),t2->getThreadId());
t2->Fork(SimpleThread, (void*)2);
Thread* t3 = new Thread("forkThread3", 3);
printf("-->name=%s,threadId=%d\n",t3->getName(),t3->getThreadId());
t3->Fork(SimpleThread, (void*)3);
Thread* t4 = new Thread("forkThread4", 4);
printf("-->name=%s,threadId=%d\n",t4->getName(),t4->getThreadId());
t4->Fork(SimpleThread, (void*)4);
currentThread->Yield();
SimpleThread(0);
}
// 運行結果,可看到usedTicks >= TimserSlice時都讓出cpu
// 並且線程執行順序為1 2 3 4 0,直到結束
root@yangyu-ubuntu-32:/mnt/nachos-3.4-Lab/nachos-3.4/threads#
root@yangyu-ubuntu-32:/mnt/nachos-3.4-Lab/nachos-3.4/threads# ./nachos -q 3
-->name=forkThread1,threadId=1
-->name=forkThread2,threadId=2
-->name=forkThread3,threadId=3
-->name=forkThread4,threadId=4
userId=0,threadId=1,prio=1,loop:0,lastSwitchTick=50,systemTicks=60,usedTicks=10,TimerSlice=10
threadId=1 Yield
userId=0,threadId=2,prio=2,loop:0,lastSwitchTick=60,systemTicks=70,usedTicks=10,TimerSlice=10
threadId=2 Yield
userId=0,threadId=3,prio=3,loop:0,lastSwitchTick=70,systemTicks=80,usedTicks=10,TimerSlice=10
threadId=3 Yield
userId=0,threadId=4,prio=4,loop:0,lastSwitchTick=80,systemTicks=90,usedTicks=10,TimerSlice=10
threadId=4 Yield
userId=0,threadId=0,prio=6,loop:0,lastSwitchTick=90,systemTicks=100,usedTicks=10,TimerSlice=10
threadId=0 Yield
userId=0,threadId=1,prio=1,loop:1,lastSwitchTick=100,systemTicks=110,usedTicks=10,TimerSlice=10
threadId=1 Yield
userId=0,threadId=2,prio=2,loop:1,lastSwitchTick=110,systemTicks=120,usedTicks=10,TimerSlice=10
threadId=2 Yield
userId=0,threadId=3,prio=3,loop:1,lastSwitchTick=120,systemTicks=130,usedTicks=10,TimerSlice=10
threadId=3 Yield
userId=0,threadId=4,prio=4,loop:1,lastSwitchTick=130,systemTicks=140,usedTicks=10,TimerSlice=10
threadId=4 Yield
userId=0,threadId=0,prio=6,loop:1,lastSwitchTick=140,systemTicks=150,usedTicks=10,TimerSlice=10
threadId=0 Yield
userId=0,threadId=1,prio=1,loop:2,lastSwitchTick=150,systemTicks=160,usedTicks=10,TimerSlice=10
threadId=1 Yield
userId=0,threadId=2,prio=2,loop:2,lastSwitchTick=160,systemTicks=170,usedTicks=10,TimerSlice=10
threadId=2 Yield
userId=0,threadId=3,prio=3,loop:2,lastSwitchTick=170,systemTicks=180,usedTicks=10,TimerSlice=10
threadId=3 Yield
userId=0,threadId=4,prio=4,loop:2,lastSwitchTick=180,systemTicks=190,usedTicks=10,TimerSlice=10
threadId=4 Yield
userId=0,threadId=0,prio=6,loop:2,lastSwitchTick=190,systemTicks=200,usedTicks=10,TimerSlice=10
threadId=0 Yield
userId=0,threadId=1,prio=1,loop:3,lastSwitchTick=200,systemTicks=210,usedTicks=10,TimerSlice=10
threadId=1 Yield
userId=0,threadId=2,prio=2,loop:3,lastSwitchTick=210,systemTicks=220,usedTicks=10,TimerSlice=10
threadId=2 Yield
userId=0,threadId=3,prio=3,loop:3,lastSwitchTick=220,systemTicks=230,usedTicks=10,TimerSlice=10
threadId=3 Yield
userId=0,threadId=4,prio=4,loop:3,lastSwitchTick=230,systemTicks=240,usedTicks=10,TimerSlice=10
threadId=4 Yield
userId=0,threadId=0,prio=6,loop:3,lastSwitchTick=240,systemTicks=250,usedTicks=10,TimerSlice=10
threadId=0 Yield
userId=0,threadId=1,prio=1,loop:4,lastSwitchTick=250,systemTicks=260,usedTicks=10,TimerSlice=10
threadId=1 Yield
userId=0,threadId=2,prio=2,loop:4,lastSwitchTick=260,systemTicks=270,usedTicks=10,TimerSlice=10
threadId=2 Yield
userId=0,threadId=3,prio=3,loop:4,lastSwitchTick=270,systemTicks=280,usedTicks=10,TimerSlice=10
threadId=3 Yield
userId=0,threadId=4,prio=4,loop:4,lastSwitchTick=280,systemTicks=290,usedTicks=10,TimerSlice=10
threadId=4 Yield
userId=0,threadId=0,prio=6,loop:4,lastSwitchTick=290,systemTicks=300,usedTicks=10,TimerSlice=10
threadId=0 Yield
No threads ready or runnable, and no pending interrupts.
Assuming the program completed.
Machine halting!
Ticks: total 350, idle 0, system 350, user 0
Disk I/O: reads 0, writes 0
Console I/O: reads 0, writes 0
Paging: faults 0
Network I/O: packets received 0, sent 0
Cleaning up...
root@yangyu-ubuntu-32:/mnt/nachos-3.4-Lab/nachos-3.4/threads#
內容三:遇到的困難以及解決方法
困難1
切換線程過程中,產生段錯誤,通過定位,誤把銷毀的線程掛入就緒對了所致。
內容四:收獲及感想
自己動手實現后,發現時間片輪轉算法,線程調度,FIFO,時鍾中斷等其實並不陌生。一切只要你不懶和肯付出實際行動的難題都是紙老虎。
內容五:對課程的意見和建議
暫無。
內容六:參考文獻
暫無。
