引子
我們都生活在時間中,但卻無法去思考它。什麼是時間呢?似乎這是一個永遠也不能被回答的問題。然而作為一個程序員,在工作中,總有那麼幾次我必須思考什麼是時間。比如,需要知道一段代碼運行了多久;要在 log 文件中記錄事件發生時的時間戳;再比如需要一個定時器以便能夠定期做某些計算機操作。我發現,在計算機世界中,時間在不同場合也往往有不同的含義,讓試圖思考它的人感到迷茫。但值得慶幸的是,Linux 中的時間終究是可以理解的。因此我打算討論一下有關時間的話題,嘗試著深入理解 Linux 系統中 C 語言編程中的時間問題。主要內容如下:
第 1 部分是應用程序中的時間問題。有三個方面:程序計時需要;獲取當前時間;定時器。
第 2 部分包括時間硬件簡介和 GlibC 實現時間函數的原理。
第 3 和第 4 部分是 Linux 內核對時間的支持和實現原理。
現在開始第 1 部分,探討應用開發中的時間編程問題。在這一部分中,所有的例子代碼都在 GlibC 2.14,內核 2.6.33 的 Linux 系統下編譯並驗證執行過。讀者如果使用低版本的 GlibC 和 Linux 內核有可能無法正確執行。
獲取當前時間
時間的獲取
在程序當中, 我們經常要輸出系統當前的時間,比如日志文件中的每一個事件都要記錄其產生時間。在 C 語言中獲取當前時間的方法有以下幾種,它們所獲得的時間精度從秒級到納秒,各有所不同。
表 1. C 時間函數
GUN/Linux 提供了三個標准的 API 用來獲取當前時間,time()/gettimeofday()/clock_gettime(),它們的區別僅在於獲取的時間精度不同,您可以根據需要選取合適的調用。ftime() 是老的一些系統中的時間調用,很多 Linux 版本雖然支持它,但僅僅是為了向前兼容性,新開發的軟件不建議使用 ftime() 來獲得當前時間。
時間顯示和轉換
目前我們得到的時間是一個數字,無論精度如何,它代表的僅是一個差值。比如精度為秒的 time() 函數,返回一個 time_t 類型的整數。假設當前時間為 2011 年 12 月 7 日下午 20 點 29 分 51 秒,那麼 time_t 的值為:1323318591。即距離 1970 年 1 月 1 日零點,我們已經過去了 1323318591 秒。(這裡的 1970 年 1 月 1 日零點是格林威治時間,而不是北京時間。)我們下面討論的時間如果不特別說明都是格林威治時間,也叫 GMT 時間,或者 UTC 時間。
字符串“1323318591 秒”對於多數人都沒有太大的意義,我們更願意看到“2011 年 12 月 7 日”這樣的顯示。因此當我們得到秒,毫秒,甚至納秒表示的當前時間之後,往往需要將這些數字轉換為人們所熟悉的時間表示方法。
由於國家,習慣和時區的不同,時間的表示方法並沒有一個統一的格式。為了滿足各種時間顯示的需求,標准 C 庫提供了許多時間格式轉換的函數。這些函數的數量眾多,容易讓人迷惑,記住它們的用法十分不易。在這裡我借用 Michael Kerrisk 在《Linux Programming Interface》一書中的插圖,來對這些標准 C 函數進行一個總體的概覽。
圖 1. 各種時間顯示格式轉換函數關系圖
從上圖可以看到,time()/gettimeofday() 從內核得到當前時間之後,該當前時間值可以被兩大類函數轉換為更加容易閱讀的顯示格式:
固定格式轉換
用戶指定格式轉換函數。
固定格式轉換
用 ctime() 函數轉換出來的時間格式是系統固定的,調用者無法改動,因此被稱為固定格式轉換。如果您對日期格式沒有特殊的要求,那麼用它基本上就可以了,簡單,不用記憶很多的參數。
用戶指定格式轉換
典型的 ctime() 格式如下:
Wed Dec 7 20:45:43 PST 2011
有些人覺得這個格式太長,類似 Wed,星期三這樣的信息很多情況下都沒有啥用途。人們可能更喜歡其他格式:比如2011-12-07 20:45。在這種情況下,就需要進行時間顯示格式轉換。做法為:先把從內核得到的時間值轉換為 struct tm 類型的值,然後調用 strftime() 等函數來輸出自定義的時間格式字符串。
下面我列舉一些實例,以便讀者更清晰地理解眾多的時間轉換函數的用法。
各標准 C 時間轉換函數的解釋和舉例
char *ctime(const time_t *clock);
使用函數 ctime 將秒數轉化為字符串. 這個函數的返回類型是固定的:一個可能值為”Thu Dec 7 14:58:59 2000”。這個字符串的長度和顯示格式是固定的。
清單 1,time 的使用
#include <time.h> int main () { time_t time_raw_format; time ( &time_raw_format ); //獲取當前時間 printf (" time is [%d]\n", time_raw_format); //用 ctime 將時間轉換為字符串輸出 printf ( "The current local time: %s", ctime(&time_raw_format)); return 0; }
自定義格式轉換
為了更靈活的顯示,需要把類型 time_t 轉換為 tm 數據結構。tm 數據結構將時間分別保存到代表年,月,日,時,分,秒等不同的變量中。不再是一個令人費解的 64 位整數了。這種數據結構是各種自定義格式轉換函數所需要的輸入形式。
清單 2,數據結構 tm
struct tm { int tm_sec; /* Seconds (0-60) */ int tm_min; /* Minutes (0-59) */ int tm_hour; /* Hours (0-23) */ int tm_mday; /* Day of the month (1-31) */ int tm_mon; /* Month (0-11) */ int tm_year; /* Year since 1900 */ int tm_wday; /* Day of the week (Sunday = 0)*/ int tm_yday; /* Day in the year (0-365; 1 Jan = 0)*/ int tm_isdst; /* Daylight saving time flag > 0: DST is in effect; = 0: DST is not effect; < 0: DST information not available */ };
可以使用 gmtime() 和 localtime() 把 time_t 轉換為 tm 數據格式,其中 gmtime() 把時間轉換為格林威治時間;localtime 則轉換為當地時間。
清單 3,時間轉換函數定義
#include <time.h>
struct tm *gmtime(const time_t *timep);
struct tm *localtime(const time_t *timep);
使用 tm 來表示時間,您就可以調用 asctime() 和 strftime() 將時間轉換為字符串了。asctime() 的輸出格式固定,和 ctime() 相同。strftime() 則類似我們最熟悉的 printf() 函數,您可以通過輸入參數自定義時間的輸出格式。
size_t strftime(char *outstr, size_t maxsize, const char *format,
const struct tm *timeptr);
清單 4,時間顯示轉換
int main () { time_t time_raw_format; struct tm * time_struct; char buf [100]; time ( &time_raw_format ); time_struct = localtime ( &time_raw_format ); strftime (buf,100,"It is now: %I:%M%p.",time_struct); puts (buf); return 0; }
該例子程序的輸出結果如下:
It is now: 02:45PM.
從以上的例子可以看到,利用從 time() 得到的時間值,可以調用各種轉換函數將其轉換成更方便人們閱讀的形式。
此外從前面的總結中我們也了解到,還有兩個 C 函數可以獲得當前時間,gettimeofday() 以及 clock_gettime(),它們分別返回 struct timeval 或者 timespec 代表的高精度的時間值。在目前的 GLibC 中,還沒有直接把 struct timeval/timespec 轉換為 struct tm 的函數。一般的做法是將 timeval 中的 tv_sec 轉換為 tm,使用上面所述的方法轉換為字符串,最後在顯示的時候追加上 tv_usec,比如下面的例子代碼:
清單 5,更多時間顯示轉換
struct timeval tv; time_t nowtime; struct tm *nowtm; char tmbuf[64], buf[64]; gettimeofday(&tv, NULL); //獲取當前時間到 tv nowtime = tv.tv_sec; //nowtime 存儲了秒級的時間值 nowtm = localtime(&nowtime); //轉換為 tm 數據結構 //用 strftime 函數將 tv 轉換為字符串,但 strftime 函數只能達到秒級精度 strftime(tmbuf, sizeof tmbuf, "%Y-%m-%d %H:%M:%S", nowtm); //將毫秒值追加到 strftime 轉換的字符串末尾 snprintf(buf, sizeof buf, "%s.%06d", tmbuf, tv.tv_usec);
時間的測量
有時候我們要計算某段程序執行的時間,比如需要對算法進行時間分析。基本的實現思路為在被測試代碼的開始和結束的地方獲取當時時間,相減後得到相對值,即所需要的統計時間。為了實現高精度的時間測量,必須使用高精度的時間獲取方式,一般有兩種方法:
系統調用 gettimeofday
匯編指令 RDTSC。
gettimeofday
可以使用 gettimeofday() 函數進行時間測量,其精度在 us 級別,可以用來做一般的時間分析。
gettimeofday() 將時間保存在結構 tv 之中。gettimeofday() 的第二個參數代表時區,在 Linux 中已經廢棄不用,只能用 NULL 傳入。一個典型的例子程序如下:
清單 6,gettimeofday 例子程序
void function() { unsigned int i,j; double y; for(i=0;i<1000;i++) for(j=0;j<1000;j++) y=sin((double)i); //耗時操作 } main() { struct timeval tpstart,tpend; float timeuse; gettimeofday(&tpstart,NULL); //記錄開始時間戳 function(); gettimeofday(&tpend,NULL); //記錄結束時間戳 timeuse = 1000000*(tpend.tv_sec-tpstart.tv_sec)+ tpend.tv_usec-tpstart.tv_usec; //計算差值 timeuse /= 1000000; printf("Used Time:%f\n",timeuse); exit(0); }
這個程序輸出函數的執行時間,我們可以使用這個來進行系統性能的測試,或者是函數算法的效率分析。在我個人機器上的輸出結果是:Used Time:0.556070
RDTSC
gettimeofday() 是一個系統調用,在某些場合下頻繁調用它是不合適的。比如性能要求很高的代碼段內。因為 gettimeofday() 需要用戶態/內核態切換,開銷較大。Intel X86 處理器提供了 TSC 硬件,並且可以用非特權指令 rdtsc 來讀取該硬件的時間值,這就避免了過度的內核用戶態切換。
如何使用 RDTSC
參考下面的例子代碼,采用 GCC 的匯編擴展,定義 rdtsc 的函數,它返回當前時間戳。
#define rdtsc(low,high) __asm__ \
__volatile__("rdtsc" : "=a" (low), "=d" (high))
在 C 代碼中使用 rdtsc 十分簡單。比如:
清單 7,RDTSC 例子程序
unsigned long long get_cycles() { unsigned low, high; unsigned long long val; rdtsc(low,high); val = high; val = (val << 32) | low; //將 low 和 high 合成一個 64 位值 return val; } double get_cpu_mhz(void) { FILE* f; char buf[256]; double mhz = 0.0; f = fopen("/proc/cpuinfo","r"); //打開 proc/cpuinfo 文件 if (!f) return 0.0; while(fgets(buf, sizeof(buf), f)) { double m; int rc; rc = sscanf(buf, "cpu MHz : %lf", &m); //讀取 cpu MHz if (mhz == 0.0) { mhz = m; break; } } fclose(f); return mhz; //返回 HZ 值 } int main() { double mhz; mhz = get_cpu_mhz(); cycles_t c1, c2; for(;;) { c1 = get_cycles(); sleep(1); c2 = get_cycles(); //c2 和 c1 的差值應該為 1000000us,即 1 秒 printf("1 sec = %g usec\n", (c2 - c1) / mhz); } }
函數 get_cycles 將返回 64 位整數,代表當前時間,單位是 CPU 的 cycle 數。函數 get_cpu_mhz 獲得當前 CPU 的工作頻率。用兩個 CPU cycle 的差值除以 CPU 頻率,就是微妙。
但 RDTSC 只能在 IA 系列處理器上使用。而且由於處理器的亂序執行,RDTSC 有些情況下並不准確,在 SMP 下使用 RDTSC 也有一定的問題。但這些問題只有在需要極高時間精度的情況下才會出現,對於一般的時間測量要求,采用 RDTSC 是一個可以考慮的選擇。
計時器的使用
有時我們需要定時完成一些任務。簡單的方法是使用 while 循環加 sleep。比如每隔 1 分鐘檢查鏈接情況的 heartbeat 任務等。
清單 8,sleep 加循環
while(condtion) { //do something sleep(interval); }
這可以滿足很多程序的定時需要,但假如您不希望程序“偷懶”,即上例中 sleep 的時候您還是希望程序做些有用的工作,那麼使用定時器是通常的選擇。Linux 系統上最常用的定時器是 setitmer 計時器。
setitimer
Linux 為每一個進程提供了 3 個 setitimer 間隔計時器:
ITIMER_REAL:減少實際時間,到期的時候發出 SIGALRM 信號。
ITIMER_VIRTUAL:減少有效時間 (進程執行的時間),產生 SIGVTALRM 信號。
ITIMER_PROF:減少進程的有效時間和系統時間 (為進程調度用的時間)。這個經常和上面一個使用用來計算系統內核時間和用戶時間。產生 SIGPROF 信號。
所謂 REAL 時間,即我們人類自然感受的時間,英文計算機文檔中也經常使用 wall-clock 這個術語。說白了就是我們通常所說的時間,比如現在是下午 5 點 10 分,那麼一分鐘的 REAL 時間之後就是下午 5 點 11 分。
VIRTUAL 時間是進程執行的時間,Linux 是一個多用戶多任務系統,在過去的 1 分鐘內,指定進程實際在 CPU 上的執行時間往往並沒有 1 分鐘,因為其他進程會被 Linux 調度執行,在那些時間內,雖然自然時間在流逝,但指定進程並沒有真正的運行。VIRTUAL 時間就是指定進程真正的有效執行時間。比如 5 點 10 分開始的 1 分鐘內,進程 P1 被 Linux 調度並占用 CPU 的執行時間為 30 秒,那麼 VIRTUAL 時間對於進程 P1 來講就是 30 秒。此時自然時間已經到了 5 點 11 分,但從進程 P1 的眼中看來,時間只過了 30 秒。
PROF 時間比較獨特,對進程 P1 來說從 5 點 10 分開始的 1 分鐘內,雖然自己的執行時間為 30 秒,但實際上還有 10 秒鐘內核是在執行 P1 發起的系統調用,那麼這 10 秒鐘也被加入到 PROF 時間。這種時間定義主要用於全面衡量進程的性能,因為在統計程序性能的時候,10 秒的系統調用時間也應該算到 P1 的頭上。這也許就是 PROF 這個名字的來歷吧。
使用 setitimer Timer 需要了解下面這些接口 API:
int getitimer(int which,struct itimerval *value);
int setitimer(int which,struct itimerval *newval,
struct itimerval *oldval);
itimerval 的定義如下:
struct itimerval {
struct timeval it_interval;
struct timeval it_value;
}
 
getitimer 函數得到間隔計時器的時間值,保存在 value 中。
setitimer 函數設置間隔計時器的時間值為 newval. 並將舊值保存在 oldval 中;which 表示使用三個計時器中的哪一個。
itimerval 結構中的 it_value 是第一次調用後觸發定時器的時間,當這個值遞減為 0 時,系統會向進程發出相應的信號。此後將以 it_internval 為周期定時觸發定時器。
給出一個具體的例子:
清單 9,setitmer 例子
void print_info(int signo) { printf(“timer fired\n”); //簡單的打印,表示 timer 到期 } void init_sigaction(void) { struct sigaction act; act.sa_handler= print_info; act.sa_flags=0; sigemptyset(&act.sa_mask); sigaction(SIGPROF,&act,NULL); //設置信號 SIGPROF 的處理函數為 print_info } void init_time() { struct itimerval value; value.it_value.tv_sec=2; value.it_value.tv_usec=0; value.it_interval=value.it_value; setitimer(ITIMER_PROF,&value,NULL); //初始化 timer,到期發送 SIGPROF 信號 } int main() { len=strlen(prompt); init_sigaction(); init_time(); while(1); exit(0); }
這個程序使用 PROF 時間,每經過兩秒 PROF 時間之後就會打印一下 timer fired
字符串。
需要指出:setitimer 計時器的精度為 ms,即 1000 分之 1 秒,足以滿足絕大多數應用程序的需要。但多媒體等應用可能需要更高精度的定時,那麼就需要考慮使用下一類定時器:POSIX Timer。
POSIX Timer
間隔定時器 setitimer 有一些重要的缺點,POSIX Timer 對 setitimer 進行了增強,克服了 setitimer 的諸多問題:
首先,一個進程同一時刻只能有一個 timer。假如應用需要同時維護多個 Interval 不同的計時器,必須自己寫代碼來維護。這非常不方便。使用 POSIX Timer,一個進程可以創建任意多個 Timer。
setitmer 計時器時間到達時,只能使用信號方式通知使用 timer 的進程,而 POSIX timer 可以有多種通知方式,比如信號,或者啟動線程。
使用 setitimer 時,通知信號的類別不能改變:SIGALARM,SIGPROF 等,而這些都是傳統信號,而不是實時信號,因此有 timer overrun 的問題;而 POSIX Timer 則可以使用實時信號。
setimer 的精度是 ms,POSIX Timer 是針對有實時要求的應用所設計的,接口支持 ns 級別的時鐘精度。
CLOCK_REALTIME 時間是系統保存的時間,即可以由 date 命令顯示的時間,該時間可以重新設置。比如當前時間為上午 10 點 10 分,Timer 打算在 10 分鐘後到時。假如 5 分鐘後,我用 date 命令修改當前時間為 10 點 10 分,那麼 Timer 還會再等十分鐘到期,因此實際上 Timer 等待了 15 分鐘。假如您希望無論任何人如何修改系統時間,Timer 都嚴格按照 10 分鐘的周期進行觸發,那麼就可以使用 CLOCK_MONOTONIC。
CLOCK_PROCESS_CPUTIME_ID 的含義與 setitimer 的 ITIMER_VIRTUAL 類似。計時器只記錄當前進程所實際花費的時間;比如還是上面的例子,假設系統非常繁忙,當前進程只能獲得 50%的 CPU 時間,為了讓進程真正地運行 10 分鐘,應該到 10 點 30 分才允許 Timer 到期。
CLOCK_THREAD_CPUTIME_ID 以線程為計時實體,當前進程中的某個線程真正地運行了一定時間才觸發 Timer。
設置到期通知方式
timer_create 的第二個參數 struct sigevent 用來設置定時器到時時的通知方式。該數據結構如下:
清單 10,結構 sigevent
struct sigevent { int sigev_notify; /* Notification method */ int sigev_signo; /* Notification signal */ union sigval sigev_value; /* Data passed with notification */ void (*sigev_notify_function) (union sigval); /* Function used for thread notification (SIGEV_THREAD) */ void *sigev_notify_attributes; /* Attributes for notification thread (SIGEV_THREAD) */ pid_t sigev_notify_thread_id; /* ID of thread to signal (SIGEV_THREAD_ID) */ };
其中 sigev_notify 表示通知方式,有如下幾種:
如果采用 SIGEV_NONE 方式,使用者必須調用timer_gettime 函數主動讀取定時器已經走過的時間。類似輪詢。
如果采用 SIGEV_SIGNAL 方式,使用者可以選擇使用什麼信號,用 sigev_signo 表示信號值,比如 SIG_ALARM。
如果使用 SIGEV_THREAD 方式,則需要設置 sigev_notify_function,當 Timer 到期時,將使用該函數作為入口啟動一個線程來處理信號;sigev_value 保存了傳入 sigev_notify_function 的參數。sigev_notify_attributes 如果非空,則應該是一個指向 pthread_attr_t 的指針,用來設置線程的屬性(比如 stack 大小,detach 狀態等)。
SIGEV_THREAD_ID 通常和 SIGEV_SIGNAL 聯合使用,這樣當 Timer 到期時,系統會向由 sigev_notify_thread_id 指定的線程發送信號,否則可能進程中的任意線程都可能收到該信號。這個選項是 Linux 對 POSIX 標准的擴展,目前主要是 GLibc 在實現 SIGEV_THREAD 的時候使用到,應用程序很少會需要用到這種模式。
啟動定時器
創建 Timer 之後,便可以調用 timer_settime() 函數指定定時器的時間間隔,並啟動該定時器了。
int timer_settime(timer_t timerid, int flags,
const struct itimerspec *new_value,
struct itimerspec * old_value);
第一次看到 timer_settime 的參數列表或許會令人覺得費解。先來看看 new_value 和 old_value,它們都是 struct itimerspec 數據結構。
struct itimerspec
{
struct timespec it_interval; //定時器周期值
struct timespec it_value; //定時器到期值
};
啟動和停止 Timer 都可以通過設置 new_value 來實現:
new_value->it_interval 為定時器的周期值,比如 1 秒,表示定時器每隔 1 秒到期;
new_value->it_value 如果大於 0,表示啟動定時器,Timer 將在 it_value 這麼長的時間過去後到期,此後每隔 it_interval 便到期一次。如果 it_value 為 0,表示停止該 Timer。
有些時候,應用程序會先啟動用一個時間間隔啟動定時器,隨後又修改該定時器的時間間隔,這都可以通過修改 new_value 來實現;假如應用程序在修改了時間間隔之後希望了解之前的時間間隔設置,則傳入一個非 NULL 的 old_value 指針,這樣在 timer_settime() 調用返回時,old_value 就保存了上一次 Timer 的時間間隔設置。多數情況下我們並不需要這樣,便可以簡單地將 old_value 設置為 NULL,忽略它。
下面給出一個使用 posix timer 的例子程序。最傳統的例子就是創建通知方式為 SIGEV_SIGNAL 的 Timer。這樣當定時器到期時,將產生信號通知,主程序需要定義自己的信號處理函數,來處理信號到期事件。這種例子比比皆是,我打算在這裡寫一個采用通知方式為 SIGEV_THREAD 的例子。該例子程序從 main 函數開始主線程,在開始的時候打印出主線程的進程 ID 和線程 ID。
清單 11,打印 TID
pid_t tid = (pid_t) syscall (SYS_gettid);
printf("start program in PID:[%d]TID:[%d]\n",getpid(),tid);
獲得 ThreadID 的系統調用尚未被 GLibC 標准化,因此這裡直接調用 syscall。
然後,主線程初始化創建 Timer 所需要的數據結構:
清單 12,設置通知方式
se.sigev_notify = SIGEV_THREAD;
se.sigev_value.sival_ptr = &timer_id;
se.sigev_notify_function = timer_thread;
se.sigev_notify_attributes = NULL;
status = timer_create(CLOCK_REALTIME, &se, &timer_id);
這裡將通知方式設為 SIGEV_THREAD,timer_thread 為線程入口函數。
然後主線程設置定時器間隔,並啟動 Timer:
清單 13,啟動 Timer
ts.it_value.tv_sec = 5;
ts.it_value.tv_nsec = 0;
ts.it_interval.tv_sec = 5;
ts.it_interval.tv_nsec = 0;
status = timer_settime(timer_id, 0, &ts, 0);
此後主線程進入一個循環,在循環中等待線程條件變量:
清單 14,主程序中的循環
while (counter < 5) {
status = pthread_cond_wait (&cond, &mutex);
}
條件變量 cond 將在 timer_thread() 處理函數中觸發,這樣每 5 秒鐘,定時器將調用 timer_thread() 處理函數,並喚醒主線程等待的條件變量一次。5 次之後測試程序退出。
現在我們看看 timer_thread() 函數:
清單 15,timer_thread 函數
void timer_thread (void *arg) { status = pthread_mutex_lock (&mutex); if (++counter >= 5) { status = pthread_cond_signal (&cond); } status = pthread_mutex_unlock (&mutex); pid_t tid = (pid_t) syscall (SYS_gettid); printf ("Timer %d in PID:[%d]TID:[%d]\n", counter,getpid(),tid); }
在整個程序中我們都沒有使用信號,定時器到期時,將啟動新的線程運行 timer_thread。因此在該函數中,我們還打印了當前的線程號以便可以看出它們確實在不同線程中運行。
這裡是運行該程序的一個輸出:
-bash-3.2$ gcc threadtimer.c -lrt -lpthread -o test
-bash-3.2$ ./test
start program in PID:[21483]TID:[21483]
Timer 1 in PID:[21483]TID:[21498]
Timer 2 in PID:[21483]TID:[21510]
Timer 3 in PID:[21483]TID:[21534]
可以看到每次 Timer 都運行在不同的線程中。
小結
至此,希望我已經講述了 Linux 系統提供的大多數關於時間的編程方法。使用這些方法我們可以:
獲得當前時間,並轉換為合適的顯示方式;
衡量程序運行經過的時間;
使用定時器完成周期性的任務;
另外不知道您是否和我一樣,對於 Linux 系統如何實現這些機制十分好奇。計算機畢竟是一個機器,底層硬件提供了怎樣的功能,操作系統和 C 庫如何協同工作才可以提供這些一絲不苟的,優美的方法呢?我將在後續的部分試圖探討這個話題。