歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Unix知識 >> 關於Unix >> 第七章 Linux內核的時鐘中斷 (下2)

第七章 Linux內核的時鐘中斷 (下2)

日期:2017/3/6 15:19:20   编辑:關於Unix
7.7 進程間隔定時器itimer 7.8 時間系統調用的實現 7.7 進程間隔定時器itimer 所謂“間隔定時器(IntervalTimer,簡稱itimer)就是指定時器采用“間隔”值(interval)來作為計時方式,當定時器啟動後,間隔值interval將不斷減 7.7 進程間隔定時器itimer
7.8 時間系統調用的實現

7.7 進程間隔定時器itimer
所謂“間隔定時器(IntervalTimer,簡稱itimer)就是指定時器采用“間隔”值(interval)來作為計時方式,當定時器啟動後,間隔值interval將不斷減小。當interval值減到0時,我們就說該間隔定時器到期。與上一節所說的內核動態定時器相比,二者最大的區別在於定時器的計時方式不同。內核定時器是通過它的到期時刻expires值來計時的,當全局變量jiffies值大於或等於內核動態定時器的expires值時,我們說內核內核定時器到期。而間隔定時器則實際上是通過一個不斷減小的計數器來計時的。雖然這兩種定時器並不相同,但卻也是相互聯系的。假如我們每個時鐘節拍都使間隔定時器的間隔計數器減1,那麼在這種情形下間隔定時器實際上就是內核動態定時器(下面我們會看到進程的真實間隔定時器就是這樣通過內核定時器來實現的)。
間隔定時器主要被應用在用戶進程上。每個Linux進程都有三個相互關聯的間隔定時器。其各自的間隔計數器都定義在進程的task_struct結構中,如下所示(include/linux/sched.h):
struct task_struct{
……
unsigned long it_real_value, it_prof_value, it_virt_value;
unsigned long it_real_incr, it_prof_incr, it_virt_incr;
struct timer_list real_timer;
……
}
(1)真實間隔定時器(ITIMER_REAL):這種間隔定時器在啟動後,不管進程是否運行,每個時鐘滴答都將其間隔計數器減1。當減到0值時,內核向進程發送SIGALRM信號。結構類型task_struct中的成員it_real_incr則表示真實間隔定時器的間隔計數器的初始值,而成員it_real_value則表示真實間隔定時器的間隔計數器的當前值。由於這種間隔定時器本質上與上一節的內核定時器時一樣的,因此Linux實際上是通過real_timer這個內嵌在task_struct結構中的內核動態定時器來實現真實間隔定時器ITIMER_REAL的。
(2)虛擬間隔定時器ITIMER_VIRT:也稱為進程的用戶態間隔定時器。結構類型task_struct中成員it_virt_incr和it_virt_value分別表示虛擬間隔定時器的間隔計數器的初始值和當前值,二者均以時鐘滴答次數位計數單位。當虛擬間隔定時器啟動後,只有當進程在用戶態下運行時,一次時鐘滴答才能使間隔計數器當前值it_virt_value減1。當減到0值時,內核向進程發送SIGVTALRM信號(虛擬鬧鐘信號),並將it_virt_value重置為初值it_virt_incr。具體請見7.4.3節中的do_it_virt()函數的實現。
(3)PROF間隔定時器ITIMER_PROF:進程的task_struct結構中的it_prof_value和it_prof_incr成員分別表示PROF間隔定時器的間隔計數器的當前值和初始值(均以時鐘滴答為單位)。當一個進程的PROF間隔定時器啟動後,則只要該進程處於運行中,而不管是在用戶態或核心態下執行,每個時鐘滴答都使間隔計數器it_prof_value值減1。當減到0值時,內核向進程發送SIGPROF信號,並將it_prof_value重置為初值it_prof_incr。具體請見7.4.3節的do_it_prof()函數。
Linux在include/linux/time.h頭文件中為上述三種進程間隔定時器定義了索引標識,如下所示:
#defineITIMER_REAL0
#defineITIMER_VIRTUAL1
#defineITIMER_PROF2

7.7.1 數據結構itimerval
雖然,在內核中間隔定時器的間隔計數器是以時鐘滴答次數為單位,但是讓用戶以時鐘滴答為單位來指定間隔定時器的間隔計數器的初值顯然是不太方便的,因為用戶習慣的時間單位是秒、毫秒或微秒等。所以Linux定義了數據結構itimerval來讓用戶以秒或微秒為單位指定間隔定時器的時間間隔值。其定義如下(include/linux/time.h):
structitimerval {
structtimeval it_interval;/* timer interval */
structtimeval it_value;/* current value */
};
其中,it_interval成員表示間隔計數器的初始值,而it_value成員表示間隔計數器的當前值。這兩個成員都是timeval結構類型的變量,因此其精度可以達到微秒級。

ltimeval與jiffies之間的相互轉換
由於間隔定時器的間隔計數器的內部表示方式與外部表現方式互不相同,因此有必要實現以微秒為單位的timeval結構和為時鐘滴答次數單位的jiffies之間的相互轉換。為此,Linux在kernel/itimer.c中實現了兩個函數實現二者的互相轉換——tvtojiffies()函數和jiffiestotv()函數。它們的源碼如下:
static unsigned long tvtojiffies(struct timeval *value)
{
unsigned long sec = (unsigned) value->tv_sec;
unsigned long usec = (unsigned) value->tv_usec;

if (sec > (ULONG_MAX / HZ))
return ULONG_MAX;
usec += 1000000 / HZ - 1;
usec /= 1000000 / HZ;
return HZ*sec+usec;
}

static void jiffiestotv(unsigned long jiffies, struct timeval *value)
{
value->tv_usec = (jiffies % HZ) * (1000000 / HZ);
value->tv_sec = jiffies / HZ;
}

7.7.2 真實間隔定時器ITIMER_REAL的底層運行機制
間隔定時器ITIMER_VIRT和ITIMER_PROF的底層運行機制是分別通過函數do_it_virt()函數和do_it_prof()函數來實現的,這裡就不再重述(可以參見7.4.3節)。
由於間隔定時器ITIMER_REAL本質上與內核動態定時器並無區別。因此內核實際上是通過內核動態定時器來實現進程的ITIMER_REAL間隔定時器的。為此,task_struct結構中專門設立一個timer_list結構類型的成員變量real_timer。動態定時器real_timer的函數指針function總是被task_struct結構的初始化宏INIT_TASK設置為指向函數it_real_fn()。如下所示(include/linux/sched.h):
#define INIT_TASK(tsk)
……
real_timer: {
function: it_real_fn
}
……
}
而real_timer鏈表元素list和data成員總是被進程創建時分別初始化為空和進程task_struct結構的地址,如下所示(kernel/fork.c):
int do_fork(……)
{
……
p->it_real_value = p->it_virt_value = p->it_prof_value = 0;
p->it_real_incr = p->it_virt_incr = p->it_prof_incr = 0;
init_timer(&p->real_timer);
p->real_timer.data = (unsigned long)p;
……
}
當用戶通過setitimer()系統調用來設置進程的ITIMER_REAL間隔定時器時,it_real_incr被設置成非零值,於是該系統調用相應地設置好real_timer.expires值,然後進程的real_timer定時器就被加入到內核動態定時器鏈表中,這樣該進程的ITIMER_REAL間隔定時器就被啟動了。當real_timer定時器到期時,它的關聯函數it_real_fn()將被執行。注意!所有進程的real_timer定時器的function函數指針都指向it_real_fn()這同一個函數,因此it_real_fn()函數必須通過其參數來識別是哪一個進程,為此它將unsignedlong類型的參數p解釋為進程task_struct結構的地址。該函數的源碼如下(kernel/itimer.c):
void it_real_fn(unsigned long __data)
{
struct task_struct * p = (struct task_struct *) __data;
unsigned long interval;

send_sig(SIGALRM, p, 1);
interval = p->it_real_incr;
if (interval) {
if (interval > (unsigned long) LONG_MAX)
interval = LONG_MAX;
p->real_timer.expires = jiffies + interval;
add_timer(&p->real_timer);
}
}
函數it_real_fn()的執行過程大致如下:
(1)首先將參數p通過強制類型轉換解釋為進程的task_struct結構類型的指針。
(2)向進程發送SIGALRM信號。
(3)在進程的it_real_incr非0的情況下繼續啟動real_timer定時器。首先,計算real_timer定時器的expires值為(jiffies+it_real_incr)。然後,調用add_timer()函數將real_timer加入到內核動態定時器鏈表中。

7.7.3 itimer定時器的系統調用
與itimer定時器相關的syscall有兩個:getitimer()和setitimer()。其中,getitimer()用於查詢調用進程的三個間隔定時器的信息,而setitimer()則用來設置調用進程的三個間隔定時器。這兩個syscall都是現在kernel/itimer.c文件中。

7.7.3.1 getitimer()系統調用的實現
函數sys_getitimer()有兩個參數:(1)which,指定查詢調用進程的哪一個間隔定時器,其取值可以是ITIMER_REAL、ITIMER_VIRT和ITIMER_PROF三者之一。(2)value指針,指向用戶空間中的一個itimerval結構,用於接收查詢結果。該函數的源碼如下:
/* SMP: Only we modify our itimer values. */
asmlinkage long sys_getitimer(int which, struct itimerval *value)
{
int error = -EFAULT;
struct itimerval get_buffer;

if (value) {
error = do_getitimer(which, &get_buffer);
if (!error &&
copy_to_user(value, &get_buffer, sizeof(get_buffer)))
error = -EFAULT;
}
return error;
}
顯然,sys_getitimer()函數主要通過do_getitimer()函數來查詢當前進程的間隔定時器信息,並將查詢結果保存在內核空間的結構變量get_buffer中。然後,調用copy_to_usr()宏將get_buffer中結果拷貝到用戶空間緩沖區中。
函數do_getitimer()的源碼如下(kernel/itimer.c):
int do_getitimer(int which, struct itimerval *value)
{
register unsigned long val, interval;

switch (which) {
case ITIMER_REAL:
interval = current->it_real_incr;
val = 0;
/*
* FIXME! This needs to be atomic, in case the kernel timer happens!
*/
if (timer_pending(&current->real_timer)) {
val = current->real_timer.expires - jiffies;

/* look out for negative/zero itimer.. */
if ((long) val <= 0)
val = 1;
}
break;
case ITIMER_VIRTUAL:
val = current->it_virt_value;
interval = current->it_virt_incr;
break;
case ITIMER_PROF:
val = current->it_prof_value;
interval = current->it_prof_incr;
break;
default:
return(-EINVAL);
}
jiffiestotv(val, &value->it_value);
jiffiestotv(interval, &value->it_interval);
return 0;
}
查詢的過程如下:
(1)首先,用局部變量val和interval分別表示待查詢間隔定時器的間隔計數器的當前值和初始值。
(2)如果which=ITIMER_REAL,則查詢當前進程的ITIMER_REAL間隔定時器。於是從current->it_real_incr中得到ITIMER_REAL間隔定時器的間隔計數器的初始值,並將其保存到interval局部變量中。而對於間隔計數器的當前值,由於ITITMER_REAL間隔定時器是通過real_timer這個內核動態定時器來實現的,因此不能通過current->it_real_value來獲得ITIMER_REAL間隔定時器的間隔計數器的當前值,而必須通過real_timer來得到這個值。為此先用timer_pending()函數來判斷current->real_timer是否已被起動。如果未啟動,則說明ITIMER_REAL間隔定時器也未啟動,因此其間隔計數器的當前值肯定是0。因此將val變量簡單地置0就可以了。如果已經啟動,則間隔計數器的當前值應該等於(timer_real.expires-jiffies)。
(3)如果which=ITIMER_VIRT,則查詢當前進程的ITIMER_VIRT間隔定時器。於是簡單地將計數器初值it_virt_incr和當前值it_virt_value分別保存到局部變量interval和val中。
(4)如果which=ITIMER_PROF,則查詢當前進程的ITIMER_PROF間隔定時器。於是簡單地將計數器初值it_prof_incr和當前值it_prof_value分別保存到局部變量interval和val中。
(5)最後,通過轉換函數jiffiestotv()將val和interval轉換成timeval格式的時間值,並保存到value->it_value和value->it_interval中,作為查詢結果返回。

7.7.3.2 setitimer()系統調用的實現
函數sys_setitimer()不僅設置調用進程的指定間隔定時器,而且還返回該間隔定時器的原有信息。它有三個參數:(1)which,含義與sys_getitimer()中的參數相同。(2)輸入參數value,指向用戶空間中的一個itimerval結構,含有待設置的新值。(3)輸出參數ovalue,指向用戶空間中的一個itimerval結構,用於接收間隔定時器的原有信息。
該函數的源碼如下(kernel/itimer.c):
/* SMP: Again, only we play with our itimers, and signals are SMP safe
* now so that is not an issue at all anymore.
*/
asmlinkage long sys_setitimer(int which, struct itimerval *value,
struct itimerval *ovalue)
{
struct itimerval set_buffer, get_buffer;
int error;

if (value) {
if(copy_from_user(&set_buffer, value, sizeof(set_buffer)))
return -EFAULT;
} else
memset((char *) &set_buffer, 0, sizeof(set_buffer));

error = do_setitimer(which, &set_buffer, ovalue ? &get_buffer : 0);
if (error || !ovalue)
return error;

if (copy_to_user(ovalue, &get_buffer, sizeof(get_buffer)))
return -EFAULT;
return 0;
}
對該函數的注釋如下:
(1)在輸入參數指針value非空的情況下,調用copy_from_user()宏將用戶空間中的待設置信息拷貝到內核空間中的set_buffer結構變量中。如果value指針為空,則簡單地將set_buffer結構變量全部置0。
(2)調用do_setitimer()函數完成實際的設置操作。如果輸出參數ovalue指針有效,則以內核變量get_buffer的地址作為do_setitimer()函數的第三那個調用參數,這樣當do_setitimer()函數返回時,get_buffer結構變量中就將含有當前進程的指定間隔定時器的原來信息。Do_setitimer()函數返回0值表示成功,非0值表示失敗。
(3)在do_setitimer()函數返回非0值的情況下,或者ovalue指針為空的情況下(不需要輸出間隔定時器的原有信息),函數就可以直接返回了。
(4)如果ovalue指針非空,調用copy_to_user()宏將get_buffer()結構變量中值拷貝到ovalue所指向的用戶空間中去,以便讓用戶得到指定間隔定時器的原有信息值。

函數do_setitimer()的源碼如下(kernel/itimer.c):
int do_setitimer(int which, struct itimerval *value, struct itimerval *ovalue)
{
register unsigned long i, j;
int k;

i = tvtojiffies(&value->it_interval);
j = tvtojiffies(&value->it_value);
if (ovalue && (k = do_getitimer(which, ovalue)) < 0)
return k;
switch (which) {
case ITIMER_REAL:
del_timer_sync(&current->real_timer);
current->it_real_value = j;
current->it_real_incr = i;
if (!j)
break;
if (j > (unsigned long) LONG_MAX)
j = LONG_MAX;
i = j + jiffies;
current->real_timer.expires = i;
add_timer(&current->real_timer);
break;
case ITIMER_VIRTUAL:
if (j)
j++;
current->it_virt_value = j;
current->it_virt_incr = i;
break;
case ITIMER_PROF:
if (j)
j++;
current->it_prof_value = j;
current->it_prof_incr = i;
break;
default:
return -EINVAL;
}
return 0;
}
對該函數的注釋如下:
(1)首先調用tvtojiffies()函數將timeval格式的初始值和當前值轉換成以時鐘滴答為單位的時間值。並分別保存在局部變量i和j中。
(2)如果ovalue指針非空,則調用do_getitimer()函數查詢指定間隔定時器的原來信息。如果do_getitimer()函數返回負值,說明出錯。因此就要直接返回錯誤值。否則繼續向下執行開始真正地設置指定的間隔定時器。
(3)如果which=ITITMER_REAL,表示設置ITIMER_REAL間隔定時器。(a)調用del_timer_sync()函數(該函數在單CPU系統中就是del_timer()函數)將當前進程的real_timer定時器從內核動態定時器鏈表中刪除。(b)將it_real_incr和it_real_value分別設置為局部變量i和j。(c)如果j=0,說明不必啟動real_timer定時器,因此執行break語句退出switch…case控制結構,而直接返回。(d)將real_timer的expires成員設置成(jiffies+當前值j),然後調用add_timer()函數將當前進程的real_timer定時器加入到內核動態定時器鏈表中,從而啟動該定時器。
(4)如果which=ITIMER_VIRT,則簡單地用局部變量i和j的值分別更新it_virt_incr和it_virt_value就可以了。
(5)如果which=ITIMER_PROF,則簡單地用局部變量i和j的值分別更新it_prof_incr和it_prof_value就可以了。
(6)最後,返回0值表示成功。

7.7.3.3 alarm系統調用
系統調用alarm可以讓調用進程在指定的秒數間隔後收到一個SIGALRM信號。它只有一個參數seconds,指定以秒數計的定時間隔。函數sys_alarm()的源碼如下(kernel/timer.c):
/*
* For backwards compatibility? This can be done in libc so Alpha
* and all newer ports shouldn't need it.
*/
asmlinkage unsigned long sys_alarm(unsigned int seconds)
{
struct itimerval it_new, it_old;
unsigned int oldalarm;

it_new.it_interval.tv_sec = it_new.it_interval.tv_usec = 0;
it_new.it_value.tv_sec = seconds;
it_new.it_value.tv_usec = 0;
do_setitimer(ITIMER_REAL, &it_new, &it_old);
oldalarm = it_old.it_value.tv_sec;
/* ehhh.. We can't return 0 if we have an alarm pending.. */
/* And we'd better return too much than too little anyway */
if (it_old.it_value.tv_usec)
oldalarm++;
return oldalarm;
}
這個系統調用實際上就是啟動進程的ITIMER_REAL間隔定時器。因此它完全可放到用戶空間的C函數庫(比如libc和glibc)中來實現。但是為了保此內核的向後兼容性,2.4.0版的內核仍然將這個syscall放在內核空間中來實現。函數sys_alarm()的實現過程如下:
(1)根據參數seconds的值構造一個itimerval結構變量it_new。注意!由於alarm啟動的ITIMER_REAL間隔定時器是一次性而不是循環重復的,因此it_new變量中的it_interval成員一定要設置為0。
(2)調用函數do_setitimer()函數以新構造的定時器it_new來啟動當前進程的ITIMER_REAL定時器,同時將該間隔定時器的原定時間隔保存到局部變量it_old中。
(3)返回值oldalarm表示以秒數計的ITIMER_REAL間隔定時器的原定時間隔值。因此先把it_old.it_value.tv_sec賦給oldalarm,並且在it_old.it_value.tv_usec非0的情況下,將oldalarm的值加1(也即不足1秒補足1秒)。





7.8 時間系統調用的實現
本節講述與時間相關的syscall,這些系統調用主要用來供用戶進程向內核檢索當前時間與日期,因此他們是內核的時間服務接口。主要的時間系統調用共有5個:time、stime和gettimeofday、settimeofday,以及與網絡時間協議NTP相關的adjtimex系統調用。這裡我們不關心NTP,因此僅分析前4個時間系統調用。前4個時間系統調用可以分為兩組:(1)time和stime是一組;(2)gettimeofday和settimeofday是一組。

7.8.1 系統調用time和stime
系統調用time()用於獲取以秒數表示的系統當前時間(即內核全局時間變量xtime中的tv_sec成員的值)。它只有一個參數——整型指針tloc,指向用戶空間中的一個整數,用來接收返回的當前時間值。函數sys_time()的源碼如下(kernel/time.c):
asmlinkage long sys_time(int * tloc)
{
int i;

/* SMP: This is fairly trivial. We grab CURRENT_TIME and
stuff it to user space. No side effects */
i = CURRENT_TIME;
if (tloc) {
if (put_user(i,tloc))
i = -EFAULT;
}
return i;
}
注釋如下:
(1)首先,函數調用CURRENT_TIME宏來得到以秒數表示的內核當前時間值,並將該值保存在局部變量i中。宏CURRENT_TIME定義在include/linux/sched.h頭文件中,它實際上就是內核全局時間變量xtime中的tv_sec成員。如下所示:
#define CURRENT_TIME (xtime.tv_sec)
(2)然後,在參數指針tloc非空的情況下將i的值通過put_user()宏傳遞到有tloc所指向的用戶空間中去,以作為函數的輸出結果。
(3)最後,將局部變量I的值——也即也秒數表示的系統當前時間值作為返回值返回。

系統調用stime()與系統調用time()剛好相反,它可以讓用戶設置系統的當前時間(以秒數為單位)。它同樣也只有一個參數——整型指針tptr,指向用戶空間中待設置的時間秒數值。函數sys_stime()的源碼如下(kernel/time.c):
asmlinkage long sys_stime(int * tptr)
{
int value;

if (!capable(CAP_SYS_TIME))
return -EPERM;
if (get_user(value, tptr))
return -EFAULT;
write_lock_irq(&xtime_lock);
xtime.tv_sec = value;
xtime.tv_usec = 0;
time_adjust = 0;/* stop active adjtime() */
time_status |= STA_UNSYNC;
time_maxerror = NTP_PHASE_LIMIT;
time_esterror = NTP_PHASE_LIMIT;
write_unlock_irq(&xtime_lock);
return 0;
}
注釋如下:
(1)首先檢查調用進程的權限,顯然,只有root用戶才能有權限修改系統時間。
(2)調用get_user()宏將tptr指針所指向的用戶空間中的時間秒數值拷貝到內核空間中來,並保存到局部變量value中。
(3)將局部變量value的值更新到全局時間變量xtime的tv_sec成員中,並將xtime的tv_usec成員清零。
(4)在相應地重置其它狀態變量後,函數就可以返回了(返回值0表示成功)。

7.8.2 系統調用gettimeofday
這個syscall用來供用戶獲取timeval格式的當前時間信息(精確度為微秒級),以及系統的當前時區信息(timezone)。結構類型timeval的指針參數tv指向接受時間信息的用戶空間緩沖區,參數tz是一個timezone結構類型的指針,指向接收時區信息的用戶空間緩沖區。這兩個參數均為輸出參數,返回值0表示成功,返回負值表示出錯。函數sys_gettimeofday()的源碼如下(kernel/time.c):
asmlinkage long sys_gettimeofday(struct timeval *tv, struct timezone *tz)
{
if (tv) {
struct timeval ktv;
do_gettimeofday(&ktv);
if (copy_to_user(tv, &ktv, sizeof(ktv)))
return -EFAULT;
}
if (tz) {
if (copy_to_user(tz, &sys_tz, sizeof(sys_tz)))
return -EFAULT;
}
return 0;
}
顯然,函數的實現主要分成兩個大的方面:
(1)如果tv指針有效,則說明用戶要以timeval格式來檢索系統當前時間。為此,先調用do_gettimeofday()函數來檢索系統當前時間並保存到局部變量ktv中。然後再調用copy_to_user()宏將保存在內核空間中的當前時間信息拷貝到由參數指針tv所指向的用戶空間緩沖區中。
(2)如果tz指針有效,則說明用戶要檢索當前時區信息,因此調用copy_to_user()宏將全局變量sys_tz中的時區信息拷貝到參數指針tz所指向的用戶空間緩沖區中。
(3)最後,返回0表示成功。

函數do_gettimeofday()的源碼如下(arch/i386/kernel/time.c):
/*
* This version of gettimeofday has microsecond resolution
* and better than microsecond precision on fast x86 machines with TSC.
*/
void do_gettimeofday(struct timeval *tv)
{
unsigned long flags;
unsigned long usec, sec;

read_lock_irqsave(&xtime_lock, flags);
usec = do_gettimeoffset();
{
unsigned long lost = jiffies - wall_jiffies;
if (lost)
usec += lost * (1000000 / HZ);
}
sec = xtime.tv_sec;
usec += xtime.tv_usec;
read_unlock_irqrestore(&xtime_lock, flags);

while (usec >= 1000000) {
usec -= 1000000;
sec++;
}

tv->tv_sec = sec;
tv->tv_usec = usec;
}
該函數的完成實際的當前時間檢索工作。由於gettimeofday()系統調用要求時間精度要達到微秒級,因此do_gettimeofday()函數不能簡單地返回xtime中的值即可,而必須精確地確定自從時鐘驅動的BottomHalf上一次更新xtime的那個時刻(由wall_jiffies變量表示,參見7.3節)到do_gettimeofday()函數的當前執行時刻之間的具體時間間隔長度,以便精確地修正xtime的值.如下圖7-9所示:
假定被do_gettimeofday()用來修正xtime的時間間隔為fixed_usec,而從wall_jiffies到jiffies之間的時間間隔是lost_usec,而從jiffies到do_gettimeofday()函數的執行時刻的時間間隔是offset_usec。則下列三個等式成立:
fixed_usec=(lost_usec+offset_usec)
lost_usec=(jiffies-wall_jiffies)*TICK_SIZE=(jiffies-wall_jiffies)*(1000000/HZ)
由於全局變量last_tsc_low表示上一次時鐘中斷服務函數timer_interrupt()執行時刻的CPU TSC寄存器的值,因此我們可以用X86 CPU的TSC寄存器來計算offset_usec的值。也即:
offset_usec=delay_at_last_interrupt+(current_tsc_low-last_tsc_low)*fast_gettimeoffset_quotient
其中,delay_at_last_interrupt是從上一次發生時鐘中斷到timer_interrupt()服務函數真正執行時刻之間的時間延遲間隔。每一次timer_interrupt()被執行時都會計算這一間隔,並利用TSC的當前值更新last_tsc_low變量(可以參見7.4節)。假定current_tsc_low是do_gettimeofday()函數執行時刻TSC的當前值,全局變量fast_gettimeoffset_quotient則表示TSC寄存器每增加1所代表的時間間隔值,它是由time_init()函數所計算的。
根據上述原理分析,do_gettimeofday()函數的執行步驟如下:
(1)調用函數do_gettimeoffset()計算從上一次時鐘中斷發生到執行do_gettimeofday()函數的當前時刻之間的時間間隔offset_usec。
(2)通過wall_jiffies和jiffies計算lost_usec的值。
(3)然後,令sec=xtime.tv_sec,usec=xtime.tv_usec+lost_usec+offset_usec。顯然,sec表示系統當前時間在秒數量級上的值,而usec表示系統當前時間在微秒量級上的值。
(4)用一個while{}循環來判斷usec是否已經溢出而超過106us=1秒。如果溢出,則將usec減去106us並相應地將sec增加1,直到usec不溢出為止。
(5)最後,用sec和usec分別更新參數指針所指向的timeval結構變量。至此,整個查詢過程結束。

函數do_gettimeoffset()根據CPU是否配置有TSC寄存器這一條件分別有不同的實現。其定義如下(arch/i386/kernel/time.c):
#ifndef CONFIG_X86_TSC
static unsigned long do_slow_gettimeoffset(void)
{
……
}
static unsigned long (*do_gettimeoffset)(void) = do_slow_gettimeoffset;
#else
#define do_gettimeoffset() do_fast_gettimeoffset()
#endif
顯然,在配置有TSC寄存器的i386平台上,do_gettimeoffset()函數實際上就是do_fast_gettimeoffset()函數。它通過TSC寄存器來計算do_fast_gettimeoffset()函數被執行的時刻到上一次時鐘中斷發生時的時間間隔值。其源碼如下(arch/i386/kernel/time.c):
static inline unsigned long do_fast_gettimeoffset(void)
{
register unsigned long eax, edx;

/* Read the Time Stamp Counter */

rdtsc(eax,edx);

/* .. relative to previous jiffy (32 bits is enough) */
eax -= last_tsc_low;/* tsc_low delta */

/*
* Time offset = (tsc_low delta) * fast_gettimeoffset_quotient
* = (tsc_low delta) * (usecs_per_clock)
* = (tsc_low delta) * (usecs_per_jiffy / clocks_per_jiffy)
*
* Using a mull instead of a divl saves up to 31 clock cycles
* in the critical path.
*/

__asm__("mull %2"
:"=a" (eax), "=d" (edx)
:"rm" (fast_gettimeoffset_quotient),
"0" (eax));

/* our adjusted time offset in microseconds */
return delay_at_last_interrupt + edx;
}
對該函數的注釋如下:
(1)先調用rdtsc()函數讀取當前時刻TSC寄存器的值,並將其高32位保存在edx局部變量中,低32位保存在局部變量eax中。
(2)讓局部變量eax=Δtsc_low=eax-last_tsc_low;也即計算當前時刻的TSC值與上一次時鐘中斷服務函數timer_interrupt()執行時的TSC值之間的差值。
(3)顯然,從上一次timer_interrupt()到當前時刻的時間間隔就是(Δtsc_low*fast_gettimeoffset_quotient)。因此用一條mul指令來計算這個乘法表達式的值。
(4)返回值delay_at_last_interrupt+(Δtsc_low*fast_gettimeoffset_quotient)就是從上一次時鐘中斷發生時到當前時刻之間的時間偏移間隔值。

7.8.3 系統調用settimeofday
這個系統調用與gettimeofday()剛好相反,它供用戶設置當前時間以及當前時間信息。它也有兩個參數:(1)參數指針tv,指向含有待設置時間信息的用戶空間緩沖區;(2)參數指針tz,指向含有待設置時區信息的用戶空間緩沖區。函數sys_settimeofday()的源碼如下(kernel/time.c):
asmlinkage long sys_settimeofday(struct timeval *tv, struct timezone *tz)
{
struct timevalnew_tv;
struct timezone new_tz;

if (tv) {
if (copy_from_user(&new_tv, tv, sizeof(*tv)))
return -EFAULT;
}
if (tz) {
if (copy_from_user(&new_tz, tz, sizeof(*tz)))
return -EFAULT;
}

return do_sys_settimeofday(tv ? &new_tv : NULL, tz ? &new_tz : NULL);
}
函數首先調用copy_from_user()宏將保存在用戶空間中的待設置時間信息和時區信息拷貝到內核空間中來,並保存到局部變量new_tv和new_tz中。然後,調用do_sys_settimeofday()函數完成實際的時間設置和時區設置操作。
函數do_sys_settimeofday()的源碼如下(kernel/time.c):
int do_sys_settimeofday(struct timeval *tv, struct timezone *tz)
{
static int firsttime = 1;

if (!capable(CAP_SYS_TIME))
return -EPERM;

if (tz) {
/* SMP safe, global irq locking makes it work. */
sys_tz = *tz;
if (firsttime) {
firsttime = 0;
if (!tv)
warp_clock();
}
}
if (tv)
{
/* SMP safe, again the code in arch/foo/time.c should
* globally block out interrupts when it runs.
*/
do_settimeofday(tv);
}
return 0;
}
該函數的執行過程如下:
(1)首先,檢查調用進程是否有相應的權限。如果沒有,則返回錯誤值-EPERM。
(2)如果執政tz有效,則用tz所指向的新時區信息更新全局變量sys_tz。並且如果是第一次設置時區信息,則在tv指針不為空的情況下調用wrap_clock()函數來調整xtime中的秒數值。函數wrap_clock()的源碼如下(kernel/time.c):
inline static void warp_clock(void)
{
write_lock_irq(&xtime_lock);
xtime.tv_sec += sys_tz.tz_minuteswest * 60;
write_unlock_irq(&xtime_lock);
}
(3)如果參數tv指針有效,則根據tv所指向的新時間信息調用do_settimeofday()函數來更新內核的當前時間xtime。
(4)最後,返回0值表示成功。
函數do_settimeofday()執行剛好與do_gettimeofday()相反的操作。這是因為全局變量xtime所表示的時間是與wall_jiffies相對應的那一個時刻。因此,必須從參數指針tv所指向的新時間中減去時間間隔fixed_usec(其含義見7.8.2節)。函數源碼如下(arch/i386/kernel/time.c):
void do_settimeofday(struct timeval *tv)
{
write_lock_irq(&xtime_lock);
/*
* This is revolting. We need to set "xtime" correctly. However, the
* value in this location is the value at the most recent update of
* wall time. Discover what correction gettimeofday() would have
* made, and then undo it!
*/
tv->tv_usec -= do_gettimeoffset();
tv->tv_usec -= (jiffies - wall_jiffies) * (1000000 / HZ);

while (tv->tv_usec < 0) {
tv->tv_usec += 1000000;
tv->tv_sec--;
}

xtime = *tv;
time_adjust = 0;/* stop active adjtime() */
time_status |= STA_UNSYNC;
time_maxerror = NTP_PHASE_LIMIT;
time_esterror = NTP_PHASE_LIMIT;
write_unlock_irq(&xtime_lock);
}
該函數的執行步驟如下:
(1)調用do_gettimeoffset()函數計算上一次時鐘中斷發生時刻到當前時刻之間的時間間隔值。
(2)通過wall_jiffies與jiffies計算二者之間的時間間隔lost_usec。
(3)從tv->tv_usec中減去fixed_usec,即:tv->tv_usec-=(lost_usec+offset_usec)。
(4)用一個while{}循環根據tv->tv_usec是否小於0來調整tv結構變量。如果tv->tv_usec小於0,則將tv->tv_usec加上106us,並相應地將tv->tv_sec減1。直到tv->tv_usec不小於0為止。
(5)用修正後的時間tv來更新內核全局時間變量xtime。
(6)最後,重置其它時間狀態變量。

至此,我們已經完全分析了整個Linux內核的時鐘機制!

Copyright © Linux教程網 All Rights Reserved