【原創】(三)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. 當進程切換的時候,到底切換的什麼,是怎麼實現的?

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

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標誌吧:3d

  • 內核提供了set_tsk_need_resched函數來將thread_infoflag字段設置成TIF_NEED_RESCHED
  • 設置了TIF_NEED_RESCHED標誌,代表須要發生搶佔調度;

2.1.2 搶佔執行點

用戶搶佔:搶佔執行發生在進程處於用戶態。
搶佔的執行,最明顯的標誌就是調用了schedule()函數,來完成任務的切換。
具體來講,在用戶態執行搶佔在如下幾種狀況:code

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

以下圖:blog

  • 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的開銷。在實際的開發中,也須要去評估新增進程帶來的調度開銷。

相關文章
相關標籤/搜索