Linux內核設計與實現 學習筆記(1)定時器和時間管理

系統中有很多與時間相關的程序(比如定期執行的任務,某一時間執行的任務,推遲一段時間執行的任務),因此,時間的管理對於linux來說非常重要。

 

主要內容:

  • 系統時間
  • 定時器
  • 定時器相關概念
  • 定時器執行流程
  • 實現程序延遲的方法
  • 定時器和延遲的例子

 

1. 系統時間

系統中管理的時間有2種:實際時間和定時器。

1.1  實際時間

實際時間就是現實中鐘錶上顯示的時間,其實內核中並不常用這個時間,主要是用戶空間的程序有時需要獲取當前時間,

所以內核中也管理着這個時間。

 

實際時間的獲取是在開機後,內核初始化時從RTC讀取的。

內核讀取這個時間後就將其放入內核中的 xtime 變量中,並且在系統的運行中不斷更新這個值。

注:RTC就是實時時鐘的縮寫,它是用來存放系統時間的設備。一般和BIOS一樣,由主板上的電池供電的,所以即使關機也可將時間保存。

 

實際時間存放的變量 xtime 在文件 kernel/time/timekeeping.c中。

複製代碼
/* 按照16位對齊,其實就是2個long型的數據 */
struct timespec xtime __attribute__ ((aligned (16)));

/* timespec結構體的定義如下, 參考 <linux/time.h>  */
struct timespec {
    __kernel_time_t    tv_sec;            /* seconds */
    long        tv_nsec;        /* nanoseconds */
};

/* _kernel_time_t 定義如下 */
typedef long        __kernel_time_t;
複製代碼

 

系統讀寫 xtime 時用的就是順序鎖。

複製代碼
/* 寫入 xtime 參考 do_sometimeofday 方法 */
int do_settimeofday(struct timespec *tv)
{
/* 省略 。。。。 */
    write_seqlock_irqsave(&xtime_lock, flags); /* 獲取寫鎖 */
/* 更新 xtime */
    write_sequnlock_irqrestore(&xtime_lock, flags); /* 釋放寫鎖 */
/* 省略 。。。。 */
    return 0;
}

/* 讀取 xtime 參考 do_gettimeofday 方法 */
void do_gettimeofday(struct timeval *tv)
{
    struct timespec now;

    getnstimeofday(&now); /* 就是在這個方法中獲取讀鎖,並讀取 xtime */
    tv->tv_sec = now.tv_sec;
    tv->tv_usec = now.tv_nsec/1000;
}

void getnstimeofday(struct timespec *ts)
{
/* 省略 。。。。 */

/* 順序鎖中讀鎖來循環獲取 xtime,直至讀取過程中 xtime 沒有被改變過 */
    do {
        seq = read_seqbegin(&xtime_lock);

        *ts = xtime;
        nsecs = timekeeping_get_ns();

        /* If arch requires, add in gettimeoffset() */
        nsecs += arch_gettimeoffset();

    } while (read_seqretry(&xtime_lock, seq));
/* 省略 。。。。 */
}
複製代碼

上述場景中,寫鎖必須要優先於讀鎖(因爲 xtime 必須及時更新),而且寫鎖的使用者很少(一般只有系統定期更新xtime的線程需要持有這個鎖)。

這正是 順序鎖的應用場景。

 

1.2 定時器

定時器是內核中主要使用的時間管理方法,通過定時器,可以有效的調度程序的執行。

動態定時器是內核中使用比較多的定時器,下面重點討論的也是動態定時器。

 

2. 定時器

內核中的定時器有2種,靜態定時器和動態定時器。

靜態定時器一般執行了一些週期性的固定工作:

  • 更新系統運行時間
  • 更新實際時間
  • 在SMP系統上,平衡各個處理器上的運行隊列
  • 檢查當前進程是否用盡了自己的時間片,如果用盡,需要重新調度。
  • 更新資源消耗和處理器時間統計值

 

動態定時器顧名思義,是在需要時(一般是推遲程序執行)動態創建的定時器,使用後銷燬(一般都是隻用一次)。

一般我們在內核代碼中使用的定時器基本都是動態定時器,下面重點討論動態定時器相關的概念和使用方法。

 

3. 定時器相關概念

定時器的使用中,下面3個概念非常重要:

  1. HZ
  2. jiffies
  3. 時間中斷處理程序

 

3.1 HZ

節拍率(HZ)是時鐘中斷的頻率,表示的一秒內時鐘中斷的次數。

比如 HZ=100 表示一秒內觸發100次時鐘中斷程序。

 

HZ的值一般與體系結構有關,x86 體系結構一般定義爲 100,參考文件 include/asm-generic/param.h

HZ值的大小的設置過程其實就是平衡 精度和性能 的過程,並不是HZ值越高越好。

HZ值

優勢

劣勢

高HZ 時鐘中斷程序運行的更加頻繁,依賴時間執行的程序更加精確, 
對資源消耗和系統運行時間的統計更加精確。
時鐘中斷執行的頻繁,增加系統負擔 
時鐘中斷佔用的CPU時間過多

 

此外,有一點需要注意,內核中使用的HZ可能和用戶空間中定義的HZ值不一致,爲了避免用戶空間取得錯誤的時間,

內核中也定義了 USER_HZ,即用戶空間使用的HZ值。

一般來說,USER_HZ 和 HZ 都是相差整數倍,內核中通過函數 jiffies_to_clock_t 來將內核來將內核中的 jiffies轉爲 用戶空間 jiffies

複製代碼
/* 參見文件: kernel/time.c  *
//*
 * Convert jiffies/jiffies_64 to clock_t and back.
 */
clock_t jiffies_to_clock_t(unsigned long x)
{
#if (TICK_NSEC % (NSEC_PER_SEC / USER_HZ)) == 0
# if HZ < USER_HZ
    return x * (USER_HZ / HZ);
# else
    return x / (HZ / USER_HZ);
# endif
#else
    return div_u64((u64)x * TICK_NSEC, NSEC_PER_SEC / USER_HZ);
#endif
}
EXPORT_SYMBOL(jiffies_to_clock_t);
複製代碼

 

3.2 jiffies

jiffies用來記錄自系統啓動以來產生的總節拍數。比如系統啓動了 N 秒,那麼 jiffies就爲 N×HZ

jiffies的相關定義參考頭文件 <linux/jiffies.h>  include/linux/jiffies.h

/* 64bit和32bit的jiffies定義如下 */
extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;

 

使用定時器時一般都是以jiffies爲單位來延遲程序執行的,比如延遲5個節拍後執行的話,執行時間就是 jiffies+5

32位的jiffies的最大值爲 2^32-1,在使用時有可能會出現迴繞的問題。

比如下面的代碼:

複製代碼
unsigned long timeout = jiffies + HZ/2; /* 設置超時時間爲 0.5秒 */

while (timeout < jiffies)
{
    /* 還沒有超時,繼續執行任務 */
}

/* 執行超時後的任務 */
複製代碼

正常情況下,上面的代碼沒有問題。當jiffies接近最大值的時候,就會出現迴繞問題。

由於是unsinged long類型,所以jiffies達到最大值後會變成0然後再逐漸變大,如下圖所示:

unsigned_jiffies

 

所以在上述的循環代碼中,會出現如下情況:

jiffies_rewind

  1. 循環中第一次比較時,jiffies = J1,沒有超時
  2. 循環中第二次比較時,jiffies = J2,實際已經超時了,但是由於jiffies超過的最大值後又從0開始,所以J2遠遠小於timeout
  3. while循環會執行很長時間(> 2^32-1 個節拍)不會結束,幾乎相當於死循環了

 

爲了迴避回擾的問題,可以使用<linux/jiffies.h>頭文件中提供的 time_aftertime_before等宏

複製代碼
#define time_after(a,b)        \
    (typecheck(unsigned long, a) && \
     typecheck(unsigned long, b) && \
     ((long)(b) - (long)(a) < 0))
#define time_before(a,b)    time_after(b,a)

#define time_after_eq(a,b)    \
    (typecheck(unsigned long, a) && \
     typecheck(unsigned long, b) && \
     ((long)(a) - (long)(b) >= 0))
#define time_before_eq(a,b)    time_after_eq(b,a)
複製代碼

上述代碼的原理其實就是將 unsigned long 類型轉換爲 long 類型來避免回擾帶來的錯誤,

long 類型超過最大值時變化趨勢如下:

signed_jiffies

 

long 型的數據的迴繞會出現在 2^31-1 變爲 -2^32 的時候,如下圖所示:

long_rewind

  1. 第一次比較時,jiffies = J1,沒有超時
  2. 第二次比較時,jiffies = J2,一般 J2 是負數 
    理論上 (long)timeout - (long)J2 = 正數 - 負數 = 正數(result) 
    但是,這個正數(result)一般會大於 2^31 - 1,所以long型的result又發生了一次迴繞,變成了負數。 
    除非timeout和J2之間的間隔 > 2^32 個節拍,result的值纔會爲正數(注1)。

注1:result的值爲正數時,必須是在result的值 小於 2^31-1 的情況下,大於 2^31-1 會發生迴繞。

long_result

上圖中 X + Y 表示timeout 和 J2之間經過的節拍數。

result 小於 2^31-1 ,也就是 timeout - J2 < 2^31 – 1

timeout 和 -J2 表示的節拍數如上圖所示。(因爲J2是負數,所有-J2表示上圖所示範圍的值)

因爲 timeout + X + Y - J2 = 2^31-1 + 2^32

所以 timeout - J2 < 2^31 - 1 時, X + Y > 2^32

也就是說,當timeout和J2之間經過至少 2^32 個節拍後,result纔可能變爲正數。

timeout和J2之間相差這麼多節拍是不可能的(不信可以用HZ將這些節拍換算成秒就知道了。。。)

 

利用time_after宏就可以巧妙的避免迴繞帶來的超時判斷問題,將之前的代碼改成如下代碼即可:

複製代碼
unsigned long timeout = jiffies + HZ/2; /* 設置超時時間爲 0.5秒 */

while (time_after(jiffies, timeout))
{
    /* 還沒有超時,繼續執行任務 */
}

/* 執行超時後的任務 */
複製代碼

 

3.3 時鐘中斷處理程序

時鐘中斷處理程序作爲系統定時器而註冊到內核中,體系結構的不同,可能時鐘中斷處理程序中處理的內容不同。

但是以下這些基本的工作都會執行:

  • 獲得 xtime_lock 鎖,以便對訪問 jiffies_64 和牆上時間 xtime 進行保護
  • 需要時應答或重新設置系統時鐘
  • 週期性的使用牆上時間更新實時時鐘
  • 調用 tick_periodic()

 

tick_periodic函數位於: kernel/time/tick-common.c 中

複製代碼
static void tick_periodic(int cpu)
{
    if (tick_do_timer_cpu == cpu) {
        write_seqlock(&xtime_lock);

        /* Keep track of the next tick event */
        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);
}
複製代碼

其中最重要的是 do_timer 和 update_process_times 函數。

我瞭解的步驟進行了簡單的註釋。

複製代碼
void do_timer(unsigned long ticks)
{
    /* jiffies_64 增加指定ticks */
    jiffies_64 += ticks;
    /* 更新實際時間 */
    update_wall_time();
    /* 更新系統的平均負載值 */
    calc_global_load();
}

void update_process_times(int user_tick)
{
    struct task_struct *p = current;
    int cpu = smp_processor_id();

    /* 更新當前進程佔用CPU的時間 */
    account_process_tick(p, user_tick);
    /* 同時觸發軟中斷,處理所有到期的定時器 */
    run_local_timers();
    rcu_check_callbacks(cpu, user_tick);
    printk_tick();
    /* 減少當前進程的時間片數 */
    scheduler_tick();
    run_posix_cpu_timers(p);
}
複製代碼

 

4. 定時器執行流程

這裏討論的定時器執行流程是動態定時器的執行流程。

 

4.1 定時器的定義

定時器在內核中用一個鏈表來保存的,鏈表的每個節點都是一個定時器。

參見頭文件 <linux/timer.h>

複製代碼
struct timer_list {
    struct list_head entry;
    unsigned long expires;

    void (*function)(unsigned long);
    unsigned long data;

    struct tvec_base *base;
#ifdef CONFIG_TIMER_STATS
    void *start_site;
    char start_comm[16];
    int start_pid;
#endif
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};
複製代碼

通過加入條件編譯的參數,可以追加一些調試信息。

 

4.2 定時器的生命週期

一個動態定時器的生命週期中,一般會經過下面的幾個步驟:

timer_life

1. 初始化定時器:

struct timer_list my_timer; /* 定義定時器 */
init_timer(&my_timer);      /* 初始化定時器 */

 

2. 填充定時器:

my_timer.expires = jiffies + delay; /* 定義超時的節拍數 */
my_timer.data = 0;                  /* 給定時器函數傳入的參數 */
my_timer.function = my_function;    /* 定時器超時時,執行的自定義函數 */

/* 從定時器結構體中,我們可以看出這個函數的原型應該如下所示: */
void my_function(unsigned long data);

 

3. 激活定時器和修改定時器:

激活定時器之後纔會被觸發,否則定時器不會執行。

修改定時器主要是修改定時器的延遲時間,修改定時器後,不管原先定時器有沒有被激活,都會處於激活狀態。

 

填充定時器結構之後,可以只激活定時器,也可以只修改定時器,也可以激活定時器後再修改定時器。

所以填充定時器結構和觸發定時器之間的步驟,也就是虛線框中的步驟是不確定的。

add_timer(&my_timer);  /* 激活定時器 */
mod_timer(&my_timer, jiffies + new_delay);  /* 修改定時器,設置新的延遲時間 */

 

4. 觸發定時器:

每次時鐘中斷處理程序會檢查已經激活的定時器是否超時,如果超時就執行定時器結構中的自定義函數。

 

5. 刪除定時器:

激活和未被激活的定時器都可以被刪除,已經超時的定時器會自動刪除,不用特意去刪除。

/*
 * 刪除激活的定時器時,此函數返回1
 * 刪除未激活的定時器時,此函數返回0
 */
del_timer(&my_timer);

在多核處理器上用 del_timer 函數刪除定時器時,可能在刪除時正好另一個CPU核上的時鐘中斷處理程序正在執行這個定時器,於是就形成了競爭條件。

爲了避免競爭條件,建議使用 del_timer_sync 函數來刪除定時器。

del_timer_sync 函數會等待其他處理器上的定時器處理程序全部結束後,才刪除指定的定時器。

/*
 * 和del_timer 不同,del_timer_sync 不能在中斷上下文中執行
 */
del_timer_sync(&my_timer);

 

5. 實現程序延遲的方法

內核中有個利用定時器實現延遲的函數 schedule_timeout

這個函數會將當前的任務睡眠到指定時間後喚醒,所以等待時不會佔用CPU時間。

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

/* 小睡一會兒,“s“秒後喚醒 */
schedule_timeout(s*HZ);

 

查看 schedule_timeout 函數的實現方法,可以看出是如何使用定時器的。

複製代碼
signed long __sched schedule_timeout(signed long timeout)
{
    /* 定義一個定時器 */
    struct timer_list timer;
    unsigned long expire;

    switch (timeout)
    {
    case MAX_SCHEDULE_TIMEOUT:
        /*
         * These two special cases are useful to be comfortable
         * in the caller. Nothing more. We could take
         * MAX_SCHEDULE_TIMEOUT from one of the negative value
         * but I' d like to return a valid offset (>=0) to allow
         * the caller to do everything it want with the retval.
         */
        schedule();
        goto out;
    default:
        /*
         * Another bit of PARANOID. Note that the retval will be
         * 0 since no piece of kernel is supposed to do a check
         * for a negative retval of schedule_timeout() (since it
         * should never happens anyway). You just have the printk()
         * that will tell you if something is gone wrong and where.
         */
        if (timeout < 0) {
            printk(KERN_ERR "schedule_timeout: wrong timeout "
                "value %lx\n", timeout);
            dump_stack();
            current->state = TASK_RUNNING;
            goto out;
        }
    }

    /* 設置超時時間 */
    expire = timeout + jiffies;

    /* 初始化定時器,超時處理函數是 process_timeout,後面再補充說明一下這個函數 */
    setup_timer_on_stack(&timer, process_timeout, (unsigned long)current);
    /* 修改定時器,同時會激活定時器 */
    __mod_timer(&timer, expire, false, TIMER_NOT_PINNED);
    /* 將本任務睡眠,調度其他任務 */
    schedule();
    /* 刪除定時器,其實就是 del_timer_sync 的宏
    del_singleshot_timer_sync(&timer);

    /* Remove the timer from the object tracker */
    destroy_timer_on_stack(&timer);

    timeout = expire - jiffies;

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

/* 
 * 超時處理函數 process_timeout 裏面只有一步操作,喚醒當前任務。
 * process_timeout 的參數其實就是 當前任務的地址
 */
static void process_timeout(unsigned long __data)
{
    wake_up_process((struct task_struct *)__data);
}
複製代碼

schedule_timeout 一般用於延遲時間較長的程序。

這裏的延遲時間較長是對於計算機而言的,其實也就是延遲大於 1 個節拍(jiffies)。

 

對於某些極其短暫的延遲,比如只有1ms,甚至1us,1ns的延遲,必須使用特殊的延遲方法。

1s = 1000ms = 1000000us = 1000000000ns (1秒=1000毫秒=1000000微秒=1000000000納秒)

假設 HZ=100,那麼 1個節拍的時間間隔是 1/100秒,大概10ms左右。

所以對於那些極其短暫的延遲,schedule_timeout 函數是無法使用的。

好在內核對於這些短暫,精確的延遲要求也提供了相應的宏。

/* 具體實現參見 include/linux/delay.h
 * 以及 arch/x86/include/asm/delay.h
 */
#define mdelay(n) ...
#define udelay(n) ...
#define ndelay(n) ...

通過這些宏,可以簡單的實現延遲,比如延遲 5ns,只需 ndelay(5); 即可。

 

這些短延遲的實現原理並不複雜,

首先,內核在啓動時就計算出了當前處理器1秒能執行多少次循環,即 loops_per_jiffy

(loops_per_jiffy 的計算方法參見 init/main.c 文件中的 calibrate_delay 方法)。

然後算出延遲 5ns 需要循環多少次,執行那麼多次空循環即可達到延遲的效果。

 

loops_per_jiffy 的值可以在啓動信息中看到:

[root@vbox ~]# dmesg | grep delay
Calibrating delay loop (skipped), value calculated using timer frequency.. 6387.58 BogoMIPS (lpj=3193792)

我的虛擬機中看到 (lpj=3193792)

 

6. 定時器和延遲的例子

下面的例子測試了短延遲,自定義定時器以及 schedule_timeout 的使用:

複製代碼
#include <linux/sched.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
#include <asm/param.h>
#include <linux/delay.h>
#include "kn_common.h"

MODULE_LICENSE("Dual BSD/GPL");

static void test_short_delay(void);
static void test_delay(void);
static void test_schedule_timeout(void);
static void my_delay_function(unsigned long);

static int testdelay_init(void)
{
    printk(KERN_ALERT "HZ in current system: %dHz\n", HZ);

    /* test short delay */
    test_short_delay();

    /* test delay */
    test_delay();

    /* test schedule timeout */
    test_schedule_timeout();

    return 0;
}

static void testdelay_exit(void)
{
    printk(KERN_ALERT "*************************\n");
    print_current_time(0);
    printk(KERN_ALERT "testdelay is exited!\n");
    printk(KERN_ALERT "*************************\n");
}

static void test_short_delay()
{
    printk(KERN_ALERT "jiffies [b e f o r e] short delay: %lu", jiffies);
    ndelay(5);
    printk(KERN_ALERT "jiffies [a f t e r] short delay: %lu", jiffies);
}

static void test_delay()
{
    /* 初始化定時器 */
    struct timer_list my_timer;
    init_timer(&my_timer);

    /* 填充定時器 */
    my_timer.expires = jiffies + 1*HZ; /* 2秒後超時函數執行 */
    my_timer.data = jiffies;
    my_timer.function = my_delay_function;

    /* 激活定時器 */
    add_timer(&my_timer);
}

static void my_delay_function(unsigned long data)
{
    printk(KERN_ALERT "This is my delay function start......\n");
    printk(KERN_ALERT "The jiffies when init timer: %lu\n", data);
    printk(KERN_ALERT "The jiffies when timer is running: %lu\n", jiffies);
    printk(KERN_ALERT "This is my delay function end........\n");
}

static void test_schedule_timeout()
{
    printk(KERN_ALERT "This sample start at : %lu", jiffies);

    /* 睡眠2秒 */
    set_current_state(TASK_INTERRUPTIBLE);
    printk(KERN_ALERT "sleep 2s ....\n");
    schedule_timeout(2*HZ);

    printk(KERN_ALERT "This sample end at : %lu", jiffies);
}

module_init(testdelay_init);
module_exit(testdelay_exit);
複製代碼

其中用到的 kn_common.h 和 kn_common.c 參見之前的博客 《Linux內核設計與實現》讀書筆記(六)- 內核數據結構

Makefile如下:

複製代碼
# must complile on customize kernel
obj-m += mydelay.o
mydelay-objs := testdelay.o kn_common.o

#generate the path
CURRENT_PATH:=$(shell pwd)
#the current kernel version number
LINUX_KERNEL:=$(shell uname -r)
#the absolute path
LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)
#complie object
all:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
#clean
clean:
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned
複製代碼

 

執行測試命令及查看結果的方法如下:(我的測試系統是 CentOS 6.3 x64)

複製代碼
[root@vbox chap11]# make
[root@vbox chap11]# insmod mydelay.ko 
[root@vbox chap11]# rmmod mydelay.ko 
[root@vbox chap11]# dmesg | tail -14
HZ in current system: 1000Hz
jiffies [b e f o r e] short delay: 4296079617
jiffies [a f t e r] short delay: 4296079617
This sample start at : 4296079619
sleep 2s ....
This is my delay function start......
The jiffies when init timer: 4296079619
The jiffies when timer is running: 4296080621
This is my delay function end........
This sample end at : 4296081622
*************************
2013-5-9 23:7:20
testdelay is exited!
*************************
複製代碼

 

結果說明:

1. 短延遲只延遲了 5ns,所以執行前後的jiffies是一樣的。

jiffies [b e f o r e] short delay: 4296079617
jiffies [a f t e r] short delay: 4296079617

 

2. 自定義定時器延遲了1秒後執行自定義函數,由於我的系統 HZ=1000,所以jiffies應該相差1000

The jiffies when init timer: 4296079619
The jiffies when timer is running: 4296080621

實際上jiffies相差了 1002,多了2個節拍

 

3. schedule_timeout 延遲了2秒,jiffies應該相差 2000

This sample start at : 4296079619
This sample end at : 4296081622

實際上jiffies相差了 2003,多了3個節拍

 

以上結果也說明了定時器的延遲並不是那麼精確,差了2,3個節拍其實就是誤差2,3毫秒(因爲HZ=1000)

如果HZ=100的話,一個節拍是10毫秒,那麼定時器的誤差可能就發現不了了(誤差只有2,3毫秒,沒有超多1個節拍)。

發佈了22 篇原創文章 · 獲贊 0 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章