1. 程式人生 > >Linux時間子系統之七:定時器的應用--msleep(),hrtimer_nanosleep()

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

get 關系 警告 mov signed num wakeup sch switch

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

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

1. msleep

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

[cpp] view plain copy

  1. void msleep(unsigned int msecs)

延時的時間由參數msecs指定,單位是毫秒,事實上,msleep的實現基於低分辨率定時器,所以msleep的實際精度只能也是1/HZ級別。內核還提供了另一個比較類似的延時函數msleep_interruptible:

[cpp] view plain copy

  1. unsigned long msleep_interruptible(unsigned int msecs)

延時的單位同樣毫秒數,它們的區別如下:

函數
延時單位
返回值
是否可被信號中斷

msleep
毫秒

msleep_interruptible
毫秒
未完成的毫秒數

最主要的區別就是msleep會保證所需的延時一定會被執行完,而msleep_interruptible則可以在延時進行到一半時被信號打斷而退出延時,剩余的延時數則通過返回值返回。兩個函數最終的代碼都會到達schedule_timeout函數,它們的調用序列如下圖所示:
技術分享

圖1.1 兩個延時函數的調用序列

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

[cpp] view plain copy

  1. signed long __sched schedule_timeout(signed long timeout)
  2. {
  3. struct timer_list timer;
  4. unsigned long expire;
  5. switch (timeout)
  6. {
  7. case MAX_SCHEDULE_TIMEOUT:
  8. schedule();
  9. goto out;
  10. default:
  11. if (timeout < 0) {
  12. printk(KERN_ERR "schedule_timeout: wrong timeout "
  13. "value %lx\n", timeout);
  14. dump_stack();
  15. current->state = TASK_RUNNING;
  16. goto out;
  17. }
  18. }

然後計算到期的jiffies數,並在堆棧上建立一個低分辨率定時器,把到期時間設置到該定時器中,啟動定時器後,通過schedule把當前進程調度出cpu的運行隊列:

[cpp] view plain copy

  1. expire = timeout + jiffies;
  2. setup_timer_on_stack(&timer, process_timeout, (unsigned long)current);
  3. __mod_timer(&timer, expire, false, TIMER_NOT_PINNED);
  4. schedule();

到這個時候,進程已經被調度走,那它如何返回繼續執行?我們看到定時器的到期回調函數是process_timeout,參數是當前進程的task_struct指針,看看它的實現:

[cpp] view plain copy

  1. static void process_timeout(unsigned long __data)
  2. {
  3. wake_up_process((struct task_struct *)__data);
  4. }

噢,沒錯,定時器一旦到期,進程會被喚醒並繼續執行:

[cpp] view plain copy

  1. del_singleshot_timer_sync(&timer);
  2. /* Remove the timer from the object tracker */
  3. destroy_timer_on_stack(&timer);
  4. timeout = expire - jiffies;
  5. out:
  6. return timeout < 0 ? 0 : timeout;
  7. }

schedule返回後,說明要不就是定時器到期,要不就是因為其它時間導致進程被喚醒,函數要做的就是刪除在堆棧上建立的定時器,返回剩余未完成的jiffies數。

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

[cpp] view plain copy

  1. signed long __sched schedule_timeout_uninterruptible(signed long timeout)
  2. {
  3. __set_current_state(TASK_UNINTERRUPTIBLE);
  4. return schedule_timeout(timeout);
  5. }
  6. void msleep(unsigned int msecs)
  7. {
  8. unsigned long timeout = msecs_to_jiffies(msecs) + 1;
  9. while (timeout)
  10. timeout = schedule_timeout_uninterruptible(timeout);
  11. }

msleep先是把毫秒轉換為jiffies數,通過一個while循環保證所有的延時被執行完畢,延時操作通過schedule_timeout_uninterruptible函數完成,它僅僅是在把進程的狀態修改為TASK_UNINTERRUPTIBLE後,調用上述的schedule_timeout來完成具體的延時操作,TASK_UNINTERRUPTIBLE狀態保證了msleep不會被信號喚醒,也就意味著在msleep期間,進程不能被kill掉。

看看msleep_interruptible的實現:

[cpp] view plain copy

  1. signed long __sched schedule_timeout_interruptible(signed long timeout)
  2. {
  3. __set_current_state(TASK_INTERRUPTIBLE);
  4. return schedule_timeout(timeout);
  5. }
  6. unsigned long msleep_interruptible(unsigned int msecs)
  7. {
  8. unsigned long timeout = msecs_to_jiffies(msecs) + 1;
  9. while (timeout && !signal_pending(current))
  10. timeout = schedule_timeout_interruptible(timeout);
  11. return jiffies_to_msecs(timeout);
  12. }

msleep_interruptible通過schedule_timeout_interruptible中轉,schedule_timeout_interruptible的唯一區別就是把進程的狀態設置為了TASK_INTERRUPTIBLE,說明在延時期間有信號通知,while循環會馬上終止,剩余的jiffies數被轉換成毫秒返回。實際上,你也可以利用schedule_timeout_interruptible或schedule_timeout_uninterruptible構造自己的延時函數,同時,內核還提供了另外一個類似的函數,不用我解釋,看代碼就知道它的用意了:

[cpp] view plain copy

  1. signed long __sched schedule_timeout_killable(signed long timeout)
  2. {
  3. __set_current_state(TASK_KILLABLE);
  4. return schedule_timeout(timeout);
  5. }

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的代碼:

[cpp] view plain copy

  1. long hrtimer_nanosleep(struct timespec *rqtp, struct timespec __user *rmtp,
  2. const enum hrtimer_mode mode, const clockid_t clockid)
  3. {
  4. struct restart_block *restart;
  5. struct hrtimer_sleeper t;
  6. int ret = 0;
  7. unsigned long slack;
  8. slack = current->timer_slack_ns;
  9. if (rt_task(current))
  10. slack = 0;
  11. hrtimer_init_on_stack(&t.timer, clockid, mode);
  12. hrtimer_set_expires_range_ns(&t.timer, timespec_to_ktime(*rqtp), slack);
  13. if (do_nanosleep(&t, mode))
  14. goto out;
  15. /* Absolute timers do not update the rmtp value and restart: */
  16. if (mode == HRTIMER_MODE_ABS) {
  17. ret = -ERESTARTNOHAND;
  18. goto out;
  19. }
  20. if (rmtp) {
  21. ret = update_rmtp(&t.timer, rmtp);
  22. if (ret <= 0)
  23. goto out;
  24. }
  25. restart = ¤t_thread_info()->restart_block;
  26. restart->fn = hrtimer_nanosleep_restart;
  27. restart->nanosleep.clockid = t.timer.base->clockid;
  28. restart->nanosleep.rmtp = rmtp;
  29. restart->nanosleep.expires = hrtimer_get_expires_tv64(&t.timer);
  30. ret = -ERESTART_RESTARTBLOCK;
  31. out:
  32. destroy_hrtimer_on_stack(&t.timer);
  33. return ret;
  34. }

接著我們看看do_nanosleep的實現代碼,它首先通過hrtimer_init_sleeper函數,把定時器的回調函數設置為hrtimer_wakeup,把當前進程的task_struct結構指針保存在hrtimer_sleeper結構的task字段中:

[cpp] view plain copy

  1. void hrtimer_init_sleeper(struct hrtimer_sleeper *sl, struct task_struct *task)
  2. {
  3. sl->timer.function = hrtimer_wakeup;
  4. sl->task = task;
  5. }
  6. EXPORT_SYMBOL_GPL(hrtimer_init_sleeper);
  7. static int __sched do_nanosleep(struct hrtimer_sleeper *t, enum hrtimer_mode mode)
  8. {
  9. hrtimer_init_sleeper(t, current);

然後,通過一個do/while循環內:啟動定時器,掛起當前進程,等待定時器或其它事件喚醒進程。這裏的循環體實現比較怪異,它使用hrtimer_active函數間接地判斷定時器是否到期,如果hrtimer_active返回false,說明定時器已經過期,然後把hrtimer_sleeper結構的task字段設置為NULL,從而導致循環體的結束,另一個結束條件是當前進程收到了信號事件,所以,當因為是定時器到期而退出時,do_nanosleep返回true,否則返回false,上述的hrtimer_nanosleep正是利用了這一特性來決定它的返回值。以下是do_nanosleep循環體的代碼:

[cpp] view plain copy

  1. do {
  2. set_current_state(TASK_INTERRUPTIBLE);
  3. hrtimer_start_expires(&t->timer, mode);
  4. if (!hrtimer_active(&t->timer))
  5. t->task = NULL;
  6. if (likely(t->task))
  7. schedule();
  8. hrtimer_cancel(&t->timer);
  9. mode = HRTIMER_MODE_ABS;
  10. } while (t->task && !signal_pending(current));
  11. __set_current_state(TASK_RUNNING);
  12. return t->task == NULL;
  13. }

除了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。

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