Linux時間子系統之七:定時器的應用--msleep(),hrtimer_nanosleep()

droidphone發表於2012-10-23

我們已經在前面幾章介紹了低解析度定時器和高精度定時器的實現原理,核心為了方便其它子系統,在時間子系統中提供了一些用於延時或排程的API,例如msleep,hrtimer_nanosleep等等,這些API基於低解析度定時器或高精度定時器來實現,本章的內容就是討論這些方便、好用的API是如何利用定時器系統來完成所需的功能的。

/*****************************************************************************************************/
宣告:本博內容均由http://blog.csdn.net/droidphone原創,轉載請註明出處,謝謝!
/*****************************************************************************************************/

1.  msleep

msleep相信大家都用過,它可能是核心用使用最廣泛的延時函式之一,它會使當前程式被排程並讓出cpu一段時間,因為這一特性,它不能用於中斷上下文,只能用於程式上下文中。要想在中斷上下文中使用延時函式,請使用會阻塞cpu的無排程版本mdelay。msleep的函式原型如下:

void msleep(unsigned int msecs)
延時的時間由引數msecs指定,單位是毫秒,事實上,msleep的實現基於低解析度定時器,所以msleep的實際精度只能也是1/HZ級別。核心還提供了另一個比較類似的延時函式msleep_interruptible:

unsigned long msleep_interruptible(unsigned int msecs)
延時的單位同樣毫秒數,它們的區別如下:

函式 延時單位 返回值 是否可被訊號中斷
msleep 毫秒
msleep_interruptible 毫秒 未完成的毫秒數
最主要的區別就是msleep會保證所需的延時一定會被執行完,而msleep_interruptible則可以在延時進行到一半時被訊號打斷而退出延時,剩餘的延時數則通過返回值返回。兩個函式最終的程式碼都會到達schedule_timeout函式,它們的呼叫序列如下圖所示:
                                             

                                                                            圖1.1  兩個延時函式的呼叫序列

下面我們看看schedule_timeout函式的實現,函式首先處理兩種特殊情況,一種是傳入的延時jiffies數是個負數,則列印一句警告資訊,然後馬上返回,另一種是延時jiffies數是MAX_SCHEDULE_TIMEOUT,表明需要一直延時,直接執行排程即可:

signed long __sched schedule_timeout(signed long timeout)
{
	struct timer_list timer;
	unsigned long expire;

	switch (timeout)
	{
	case MAX_SCHEDULE_TIMEOUT:
		schedule();
		goto out;
	default:
		if (timeout < 0) {
			printk(KERN_ERR "schedule_timeout: wrong timeout "
				"value %lx\n", timeout);
			dump_stack();
			current->state = TASK_RUNNING;
			goto out;
		}
	}
然後計算到期的jiffies數,並在堆疊上建立一個低解析度定時器,把到期時間設定到該定時器中,啟動定時器後,通過schedule把當前程式排程出cpu的執行佇列:

	expire = timeout + jiffies;

	setup_timer_on_stack(&timer, process_timeout, (unsigned long)current);
	__mod_timer(&timer, expire, false, TIMER_NOT_PINNED);
	schedule();
到這個時候,程式已經被排程走,那它如何返回繼續執行?我們看到定時器的到期回撥函式是process_timeout,引數是當前程式的task_struct指標,看看它的實現:

static void process_timeout(unsigned long __data)
{
	wake_up_process((struct task_struct *)__data);
}
噢,沒錯,定時器一旦到期,程式會被喚醒並繼續執行:

	del_singleshot_timer_sync(&timer);

	/* Remove the timer from the object tracker */
	destroy_timer_on_stack(&timer);

	timeout = expire - jiffies;

 out:
	return timeout < 0 ? 0 : timeout;
}
schedule返回後,說明要不就是定時器到期,要不就是因為其它時間導致程式被喚醒,函式要做的就是刪除在堆疊上建立的定時器,返回剩餘未完成的jiffies數。

說完了關鍵的schedule_timeout函式,我們看看msleep如何實現:

signed long __sched schedule_timeout_uninterruptible(signed long timeout)
{
	__set_current_state(TASK_UNINTERRUPTIBLE);
	return schedule_timeout(timeout);
}

void msleep(unsigned int msecs)
{
	unsigned long timeout = msecs_to_jiffies(msecs) + 1;

	while (timeout)
		timeout = schedule_timeout_uninterruptible(timeout);
}
msleep先是把毫秒轉換為jiffies數,通過一個while迴圈保證所有的延時被執行完畢,延時操作通過schedule_timeout_uninterruptible函式完成,它僅僅是在把程式的狀態修改為TASK_UNINTERRUPTIBLE後,呼叫上述的schedule_timeout來完成具體的延時操作,TASK_UNINTERRUPTIBLE狀態保證了msleep不會被訊號喚醒,也就意味著在msleep期間,程式不能被kill掉。

看看msleep_interruptible的實現:

signed long __sched schedule_timeout_interruptible(signed long timeout)
{
	__set_current_state(TASK_INTERRUPTIBLE);
	return schedule_timeout(timeout);
}

unsigned long msleep_interruptible(unsigned int msecs)
{
	unsigned long timeout = msecs_to_jiffies(msecs) + 1;

	while (timeout && !signal_pending(current))
		timeout = schedule_timeout_interruptible(timeout);
	return jiffies_to_msecs(timeout);
}
msleep_interruptible通過schedule_timeout_interruptible中轉,schedule_timeout_interruptible的唯一區別就是把程式的狀態設定為了TASK_INTERRUPTIBLE,說明在延時期間有訊號通知,while迴圈會馬上終止,剩餘的jiffies數被轉換成毫秒返回。實際上,你也可以利用schedule_timeout_interruptible或schedule_timeout_uninterruptible構造自己的延時函式,同時,核心還提供了另外一個類似的函式,不用我解釋,看程式碼就知道它的用意了:

signed long __sched schedule_timeout_killable(signed long timeout)
{
	__set_current_state(TASK_KILLABLE);
	return schedule_timeout(timeout);
}

2.  hrtimer_nanosleep

第一節討論的msleep函式基於時間輪定時系統,只能提供毫秒級的精度,實際上,它的精度取決於HZ的配置值,如果HZ小於1000,它甚至無法達到毫秒級的精度,要想得到更為精確的延時,我們自然想到的是要利用高精度定時器來實現。沒錯,linux為使用者空間提供了一個api:nanosleep,它能提供納秒級的延時精度,該使用者空間函式對應的核心實現是sys_nanosleep,它的工作交由高精度定時器系統的hrtimer_nanosleep函式實現,最終的大部分工作則由do_nanosleep完成。呼叫過程如下圖所示:


                 圖  2.1  nanosleep的呼叫過程
與msleep的實現相類似,hrtimer_nanosleep函式首先在堆疊中建立一個高精度定時器,設定它的到期時間,然後通過do_nanosleep完成最終的延時工作,當前程式在掛起相應的延時時間後,退出do_nanosleep函式,銷燬堆疊中的定時器並返回0值表示執行成功。不過do_nanosleep可能在沒有達到所需延時數量時由於其它原因退出,如果出現這種情況,hrtimer_nanosleep的最後部分把剩餘的延時時間記入程式的restart_block中,並返回ERESTART_RESTARTBLOCK錯誤程式碼,系統或者使用者空間可以根據此返回值決定是否重新呼叫nanosleep以便把剩餘的延時繼續執行完成。下面是hrtimer_nanosleep的程式碼:
long hrtimer_nanosleep(struct timespec *rqtp, struct timespec __user *rmtp,
		       const enum hrtimer_mode mode, const clockid_t clockid)
{
	struct restart_block *restart;
	struct hrtimer_sleeper t;
	int ret = 0;
	unsigned long slack;

	slack = current->timer_slack_ns;
	if (rt_task(current))
		slack = 0;

	hrtimer_init_on_stack(&t.timer, clockid, mode);
	hrtimer_set_expires_range_ns(&t.timer, timespec_to_ktime(*rqtp), slack);
	if (do_nanosleep(&t, mode))
		goto out;

	/* Absolute timers do not update the rmtp value and restart: */
	if (mode == HRTIMER_MODE_ABS) {
		ret = -ERESTARTNOHAND;
		goto out;
	}

	if (rmtp) {
		ret = update_rmtp(&t.timer, rmtp);
		if (ret <= 0)
			goto out;
	}

	restart = ¤t_thread_info()->restart_block;
	restart->fn = hrtimer_nanosleep_restart;
	restart->nanosleep.clockid = t.timer.base->clockid;
	restart->nanosleep.rmtp = rmtp;
	restart->nanosleep.expires = hrtimer_get_expires_tv64(&t.timer);

	ret = -ERESTART_RESTARTBLOCK;
out:
	destroy_hrtimer_on_stack(&t.timer);
	return ret;
}
接著我們看看do_nanosleep的實現程式碼,它首先通過hrtimer_init_sleeper函式,把定時器的回撥函式設定為hrtimer_wakeup,把當前程式的task_struct結構指標儲存在hrtimer_sleeper結構的task欄位中:
void hrtimer_init_sleeper(struct hrtimer_sleeper *sl, struct task_struct *task)
{
	sl->timer.function = hrtimer_wakeup;
	sl->task = task;
}
EXPORT_SYMBOL_GPL(hrtimer_init_sleeper);

static int __sched do_nanosleep(struct hrtimer_sleeper *t, enum hrtimer_mode mode)
{
	hrtimer_init_sleeper(t, current);
然後,通過一個do/while迴圈內:啟動定時器,掛起當前程式,等待定時器或其它事件喚醒程式。這裡的迴圈體實現比較怪異,它使用hrtimer_active函式間接地判斷定時器是否到期,如果hrtimer_active返回false,說明定時器已經過期,然後把hrtimer_sleeper結構的task欄位設定為NULL,從而導致迴圈體的結束,另一個結束條件是當前程式收到了訊號事件,所以,當因為是定時器到期而退出時,do_nanosleep返回true,否則返回false,上述的hrtimer_nanosleep正是利用了這一特性來決定它的返回值。以下是do_nanosleep迴圈體的程式碼:
	do {
		set_current_state(TASK_INTERRUPTIBLE);
		hrtimer_start_expires(&t->timer, mode);
		if (!hrtimer_active(&t->timer))
			t->task = NULL;

		if (likely(t->task))
			schedule();

		hrtimer_cancel(&t->timer);
		mode = HRTIMER_MODE_ABS;

	} while (t->task && !signal_pending(current));

	__set_current_state(TASK_RUNNING);

	return t->task == NULL;
}
除了hrtimer_nanosleep,高精度定時器系統還提供了幾種用於延時/掛起程式的api:
  • schedule_hrtimeout    使得當前程式休眠指定的時間,使用CLOCK_MONOTONIC計時系統;
  • schedule_hrtimeout_range    使得當前程式休眠指定的時間範圍,使用CLOCK_MONOTONIC計時系統;
  • schedule_hrtimeout_range_clock    使得當前程式休眠指定的時間範圍,可以自行指定計時系統;
  • usleep_range 使得當前程式休眠指定的微妙數,使用CLOCK_MONOTONIC計時系統;
它們之間的呼叫關係如下:

                                                          圖 2.2  schedule_hrtimeout_xxxx系列函式
最終,所有的實現都會進入到schedule_hrtimeout_range_clock函式。需要注意的是schedule_hrtimeout_xxxx系列函式在呼叫前,最好利用set_current_state函式先設定程式的狀態,在這些函式返回前,進城的狀態會再次被設定為TASK_RUNNING。如果事先把狀態設定為TASK_UNINTERRUPTIBLE,它們會保證函式返回前一定已經經過了所需的延時時間,如果事先把狀態設定為TASK_INTERRUPTIBLE,則有可能在尚未到期時由其它訊號喚醒程式從而導致函式返回。主要實現該功能的函式schedule_hrtimeout_range_clock和前面的do_nanosleep函式實現原理基本一致。大家可以自行參考核心的程式碼,它們位於:kernel/hrtimer.c。

相關文章