進程調度之8:nanosleep與內核定時器

date: 2014-11-08 14:16數組

某些狀況下,運行中的進程須要主動進入睡眠狀態,這裏「睡眠」的原語是:當前進程的狀態變成TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE,並從可執行隊列中脫鉤,調度的結果是其餘進程投入運行。而且進程一旦進入睡眠狀態,就須要通過喚醒才能恢復成TASK_RUNNING,並回到可執行隊列中。app

系統調用nanosleep()是當前進程進入睡眠狀態,在指定的時間之後內核將該進程喚醒,其底層實現是內核定時器。函數

nanaosleep()的原型是:oop

int nanosleep(struct timespec *rqtp, struct timespec *rmtp);

結構體timespec的定義以下:this

struct timespec {
	    time_t tv_sec;		/* seconds */
	    long	tv_nsec;	   /* nanoseconds */
    };

結構包含兩個成員,tv_sec表示秒,time_t其實爲long型;tv_nsec表示納秒,但這並不表示睡眠的精度能夠到達納秒級,在典型的內核配置中時鐘中斷頻率HZ通常配置爲100,也就是說每一個時鐘週期爲10ms,這就意味着若是進程進入睡眠而循正常途徑由時鐘中斷服務程序來喚醒的話,只能達到10ms的精度。code

nanosleep()函數的第一個參數rqtp表示指望睡眠的時間;若是進程提早被喚醒,第二個參數rmtp返回剩餘的睡眠時間。blog

1 nanosleep()的流程

1.1 主要流程以下:

nanosleep流程

1.2 udelay

udelay的語義是延遲多少微秒。其調用鏈以下:sys_nanosleep(timer.c)-->udelay(delay.h)-->__udelay(delay.c)-->__const_udelay(delay.c)。咱們來看看__const_udelay的源碼:排序

static void __loop_delay(unsigned long loops)
    {
    	int d0;
    	__asm__ __volatile__(
    		"\tjmp 1f\n"
    		".align 16\n"
    		"1:\tjmp 2f\n"
    		".align 16\n"
    		"2:\tdecl %0\n\tjns 2b"
    		:"=&a" (d0)
    		:"0" (loops));
    }
    
    void __delay(unsigned long loops)
    {
    	if(x86_udelay_tsc)
    		__rdtsc_delay(loops);
    	else
    		__loop_delay(loops);
    }
    
    inline void __const_udelay(unsigned long xloops)
    {
    	int d0;
    	__asm__("mull %0"
    		:"=d" (xloops), "=&a" (d0)
    		:"1" (xloops),"0" (current_cpu_data.loops_per_jiffy));
            __delay(xloops * HZ);
    }

current_cpu_data.loops_per_jiffy的數值在系統初始化時,由內核根據採集到的數據來肯定。隊列

若是CUP支持硬件延遲,則調用__rdtsc_delay(),不然調用__loop_delay()進行軟件的延遲。用三步法分析__loop_delay()的源碼以下:進程

;寄存器約束
    ;   loops --> eax
    
    ;彙編代碼
        jmp 1f
    1: jmp 2f
    2: dec eax ;循環次數遞減
        jns 2b

可見,__loop_delay()經過循環來殺死時間,這的確不是一個好辦法,但爲了保證延遲的精度也只好不得已爲之了。

1.3 timespec_to_jiffies

timespec_to_jiffies()將timespec結構表示的睡眠時間轉化成時鐘中斷數,其代碼以下:

#define MAX_JIFFY_OFFSET ((~0UL >> 1)-1)
    
    static __inline__ unsigned long
    timespec_to_jiffies(struct timespec *value)
    {
    	unsigned long sec = value->tv_sec;
    	long nsec = value->tv_nsec;
    
    	if (sec >= (MAX_JIFFY_OFFSET / HZ))
    		return MAX_JIFFY_OFFSET;
    	nsec += 1000000000L / HZ - 1;
    	nsec /= 1000000000L / HZ;
    	return HZ * sec + nsec;
    }

對於timespec中秒的成分,固然比較好辦,將其乘以HZ(每秒時鐘中斷的次數)即獲得對應的時鐘中斷數;對應納秒的成分,先計算出每一個時鐘中斷對應多少個納秒(1000000000L / HZ),計爲NanoPerHZ,爲了四捨五入(圓整),在納秒數nsec上加NanoPerHZ – 1,以後nsec除以NanoPerHZ即獲得對應的時鐘中斷數。

2 內核定時器

2.1 如何組織管理內核定時器

在sys_nanosleep()的實現中,調用add_timer()將timer掛入內核定時器隊列後調用schedule(),當前進程被調度出去而進入睡眠,此後就安心等待內核定時器將其喚醒。

能夠想象下,內核中有大量的timer_list結構即定時器,每一個定時器都有一個到點時間以及到點後要執行的操做,那麼該怎麼組織這些定時器呢?

比較容易想到的辦法就是:將定時器按到點時間「升序」排列,這樣,每次時鐘中斷jiffies自增1之後,從頭掃描這個已排序的隊列,直到發現第一個還沒有到點的定時器便可結束了。但這有一個缺點,那就是是每次插入一個新的定時器時,都要爲它查找合適的位置,這種方式其實就是「插入排序」,在定時器較多時,插入一個定時器的開銷是很大的。

用哈希表來提升效率呢?也就是說再也不經過單一的一個隊列來管定時器,而是用一個定時器隊列數組來管理。每次插入一個定時器時,根據其到點時間進行哈希運算,找到它所屬的隊列並將其歸隊。但定時器的到點時間用無符號的整形數(unsigned long)來表示,因此不能直接拿到點時間做爲哈希表的鍵值(那樣的話就得有2^32隊列),最簡單的作法是從到點時間中抽取最低的若干位,好比取bit0~bit10(這樣的話就有2^10個隊列)。但這種作法的缺點也很明顯,那就是每一個隊列中定時器,雖然其鍵值同樣,但到點時間卻千差萬別,好比若是取bit0~bit10爲鍵值,極端狀況下同一個隊列中能夠有2^22種到點時間。當jiffies改變時,還得遍歷每一個隊列來肯定哪些定時器到點了。

有沒有插入定時器時很便捷,檢查定時器到期時也很快速的方案呢?

考慮現實世界中的時鐘,它只有60個刻度卻能夠表示12小時即43200秒以內的任何1秒,它是如何作到呢,它是利用進位制和進位的思想,60秒進位爲1分鐘,60分鐘進位爲1小時,咱們能從中學到什麼?不過這裏有個區別,隨着時間的推移,時鐘表示的秒數在增長;而隨着時鐘中斷的發生,定時器的到點時間再減小。

沒錯,內核正是利用分段與進位的思想來組織定時器。首先內核將32位的到點時間分紅5段,以下圖所示:

到點時間分段

內核將到點時間分爲5段,每段對應一個哈希表。在每一個分段內,哈希表的鍵值是「窮舉」的,意即不存在衝突的狀況。5個分段一共有(256 + 64 * 4)即512個哈希隊列。內核是如何用這512個「刻度」徹底表示2^32的呢?

先來看插入定時器的狀況(對應的函數爲internal_add_timer())。若是到點時間<256,則根據到點時間的低8位插入到tv1哈希表的某個隊列中;若是到點時間≥256並且<2^14,那麼則根據到點時間的bit8~bit13將定時器插入到tv2哈希表的某個隊列中;若是到點時間≥2^14並且<2^20,則根據到點時間的bit14~bit19將定時器插入到tv3哈希表的某個隊列中,依次類推。因此,插入一個定時器的時間是比較短的,其代價爲一常數。能夠理解爲這是有5個「刻度」的時鐘,每一個「刻度」分管時間的不一樣部分,下一級的刻度滿則進位到上一級刻度。

再來看看時鐘到期的狀況。注意,隨着時鐘中斷的發生,定時器的到點時間是遞減的。每一個分段內都有一個index成員,用來指示「下一個」時鐘中斷髮生時(或者說該刻度減1時)要處理的隊列。首先tv1中的定時器,每次時鐘中斷從而將jiffies往前推動一步時,其index指示的哈希隊列上的定時器都到期了,處理完這些定時器的「到期操做」後便可將它們脫鏈並釋放,同時index加1。而當index加至256時,當前刻度滿,tv1.index從新設置爲0,開始另外一輪的256次時鐘中斷。同時上一級的tv2.index所指向哈希隊列上的定時器,通過256次時鐘中斷後,其第二級刻度已經「耗盡」(所以要降級到第一級刻度),所以能夠將它們搬遷至tv1中了(經過函數internal_add_timer()從新加入一遍,由於這些定時器的到點時間與當前jiffies的差值確定<256,因此加入到tv1的哈希表中),同時將tv2.index加1。依次類推,若是tv2.index增長至56則表示當前刻度滿,將其從新設置爲0開始另外一輪的256*56次時鐘中斷,同時其上級的tv3.index指示的哈希隊列中的定時器能夠移到tv2中了。能夠想象一下,一個時鐘「倒着走」是什麼狀況。

2.2 添加定時器的操做

tv1至tv5的定義在<kernel/timer.c>中:

#define TVN_BITS 6
    #define TVR_BITS 8
    #define TVN_SIZE (1 << TVN_BITS)
    #define TVR_SIZE (1 << TVR_BITS)
    #define TVN_MASK (TVN_SIZE - 1)
    #define TVR_MASK (TVR_SIZE - 1)
    
    struct timer_vec {
    	int index;
    	struct list_head vec[TVN_SIZE];
    };
    
    struct timer_vec_root {
    	int index;
    	struct list_head vec[TVR_SIZE];
    };
    
    static struct timer_vec tv5;
    static struct timer_vec tv4;
    static struct timer_vec tv3;
    static struct timer_vec tv2;
    static struct timer_vec_root tv1;
    
    static struct timer_vec * const tvecs[] = {
    	(struct timer_vec *)&tv1, &tv2, &tv3, &tv4, &tv5
    };

這裏爲了將tv1也編入內核定時器總隊tvecs,對它進行強轉類型轉換。

在sys_nanosleep()中調用了add_timer()將timer掛入內核定時器隊列,add_timer()調用internal_add_timer()來完成核心工做,後者的定義也在本文件中:

static inline void internal_add_timer(struct timer_list *timer)
    {
    	/*
    	 * must be cli-ed when calling this
    	 */
    	unsigned long expires = timer->expires;
    	unsigned long idx = expires - timer_jiffies;
    	struct list_head * vec;
    
    	if (idx < TVR_SIZE) {
    		int i = expires & TVR_MASK;
    		vec = tv1.vec + i;
    	} else if (idx < 1 << (TVR_BITS + TVN_BITS)) {
    		int i = (expires >> TVR_BITS) & TVN_MASK;
    		vec = tv2.vec + i;
    	} else if (idx < 1 << (TVR_BITS + 2 * TVN_BITS)) {
    		int i = (expires >> (TVR_BITS + TVN_BITS)) & TVN_MASK;
    		vec =  tv3.vec + i;
    	} else if (idx < 1 << (TVR_BITS + 3 * TVN_BITS)) {
    		int i = (expires >> (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK;
    		vec = tv4.vec + i;
    	} else if ((signed long) idx < 0) {
    		/* can happen if you add a timer with expires == jiffies,
    		 * or you set a timer to go off in the past
    		 */
    		vec = tv1.vec + tv1.index;
    	} else if (idx <= 0xffffffffUL) {
    		int i = (expires >> (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK;
    		vec = tv5.vec + i;
    	} else {
    		/* Can only get here on architectures with 64-bit jiffies */
    		INIT_LIST_HEAD(&timer->list);
    		return;
    	}
    	/*
    	 * Timers are FIFO!
    	 */
    	list_add(&timer->list, vec->prev);
    }

有了前文的描述,相信這段代碼不難理解。

timer_jiffies爲一全局變量,表示當前對定時器隊列的處理在時間上已經推動到哪一點了,同時也是設置定時器的基準點,其數值有可能會不一樣與jiffies。最後一行代碼,可見定時器總數插入到對應哈希隊列的隊尾。

2.3 時鐘中斷與定時器到期

在第三章的「時鐘中斷」一節中,咱們看到從時鐘中斷返回以前要執行與時鐘有關的bh函數bh_timer(),而在bh_timer()函數中要調用run_timer_list()函數,該函數的定義也在本文件中:

static inline void run_timer_list(void)
    {
    	spin_lock_irq(&timerlist_lock);
    	while ((long)(jiffies - timer_jiffies) >= 0) {
    		struct list_head *head, *curr;
    		if (!tv1.index) {
    			int n = 1;
    			do {
    				cascade_timers(tvecs[n]);
    			} while (tvecs[n]->index == 1 && ++n < NOOF_TVECS);
    		}
    repeat:
    		head = tv1.vec + tv1.index;
    		curr = head->next;
    		if (curr != head) {
    			struct timer_list *timer;
    			void (*fn)(unsigned long);
    			unsigned long data;
    
    			timer = list_entry(curr, struct timer_list, list);
     			fn = timer->function;
     			data= timer->data;
    
    			detach_timer(timer);
    			timer->list.next = timer->list.prev = NULL;
    			timer_enter(timer);
    			spin_unlock_irq(&timerlist_lock);
    			fn(data);
    			spin_lock_irq(&timerlist_lock);
    			timer_exit();
    			goto repeat;
    		}
    		++timer_jiffies; 
    		tv1.index = (tv1.index + 1) & TVR_MASK;
    	}
    	spin_unlock_irq(&timerlist_lock);
    }

說明:

  • 在「時鐘中斷一節」咱們見過特殊狀況下jiffies向前推動的步長可能大於1,因此函數最外層循環讓定時器基準timer_jiffies一路小跑,步長爲1,逐步跟上jiffies。

  • 每次循環即時鐘基準timer_jiffies往前推動一步時,主要幹兩件事:

    1. 其一,若是tv1.index爲0,說明又一輪「256次的時鐘中斷」已通過去了,經過調用cascade_timers()將tv2中的一個隊列「搬遷」到tv1中;並將tv2.index向前推動,若是tv2.index爲1,即表示又一輪「64*256」次時鐘中斷過去了,須要將tv3中的一個隊列「搬遷」到tv2中,並將tv3.index往前推薦,依次類推,這部分也是一個循環,常量NOOF_TVECS值爲5即內核定時器分段(哈希表)個數。對tv1來講tv1.index爲0時表示「一輪256次的時鐘中斷過去了」,tv1.index的推動軌跡是:從0(若是定時器到點時間就是0)開始逐步推動,推動至255則迴歸0,即0-->255-->0,而對tv2(以及tv三、tv四、tv5)爲何tv2.index爲1時表示「一輪256*64的時鐘中斷過去了」呢?這是由於在插入定時器時,若是到點時間爲256(這已是tv2所表示的最小到點時間了),則將其插入tv2中的第1個哈希隊列,而tv2的的0個哈希隊列永遠爲空。所以tv2.index從1開始推動,其推動軌跡爲1-->63-->0-->1,所以當tv2.index回到1,才表示一個週期的結束。
    2. 其二,tv1中到點定時器,其到點操做該執行了。代碼中有goto實現的循環就是處理在這一步中到點的隊列。sys_nanosleep()所設置的定時器,其到點操做爲process_timeout(),定義在sched.c中,該函數調用wake_up_process()將睡眠的進程喚醒。
    static void process_timeout(unsigned long __data)
            {
    	        struct task_struct * p = (struct task_struct *) __data;    
    	        wake_up_process(p);
            }
相關文章
相關標籤/搜索