Linux進程管理(四)進程調度之搶佔式調度

Linux進程管理

Linux進程管理(一)進程數據結構

Linux進程管理(二)進程調度

Linux進程管理(三)進程調度之主動調度

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 的時機可分爲用戶態內核態
  • 用戶態搶佔時機有系統調用返回中斷返回用戶態
  • 內核態搶佔時機有開啓搶佔還有中斷返回內核態
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章