Linux進程管理
Linux進程管理(四)進程調度之搶佔式調度
文章目錄
上篇文章我們將了內核調度分爲主動調度和搶佔式調度,主動調度我們已經講解過了,這篇文章我們來講解一下搶佔式調度
一、搶佔式調度
我們說過,進程真正的調度都是通過調用 schedule 函數發生的,所謂搶佔搶佔式調度,就是非進程自願調用 schedule 來發生進程調度
搶佔式調度主要分爲兩步
- 第一步:在current進程設置需要重新調度的標誌(TIF_NEED_RESCHED)
- 第二步:在某些特定的時機,檢測是否設置了 TIF_NEED_RESCHED 標誌,如果設置了,就調用 schedule 函數發生進程調度
這篇文章的主要目的就是討論這兩個動作發生的時期
二、設置需要重新調度的標誌的時機(TIF_NEED_RESCHED)
設置 TIF_NEED_RESCHED 標誌有兩個時機
- 週期性調度器處理時
- 喚醒進程時
首先討論週期性調度器處理
週期性調度器處理
前面我們,在硬件電路有一個滴答定時器,週期性的產生時鐘中斷(一般爲10ms)
每當時鐘中斷產生的時候,就會調用 scheduler_tick 函數進行處理,我們稱之爲週期性調度器
scheduler_tick 的定義如下
void scheduler_tick(void)
{
curr->sched_class->task_tick(rq, curr, 0);
}
它會調用current進程所指向的調度類中的 task_tick 函數
如果該調度類是 fair_sched_class,那麼 task_tick 就對應 task_tick_fair,其定義如下
// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202
static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
{
...
entity_tick(cfs_rq, se, queued);
...
}
entity_tick 函數中會判斷當前進程是否需要被搶佔,如果需要,那麼就會設置 TIF_NEED_RESCHED 標誌,其定義如下
// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202
static void
entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
/* 更新統計信息 */
update_curr(cfs_rq);
...
/* 檢查是否需要發生搶佔 */
check_preempt_tick(cfs_rq, curr);
}
update_curr 會更新進程的運行時間,check_preempt_tick 會判斷當前進程是否需要被搶佔
check_preempt_tick 的定義如下
// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202
static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
...
/* 挑選紅黑樹最左邊的節點 */
se = __pick_first_entity(cfs_rq);
/* 計算當前進程運行時間是否比紅黑樹第一個節點長? */
delta = curr->vruntime - se->vruntime;
/* 如果不是,就退出 */
if (delta < 0)
return;
...
/* 如果是,那麼就設置 TIF_NEED_RESCHED 標誌 */
resched_curr(rq_of(cfs_rq));
}
這裏面的判斷方式是 CFS 算法相關的內容,由於前面文章已經講解過了,所以這裏我不再重述
我們主要看當判斷當前進程需要被搶佔的時候,會調用 resched_curr,這個函數會設置 TIF_NEED_RESCHED 標誌,下面我們看一看它的定義
// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202
static inline void set_tsk_need_resched(struct task_struct *tsk)
{
set_tsk_thread_flag(tsk,TIF_NEED_RESCHED);
}
void resched_curr(struct rq *rq)
{
...
set_tsk_need_resched(curr);
...
}
可以看到,最終會設置 TIF_NEED_RESCHED 標誌
接下下我們看喚醒進程的情況
喚醒進程
進程在等待某個條件的時候,可能會進入睡眠,當條件滿足的時候,進程會被喚醒
喚醒進程的過程中,會調用 try_to_wake_up 來喚醒進程,其定義如下,這個函數的調用過程如下
最終會調用 enqueue_task,將進程添加到運行隊列,其定義如下
// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202
static inline void enqueue_task(struct rq *rq, struct task_struct *p, int flags)
{
...
p->sched_class->enqueue_task(rq, p, flags);
}
然後調用 check_preempt_curr,檢查當前進程是否需要被搶佔,如果需要,就會設置 TIF_NEED_RESCHED 標誌
check_preempt_curr 的定義如下
// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202
void check_preempt_curr(struct rq *rq, struct task_struct *p, int flags)
{
...
rq->curr->sched_class->check_preempt_curr(rq, p, flags);
...
}
它會調用進程所指向的調度類中的 check_preempt_curr 函數,如果調度類是 fair_sched_class,那麼 check_preempt_curr 就是 check_preempt_wakeup,其定義如下
// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202
static void check_preempt_wakeup(struct rq *rq, struct task_struct *p, int wake_flags)
{
...
resched_curr(rq);
...
}
根據相應的算法計算,如果需要重新調度,就會調用 resched_curr 函數,這個函數我們上面講過,它最終會設置 TIF_NEED_RESCHED 標誌
關於設置需要重新調度標誌的部分已經講完,下面來看進程的搶佔時機
三、進程搶佔的時機
進程搶佔的時機指的是在什麼時候檢查 TIF_NEED_RESCHED 標誌,然後調用 schedule 函數
進程搶佔時機可以分爲兩部分,一部分是 用戶態的搶佔時機,一部分是內核態的搶佔時機
3.1 用戶態的搶佔時機
用戶態的搶佔有兩個時機
- 系統調用返回
- 中斷返回用戶態
我們先來看系統調用返回
系統調用返回
對於 X86 來說,發生系統調用時,最終會調用到 do_syscall_32_irqs_on,其定義如下
// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202
static __always_inline void do_syscall_32_irqs_on(struct pt_regs *regs)
{
...
/* 系統調用處理 */
...
/* 系統調用返回 */
syscall_return_slowpath(regs);
}
do_syscall_32_irqs_on 首先做系統調用處理,在系統調用返回的時候,會調用 syscall_return_slowpath
syscall_return_slowpath 最終會調用到 exit_to_usermode_loop,exit_to_usermode_loop 會檢查是否設置了 TIF_NEED_RESCHED 標誌,如果設置了,就調用 schedule 函數,如下
exit_to_usermode_loop 的定義如下
// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202
static void exit_to_usermode_loop(struct pt_regs *regs, u32 cached_flags)
{
...
if (cached_flags & _TIF_NEED_RESCHED)
schedule();
...
}
下面在來看看中斷返回用戶態時
中斷返回用戶態
中斷的處理流程如下
在 arch\x86\entry\entry_32.S 文件中,定義了中斷處理
當發生中斷時,會跳轉到 irq_entries_start 處理中斷
// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202
ENTRY(irq_entries_start)
...
jmp common_interrupt
...
END(irq_entries_start)
irq_entries_start 會跳轉到 common_interrupt,其定義如下
// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202
common_interrupt:
...
call do_IRQ
...
jmp ret_from_intr
ENDPROC(common_interrupt)
common_interrupt 會調用 do_IRQ 來進行中斷處理,在中斷處理完成後,會調用 ret_from_intr 做中斷返回處理
ret_from_intr 的定義如下
// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202
ret_from_intr:
...
cmpl $USER_RPL, %eax
jb resume_kernel
ENTRY(resume_userspace)
...
call prepare_exit_to_usermode
...
END(ret_from_exception)
ENTRY(resume_kernel)
...
call preempt_schedule_irq
...
END(resume_kernel)
如果是返回用戶態,那麼會運行 resume_userspace,然後調用 prepare_exit_to_usermode。如果是返回內核態,那麼就跳轉到 resume_kernel,然後調用 preempt_schedule_irqs
這裏我們看返回用戶態的情況
其中的 prepare_exit_to_usermode 我們上面已經分析過了,最終它會檢查是否設置了 TIF_NEED_RESCHED 標誌,如果設置了,就調用 schedule 發生進程調度
關於用戶態的搶佔時機這裏就介紹完了,下面介紹內核態的搶佔時機
3.2 內核態的搶佔時機
內核態的搶佔有兩個時機
- 中斷返回內核態時
- 開啓搶佔時
由於上面我們已經分析了中斷了,所以這裏先看一下中斷,中斷的處理流程如下
當返回內核態的時候,調用的是 preempt_schedule_irq 函數,其定義如下
// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202
asmlinkage __visible void __sched preempt_schedule_irq(void)
{
...
do {
...
__schedule(true);
...
} while (need_resched());
...
}
在 preempt_schedule_irq 中,調用了 __schedule,你會發現這裏調用的是 __schedule 而不是 schedule,其實你可以簡單地認爲兩者是差不多的,在 schedule 函數中會調用 __schedule
接下倆看進程搶佔的另一個時機
開啓搶佔
在某些情況下需要通過 preempt_disable 來關閉搶佔,當處理完某件事後,會調用 preempt_enable 來重新開啓搶佔,preempt_enable 的定義如下
// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202
#define preempt_enable() \
do { \
barrier(); \
if (unlikely(preempt_count_dec_and_test())) \
__preempt_schedule(); \
} while (0)
preempt_enable 的調用流程如下
首先通過 preempt_count_dec_and_test 判斷是否開啓了搶佔和設置了 TIF_NEED_RESCHED 標誌,如果返回真,那麼就調用 __preempt_schedule,最後調用 __schedule 來發生進程調度
preempt_count_dec_and_test 的定義如下
#define preempt_count_dec_and_test() \
({ preempt_count_sub(1); should_resched(0); })
其中 preempt_count_sub 是判斷是否開啓了搶佔,should_resched 是判斷是否設置了 TIF_NEED_RESCHED 標誌,下面我們看一下 should_resched的定義
// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202
static __always_inline bool should_resched(int preempt_offset)
{
return unlikely(preempt_count() == preempt_offset &&
tif_need_resched());
}
其中的 tif_need_resched 就是判斷是否設置了 TIF_NEED_RESCHED 標誌,其定義如下
#define tif_need_resched() test_thread_flag(TIF_NEED_RESCHED)
可以看到其就是判斷是否設置了 TIF_NEED_RESCHED 標誌
接下來回到 preempt_enable 函數,我們接着看 __preempt_schedule,其定義如下
// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202
#define __preempt_schedule() preempt_schedule()
asmlinkage __visible void __sched notrace preempt_schedule(void)
{
...
preempt_schedule_common();
}
其中的 preempt_schedule_common 定義如下
// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202
static void __sched notrace preempt_schedule_common(void)
{
do {
...
__schedule(true);
...
} while (need_resched());
}
可以看到,最後調用了 __schedule 函數
好了,關於搶佔式調度就講解到這裏,下面進入總結時刻
四、總結
下面我們對整個進程調度時機來做一個總結
- 進程調度分爲主動調度和搶佔式調度
- 主動調度是進程主動調用 schedule 函數,搶佔式調度是 進程非自願調用 schedule 函數
- 搶佔式調度分爲兩步,第一步是設置 TIF_NEED_RESCHED 標誌,第二步是在某個時機調用 schedule 發生進程搶佔
- 設置 TIF_NEED_RESCHED 標誌的時機爲週期性調度器處理時還有喚醒進程時
- 檢測 TIF_NEED_RESCHED 並調用 schedule 的時機可分爲用戶態和內核態
- 用戶態搶佔時機有系統調用返回和中斷返回用戶態
- 內核態搶佔時機有開啓搶佔還有中斷返回內核態