Linux定時器和時間管理


這部分講Linux內核定時器。

基本概念

  • 系統定時器:一種可編程硬件芯片,能以固定頻率產生中斷。
  • 定時器中斷:系統定時器固定時間周期產生的中斷,其中斷處理程序負責更新系統時間,執行周期性任務。
  • 動態定時器:一種用來推遲執行程序的工具。內核可以動態創建、銷毀動態定時器。
  • 節拍率(tick rate):系統定時器頻率,系統定時器以某種頻率自行觸發(又稱為擊中(hitting)或射中(popping))時鍾中斷,該頻率可以通過編程設定。
  • 節拍(tick):預編的節拍率對內核來說是可知的,因此內核知道連續2次時鍾中斷間隔時間。這個時間稱為節拍。節拍 = 1/ 節拍率。常用來計算牆上時間和系統運行時間。
  • 牆上時間(walk clock time):實際時間,對用戶空間的應用程序來說很重要。代表從進程開始運行到結束,系統時鍾走過的時間(時鍾數),包含了進程阻塞的時間。每秒滴答數(節拍率)可通過sysconf(_SC_CLK_TCK)獲取。
  • 系統運行時間:自系統啟動開始所經過的時間,對用戶空間和內核都很有用。牆上時間 = 阻塞時間 + 就緒時間 + 運行時間,運行時間 = 用戶CPU實際 + 系統CPU時間。

系統定時器中斷周期性執行的任務:

  • 更新系統時間。
  • 更新實際時間。
  • 在smp系統上,均衡調度程序中各處理器上的運行隊列。如果運行隊列負載不均衡的話,盡量使它們均衡。
  • 檢查當前進程是否用盡了自己的時間片。如果用盡,就重新調度。
  • 運行超時的動態定時器。
  • 更新資源消耗和處理器時間的統計值。

[======]

節拍率:HZ

系統定時器(節拍率)通過靜態預處理定義,系統啟動時按HZ值對硬件進行設置。HZ值取決於體系結構。如i386體系結構,HZ值為1000(Hz),代表每秒鍾產生1000次節拍

#include <asm/param.h>

#define HZ 1000 /* 內核時鍾頻率 */

其他體系結構節拍率:

系統定時器使用高頻率優缺點
優點:

  • 內核定時器能以更高頻度和准確度運行。
  • 依賴定時值執行的系統調用,如select,poll,能以更高精度運行。
  • 對諸如資源消耗和系統運行時間等的測量會有精細的解析度。
  • 提高進程搶占的准確度。

缺點:

  • 節拍率越高,系統時鍾中斷頻率越高,意味着系統負擔越重,即中斷處理處理程序占用的處理器時間越多,減少了處理其他工作的時間。

[======]

jiffies

全局變量jiffies用來記錄自系統啟動以來產生的節拍總數。啟動時,初值0;之后,每次時鍾中斷處理程序都會讓jiffies+1。

jiffies定義:

#include <linux/jiffies.h>

extern unsigned long volatile jiffies;

jiffies內部表示

32bit體系結構上,jiffies是32bit,如果時鍾頻率100Hz,497天后會溢出;頻率1000Hz,49.7天后溢出。
64bit體系結構上,幾乎不可能會看到它溢出。

除了前面定義,jiffies還有第二個變量定義:

#include <linux/jiffies.h>

extern u64 jiffies_64;

ld(1) 腳本用於連接主內核映像,然后用jiffies_64初值覆蓋jiffies變量:

// x86, arch/i386/kernel/vmlinux.lds.S
jiffies = jiffies_64;

也就是說,jiffies只取jiffies_64低32bit。因為大多數代碼只使用jiffies存放流失的時間,二時間管理代碼使用整個64bit的jiffies_64,以避免溢出。

在32bit體系結構上,jiffies 讀取jiffies_64低32bit值;get_jiffies_64()讀取jiffies_64整個64bit值。
周64bit體系結構上,jiffies 等價於get_jiffies_64(),和jiffies_64是同一個變量。

jiffies回繞

jiffies 溢出后,會繞回(wrap around)到0。內核提供4個宏函數,用於比較節拍計數,以避免回繞問題。

#include <linux/jiffies.h>

// unknown是jiffies, known是需要對比的值
#define timer_after(unknown, known) ((long)(known) - (long)(unknown) < 0)
#define timer_before(unknown, known) ((long)(unknown) - (long)(known) < 0)
#define timer_after_eq(unknown, known) ((long)(unknown) - (long)(known) >= 0)
#define timer_after(unknown, known) ((long)(known) - (long)(unknown) >= 0)

用戶空間和HZ

Linux內核2.6以前,如果改變內核中HZ值,會給用戶空間中某些程序造成異常結果,因為應用程序已經依賴這個特定HZ值。
要避免上面錯誤,內核需要更改所有導出的jiffies值。因此,內核定義USER_HZ代表用戶空間看到的HZ值。

例如,x86體系結構上,HZ值原來一直是100,因此USER_HZ值定義為100。
內核使用宏jiffies_to_clock_t() 將一個由HZ表示的節拍計數轉換成一個由USER_HZ表示的節拍數。
當USER_HZ是HZ的整數倍時,

#define jiffies_to_clock_t(x) ((x) / (HZ/USER_HZ))

另外,jiffies_64_to_clock_t()將64位jiffies值單位從HZ轉換為USER_HZ。

[======]

硬時鍾和定時器

體系結構提供3種硬時鍾用於計時:實時時鍾,時間戳計數,可編程中斷定時器。

實時時鍾 RTC
RTC是用來持久存放系統時間的設備,即使PC關掉電源,RTC還能依靠主板電池繼續計時。
主要作用:
1)系統啟動時,內核通過讀取RTC來初始化牆上時間,該時間存放在xtime變量中。
2)Linux只用RTC來獲得當前時間和日期。

時間戳計數 TSC
x86包含一個64位的時間戳計數器(寄存器),對每個時鍾信號進行計數。例如,如果時鍾節拍400MHz,那么TSC每2.5ns計數+1。而時鍾信號頻率沒有在預編譯時指定,必須在Linux初始化時確定。通過calibrate_tsc(),在系統初始化階段完成時鍾信號頻率計算。

可編程中斷定時器 PIT
x86體系結構中,主要采用可編程中斷時鍾(PIT)作為系統定時器。
Linux中,若PIT以100Hz頻率向IRQ0發出定時中斷,即每10ms產生1次定時中斷。這個10ms間隔,就是一個節拍(tick),以微妙為單位存放在tick變量。

TSC與PIT相比,擁有更高的精度。PIT針對編寫軟件而言,更加靈活。

[======]

時鍾中斷處理程序

時鍾中斷處理程序可划分2個部分:體系結構相關部分,體系結構無關部分。
與體系結構相關的例程作為系統定時器(PIT)的中斷處理程序而注冊到內核,以便產生時鍾中斷時能運行。
處理程序主要執行以下工作:

  • 獲得xtime_lock鎖,對訪問jiffies_64和牆上時間進行保護。
  • 需要時應答或重新設置系統時鍾。
  • 周期性地使用牆上時間更新實時時鍾。
  • 調用體系結構無關的時鍾例程:do_timer()。

中斷服務程序主要通過調用與體系結構無關的do_timer()執行工作:

  • 給jiffies_64 + 1。
  • 更新資源消耗的統計值,如當前進程所消耗的系統時間和用戶時間。
  • 執行已經到期的動態定時器。
  • 更新牆上時間,該時間存放在xtime變量中。
  • 計算平均負載值。

do_timer()看起來像:

void do_timer(struct pt_regs* regs)
{
    jiffies_64++;
    
    update_process_times(user_mode(regs)); // 對用戶或系統進行時間更新
    update_times(); // 更新牆上時鍾
}

user_mode()宏查詢處理器寄存器regs的狀態。如果時鍾中斷發生在用戶空間,它返回1;如果發生在內核,則返回0。update_process_times()函數根據時鍾中斷產生的位置(用戶態 or 內核態),對用戶或對系統進行相應的時間更新。

void update_process_times(int user_tick)
{
    struct task_struct *p = current;
    int cpu = smp_processor_id();
    int system = user_tick ^ 1; // user_tick和system只會有一個變量為1,另一個必為0

    update_one_process(p, user_tick, system, cpu); // 更新進程時間
    run_local_timers();  // 標記一個軟中斷處理所有到期的定時器
    scheduler_tick(user_tick, system); // 負責減少當前運行進程的時間片計數值,並在需要時設置need_resched標志
}

update_one_process() 通過判斷分支,將user_tick和system加到進程相應的計數上:

/* 更新恰當的時間計數器,給其加一個jiffy */
p->utime += user;
p->stime += system;

update_times()負責更新牆上時鍾:

void update_times(void)
{
    unsigned long ticks; // 記錄最近一次更新后新產生的節拍數
    
    ticks = jiffies - wall_jiffies;
    if (ticks) {
        wall_jiffies += ticks;
        update_wall_time(ticks); // 更新存儲牆上時間的xtime
    }
    last_time_offset = 0;
    calc_load(ticks); // 更新載入平均值
}

ticks記錄最近一次更新后新產生的節拍數。通常,ticks應為1,但時鍾中斷可能丟失,導致節拍丟失。中斷長時間被禁止時,就會出現這種情況(雖然很可能是bug)。

cal_load(0更新載入平均值,到此,update_times()執行完畢。do_timer()亦執行完畢並返回與體系結構相關的中斷處理程序,繼續執行后面的工作,釋放xtime_lock鎖,然后退出。

牆上時間(實際時間)

牆上時間定義在kernel/timer.c中

struct timespec xtime;

timespec結構定義:

#include <linux/time.h>
struct timespec {
    time_t tv_sec; /* 秒 */
    long tv_nsec;  /* 納秒 */
};

xtime.tv_sec 存放着自1970年7月1日(UTC)以來經過的時間。1970年7月1日被稱為紀元,Unix牆上時間都是基於該紀元的。
xtime.ntv_sec記錄着自上一秒開始經過的納秒數。

讀寫xtime變量需要用xtime_lock鎖,這是一個seqlock鎖。
更新xtime:

write_seqlock(&xtime_lock);

/* 更新xtime... */

write_sequnlock(&time_lock);

讀取xtime:

/* 循環更新xtime, 直到確認循環期間沒有時鍾中斷處理程序更新xtime */
do {
    unsigned long lost;
    seq = read_seqbegin(&xtime_lock);
    
    usec = timer->get_offset();
    lost = jiffies->wall_jiffies;
    if (lost) 
        usec += lost * (1000000/HZ);
    sec = xtime.tv_sec;
    usec += (xtime.tv_nsec/1000);
} while(read_seqretry(&xtime_lock, seq));

如果循環期間有時鍾中斷處理程序更新xtime,read_seqretry()會返回無效序列號,繼續循環等待。

從用戶空間取得牆上時間的主要接口:gettimeofday(),內核中對應系統調用sys_gettimeofday():

asmlinkage long sys_gettimeofday(struct timeval* tv, struct timezone* tz)
{
    if (likely(tv)) { // <=> if (tv)
        struct timeval ktv;
        do_gettimeofday(&ktv); // 循環讀取xtime操作
    }
    if (copy_to_user(tv, &ktv, sizeof(ktv))) // 在給用戶空間拷貝牆上時間或時區
        return -EFAULT; // 拷貝時發生錯誤
    if (unlikely(tz)) { // <=> if (!tz)
        if (copy_to_user(tz, &sys_tz, sizeof(sys_tz))) return -EFAULT;
    }
    return 0;
}

/* 宏likely和unlikey在內核中定義, 便於編譯器優化, 以提升性能 */
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

time, ftime, gettimeofday關系

內核也實現了time(), ftime()系統調用,但都被gettimeofday()所取代。為保持向后兼容,Linux還保留着。

  • time() 返回從1970年1月1日午夜開始所走過的秒數。
  • ftime() 返回一個類型為timeb的數據結構,該結構包含從1970年1月1日午夜開始所走過的秒數;在最后1秒內所走過的毫秒數;時區以及夏令時當前的狀態。
  • gettimeofday() 返回的值存放在2個數據結構timeval和timezone,其中包含的信息與ftime相同。

[======]

定時器

定時器被稱為動態定時器或內核定時器,是管理內核時間的基礎。

定時器使用
思路:先進行一些初始化工作,設置一個超時時間,指定超時后執行的函數,然后激活定時器。指定的函數將在定時器到期時自動執行。
定時器不會周期運行,超時后自行銷毀。這是這種定時器被稱為動態定時器的一個原因。因此,動態定時器是在不斷的創建和銷毀,而且運行次數不受限制。在內核中應用非常普遍。

定時器由結構timer_list表示,定義於<linux/timer.h>

struct timer_list {
    struct list_head entry;               /* 定時器鏈表入口 */
    unsigned long expires;                /* 以jiffies為單位的定時值 */
    spinlock_t lock;                      /* 保護定時器的鎖 */
    void (*function)(unsigned long);      /* 定時器處理函數 */
    unsigned long data;                   /* 傳給處理函數的長整型參數 */
    struct tvec_t_base_s *base;           /* 定時器內部值, 用戶不要使用 */
};

使用定時器不用深入了解timer_list結構。內核提供一組接口簡化管理定時器的操作。

1)定義定時器

struct timer_list my_timer;

2)初始化定時器

init_timer(&my_timer);

3)填充定時器結構中需要的值

my_timer.expires = jiffies + delay;    /* 定時器超時節拍數 */
my_timer.data = 0;                     /* 給定時器處理函數傳入值0 */
my_timer.function = my_function;       /* 定時器超時調用的處理函數 */

超時處理函數必須是這種原型:

void my_timer_function(unsigned long data);

4)激活定時器

add_timer(&my_timer);

定時器工作條件:當前節拍計數jiffies >= my_timer.expires
定時器會在超時后馬上執行,但也可能推遲到下一個時鍾節拍,因此不能用於硬實時任務。

5)修改定時器
改變超時時間

mod_timer(&my_timer, jiffies + new_delay);  /* new expiration */

mod_timer可用於已經初始化但未激活的定時器;如果定時器未被激活,mod_timer會激活之。
如果調用時,定時器未被激活,函數返回0;否則,返回1.

6)刪除定時器
在定時器超時前定制定時器

del_timer(&my_timer);

激活或未被激活的定時器都可以用該函數,如果未被激活,函數返回0;否則,返回1。
已超時的定時器不需要調用該函數,因為會自動被刪除。

del_timer只能保證定時器將來不會被激活,不保證當前在其他處理器上已運行時會停止。此時,需要用del_timer_sync,等待其他處理器上運行的超時處理函數退出。

del_timer_sync(&my_timer); /* 如果有並發訪問可能性, 推薦優先使用 */
del_timer_sync() 不能在中斷上下文中使用,因為會阻塞。

定時器競爭條件

定時器與當前執行(設置定時器的)代碼是異步的,因此可能存在潛在競爭條件。因此,不能用如下方式替代mod_timer(),來改變定時器的超時時間,因為在多處理器上是不安全的:

/* 用下面代碼替換mod_timer, 修改定時器超時時間是錯誤的 */
del_timer(&my_timer);
my_timer->expires = jiffies + new_delay;
add_timer(&my_timer);

通常,用過用del_timer_sync() 取代del_timer()刪除定時器,避免並發訪問的問題,因為無法確定刪除定時器的時候,它是否在其他處理器上運行。

實現定時器

定時器作為軟中斷在下半部上下文中執行。
時鍾中斷處理程序會執行update_process_timers(),該函數會隨即調用run_local_timers()。

void run_local_timers(void)
{
    raise_softirq(TIMER_SOFTIRQ);
}

run_timer_softirq()處理軟件中斷TIMER_SOFTIRQ,從而在當前處理器上運行所有的超時定時器。

內核定時器是以鏈表形式存放,但並沒有遍歷鏈表以尋找超時定時器,也沒有在鏈表中插入和刪除定時器。
而是,將定時器按超時時間分為五組。當定時器超時時間接近時,定時器將隨組一起下移。采用分組定時器的方法可以在執行軟中斷的多數情況下,可以確保內核盡可能減少搜索超時定時器所帶來的負擔。

[======]

延遲執行

內核代碼(尤其驅動程序)除了用定時器或下半部機制外,還需要其他方法來推遲執行任務。
常適用於:短時間等待硬件完成某些工作,比如,重新設計網卡的以太網模式(2ms)。

內核提供多種延遲方法處理各種延遲要求:
1)忙等待
2)短延遲
3)schedule_timeout()
4)設置超時時間,在等待隊列上睡眠

忙等待

忙等待(或稱忙循環),是最簡單的延遲方法,也是最不理想的。
方法僅適用於想要延遲的時間是節拍的整數倍,或者精確度要求不高時使用。

忙循環使用示例:在循環中不斷旋轉直到希望的時鍾節拍數耗盡

unsigned long delay = jiffies + 10; /* 10個節拍 */

while (time_before(jiffies, delay)) /* CPU循環等待 jiffies > delay (自動處理定時器值回繞) */
    ;

上面循環不斷旋轉,等待10個節拍。HZ值為1000的x86體系結構上,每個節拍1ms,10個節拍總共耗時10ms。

unsigned long delay = jiffies + 2 * HZ; /* 2秒 */

while (time_before(jiffies, delay))
    ;

上面循環自旋時,並不會放棄CPU。下面cond_resched()將調度一個新程序投入運行,不過只有在設置完need_resched標志后,才能生效。因為cond_resched方法會調用調度程序,因此不能在中斷上下文中使用,而只能在進程上下文中使用。

unsigned long delay = jiffies + 5 * HZ;

while (time_before(jiffies, delay))
    cond_resched(); /* 調度一個新程序投入運行 */

注意:
1)所有延遲方法都只能在進程上下文使用,不能在中斷上下文使用。因為中處理程序應盡快執行。
2)延遲執行 不應在持有鎖或者禁止中斷的時候發生。

短延遲

有時驅動程序不但需要很短的延遲(比時鍾節拍typ.為1ms還短),而且要求延遲的時間很精確。不可能使用精度為1ms的jiffies節拍用於延遲。
此時,可以用內核提供的另外2個函數,用於處理微妙和毫秒級延遲。
頭文件:<linux/delay.h>

void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);

mdelay是通過udelay實現的。

如,延遲150微秒,延遲200毫秒

udelay(150);  /* 延遲150us */
mdelay(200);  /* 延遲200ms */

注意:
1)延遲超過1ms時,不要用udelay,應該用mdelay。
2)能不用則不用mdelay,盡量少用。
3)不要在持有鎖或者禁止中斷時,使用忙等待,因為類似於忙等待,會讓系統響應速度和性能大打折扣。

schedule_timeout() 睡眠到指定延遲時間

該方法會讓需要延遲執行的任務睡眠到指定的延遲時間耗盡后,再重新運行。不能保證睡眠時間剛好等於指定的延遲時間,只能是盡量接近。當指定時間到期后,內核喚醒被延遲的任務並將其重新放回運行隊列。

典型用法:

/* 將任務設置為可中斷睡眠狀態 */
set_current_state(TASK_INTERRUPTIBLE);

unsigned long S = 10;
/* 小睡一會兒,S秒后喚醒 */
schedule_timeout(s * HZ);

唯一的參數是延遲的相對時間,單位jiffies。

如果睡眠時,想接收信號,可將任務狀態設置為TASK_INTERRUPTIBLE;如果不想,可以將任務狀態設置為TASK_UNINTERRUPTIBLE。
注意:調用schedule_timeout()前,必須將任務設置為上面兩種狀態之一,否則任務不會睡眠。

schedule_timeout的簡單實現:

signed long schedule_timeout(singed long timeout)
{
    timer_t timer;
    unsigned long expire;
    
    switch(timeout)
    { /* 處理特殊情況 */
    case MAX_SCHEDULE_TIMEOUT: /* 無限期睡眠 */
        schedule(); /* 調度進程: 從就緒隊列中選一個優先級最高的進程來替代當前進程運行 */
        goto out;
    default:
        if (timeout < 0) {
            printk(KERN_ERR"schedule_timeout: wrong timeout value %lx from %p\n", timeout, __builtin_return_address(0));
            goto out;
        }
    }
    
    expire = timeout + jiffies;
    init_timer(&timer); /* 初始化動態定時器 */
    timer.expires = expire; 
    timer.data = (unsigned long)current;
    timer.funtion = process_timeout;
    
    add_timer(&timer); /* 激活定時器 */
    schedule();
    del_timer_sync(&timer); /* 同步刪除定時器 */

    timeout = expire - jiffies;

out:
    return timeout < 0 ? 0 : timeout;
}

/* 定時器超時處理函數 */
void process_timeout(unsigned long data)
{
    wake_up_progress((task_t *)data); /* 喚醒進程, 將任務設置為TASK_RUNNING */
}

因為任務被標識為TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE(在調用schedule_timeout之前),所以調度程序不會再選擇該任務投入運行,而會選擇其他新任務運行。

設置超時時間,在等待隊列上睡眠

進程上下文中為了等待特定事件發生,會將自己放入等待隊列,然后調用調度程序執行新任務。一旦事件發生,內核可調用wake_up()喚醒在睡眠隊列上的任務,使其重新投入運行。

schedule_timeout用在什么地方?
當等待隊列上的某個任務可能既在等待一個特定事件到來,又在等待一個特定時間到期,看誰先來。此時,可以用schedule_timeout替換schedule(),因為schedule()只是簡單的阻塞等待喚醒事件,而schedule_timeout除了可以等待IO事件,還會等待超時。

[======]

小結

1)講述了時間的基本概念,如牆上時間,時鍾中斷,時鍾節拍,HZ,jiffies等。
2)定時器的實現,應用方法等。
3)開發者用於延遲的方法:忙等待、短延遲、schedule_timeout。

[======]

參考

[1]RobertLove, 洛夫, 陳莉君,等. Linux內核設計與實現[M]. 機械工業出版社, 2006.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM