linux内核-系统调用nanosleep()与pause()

处于种种原因,运行中的进程常常需要主动进入睡眠状态,并发起一次调度让出CPU。这一定要通过系统调用,或者在系统调用内核才能做到。注意,前面的博客中讲到的系统调用sched_yield与此有所不同,那只是让内核进行一次调度,而当前进程继续保持可运行状态。而这里所说的是,当前进程进入睡眠,也就是将进程的状态变成TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE,并从可执行队列中脱钩,调度的结果一定是其它进程得以运行。并且,进程一旦进入睡眠状态,就需要经过唤醒才能将状态恢复成TASK_RUNNING,并回到可执行队列中。

这种主动在一段时间内放弃运行、让出CPU的行动可以分成两种。一种是隐含的,不确定的,就是说暂时让出CPU的可能性隐含在其它行为之中。此时让出CPU本身不是目的,而是在真正的目的一时不能达到,必须等待时才处于公德心,把CPU暂时让出来。这样的例子有read、write、open、send、recvfrom等等,机会所有与外设有关的系统调用都有可能在执行的过程中受阻而进入睡眠、让出CPU。另一种是明确的,目的就在进入睡眠状态。这样的系统调用主要有两个,一个是nanosleep,另一个是pause。

系统调用nanosleep使当前进程进入睡眠状态,但是在指定的时候以后由内核将该进程唤醒,所以常常用过来实现周期性的应用。程序员常常使用的sleep是个库函数,实际上时通过系统调用nanosleep来实现的。

系统调用pause也使当前进程进入睡眠,可是与时间无关,要到接收到一个信号时才被唤醒,所以常常用来协调若干进程的运行。读者在前面几篇博客中看到系统调用wait4,类似的还有wait3,实际上可以看作是pause的一种特例,因为它要在接收到特定的信号SIGCHLD并且满足若干特殊条件时才被唤醒。

还有一种特殊情况,当前进程接收到了信号SIGSTOP,然后在当前进程从系统空间返回到用户空间之前(不管是因为系统调用、中断或是异常)就会在do_signal中条用schedule,进程状态变成TASK_STOPPED,并从可执行队列中脱钩,一致要到收到一个SIGCONT信号时才能恢复到可运行状态。这种情况诗会上是强制性的,但由于形式上当前进程在do_signal的过程中主动调用schedule,所以没有把它放在强制性调度的博客中,我们在讲进程间通信时还要回到这个话题。

在这里我们集中介绍nanosleep和pause两个系统调用。

系统调用nanosleep在内核中的实现为sys_nanosleep,其代码如下:


asmlinkage long sys_nanosleep(struct timespec *rqtp, struct timespec *rmtp)
{
	struct timespec t;
	unsigned long expire;

	if(copy_from_user(&t, rqtp, sizeof(struct timespec)))
		return -EFAULT;

	if (t.tv_nsec >= 1000000000L || t.tv_nsec < 0 || t.tv_sec < 0)
		return -EINVAL;


	if (t.tv_sec == 0 && t.tv_nsec <= 2000000L &&
	    current->policy != SCHED_OTHER)
	{
		/*
		 * Short delay requests up to 2 ms will be handled with
		 * high precision by a busy wait for all real-time processes.
		 *
		 * Its important on SMP not to do this holding locks.
		 */
		udelay((t.tv_nsec + 999) / 1000);
		return 0;
	}

	expire = timespec_to_jiffies(&t) + (t.tv_sec || t.tv_nsec);

	current->state = TASK_INTERRUPTIBLE;
	expire = schedule_timeout(expire);

	if (expire) {
		if (rmtp) {
			jiffies_to_timespec(expire, &t);
			if (copy_to_user(rmtp, &t, sizeof(struct timespec)))
				return -EFAULT;
		}
		return -EINTR;
	}
	return 0;
}

库函数sleep的参数是以秒为单位的整数,而nanosleep的参数则为两个timespec结构指针。第一个指针rqtp,指向给定所需睡眠时间的数据结构;第二个指针rmtp,则指向返回剩余睡眠时间的数据结构。这是因为睡眠中的进程有可能因接收到信号而提前被唤醒,这时候函数返回-1并在rmtp所指的数据结构中返回剩余的时间(如果rmtp不是NULL),然后进程可以决定是否再次睡眠把时间用光。

数据结构timespec的定义如下:

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

这里的tv_sec,单位为秒,而tv_nsec为毫微秒,也就是10^-9秒。当然,这并表示睡眠时间的精度可以达到毫微秒的量级。以前讲过,在典型的内核配置中时钟中断的频率HZ为100,也就是说时钟中断的周期为10毫秒。这意味着,如果进程进入睡眠而循政策途径由时钟中断服务程序来唤醒的话,那就只能达到10毫秒的精度。正因为这样,才有809-821行的特殊处理,那就是如果要求睡眠的时间小于2毫秒,而要求睡眠的进程又是个有实时要求的进程(其调度政策为SCHED_FIFO或SCHED_RR),那就不能真的让这个进程进入睡眠,因为那样有可能要到10毫秒以后才能将其唤醒,对于实时应用的进程来说这是不能接受的。所以,在这样的情况下能提供的只是延迟而不是睡眠。这里由一个宏操作udelay通过计数来实现延迟,其定义如下:

#define udelay(n) (__builtin_constant_p(n) ? \
	((n) > 20000 ? __bad_udelay() : __const_udelay((n) * 0x10c6ul)) : \
	__udelay(n))

除若干预定的常数以外,都是通过函数__udelay完成延迟,其代码在arch/i386/lib/delay.c中。我们把涉及的各个函数逐层列在下面,供读者阅读:

sys_nanosleep=>udelay=>__udelay

void __udelay(unsigned long usecs)
{
	__const_udelay(usecs * 0x000010c6);  /* 2**32 / 1000000 */
}


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的数值取决于具体的CPU,系统初始化时由内核根据采集的数据确定,并保存在数据结构cpuinfo_x86的变量current_cpu_data中:

sys_nanosleep=>udelay=>__udelay=>__const_udelay=>__delay

void __delay(unsigned long loops)
{
	if(x86_udelay_tsc)
		__rdtsc_delay(loops);
	else
		__loop_delay(loops);
}

如果CPU支持基于硬件的延迟,那就通过__rdtsc_delay完成所需的延迟,否则由软件通过计数实现。

sys_nanosleep=>udelay=>__udelay=>__const_udelay=>__delay=>__loop_delay

/*
 *	Non TSC based delay loop for 386, 486, MediaGX
 */
 
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));
}

读者对于嵌入C代码的汇编语句已经不陌生了,所以这里不再解释。从这段代码中可以看出,udelay是通过计数循环来达到延迟的。也就是说,这种情况下当前进程并不真的进入睡眠,并不让出CPU,而只是通过循环来消磨掉一些时间。这当然不是个好方法,但对于有实时性要求的进程也只好不得已而为之。再说,即使对于有实时要求的进程,只要延迟的时间超过2毫秒,也不用通过这个方法来实现。可是,为什么会有这么短的延迟要求呢?这一般与外设操作相联系的,有些外设要求连续两次操作之间的时间间隔不得小于某个特定值,所以就有了这么短的延时要求。

回到sys_nanosleep的代码中,对于正常的睡眠要求,先调用timespec_to_jiffies,将数据结构t中的数值换算成时钟中断的次数,换算的方法在time.h中,我们把它留给读者自己阅读(time.h):

sys_nanosleep=>timespec_to_jiffies


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;
}

注意,前面sys_nanosleep中的822行的(t.tv_sec || t.tv_nsec)是关系表达式,其值为1或者0。

然后,将当前进程的状态改成TASK_INTERRUPTIBLE并调用schedule_timeout进入睡眠。以前讲过,睡眠状态TASK_INTERRUPTIBLE与TASK_UNINTERRUPTIBLE的区别在于后者在进程接收到信号时不会被唤醒。函数schedule_timeout的代码如下(sched.c):

sys_nanosleep=>schedule_timeout


signed long 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 from %p\n", timeout,
			       __builtin_return_address(0));
			current->state = TASK_RUNNING;
			goto out;
		}
	}

	expire = timeout + jiffies;

	init_timer(&timer);
	timer.expires = expire;
	timer.data = (unsigned long) current;
	timer.function = process_timeout;

	add_timer(&timer);
	schedule();
	del_timer_sync(&timer);

	timeout = expire - jiffies;

 out:
	return timeout < 0 ? 0 : timeout;
}

在内核中把时钟中断的次数作为计时的统一尺度,并给时钟中断之间的间隔起了名字叫做jiffy(瞬间的意思)。与此相应,内核中设置了一个全局的计数器jiffies,用来对系统自初始化一来时钟中断的次数计数。所以,在调用schedule_timeout之前把需要睡眠的时间先换算成时钟中断的数量,把这个数量与当前的jiffies相加就得到了到点的时间。但是,当所要求的时间太长,倡导不能用带符号整数表达时(其实是最大的正整数减1,见前面sys_nanosleep函数代码中对timespec_to_jiffies的注解以及代码中的第822行),就返回一个常数MAX_JIFFY_OFFSET。这个常数在schedule_timeout中被视为无限期,所以在384行中调用schedule就完事了。既然是无限期睡眠,内核就不承担按时将其唤醒的责任,这个进程要一直睡眠到有另一个进程向其发送一个信号时才会被唤醒。

函数schedule_timeout的返回值是进程被唤醒时剩下的还未睡完的时间。我们来看看当调用参数为MAX_JIFFY_OFFSET时的返回值。在这种情况下,当进程被唤醒而从schedule返回时就通过goto语句转到标号out处,而变量timeout的数值在这个整个过程中并未改变,仍旧是MAX_JIFFY_OFFSET,这体现了从无限减去有限后结果还是无限的原理。

当要求的睡眠时间在规定的范围以内时,内核就要承担其按时将此进程唤醒的责任了。为此目的,内核要设置好一个定时器,也就是这里的数据结构timer,并将其挂入一个定时器队列,而每次时钟中断时都要检查这些定时器是否到点。数据结构timer的类型为timer_list。我们在时钟中断博客那里调到了这个数据结构及其作用,但没有深入加以讨论,这是因为那时我们还没有讲过进程调度及有关的机制,很难真正讲清楚。而现在,结合schedule_timeout的代码,就可以把整个过程和机制讲清楚了。这里,在init_timer以后,将定时器的到点时间设置成计算得到的expire。到点时要执行的函数则为process_timeout,等一下我们就会看到它到底干些什么了。准备传给process_timeout的参数为current,读者应该还记得,这实际上是一个得到当前进程task_struct指针的宏操作。读者也许会问,为什么不干脆把数据结构中的变量data改变task_struct指针?这是因为这样个能为灵活、通用,再说到点要调用的函数也并不总是与某个进程直接有关的。函数add_timer将timer挂入定时器队列,其代码如下:

sys_nanosleep=>schedule_timeout=>add_timer


void add_timer(struct timer_list *timer)
{
	unsigned long flags;

	spin_lock_irqsave(&timerlist_lock, flags);
	if (timer_pending(timer))
		goto bug;
	internal_add_timer(timer);
	spin_unlock_irqrestore(&timerlist_lock, flags);
	return;
bug:
	spin_unlock_irqrestore(&timerlist_lock, flags);
	printk("bug: kernel timer added twice at %p.\n",
			__builtin_return_address(0));
}

核心的操作是internal_add_timer完成的,这里多了一层包装,目的是将核心的队列操作保护起来。由spin_lock_irqsave先将中断关闭,而spin_unlock_irqrestore则在操作完成以后再恢复原状。函数internal_add_timer的代码如下:

sys_nanosleep=>schedule_timeout=>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);
}

在128行中引用的timer_jiffies也是个全局变量,表示当前对定时器队列的处理器在时间上已经推进到了哪一点,同时也是设置定时器的基准点,其数值有可能会不同于jiffies,等一会儿我们就会看到它的作用。

在进一步深入到internal_add_timer的代码中去之前,有必要先大致介绍一下定时器队列的组织。本来,最简单的办法是将所有的timer_list结构,即定时器,按到点的先后链接在一起成为一个队列,然后每当jiffies改变时就从该队列的头部开始逐个检查并 处理这些数据结构,直到发现第一个尚未到点的定时器就可以结束了。可是这样有个缺点,就是每当将一个新的定时器加入到这个队列中去时,要在队列中进行线性搜索,寻找适当的链入位置,在最坏的情况下要扫描过队列中所有的数据结构。当队列中的成员数量有可能很大时,这种方案的效率就不能令人满意了。学过数据结构与算法的读者可能马上会想到可以通过杂凑哈数来改善效率。也就是说,将这些定时器数据结构组织成一个队列数组,或者说队列的阵列,而不是一个单一的队列,然后根据每个定时器到点的时间经过杂凑计算决定应该将其链入到哪一个队列中。这样,通过将定时器分散链入到不同的队列中,就可以减小各个队列的平均长度,从而提高效率。最简单的杂凑计算莫过于从数值中抽取最低的若干位,也就是通过与运算将数值中的高位屏蔽掉,这实际上相当于将数值除以一个2的整数次幂以后取其余数。但是,在这种简单的杂凑表组织里每个队列中还会由很多分属于不同到点时间的定时器,这是因为饿只要杂凑计算后的结果相同就会被链入到同一个队列中。例如,jiffies是个32位无符号整数,假如我们取最低的10位作为杂凑计算的结果,也就是说数组中有2^10个队列,那么从理论上说在最坏的情况下在一个队列中可以分属2^22种不同到点时间的定时器。当然,在实际运行中是不会这么糟糕的,但是总叫人觉得不尽人意。理想的解决方案是每隔队列中只有属于同一到点时间的定时器。可是总不可能设置2^22个定时器队列把?所以,既要顾及时钟中断发生时检查并处理这些定时器的效率,又要顾及在将定时器插入到这些队列中去时的效率,对此机制的设计和实现是一种挑战。linux内核比较好地解决了这个问题,设计并实现了一种相当于巧妙的方案。

在linux内核中设置了五个而不是一个这样的杂凑表,即定时器队列数组。详见下列代码(timer.c):

/*
 * Event timer code
 */
#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、tv2、...、tv5每个都包含了一个timer_list指针数组,这就是所谓杂凑表(bucket),表中的每个指针都指向一个定时器队列。其中tv1与其它几个数据结构的不同仅在于数组的大小,tv1中的数组大小为2^8,而其它几个的大小都是2^6。这样,队列的数量总共是2^8+4*2^6=512,还是可以接受的。每个数组都与一个变量index相联系,用来指示当下一个时钟中断发生时要处理的队列。与此同时,将32位的到点时间也划分成五段,其中最低的一段为8位,与tv1相对应,其它四段则都是6位。要将一个定时器挂入队列中去时,先根据到点时间和当前时间计算出这个定时器应该在多少次时钟中断以后到点,如果这个差值小于256的话就取到点的最低8位作为其杂凑值,然后用和这个杂凑值作为下标在tv1的数组中找到相应的队列,并将此定时器链入到这个队列中。由于tv1的数组中有256个队列,所以每个队列中的定时器都具有相同的到点时间。可是,当差值大于等于256时怎么办呢?这时候就看差值是否小于2^14,如果是,就取到点时间的数值中的第二段(6位,从第8位至第14位)为杂凑值,或下标,并将定时器插入到tv2的某个队列中去。示意图如下。

显然,tv2的队列与tv1中的不同,因为tv1中每个队列里的定时器都属于同一个到点时间,而tv2中的队列则不然。理论上tv2中的每个队列都可能含有分属256个不同到点时间的定时器。也就是说,tv2的尺度与tv1不同。当差值大于2^14时,那就要进一步看差值是否大于2^20了,余类推。

现在可以回到internal_add_timer的代码中了。读者应该可以自己读懂这段代码,其中具体讲定时器链入队列中的操作由list_add完成。

也就是说,每次都是插入到队列的尾部。对于tv1中的队列来说,由于每个队列中所有的定时器都是在同一时间到点,所以插入的位置根本没关系;而对于其它的队列来说,下面就会看到其实也没有关系。这样,将一定时期链入到队列中的操作变得很简单,根本就不需要在队列中寻找合适的插入位置了,从而其代价成了一个常数,而与队列长度无关了。同时,当时钟中断发生,从而将jiffies向前推进一步时,只要在tv1中根据index的指示将一个队列中所有的定时器都处理一遍(执行定时器所指定的函数)并将这些定时器释放,然后将index也向前推进一步就行了。当tv1.index达到256时就又将其设成0,回到数组的开头,开始另外一轮的256次时钟中断。此时,由于一个tv1周期已经完成,就从tv2中根据tv2.index的指引将tv2中的一个队列搬运到tv1中。在搬运的过程中,对队列中的每个定时器都再调用一次internal_add_timer。此时该队列中所有定时器的到点时间与当前时间的差都已小于256(由于当前时间的推进),所以都会被分散到tv1中的各个队列中期,而与各个定时器在队列中的位置无关。由此可见,链入tv2各个队列里的定时器是分成两步到位进入tv1中的队列(第一步将如tv2,第二步进入tv1)。以此类推,当到点时间与当前时间的差值大于2^26时要先进入tv5,分5步才能进入tv1。虽然有些定时器要分好几步才能到达tv1中,其代价仍然与队列长度无关,并且有个上限,就是最多5步。所以,这个办法要比线性搜索好得多。

将定时器链入到某个队列中以后,schedule_timeout就调用schedule,使当前进程真正进入睡眠,等待唤醒。

那么,时钟中断怎样唤醒这个进程呢?

在前面的时钟中断博客中,我们看到在从时钟中断返回之前要执行与时钟有关的bh函数timer_bh,而timer_bh要调用一个函数run_timer_list:

void timer_bh(void)
{
	update_times();
	run_timer_list();
}

函数run_timer_list的代码如下:

timer_bh=>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。正因为这样,这里通过一个循环来处理jiffies的每个单步。在每个单步中,先看tv1,index是否为0,若为0就要从tv2中搬运一个队列到tv1中。我们也把这种情况暂时搁置一下,先来看不为0时的情况。

代码中由goto实现的循环就是处理在这一步中到点的队列。处理本身是很简单的,顺着队列挨个把定时器通过detach_timer从队列中摘除出来,然后就执行该定时器所指定的函数。执行完这整个队列时,就将timer_jiffies和tv1.index也往前推进一步。但是,tv1.index的值是以256为模的(TVR_MASK),所以其数值在255以后就会到了0,下一个循环中或者下一次执行这个函数时就要通过cascade_timers从tv2中搬运一个队列到tv1中来。tv2中也有一个index,也要向前推进。每当jiffies向前推进了256步,也就是每当发生了256次时钟中断时,tv2.index就要向前推进一步。与tv1.index不同,tv2.index是以64为模的,所以在达到63以后就要回到0.当tv2,index为1时就要从tv3中搬运一个队列到tv2中和tv1中,余类推。

为什么是tv2,index为1时,而不是为0时,才从tv3中搬运呢?回头去看一下internal_add_timer的代码就清楚了。当到点时间与当前时间的差idx为TVR_SIZE即256时,经过第136行的处理以后结果为1而不是0。实际上,tv2中下标为0的那个队列一定是空的。同时,为了便于实现,代码中将tv1、tv2等五个数据结构也放在一个数组中,这就是tvecs。这里将下标设成从1开始,就是表示从tv2开始搬运,而第298行则表示如果tv2.index推进以后变成了1就要进一步从tv3搬运,余类推。

这里的NOOF_TVECS为一常数,实际上就是5(timer.c):

#define NOOF_TVECS (sizeof(tvecs) / sizeof(tvecs[0]))

函数cascade_timers的代码也在同一文件中。这是一段简单的代码,我们就不加解释了。

timer_bh=>run_timer_list=>cascade_timers

static inline void cascade_timers(struct timer_vec *tv)
{
	/* cascade all the timers from tv up one level */
	struct list_head *head, *curr, *next;

	head = tv->vec + tv->index;
	curr = head->next;
	/*
	 * We are removing _all_ timers from the list, so we don't  have to
	 * detach them individually, just clear the list afterwards.
	 */
	while (curr != head) {
		struct timer_list *tmp;

		tmp = list_entry(curr, struct timer_list, list);
		next = curr->next;
		list_del(curr); // not needed
		internal_add_timer(tmp);
		curr = next;
	}
	INIT_LIST_HEAD(head);
	tv->index = (tv->index + 1) & TVN_MASK;
}

在我们这个情景中,定时器中的函数指针为process_timeout,参数为睡眠中进程的task_struct指针,所以到点时就会调用process_timeout:

timer_bh=>run_timer_list=>process_timeout

static void process_timeout(unsigned long __data)
{
	struct task_struct * p = (struct task_struct *) __data;

	wake_up_process(p);
}

函数通过wake_up_process将睡眠中的进程唤醒。它的代码读者已经在强制性调度中看到过了。进程被唤醒并且再次被调度运行时,就回到了前面的schedule_timeout中。换句话说,是该进程从前面schedule_timeout中的schedule返回了。

回过去继续看schedule_timeout的代码,从schedule返回以后紧接着就调用了del_timer_sync,读者也许会感到奇怪,刚才在run_timer_list中不是已经通过detach_timer把定时器从队列中删除了吗?怎么这里又要del_timer_sync呢?对于单处理器的系统,del_timer_sync定义为del_timer,我们来看看detach_timer和del_timer的代码:

timer_bh=>run_timer_list=>detach_timer

static inline int detach_timer (struct timer_list *timer)
{
	if (!timer_pending(timer))
		return 0;
	list_del(&timer->list);
	return 1;
}

static inline int timer_pending (const struct timer_list * timer)
{
	return timer->list.next != NULL;
}

所以detach_timer仅在所处理的timer_list数据结构在队列中才把它从队列中删除。函数del_timer实际上调用detach_timer:

sys_nanosleep=>schedule_timeout=>del_timer

int del_timer(struct timer_list * timer)
{
	int ret;
	unsigned long flags;

	spin_lock_irqsave(&timerlist_lock, flags);
	ret = detach_timer(timer);
	timer->list.next = timer->list.prev = NULL;
	spin_unlock_irqrestore(&timerlist_lock, flags);
	return ret;
}

可见,对一个已经从队列中脱链的定时器再调用一次del_timer并没有害处。可是,即使没有害处,也没有理由做无用功啊。使得,但是要想到,run_timer_list并不是唯一可以将这个进程唤醒的函数。当另一个进程向睡眠中的进程发送一个信号时,同样可以将其唤醒。所以,在schedule_timeout中再调用一次del_timer就可以确保安全了。这里要指出,这里的timer是个局部变量,其空间在堆栈中,一旦从schedule_timeout返回,这个数据就消失了。这里可以省去动态分配和释放缓冲区的麻烦,也可以提高效率。可是将这样一个数据结构留在队列中时很危险的,一定要保证在这个数据结构还有效时其从队列中去除。

最后,期望中的到点时间expire与当前时间jiffies之差为剩下的尚未睡够的时间。这剩下的尚未睡够的时间是以时钟中断的次数为尺度的,所以在sys_nanosleep中又将其换算回timespec数据结构中的秒和毫微秒,然后返回给用户空间。当然,只有在进程因信号而被唤醒时才有可能还未睡够。否则,睡过头了的可能倒是有的。这一方面是因为在特殊的情况下也许会把好几次时钟中断合并在一起进行对jiffies的处理,所以一次就向前推进好几步。另一方面即使按时将进程唤醒也不能保证该进程马上就会被调度运行。

系统调用sys_nanosleep并非schedule_timeout的唯一用户。内核中还提供了一个函数interruptible_sleep_on_timeout,供各种设备驱动程序在内核中使用,将来在设备驱动的博客中读者还会看到它的使用。此外,在内核中也可以直接调用schedule_timeout

与sys_nanosleep相比,同样也是系统调用的sys_pause的代码就很简单了,其代码如下:

asmlinkage int sys_pause(void)
{
	current->state = TASK_INTERRUPTIBLE;
	schedule();
	return -ERESTARTNOHAND;
}

显然,当前进程通过sys_pause入睡以后,只有在接收到信号时才会被唤醒。

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值