【原創】(三)Linux進程調度器-進程切換


背景

  • Read the fucking source code! --By 魯迅
  • A picture is worth a thousand words. --By 高爾基

說明:

  1. Kernel版本:4.14
  2. ARM64處理器,Contex-A53,雙核
  3. 使用工具:Source Insight 3.5, Visio

1. 概述

進程切換:內核將CPU上正在運行的進程掛起,選擇下一個進程來運行。
ARM架構中,CPU上一次只能運行一個任務,內核需要為任務分配運行時間來進行調度,以便同時能處理多個任務請求。
如下圖所示:

當進行任務切換的時候,思考下兩個問題:

  1. 怎樣通過搶占來實現進程的切換?
  2. 當進程切換的時候,到底切換的什么,是怎么實現的?

這兩個問題,也是本文探討的主題了。

2. 搶占

2.1 用戶搶占

2.1.1 搶占觸發點

  • 可以觸發搶占的情況很多,比如進程的時間片耗盡、進程等待在某些資源上被喚醒時、進程優先級改變等。Linux內核是通過設置TIF_NEED_RESCHED標志來對進程進行標記的,設置該位則表明需要進行調度切換,而實際的切換將在搶占執行點來完成。

不看代碼來講結論,那都是耍流氓。先看一下兩個關鍵結構體:struct task_structstruct thread_info。我們在前邊的文章中也講過struct task_struct用於描述任務,該結構體的首個字段放置的正是struct thread_infostruct thread_info結構體中flag字段就可用於設置TIF_NEED_RESCHED,此外該結構體中的preempt_count也與搶占相關。

struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
	/*
	 * For reasons of header soup (see current_thread_info()), this
	 * must be the first element of task_struct.
	 */
	struct thread_info		thread_info;
#endif
        ...
}

/*
 * low level task data that entry.S needs immediate access to.
 */
struct thread_info {
	unsigned long		flags;		/* low level flags */
	mm_segment_t		addr_limit;	/* address limit */
#ifdef CONFIG_ARM64_SW_TTBR0_PAN
	u64			ttbr0;		/* saved TTBR0_EL1 */
#endif
	int			preempt_count;	/* 0 => preemptable, <0 => bug */
};

#include <asm/current.h>
#define current_thread_info() ((struct thread_info *)current)   //通過該宏可以直接獲取thread_info的信息
#endif

看看具體哪些函數過程中,設置了TIF_NEED_RESCHED標志吧:

  • 內核提供了set_tsk_need_resched函數來將thread_infoflag字段設置成TIF_NEED_RESCHED
  • 設置了TIF_NEED_RESCHED標志,表明需要發生搶占調度;

2.1.2 搶占執行點

用戶搶占:搶占執行發生在進程處於用戶態。
搶占的執行,最明顯的標志就是調用了schedule()函數,來完成任務的切換。
具體來說,在用戶態執行搶占在以下幾種情況:

  • 異常處理后返回到用戶態;
  • 中斷處理后返回到用戶態;
  • 系統調用后返回到用戶態;

如下圖:

  • ARMv8有4個Exception Level,其中用戶程序運行在EL0,OS運行在EL1,Hypervisor運行在EL2,Secure monitor運行在EL3;
  • 用戶程序在執行過程中,遇到異常或中斷后,將會跳到ENTRY(vectors)向量表處開始執行;
  • 返回用戶空間時進行標志位判斷,設置了TIF_NEED_RESCHED則需要進行調度切換,沒有設置該標志,則檢查是否有收到信號,有信號未處理的話,還需要進行信號的處理操作;

2.2 內核搶占

Linux內核有三種內核搶占模型,先上圖:

  • CONFIG_PREEMPT_NONE:不支持搶占,中斷退出后,需要等到低優先級任務主動讓出CPU才發生搶占切換;
  • CONFIG_PREEMPT_VOLUNTARY:自願搶占,代碼中增加搶占點,在中斷退出后遇到搶占點時進行搶占切換;
  • CONFIG_PREEMPT:搶占,當中斷退出后,如果遇到了更高優先級的任務,立即進行任務搶占;

2.2.1 搶占觸發點

  • 在內核中搶占觸發點,也是設置struct thread_infoflag字段,設置TIF_NEED_RESCHED表明需要請求重新調度。
  • 搶占觸發點的幾種情況,在用戶搶占中已經分析過,不管是用戶搶占還是內核搶占,觸發點都是一致的;

2.2.2 搶占執行點

內核搶占:搶占執行發生在進程處於內核態。

總體而言,內核搶占執行點可以歸屬於兩大類:

  • 中斷執行完畢后進行搶占調度;
  • 主動調用preemp_enableschedule等接口的地方進行搶占調度;

2.3 preempt_count

  • Linux內核中使用struct thread_info中的preempt_count字段來控制搶占。
  • preempt_count的低8位用於控制搶占,當大於0時表示不可搶占,等於0表示可搶占。
  • preempt_enable()會將preempt_count值減1,並判斷是否需要進行調度,在條件滿足時進行切換;
  • preempt_disable()會將preempt_count值加1;

此外,preemt_count字段還用於判斷進程處於各類上下文以及開關控制等,如圖:

3. 上下文切換

  • 進程上下文:包含CPU的所有寄存器值、進程的運行狀態、堆棧中的內容等,相當於進程某一時刻的快照,包含了所有的軟硬件信息;
  • 進程切換時,完成的就是上下文的切換,進程上下文的信息會保存在每個struct task_struct結構體中,以便在切換時能完成恢復工作;

進程上下文切換的入口就是__schedule(),分析也圍繞這函數展開。

3.1 __schedule()

__schedule()函數調用分析如下:

主要的邏輯:

  • 根據CPU獲取運行隊列,進而得到運行隊列當前的task,也就是切換前的prev;
  • 根據prev的狀態進行處理,比如pending信號的處理等,如果該任務是一個worker線程還需要將其睡眠,並喚醒同CPU上的另一個worker線程;
  • 根據調度類來選擇需要切換過去的下一個task,也就是next
  • context_switch完成進程的切換;

3.2 context_switch()

context_switch()的調用分析如下:

核心的邏輯有兩部分:

  • 進程的地址空間切換:切換的時候要判斷切入的進程是否為內核線程,1)所有的用戶進程都共用一個內核地址空間,而擁有不同的用戶地址空間;2)內核線程本身沒有用戶地址空間。在進程在切換的過程中就需要對這些因素來考慮,涉及到頁表的切換,以及cache/tlb的刷新等操作。
  • 寄存器的切換:包括CPU的通用寄存器切換、浮點寄存器切換,以及ARM處理器相關的其他一些寄存器的切換;

進程的切換,帶來的開銷不僅是頁表切換和硬件上下文的切換,還包含了Cache/TLB刷新后帶來的miss的開銷。在實際的開發中,也需要去評估新增進程帶來的調度開銷。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM