Linux內核設計與實現——讀書筆記(8)時間管理

2020-08-13 15:53:22

  內核中有大量的函數都是基於時間驅動的。例如對排程程式中的執行佇列進行平衡調整或對螢幕進行重新整理等。除此之外,內核還必須管理系統的執行時間以及當前如期和時間。

1、一些概念

1.1、節拍率:HZ

  系統定時器的節拍率是通過宏HZ靜態定義的,在 <asm/param.h> 中。

1.1.1、高HZ的優勢

  高HZ的同時會有高頻率的時鐘中斷會帶來如下優點:

  • 內核定時器能夠以更高的頻度和更高的準確度執行;
  • 依賴定時值指定的系統呼叫,比如poll()和select(),能夠以更高的精度執行;
  • 對資源消耗和系統執行時間等的測量會有更精細的解析度;
  • 提高進程搶佔的準確度

1.1.2、高HZ的劣勢

  • 時鐘中斷頻率越高,意味着系統負擔越重,中斷處理程式佔用的處理器時間越多;
  • 減少了處理器處理其他工作的時間;
  • 更頻繁地打亂處理器快取記憶體並增加耗電。

1.2、jiffies

  內核用全域性變數jiffies來記錄子系統啓動以來產生的節拍總數。系統啓動時,jiffies變數被初始化爲0,每次時鐘中斷處理器程式會對此變數加1。
  32位元系統使用jiffies,64位元系統使用jiffies_64。
  爲了方便開發, Linux 內核提供了幾個 jiffies 和 ms、 us、 ns 之間的轉換函數:

/* 將jiffies型別的參數j轉換爲對應的毫秒、微秒、納秒 */
int jiffies_to_msecs(const unsigned long j)
int jiffies_to_usecs(const unsigned long j)
u64 jiffies_to_nsecs(const unsigned long j)
/* 將毫秒、微秒、納秒轉換爲jiffies型別 */
long msecs_to_jiffies(const unsigned int m)
long usecs_to_jiffies(const unsigned int u)
unsigned long nsecs_to_jiffies(u64 n)

1.3.、實時時鐘RTC

  當系統啓動時,內核會讀取RTC來初始化實際時間(牆上時間),該時間存放在xtime變數中。

2、時鐘中斷處理程式

  時鐘中斷處理程式可以劃分爲兩個部分:體系結構相關部分體系結構無關部分。與體系結構相關的例程作爲系統定時器的中斷處理程式而註冊到內核中。雖然時鐘中斷處理程式的具體工作依賴於特定的體系結構,但是絕大多數處理程式最低限度也要執行以下工作

  • 獲得xtime_lock鎖,以便對存取jiffies_64和牆上時間xtime進行保護;
  • 需要時應答或重新設定系統時鐘;
  • 週期性地使用牆上時間更新實時時鐘;
  • 呼叫體系結構無關的時鐘例程:tick_periodic()

  tick_periodic() 執行了更多的工作:

  • 給jiffies_64變數加1;
  • 更新資源消耗的統計值,比如當前進程所消耗的系統時間和使用者時間;
  • 執行已經到期的動態定時器;
  • 執行scheduler_tick() 函數,當某個進程需要被搶佔時,scheduler_tick() 會設定need_resched 標誌;
  • 更新牆上時間,存放在xtime變數中;
  • 計算平均負載值。

  tick_periodic() 的程式碼如下:

/*
 * Called by the timer interrupt. xtime_lock must already be taken
 * by the timer IRQ!
 */
static inline void update_times(unsigned long ticks)                                                                               
{                 
    update_wall_time();		//更新牆上時間
    calc_load(ticks);		//更新系統的平均負載統計值
}   
    
/*
 * The 64-bit jiffies value is not atomic - you MUST NOT read it
 * without sampling the sequence number in xtime_lock.
 * jiffies is defined in the linker script...
 */ 
        
void do_timer(unsigned long ticks)
{
    jiffies_64 += ticks;		//增加jiffies_64計數
    update_times(ticks);
}


/*
 * Periodic tick
 */
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() 更新所耗費的各種節拍數。

/*
 * Called from the timer interrupt handler to charge one tick to the current
 * process.  user_tick is 1 if the tick is user time, 0 for system.
 */
void update_process_times(int user_tick)
{                                                                                                                                                                                       
    struct task_struct *p = current;
    int cpu = smp_processor_id();

    /* Note: this timer irq context must be accounted for as well. */
    account_process_tick(p, user_tick);
    run_local_timers();		//標記一個軟中斷去處理所有到期的定時器
    if (rcu_pending(cpu))
        rcu_check_callbacks(cpu, user_tick);
    scheduler_tick();		
    run_posix_cpu_timers(p);
}

  update_process_times()中通過user_tick區別花費在使用者空間還是內核空間。在 tick_periodic() 函數中,user_tick時通過讀取系統暫存器的值來設定的。
  account_process_tick() 函數對進程的時間進行了實質性的更新:

void account_process_tick(struct task_struct *p, int user_tick)
{
    if (user_tick) {
        account_user_time(p, jiffies_to_cputime(1));
        account_user_time_scaled(p, jiffies_to_cputime(1));
    } else {
        account_system_time(p, HARDIRQ_OFFSET, jiffies_to_cputime(1));
        account_system_time_scaled(p, jiffies_to_cputime(1));
    }    
}

  run_local_timers() 標記一個軟中斷去處理所有到期的定時器。
  scheduler_tick() 函數負責減少當前執行進程的時間片計數值,並且在需要時設定need_resched標誌。在SMP機器上,還負責平衡每個處理器的執行佇列。
  tick_periodic() 函數執行完後返回與體系結構相關的中斷處理程式,進行完後面的工作後釋放xtime_lock鎖,然後退出。
  以上全部工作每1/HZ秒要發生一次。

3、實際時間(牆上時間)

  上面提到的xtime變數就是用來表示實際時間,這個時間用struct timespec結構體表示,定義在kernel/time/timekeeping.c檔案中:

struct timespec {                                                                                                                  
        long       ts_sec;
        long       ts_nsec;
};

struct timespec xtime __attribute__ ((aligned (16)));

  __attribute__不屬於標準C語言,它是GCC對C語言的一個擴充套件用法,此關鍵字後面是跟着雙括號括起來的屬性說明。
  __attribute__((aligned(n))):此屬性指定了指定型別的變數的最小對齊(以位元組爲單位)。如果結構中有成員的長度大於n,則按照最大成員的長度來對齊。
  注意:對齊屬性的有效性會受到鏈接器(linker)固有限制的限制,即如果你的鏈接器僅僅支援8位元組對齊,即使你指定16位元組對齊,那麼它也僅僅提供8位元組對齊。
  __attribute__((packed)):此屬性取消在編譯過程中的優化對齊。
  xtime.ts_sec以秒爲單位,存放着自1970年1月1日以來經過的時間。
  xtime.ts_nsec記錄上一秒開始進過的ns數。
  更新xtime需要先申請一個seqlock——xtime_lock

write_seqlock(&xtime_lock);
/* 更新xtime,其他操作 */
write_sequnlock(&xtime_lock);

  讀取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));

  使用者空間取得牆上時間的主要介面是gettimeofday() 函數,在內核中對對應的系統呼叫爲sys_gettimeofday(),定義於kernel/time.c

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

  如果tv參數非空,do_gettimeofday() 函數會被呼叫,該函數執行回圈讀取xtime操作。如果tz參數爲空,將系統時區sys_tz返回給使用者。
  雖然內核也實現了time() 系統呼叫,但是gettimeofday() 幾乎完全取代。C庫也提供了一些牆上時間相關的庫呼叫,比如ftime()ctime()
  系統呼叫settimeofday() 用來設定當前時間,具有CAP_SYS_TIME 權能(允許改變系統時鐘)。
  內核不會像使用者空間程式那樣頻繁使用xtime,除了檔案系統的實現程式碼中存放存取時間戳(建立、存取、修改等)時需要使用xtime。

4、定時器

  內核在時鐘中斷髮生後執行定時器,定時器作爲軟中斷在下半部上下文中執行。在第二小節有講到,時鐘中斷處理程式會執行update_process_times() 函數,該函數隨後呼叫run_local_timers() 函數:

/*
 * Called by the local, per-CPU timer interrupt on SMP.
 */
void run_local_timers(void)
{
    raise_softirq(TIMER_SOFTIRQ);	//執行軟中斷
    softlockup_tick();
} 

  raise_softirq() 函數處理TIMER_SOFTIRQ 型別軟中斷,從而在當前處理器上執行所有超時的定時器。
  雖然所有定時器都以鏈表的形式存放在一起,爲了提高搜尋效率,內核將定時器按它們的超時時間劃分爲五組,當定時器超時時間接近時,定時器隨組一起下移。

4.1、使用定時器

  定時器結構由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;		//定時器內部值,使用者不要使用
};

  內核提供了一組與定時器相關的介面來管理定時器操作,這些結構也宣告在標頭檔案 <linux/timer.h> 中,定義在 kernel/timer.c中:

/* 定義和初始化一個定時器 */
struct timer_list my_timer;
init_timer(&my_timer);
my_timer.expires = jiffies + delay;//delay表示要延時的時間,可以用1.2小節提到函數將延時時間轉化爲jiffies值
my_timer.data = 0;
my_timer.function = my_function;//處理常式

/* 註冊並啓用定時器 */
add_timer(&my_timer);

/* 修改並啓用定時器 */
mod_timer(&my_timer,jiffies+new_delay);	//被刪除的定時器還未啓用,返回0,否則返回1

/* 刪除定時器 */
del_timer(&my_timer);		//被刪除的定時器還未啓用,返回0,否則返回1
del_timer_sync(&my_timer);

  有一種情況要注意,如果在SMP機器上,定時器中斷可能已經在其他處理器上執行,刪除定時器時需要等待可能在其他處理器上執行的定時器處理程式都退出,這時要使用 del_timer_sync() 函數執行刪除操作,del_timer_sync() 函數不能用於中斷上下文。
  爲了防止定時器競爭的出現,刪除時優先使用del_timer_sync() 函數。
  不能使用以下操作來代替mod_timer() 函數:

del_timer(&my_timer);
my_timer.expires = jiffies + new_delay;
add_timer(&my_timer);

  這樣的程式碼在SMP機器上是不安全的。

5、延遲執行

5.1、忙等待

  雖然簡單,但是精確率不高,延時時間是節拍的整數倍。
  簡單忙回圈,會阻塞:

/* 5秒延時 */
unsigned long timeout = jiffies+5*HZ;
while(time_before(jiffies,timeout));

  簡單忙回圈,不阻塞:

/* 5秒延時 */
unsigned long timeout = jiffies+5*HZ;
while(time_before(jiffies,timeout))
	cond_resched();		//如果need_resched標誌被設定,會發生排程

  cond_resched()如果need_resched標誌被設定,會發生排程,所以也不能在中斷上下文使用。

5.1、短延遲

  短延時,精確。
  內核提供了三個ms、ns和us級別的延時函數,他們定義在檔案 <linux/delay.h><asm/delay.h> 檔案中:

void ndelay(unsigned long nsecs);	//納秒
void udelay(unsigned long usecs);	//微秒
void mdelay(unsigned long msecs);	//毫秒