进程调度之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);
            }
相关文章
相关标签/搜索