进程调度之8:nanosleep与内核定时器

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

date: 2014-11-08 14:16

某些情况下,运行中的进程需要主动进入睡眠状态,这里“睡眠”的原语是:当前进程的状态变成TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE,并从可执行队列中脱钩,调度的结果是其他进程投入运行。并且进程一旦进入睡眠状态,就需要经过唤醒才能恢复成TASK_RUNNING,并回到可执行队列中。

系统调用nanosleep()是当前进程进入睡眠状态,在指定的时间以后内核将该进程唤醒,其底层实现是内核定时器。

nanaosleep()的原型是:

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

结构体timespec的定义如下:

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

结构包含两个成员,tv_sec表示秒,time_t其实为long型;tv_nsec表示纳秒,但这并不表示睡眠的精度可以到达纳秒级,在典型的内核配置中时钟中断频率HZ一般配置为100,也就是说每个时钟周期为10ms,这就意味着如果进程进入睡眠而循正常途径由时钟中断服务程序来唤醒的话,只能达到10ms的精度。

nanosleep()函数的第一个参数rqtp表示期望睡眠的时间;如果进程提前被唤醒,第二个参数rmtp返回剩余的睡眠时间。

1 nanosleep()的流程

1.1 主要流程如下:

进程调度之8:nanosleep与内核定时器_第1张图片

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段,如下图所示:

进程调度之8:nanosleep与内核定时器_第2张图片

内核将到点时间分为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的定义在中:

    #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(以及tv3、tv4、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);
            }
    

转载于:https://my.oschina.net/u/3857782/blog/1857566

你可能感兴趣的:(进程调度之8:nanosleep与内核定时器)