Linux內核設計與實現 總結筆記(第十一章)定時器和時間管理

時間管理在內核中佔用很是重要的地位,內核中有大量的函數都須要基於時間驅動的,內核對相對時間和絕對時間都很是須要。linux

1、內核中的時間概念

內核必須在硬件的幫助下才能計算和管理時間,系統定時器以某種頻率自行觸發(擊中hitting或者射中popping)時鐘中斷,該頻率能夠經過編程預約,稱做節拍率。編程

由於預編的節拍率對內核來講是可知的,因此內核知道連續兩次時鐘中斷的間隔時間,這個間隔時間稱爲節拍(tick),它等於節拍率分之一。緩存

下面是利用時間中斷週期執行的工做:安全

  • 更新系統運行時間
  • 更新實際時間
  • 在smp系統上,均衡調度程序中各處理器上的運行隊列。若是運行隊列負載不均衡的畫,儘可能使他們均衡。
  • 檢查當前進程是否用盡了本身的時間片。若是用盡,就從新進行調度。
  • 運行超時的動態定時器。
  • 更新資源消耗和處理器時間的統計值。

 

2、節拍率:HZ

系統定時器頻率是經過靜態預處理定義的,也就是HZ,在系統啓動時按照HZ值對硬件進行設置。數據結構

內核在<asm/param.h>文件中定義了這個值。異步

編寫內核代碼時,不要認爲HZ值是一個固定不變的值。 ide

2.1 理想的HZ值

提升節拍率意味着時鐘中斷產生得更加頻繁,因此中斷處理程序也會更頻繁地執行。函數

  • 更高的時鐘中斷解析度,可提升時間驅動事件的解析度。
  • 提升了時間驅動事件的準確度

 

2.2 高HZ的優點

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

 

2.3 高HZ的劣勢

節拍率越高,意味着時鐘中斷頻率越高,意味着系統負擔越重。中斷處理程序佔用處理器的時間越多。增長了耗電和打亂了處理器的高速緩存。工具

 

3、jiffies

全局變量jiffies用來記錄自系統啓動以來產生的節拍的總數。啓動時內核初始化爲0ui

由於一秒內時鐘中斷的次數等於HZ,因此jiffies一秒內增長的值也就爲HZ。系統運行時間以秒爲單位計算,就等於jiffies/HZ。

jiffies定義於文件<linux/jiffies.h>中:

extern unsigned long volatile jiffies;

以下的使用例子:

jiffies = seconds * HZ
jiffies/HZ = seconds
/* jiffies和seconds相互轉換 */

unsigned long time_stamp = jiffies;        /* 如今 */
unsigned long next_tick = jiffies+1;        /* 從如今開始1個節拍 */
unsigned long later = jiffies+5*HZ;        /* 從如今開始5秒 */
unsigned long fraction = jiffies + HZ / 10;    /* 從如今開始1/10秒 */
jiffies用法

 

3.1 jiffies的內部表示

jiffies變量老是無符號長整數,在32位上是32位,在64位上是64位。由於jiffies會溢出,

jiffies_64定義在<linux/jiffies.h>中:

extern u64 jiffies_64;

jiffies = jiffies_64;
jiffies

 

3.2 jiffies的迴繞

若是jiffies超過最大存放範圍後就會發生溢出,它的值會迴繞到0。

unsgined long timeout = jiffies + HZ/2;    /* 0.5秒後超過 */
/* 執行一些任務 ... */

/* 而後查看是否花的時間過長 */
if(timeout>jiffies) {
    /* 沒有超時,很好 ... */
} else {
    /* 超時了,發生錯誤 ... */
}
迴繞例子

內核提供給了四個宏來幫助比較節拍計數,他們能正確地處理節拍計數的迴繞狀況。這些宏在文件<linux/jiffies.h>中:

#define time_after(unknown, known)    ((long)(known) - (long)(unknown)<0)
#define time_before(unknown, known)    ((long)(known) - (long)(unknown)<0)
#define time_after_eq(unknown, known)    ((long)(known) - (long)(unknown)>=0)
#define time_before_eq(unknown, known)    ((long)(known) - (long)(unknown)>=0)
簡化版宏

宏time_after(unknown, known);當unkown超過指定known時,返回真,不然返回假。

宏time_before(unknown, known);當時間unknow 沒超過指定的know時,返回真,不然返回假

後面兩個宏,當兩個參數相等時,才返回真。

 

3.3 用戶空間和HZ

若是改變了內核中HZ的值,用戶空間中某些程序形成異常結果。內核是以節拍數/秒的形式給用戶空間導出這個值的。

所以內核定義了USER_HZ來表明用戶空間看到的HZ值。內核能夠用函數jiffies_to_clock_t()(在kernel/time.c中)將一個HZ表示的節拍計數轉換成一個由USER_HZ表示的節拍計數。

return x /  ( HZ / USER_HZ);
jiffies

在須要把以節拍數/秒爲單位的值導出到用戶空間時,須要使用上面這幾個函數,好比:

unsigned long start;
unsgined long total_time;

start = jiffies;
/* 執行一些任務 ... */
total_time = jiffies - start;
printk("That took %lu ticks\n", jiffies_to_clock_t(total_time));
節拍數/秒導出到用戶空間

 

4、硬時鐘和定時器

體系結構提供了兩種設備進行計時,一種系統定時器,一種實時定時器。他們有着相同的做用和設計思路。

4.1 實時時鐘

實時時鐘(RTC)是用來持久存放系統時間的設備,即使系統關閉後,它也能夠靠主板上的微型電池提供的電力保持系統的計時。 

4.2 系統定時器

系統定時器是內核定時機制中最爲重要的角色。

有些體系結構體是經過電子晶振進行分頻來實現系統定時器,還有些體系結構提供一個衰減測量器。

衰減測量器設置一個初始值,該值以固定頻率遞減,當減到零時,觸發一箇中斷。 

5、時鐘中斷處理程序

時鐘中斷處理程序能夠劃分爲兩個部分:體系結構相關部分和體系結構無關部分。

絕大多數處理程序最低限度也都要執行以下工做:

  • 得到xtime_lock鎖,以便對訪問jiffies_64和牆上時間xtime進行保護。
  • 須要時應答或從新設置系統時鐘。
  • 週期性地使用牆上時間更新實時時鐘。
  • 調用體系結構無關的時鐘例程:tick_periodic()。

tick_periodic()執行下面更多的工做:

  • 給jiffies_64變量增長1
  • 更新資源消耗的統計值,好比當前進程所消耗的系統時間和用戶時間。
  • 執行已經到期的動態定時器
  • 執行第4章曾討論的sheduler_tick()函數
  • 更新牆上時間,該時間存放在xtime變量中。
  • 計算平均負載值。

上述工做分別都由單獨的函數負責完成,因此tick_periodic()例程代碼看起來很是簡單。

static void tick_periodic(int cpu)
{
    if(tick_do_timer_cpu == cpu) {
        write_seqlock(&xtime_lock);

        /* 記錄下一節拍事件 */
        tick_next_period = ktime_add(tick_next_period, tick_period);

        do_timer(1);
        write_sequnlock(&xtime_lock);
    }

    update_process_times(user_mode(get_irq_regs()));
    profile_tick(CPU_PROFILING);
}
tick_periodic()函數

不少重要的操做都在do_timer()和update_process_times()函數中進行。前者承擔着對jiffies_64的實際增長操做:

函數update_wall_timer根據所流逝的時間更新牆上的時鐘,calc_global_load()更新系統的平均負載統計值。

void do_timer(unsigned long ticks)
{
    jiffies_64 += ticks;
    update_wall_time();
    calc_global_load();
}
do_timer()函數

do_timer返回時,調用update_process_times()更新所耗費的各類節拍數,經過user_tick區別話費在用戶空間仍是內核空間:

void update_process_times(int user_tick)
{
    struct task_struct *p = current;
    int cpu = smp_processor_id();
    /* 注意:也必須對這個時鐘irg的上下文說明一下緣由 */
    account_process_tick(p, user_tick);
    run_local_timers();
    rcu_check_callbacks(cpu, user_tick);
    printk_tick();
    scheduler_tick();
    run_posix_cpu_timers(p);
}
update_process_times

account_process_tick()函數對進程的時間進行實質性更新:

void account_process_tick(struct task_struct *p, int user_tick)
{
    cputime_t one_jiffy_scaled = cputime_to_scaled(cputime_one_jiffy);
    struct rq *rq = this_rq();

    if(user_tick)
        account_user_time(p, cputime_one_jiffy, one_jiffy_scaled);
    else if((p != rq->idle) || (irq_count() != HARDIRQ_OFFSET))
        account_system_time(p, HARDIRQ_OFFSET, cputime_one_jiffy, one_jiffy_scaled);
    else
        account_idle_time(cputime_one_jiffy);
}
account_process_tick

內核對進程進行時間計數時,是根據中斷髮生時處理器所處的模式進行分類統計的,它把上一個節拍所有算給了進程。

但事實上進程在上一個節拍期間,可能屢次進入和退出內核模式,並且在上一個節拍器件,該進程也不必定是惟一一個運行進程。

run_lock_timers()函數標記了一個軟中斷去處理全部到期的定時器。

scheduler_tick()函數負責減小當前運行進程的時間片計數值而且在須要時設置need_resched標誌。

tick_periodic()函數執行完畢後返回與體系結構相關的中斷處理程序,繼續執行後面的工做,釋放xtime_lock鎖,而後退出。

以上所有工做每1/HZ秒都要發生一次,就是說x86上時鐘中斷處理程序每秒執行100次或者1000次。

 

6、實際時間

當前實際時間(牆上時間)定義在文件kernel/time/timekeeping.c中:

struct timespec xtime;

timespec數據結構定義在文件<linux/time.h>中,形式以下:

struct timespec {
    _kernel_time_t tv_sec;        /**/
    long tv_nsec;                        /* ns */
};
timespec數據結構

xtime.tv_sec以秒爲單位,存放着自1970年1月1日(UTC)以來通過的時間,xtime變量須要使用xtime_lock鎖,它是一個seqlock鎖,不是普通的自旋鎖。

更新xtime首先要申請一個seqlock鎖:

write_seqlock(&xtime_lock);
/* 更新xtime ... */
write_sequnlock(&xtime_lock);
更新xtime

讀取xtime時也要使用read_seqbegin()和read_seqretry()函數:

unsigned long seq;
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));
read_seqretry()

該循環不斷重複,直到讀者確認讀取數據沒有寫操做介入。若是循環期間更新了xitme,read_seqretry()函數就返回無效序列號,繼續循環等待。

用戶空間取得的牆上時間接口是gettimeofday(),在內核中對應系統調用爲sys_gettimeofday(),定義於kernel/time.c:

asmlinkage long sys_gettimeofday(struct timeval *tv, struct timezone *tz)
{
    if(likely(tv)) {
        struct timeval ktv;
        do_gettimeofday(&ktv);
        if(copy_to_user(tv, &ktv, sizeof(ktv)))
            return -EFAULT;
    }
    if(unlikely(tz)) {
        if(copy_to_user(tz, &sys_tz, sizeof(sys_tz)))
            return -EFAULT;
    }
    return 0;
}
sys_gettimeofday

若是用戶提供的tv參數非空,do_gettimeofday()函數將被調用。它循環讀取xtime操做。若是tz參數爲空,將把系統時區返回用戶。若是在給用戶空間拷貝牆上時間或時區時發生錯誤,返回-EFAULT;成功返回0

gettimeodfay()函數幾乎徹底取代了time()系統調用。系統調用settimeofday()來設置當前時間,須要具備CAP_SYS_TIME權能。

除了更新xtime時間之外,內核不會像用戶空間程序那樣頻繁使用xtime。 在文件系統的實現代碼中存放訪問時間戳時須要使用xtime。

7、定時器

7.1 使用定時器

定時器是管理內核流逝的時間的基礎。咱們須要一種工具,可以使工做在指定時間點上執行。

 

7.2 實現定時器

定時器由結構timer_list表示,定義在文件<linux/timer.h>中。

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

內核提供了一組與定時器相關的結構用來簡化管理定時器的操做。全部接口都聲明在<linux/timer.h>,大多數接口在文件kernel/timer.c中實現。

定時器的例子:

/* 建立須要先定義它 */
struct timer_list my_timer;
/* 而後須要初始化它 */
init_timer(&my_timer);
/* 而後填充數據結構 */
my_timer.expires = jiffies + delay;        /* 定時器超時時的節拍數 */
my_timer.data = 0;                              /* 給定時器處理函數傳入0值 */
my_timer.function = my_function;       /* 定時器超時時調用的函數 */
/* 最後激活定時器 */
add_timer(&my_timer);
timer_list使用例子

在填充結構體中,expires表示超時時間,data是長整型的參數,function的函數原型必須符合:

void my_timer_function(unsigned long data);
my_timer_function

內核可能延誤定時器的執行,因此不能用定時器實現任何硬實時任務。

改變指定的定時器超時時間:

mod_timer(&my_timer, jiffies+new_delay);    /* 新的定時值 */
mod_timer

若是須要在定時器超時前中止定時器,可使用del_timer函數:

del_timer(&my_timer);
del_timer

 刪除定時器時須要等待可能在其餘處理器上運行的定時器處理程序都退出。

del_timer_sync(&my_timer);
del_timer_sync

 

7.2 定時器競爭條件

定時器與當前執行代碼是異步的,所以可能存在潛在競爭條件,下面的代碼是不安全的。

del_timer(my_timer);
my_timer->expires = jiffies + new_delay;
add_timer(my_timer);
不安全的代碼

通常狀況下del_timer_sync()函數取代del_timer()函數。

 

7.3 實現定時器 

內核在時鐘中斷髮生後執行定時器,定時器做爲軟中斷在下半部上下文中執行。

時鐘中斷處理程序會執行update_process_times()函數,該函數隨即調用run_local_timers()函數:

void run_local_timers(void)
{
    hrtimer_run_queues();
    raise_softirq(TIMER_SOFTIRQ);    /* 執行定時器軟中斷 */
    softlockup_tick();
}
run_local_timers

內核將定時器按它們的超時時間劃分爲五組。當定時器超時時間接近時,定時器將隨組一塊兒下移。減小搜索超時定時器所帶來的負擔。

 

8、延遲執行

除了使用定時器或下半部機制之外,還須要其餘方法來推遲執行任務。這種推遲一般發生在等待硬件完成某些工做時,並且等待的時間每每很是短。

好比,從新設置網卡的以太模式須要花費2ms,因此在設定網卡速度後,驅動程序必須至少等待2ms才能繼續運行。

8.1 忙等待

最簡單的延遲方法是忙等待,可是僅僅在想要延遲的時間是節拍的整數倍,或者精確率要求不高時纔可使用。

unsigned long time_out = jiffies + 10;        /* 10個節拍 */
while(time_before(jiffies, time_out))
    ;

unsigned long delay = jiffies + 2*HZ;        /* 2秒 */
while(time_before(jiffies, delay))
    ;

unsgined long delay = jiffies + 5*HZ;
while(time_before(jiffies, delay))
    condresched();
忙循環例子

cond_resched()函數將調度一個新程序投入運行,但它只有在設置完need_resched標誌後才能生效。

另外,延遲執行無論在那種狀況下,都不該該在持有鎖時或禁止中斷時發生。

jiffies變量在<linux/jiffies.h>中被標記爲關鍵字volatile,這樣每次都會被從內存中讀入。

8.2 短延遲

有時內核代碼不但須要很短暫的延遲,並且還要求延時的時間很精確。

內核提供了三個能夠處理ms、ns和us級別的延時函數,定義在文件<linux/delay.h>和<asm/delay.h>中

void udelay(unsgined long usecs)
void ndelay(unsigned long nsecs)
void mdelay(unsigned long msecs)
udelay,ndelay,mdelay

通產超過1ms的範圍不適用udelay()進行延遲,對於較長的延遲,mdelay()工做良好。

8.3 schedule_timeout()

更理想的方法是使用schedule_timeout()函數,該方法會讓須要延遲執行的任務睡眠到指定的延時時間耗盡後在從新運行。

可是睡眠時間不能保證指定的延時事件按,只能儘可能接近指定時間。用法以下:

/* 將任務設置爲可中斷睡眠狀態 */
set_current_state(TASK_INTERRUPTIBLE);
/* 小睡一會,"s"秒後喚醒 */
schedule_timeout(s*HZ);
schedule_timeout例子

在調用shcedule_timeout時,任務必須設置狀態爲TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE。

signed long schedule_timeout(signed long timeout)
{
    time_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));
            current->state = TASK_RUNNING;
            goto out;
        }
    }
    
    expire = timeout + jiffies;

    init_timer(&timer);
    timer.expires = expires;
    timer.data = (unsigned long)current;
    timer.function = process_timeout;

    add_timer(&timer);
    schedule();
    del_timer_sync(&timer);

    timeout = expire - jiffies;

out:
    return timeout < 0 ? 0 : timeout;
}
schedule_timeout()實現

該函數用原始的名字timer建立了一個定時器timer,而後設置它的超時時間timeout,設置超時執行函數process_timeout();接着激活定時器並且調用schedule()。

當定時器超時時,process_timeout()函數會被調用:

void process_timeout(unsigned long data)
{
    wake_up_process((taks_t *)data);
}
process_timeout

該函數將任務設置爲TASK_RUNNING狀態,而後將其放入運行隊列。

相關文章
相關標籤/搜索