中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Linux進程調度的邏輯是什么

發布時間:2022-02-07 09:45:29 來源:億速云 閱讀:138 作者:iii 欄目:建站服務器

這篇文章主要介紹“Linux進程調度的邏輯是什么”,在日常操作中,相信很多人在Linux進程調度的邏輯是什么問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Linux進程調度的邏輯是什么”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!

Linux進程調度的邏輯是什么

 pick_next_task():從就緒隊列中選中一個進程

內核在選擇進程進行調度的時候,會首先判斷當前 CPU 上是否有進程可以調度,如果沒有,執行進程遷移邏輯,從其他 CPU 遷移進程,如果有,則選擇虛擬時間較小的進程進行調度。

內核在選擇邏輯 CPU 進行遷移進程的時候,為了提升被遷移進程的性能,即避免遷移之后 L1 L2 L3 高速緩存失效,盡可能遷移那些和當前邏輯 CPU 共享高速緩存的目標邏輯 CPU,離當前邏輯 CPU 越近越好。

內核將進程抽象為調度實體,為的是可以將一批進程進行統一調度,在每一個調度層次上,都保證公平。

所謂選中高優進程,實際上選中的是虛擬時間較小的進程,進程的虛擬時間是根據進程的實際優先級和進程的運行時間等信息動態計算出來的。

 context_switch():執行上下文切換

進程上下文切換,核心要切換的是虛擬內存及一些通用寄存器。

進程切換虛擬內存,需要切換對應的 TLB 中的 ASID 及頁表,頁表也即不同進程的虛擬內存翻譯需要的 "map"。

進程的數據結構中,有一個間接字段 cpu_context 保存了通用寄存器的值,寄存器切換的本質就是將上一個進程的寄存器保存到 cpu_context 字段,然后再將下一個進程的 cpu_context 數據結構中的字段加載到寄存器中,至此完成進程的切換。

1 操作系統理論層面

學過操作系統的同學應該都知道,進程調度分為如下兩個步驟:

根據某種算法從就緒隊列中選中一個進程。

執行進程上下文切換。

其中第二個步驟又可以分為:

切換虛擬內存。

切換寄存器,即保存上一個進程的寄存器到進程的數據結構中,加載下一個進程的數據結構到寄存器中。

關于虛擬內存相關的邏輯,后續文章會詳細剖析,這篇文章會簡要概括。

這篇文章,我們從內核源碼的角度來剖析 Linux 是如何來實現進程調度的核心邏輯,基本上遵從操作系統理論。

2 調度主函數

Linux 進程調度的主函數是 schedule() 和 __schedule(),從源碼中可以看出兩者的關系:

// kernel/sched/core.c:3522
void schedule(void) {
    ...
    // 調度過程中禁止搶占
    preempt_disable(); 
    __schedule(false); //:3529
    // 調度完了,可以搶占了
    sched_preempt_enable_no_resched();
    ...
}
// kernel/sched/core.c:3395
__schedule(bool preempt) { 
    ... 
}

當一個進程主動讓出 CPU,比如 yield 系統調用,會執行 schedule() 方法,在執行進程調度的過程中,禁止其他進程搶占當前進程,說白了就是讓當前進程好好完成這一次進程切換,不要剝奪它的 CPU;

3529 行代碼表示 schedule() 調用了 __schedule(false) 方法,傳遞 fasle 參數,表示這是進程的一次主動調度,不可搶占。

等當前的進程執行完調度邏輯之后,開啟搶占,也就是說,其他進程可以剝奪當前進程的 CPU 了。

而如果某個進程像個強盜一樣一直占著 CPU 不讓,內核會通過搶占機制(比如上一篇文章提到的周期調度機制)進行一次進程調度,從而把當前進程從 CPU 上踢出去。

__schedule() 方法的框架便是這篇文章分析的主要內容,由于代碼較多,我會挑選核心部分來描述。

3 __schedule() 方法核心邏輯概覽

我們先來看看 Linux 內核中,進程調度核心函數 __schedule() 的框架:

// kernel/sched/core.c:3395
void __schedule(bool preempt) {
    struct task_struct *prev, *next;
    unsigned long *switch_count;
    struct rq *rq;
    int cpu;
    // 獲取當前 CPU
    cpu = smp_processor_id();
    // 獲取當前 CPU 上的進程隊列
    rq = cpu_rq(cpu);
    // 獲取當前隊列上正在運行的進程
    prev = rq->curr;
    ...
    // nivcsw:進程非主動切換次數
    switch_count = &prev->nivcsw; // :3430
    if (!preempt ...) {
        ...
        // nvcsw:進程主動切換次數 
        switch_count = &prev->nvcsw; // :3456
    }
    ...
    // 1 根據某種算法從就緒隊列中選中一個進程
    next = pick_next_task(rq, prev, &rf);
    ...
    if (prev != next) {
        rq->nr_switches++;
        rq->curr = next;
        ++*switch_count;
        // 2 執行進程上下文切換
        rq = context_switch(rq, prev, next, &rf);
    } 
    ...
}

可以看到,__schedule() 方法中,進程切換的核心步驟和操作系統理論中是一致的(1 和 2 兩個核心步驟)。

此外,進程切換的過程中,內核會根據是主動發起調度(preempt 為 fasle)還是被動發起調度,來統計進程上下文切換的次數,分別保存在進程的數據結構 task_struct 中:

// include/linux/sched.h:592
struct task_struct {
    ...
    // 主動切換:Number of Voluntary(自愿) Context Switches
    unsigned long nvcsw; // :811
    // 非主動切換:Number of InVoluntary(非自愿) Context Switches
    unsigned long nivcsw; // :812
    ...
};

在 Linux 中,我們可以通過 pidstat 命令來查看一個進程的主動和被動上下文切換的次數,我們寫一個簡單的 c 程序來做個測試:

// test.c
#include <stdlib.h>
#include <stdio.h>
int main() {
    while (1) {
        // 每隔一秒主動休眠一次
        // 即主動讓出 CPU
        // 理論上每秒都會主動切換一次
        sleep(1)
    }
}

然后編譯運行

gcc test.c -o test
./test

通過 pidstat 來查看

Linux進程調度的邏輯是什么

可以看到,test 應用程序每秒主動切換一次進程上下文,和我們的預期相符,對應的上下文切換數據就是從 task_struct 中獲取的。

接下來,詳細分析進程調度的兩個核心步驟:

通過 pick_next_task() 從就緒隊列中選中一個進程。

通過 context_switch 執行上下文切換。

4 pick_next_task():從就緒隊列中選中一個進程

我們回顧一下 pick_next_task() 在 __schedule() 方法中的位置

// kernel/sched/core.c:3395
void __schedule(bool preempt) {
    ...
    // rq 是當前 cpu 上的進程隊列
    next = pick_next_task(rq, pre ...); // :3459
    ...
}

跟著調用鏈往下探索:

// kernel/sched/core.c:3316
/* 
 * 找出優先級最高的進程
 * Pick up the highest-prio task:
 */
struct task_struct *pick_next_task(rq, pre ...) {
    struct task_struct *p;
    ...
    p = fair_sched_class.pick_next_task(rq, prev ...); // :3331
    ...
    if (!p)
        p = idle_sched_class.pick_next_task(rq, prev ...); // :3337
    return p;
}

從 pick_next_task() 方法的注釋上也能看到,這個方法的目的就是找出優先級最高的進程,由于系統中大多數進程的調度類型都是公平調度,我們拿公平調度相關的邏輯來分析。

從上述的核心框架中可以看到,3331 行先嘗試從公平調度類型的隊列中獲取進程,3337 行,如果沒有找到,就把每個 CPU 上的 IDLE 進程取出來運行:

// kernel/sched/idle.c:442
const struct sched_class idle_sched_class = {
    ...    
    .pick_next_task    = pick_next_task_idle, // :451
    ...
};
// kernel/sched/idle.c:385
struct task_struct * pick_next_task_idle(struct rq *rq ...) {
    ...
    // 每一個 CPU 運行隊列中都有一個 IDLE 進程 
    return rq->idle; // :383
}

接下來,我們聚焦公平調度類的進程選中算法 fair_sched_class.pick_next_task()

// kernel/sched/fair.c:10506
const struct sched_class fair_sched_class = {
   ...
   .pick_next_task = pick_next_task_fair, // : 10515 
   ...
}
// kernel/sched/fair.c:6898
static struct task_struct * pick_next_task_fair(struct rq *rq ...) {
    // cfs_rq 是當前 CPU 上公平調度類隊列
    struct cfs_rq *cfs_rq = &rq->cfs;
    struct sched_entity *se;
    struct task_struct *p;
    int new_tasks;
again:
    // 1 當前 CPU 進程隊列沒有進程可調度,則執行負載均衡邏輯
    if (!cfs_rq->nr_running) 
        goto idle;
    // 2. 當前 CPU 進程隊列有進程可調度,選中一個高優進程 p
    do {
        struct sched_entity *curr = cfs_rq->curr;
        ...
        se = pick_next_entity(cfs_rq, curr); 
        cfs_rq = group_cfs_rq(se);
    } while (cfs_rq); 
    p = task_of(se);
    ...
idle:
    // 通過負載均衡遷移進程
    new_tasks = idle_balance(rq, rf); // :7017
    ...
    
    if (new_tasks > 0)
        goto again;
    return NULL;
}

pick_next_task_fair() 的邏輯相對還是比較復雜的,但是,其中的核心思想分為兩步:

如果當前 CPU 上已無進程可調度,則執行負載邏輯,從其他 CPU 上遷移進程過來;

如果當前 CPU 上有進程可調度,從隊列中選擇一個高優進程,所謂高優進程,即虛擬時間最小的進程;

下面,我們分兩步拆解上述步驟。

4.1 負載均衡邏輯

內核為了讓各 CPU 負載能夠均衡,在某些 CPU 較為空閑的時候,會從繁忙的 CPU 上遷移進程到空閑 CPU 上運行,當然,如果進程設置了 CPU 的親和性,即進程只能在某些 CPU 上運行,則此進程無法遷移。

負載均衡的核心邏輯是 idle_balance 方法:

// kernel/sched/fair.c:9851
static int idle_balance(struct rq *this_rq ...) {
    int this_cpu = this_rq->cpu;
    struct sched_domain *sd;
    int pulled_task = 0;
    
    ...
    for_each_domain(this_cpu, sd) { // :9897
        ...
        pulled_task = load_balance(this_cpu...);
        ...
        if (pulled_task ...) // :9912
            break;
    }
    
    ...
    return pulled_task;
}

idle_balance() 方法的邏輯也相對比較復雜:但是大體上概括就是,遍歷當前 CPU 的所有調度域,直到遷移出進程位置。

這里涉及到一個核心概念:sched_domain,即調度域,下面用一張圖介紹一下什么是調度域。

Linux進程調度的邏輯是什么

內核根據處理器與主存的距離將處理器分為兩個 NUMA 節點,每個節點有兩個處理器。NUMA 指的是非一致性訪問,每個 NUMA 節點中的處理器訪問內存節點的速度不一致,不同 NUMA 節點之間不共享 L1 L2 L3 Cache。

每個 NUMA 節點下有兩個處理器,同一個 NUMA 下的不同處理器共享 L3 Cache。

每個處理器下有兩個 CPU 核,同個處理器下的不同核共享 L2 L3 Cache。

每個核下面有兩個超線程,同一個核的不同超線程共享 L1 L2 L3 Cache。

我們在應用程序里面,通過系統 API 拿到的都是超線程,也可以叫做邏輯 CPU,下文統稱邏輯 CPU。

進程在訪問一個某個地址的數據的時候,會先在 L1 Cache 中找,若未找到,則在 L2 Cache 中找,再未找到,則在 L3 Cache 上找,最后都沒找到,就訪問主存,而訪問速度方面 L1 > L2 > L3 > 主存,內核遷移進程的目標是盡可能讓遷移出來的進程能夠命中緩存。

內核按照上圖中被虛線框起來的部分,抽象出調度域的概念,越靠近上層,調度域的范圍越大,緩存失效的概率越大,因此,遷移進程的一個目標是,盡可能在低級別的調度域中獲取可遷移的進程。

上述代碼 idle_balance() 方法的 9897 行:for_each_domain(this_cpu, sd),this_cpu 就是邏輯 CPU(也即是最底層的超線程概念),sd 是調度域,這行代碼的邏輯就是逐層往上擴大調度域;

// kernel/sched/sched.h:1268
#define for_each_domain(cpu, __sd) \
    for (__sd = cpu_rq(cpu)->sd); \
            __sd; __sd = __sd->parent)

而 idle_balance() 方法的 9812 行,如果在某個調度域中,成功遷移出了進程到當前邏輯 CPU,就終止循環,可見,內核為了提升應用性能真是煞費苦心。

經過負載均衡之后,當前空閑的邏輯 CPU 進程隊列很有可能已經存在就緒進程了,于是,接下來從這個隊列中獲取最合適的進程。

4.2 選中高優進程

接下來,我們把重點放到如何選擇高優進程,而在公平調度類中,會通過進程的實際優先級和運行時間,來計算一個虛擬時間,虛擬時間越少,被選中的概率越高,所以叫做公平調度。

以下是選擇高優進程的核心邏輯:

// kernel/sched/fair.c:6898
static struct task_struct * pick_next_task_fair(struct rq *rq ...) {
    // cfs_rq 是當前 CPU 上公平調度類隊列
    struct cfs_rq *cfs_rq = &rq->cfs;
    struct sched_entity *se;
    struct task_struct *p;
    // 2. 當前 CPU 進程隊列有進程可調度,選中一個高優進程 p
    do {
        struct sched_entity *curr = cfs_rq->curr;
        ...
        se = pick_next_entity(cfs_rq, curr); 
        cfs_rq = group_cfs_rq(se);
    } while (cfs_rq); 
    // 拿到被調度實體包裹的進程
    p = task_of(se); // :6956
    ...
    return p;
}

內核提供一個調度實體的的概念,對應數據結構叫 sched_entity,內核實際上是根據調度實體為單位進行調度的:

// include/linux/sched.h:447
struct sched_entity {
    ...
    // 當前調度實體的父親
    struct sched_entity    *parent;
    // 當前調度實體所在隊列
    struct cfs_rq *cfs_rq;  // :468
    // 當前調度實體擁有的隊列,及子調度實體隊列
    // 進程是底層實體,不擁有隊列
    struct cfs_rq *my_q;
    ...
};

每一個進程對應一個調度實體,若干調度實體綁定到一起可以形成一個更高層次的調度實體,因此有個遞歸的效應,上述 do while 循環的邏輯就是從當前邏輯 CPU 頂層的公平調度實體(cfs_rq->curr)開始,逐層選擇虛擬時間較少的調度實體進行調度,直到最后一個調度實體是進程。

內核這樣做的原因是希望盡可能在每個層次上,都能夠保證調度是公平的。

拿 Docker 容器的例子來說,一個 Docker 容器中運行了若干個進程,這些進程屬于同一個調度實體,和宿主機上的進程的調度實體屬于同一層級,所以,如果 Docker 容器中瘋狂 fork 進程,內核會計算這些進程的虛擬時間總和來和宿主機其他進程進行公平抉擇,這些進程休想一直霸占著 CPU!

選擇虛擬時間最少的進程的邏輯是 se = pick_next_entity(cfs_rq, curr);  ,相應邏輯如下:

// kernel/sched/fair.c:4102
struct sched_entity *
pick_next_entity(cfs_rq *cfs_rq, sched_entity *curr) {
    // 公平運行隊列中虛擬時間最小的調度實體
    struct sched_entity *left = __pick_first_entity(cfs_rq);
    struct sched_entity *se;
    // 如果沒找到或者樹中的最小虛擬時間的進程
    // 還沒當前調度實體小,那就選擇當前實體
    if (!left || (curr && entity_before(curr, left))) 
        left = curr;
    se = left; 
    return se;
}
// kernel/sched/fair.c:489
int entity_before(struct sched_entity *a, struct sched_entity *b) {
    // 比較兩者虛擬時間
    return (s64)(a->vruntime - b->vruntime) < 0;
}

上述代碼,我們可以分析出,pick_next_entity() 方法會在當前公平調度隊列 cfs_rq 中選擇最靠左的調度實體,最靠左的調度實體的虛擬時間越小,即最優。

而下面通過 __pick_first_entity() 方法,我們了解到,公平調度隊列 cfs_rq 中的調度實體被組織為一棵紅黑樹,這棵樹的最左側節點即為最小節點:

// kernel/sched/fair.c:565
struct sched_entity *__pick_first_entity(struct cfs_rq *cfs_rq) {
    struct rb_node *left = rb_first_cached(&cfs_rq->tasks_timeline);
    if (!left)
        return NULL;
    return rb_entry(left, struct sched_entity, run_node);
}
// include/linux/rbtree.h:91
// 緩存了紅黑樹最左側節點
#define rb_first_cached(root) (root)->rb_leftmost

通過以上分析,我們依然通過一個 Docker 的例子來分析: 一個宿主機中有兩個普通進程分別為 A,B,一個 Docker 容器,容器中有 c1、c2、c3 進程。

這種情況下,系統中有兩個層次的調度實體,最高層為 A、B、c1+c2+c3,再往下為 c1、c2、c3,下面我們分情況來討論進程選中的邏輯:

1)若虛擬時間分布為:A:100s,B:200s,c1:50s,c2:100s,c3:80s

選中邏輯:先比較 A、B、c1+c2+c3 的虛擬時間,發現 A 最小,由于 A 已經是進程,選中 A,如果 A 比當前運行進程虛擬時間還小,下一個運行的進程就是 A,否則保持當前進程不變。

2)若虛擬時間分布為:A:100s,B:200s,c1:50s,c2:30s,c3:10s

選中邏輯:先比較 A、B、c1+c2+c3 的虛擬時間,發現 c1+c2+c3 最小,由于選中的調度實體非進程,而是一組進程,繼續往下一層調度實體進行選擇,比較 c1、c2、c3 的虛擬時間,發現 c3 的虛擬時間最小,如果 c3 的虛擬時間小于當前進程的虛擬時間,下一個運行的進程就是 c3,否則保持當前進程不變。

到這里,選中高優進程進行調度的邏輯就結束了,我們來做下小結。

4.3 pick_next_task() 小結

內核在選擇進程進行調度的時候,會先判斷當前 CPU 上是否有進程可以調度,如果沒有,執行進程遷移邏輯,從其他 CPU 遷移進程,如果有,則選擇虛擬時間較小的進程進行調度。

內核在選擇邏輯 CPU 進行遷移進程的時候,為了提升被遷移進程的性能,即避免遷移之后 L1 L2 L3 高速緩存失效,盡可能遷移那些和當前邏輯 CPU 共享高速緩存的目標邏輯 CPU,離當前邏輯 CPU 越近越好。

內核將進程抽象為調度實體,為的是可以將一批進程進行統一調度,在每一個調度層次上,都保證公平。

所謂選中高優進程,實際上選中的是虛擬時間較小的進程,進程的虛擬時間是根據進程的實際優先級和進程的運行時間等信息動態計算出來的。

5 context_switch():執行上下文切換

選中一個合適的進程之后,接下來就要執行實際的進程切換了,我們把目光重新聚焦到 __schedule() 方法

// kernel/sched/core.c:3395
void __schedule(bool preempt) {
    struct task_struct *prev, *next;
    ...
    // 1 根據某種算法從就緒隊列中選中一個進程
    next = pick_next_task(rq, prev,...); // :3459
    ...
    if (prev != next) {
        rq->curr = next;
        // 2 執行進程上下文切換
        rq = context_switch(rq, prev, next ...); // ::3485
    } 
    ...
}

其中,進程上下文切換的核心邏輯就是 context_switch,對應邏輯如下:

// kernel/sched/core.c:2804
struct rq *context_switch(... task_struct *prev, task_struct *next ...) {
    struct mm_struct *mm, *oldmm;
    ...
    mm = next->mm;
    oldmm = prev->active_mm;
    ...
    // 1 切換虛擬內存
    switch_mm_irqs_off(oldmm, mm, next);
    ...
    // 2 切換寄存器狀態
    switch_to(prev, next, prev);
    ...
}

上述代碼,我略去了一些細節,保留我們關心的核心邏輯。context_switch() 核心邏輯分為兩個步驟,切換虛擬內存和寄存器狀態,下面,我們展開這兩段邏輯。

5.1 切換虛擬內存

首先,簡要介紹一下虛擬內存的幾個知識點:

進程無法直接訪問到物理內存,而是通過虛擬內存到物理內存的映射機制間接訪問到物理內存的。

每個進程都有自己獨立的虛擬內存地址空間。如,進程 A 可以有一個虛擬地址 0x1234 映射到物理地址 0x4567,進程 B 也可以有一個虛擬地址 0x1234 映射到 0x3456,即不同進程可以有相同的虛擬地址。如果他們指向的物理內存相同,則兩個進程即可通過內存共享進程通信。

進程通過多級頁表機制來執行虛擬內存到物理內存的映射,如果我們簡單地把這個機制當做一個 map<虛擬地址,物理地址> 數據結構的話,那么可以理解為不同的進程有維護著不同的 map;

map<虛擬地址,物理地址> 的翻譯是通過多級頁表來實現的,訪問多級頁表需要多次訪問內存,效率太差,因此,內核使用 TLB 緩存頻繁被訪問的 <虛擬地址,物理地址> 的項目,感謝局部性原理。

由于不同進程可以有相同的虛擬地址,這些虛擬地址往往指向了不同的物理地址,因此,TLB 實際上是通過 <ASID + 虛擬地址,物理地址> 的方式來唯一確定某個進程的物理地址的,ASID 叫做地址空間 ID(Address Space ID),每個進程唯一,等價于多租戶概念中的租戶 ID。

進程的虛擬地址空間用數據結構 mm_struct 來描述,進程數據結構 task_struct 中的 mm 字段就指向此數據結構,而上述所說的進程的 "map" 的信息就藏在 mm_struct 中。

關于虛擬內存的介紹,后續的文章會繼續分析,這里,我們只需要了解上述幾個知識點即可,我們進入到切換虛擬內存核心邏輯:

// include/linux/mmu_context.h:14
# define switch_mm_irqs_off switch_mm
// arch/arm64/include/asm/mmu_context.h:241
void switch_mm(mm_struct *prev, mm_struct *next) {
    // 如果兩個進程不是同一個進程
    if (prev != next)
        __switch_mm(next);
    ...
}
// arch/arm64/include/asm/mmu_context.h:224
void __switch_mm(struct mm_struct *next) {
    unsigned int cpu = smp_processor_id();
    check_and_switch_context(next, cpu);
}

接下來,調用 check_and_switch_context 做實際的虛擬內存切換操作:

// arch/arm64/mm/context.c:194
void check_and_switch_context(struct mm_struct *mm, unsigned int cpu) {
    ...
    u64 asid;
    // 拿到要將下一個進程的 ASID
    asid = atomic64_read(&mm->context.id); // :218
    ...
    // 將下一個進程的 ASID 綁定到當前 CPU
    atomic64_set(&per_cpu(active_asids, cpu), asid);  // :236
    // 切換頁表,及切換我們上述中的 "map",
    // 將 ASID 和 "map" 設置到對應的寄存器
    cpu_switch_mm(mm->pgd, mm); // :248
}

check_and_switch_context 總體上分為兩塊邏輯:

  • 將下一個進程的 ASID 綁定到當前的 CPU,這樣 TLB 通過虛擬地址翻譯出來的物理地址,就屬于下個進程的。

  • 拿到下一個進程的 "map",也就是頁表,對應的字段是 "mm->pgd",然后執行頁表切換邏輯,這樣后續如果 TLB 沒命中,當前 CPU 就能夠知道通過哪個 "map" 來翻譯虛擬地址。

cpu_switch_mm 涉及的匯編代碼較多,這里就不貼了,本質上就是將 ASID 和頁表("map")的信息綁定到對應的寄存器。

5.2 切換通用寄存器

虛擬內存切換完畢之后,接下來切換進程執行相關的通用寄存器,對應邏輯為 switch_to(prev, next ...); 方法,這個方法也是切換進程的分水嶺,調用完之后的那一刻,當前 CPU 上執行就是 next 的代碼了。

拿 arm64 為例:

// arch/arm64/kernel/process.c:422
struct task_struct *__switch_to(task_struct *prev, task_struct *next) {
    ...
    // 實際切換方法
    cpu_switch_to(prev, next); // :444
    ...
}

cpu_switch_to 對應的是一段經典的匯編邏輯,看著很多,其實并不難理解。

// arch/arm64/kernel/entry.S:1040
// x0 -> pre
// x1 -> next
ENTRY(cpu_switch_to)
    // x10 存放 task_struct->thread.cpu_context 字段偏移量
    mov    x10, #THREAD_CPU_CONTEXT // :1041
    
    // ===保存 pre 上下文===
    // x8 存放 prev->thread.cpu_context
    add    x8, x0, x10 
    // 保存 prev 內核棧指針到 x9
    mov    x9, sp
    // 將 x19 ~ x28 保存在 cpu_context 字段中
    // stp 是 store pair 的意思
    stp    x19, x20, [x8], #16
    stp    x21, x22, [x8], #16
    stp    x23, x24, [x8], #16
    stp    x25, x26, [x8], #16
    stp    x27, x28, [x8], #16
    // 將 x29 存在 fp 字段,x9 存在 sp 字段
    stp    x29, x9, [x8], #16 
    // 將 pc 寄存器 lr 保存到 cpu_context 的 pc 字段
    str    lr, [x8] 
    
    
    // ===加載 next 上下文===
    // x8 存放 next->thread.cpu_context
    add    x8, x1, x10
    // 將 cpu_context 中字段載入到 x19 ~ x28
    // ldp 是 load pair 的意思
    ldp    x19, x20, [x8], #16
    ldp    x21, x22, [x8], #16
    ldp    x23, x24, [x8], #16
    ldp    x25, x26, [x8], #16
    ldp    x27, x28, [x8], #16
    ldp    x29, x9, [x8], #16
    // 設置 pc 寄存器
    ldr    lr, [x8]
    // 切換到 next 的內核棧
    mov    sp, x9
    
    // 將 next 指針保存到 sp_el0 寄存器
    msr    sp_el0, x1 
    ret
ENDPROC(cpu_switch_to)

上述匯編的邏輯可以和操作系統理論課里的內容一一對應,即先將通用寄存器的內容保存到進程的數據結構中對應的字段,然后再從下一個進程的數據結構中對應的字段加載到通用寄存器中。

1041 行代碼是拿到 task_struct 結構中的 thread_struct thread 字段的 cpu_contxt 字段:

// arch/arm64/kernel/asm-offsets.c:53
DEFINE(THREAD_CPU_CONTEXT,    offsetof(struct task_struct, thread.cpu_context));

我們來分析一下對應的數據結構:

// include/linux/sched.h:592
struct task_struct {
    ...
    struct thread_struct thread; // :1212
    ...
};
// arch/arm64/include/asm/processor.h:129
struct thread_struct {
    struct cpu_context  cpu_context;
    ...
}

而 cpu_context 數據結構的設計就是為了保存與進程有關的一些通用寄存器的值:

// arch/arm64/include/asm/processor.h:113
struct cpu_context {
    unsigned long x19;
    unsigned long x20;
    unsigned long x21;
    unsigned long x22;
    unsigned long x23;
    unsigned long x24;
    unsigned long x25;
    unsigned long x26;
    unsigned long x27;
    unsigned long x28;
    // 對應 x29 寄存器
    unsigned long fp;
    unsigned long sp;
    // 對應 lr 寄存器
    unsigned long pc;
};

這些值剛好與上述匯編片段的代碼一一對應上,讀者應該不需要太多匯編基礎就可以分析出來。

上述匯編中,最后一行 msr sp_el0, x1,x1 寄存器中保存了 next 的指針,這樣后續再調用 current 宏的時候,就指向了下一個指針:

// arch/arm64/include/asm/current.h:15
static struct task_struct *get_current(void) {
    unsigned long sp_el0;
    asm ("mrs %0, sp_el0" : "=r" (sp_el0));
    return (struct task_struct *)sp_el0;
}
// current 宏,很多地方會使用到
#define current get_current()

進程上下文切換的核心邏輯到這里就結束了,最后我們做下小結。

5.3 context_switch() 小結

進程上下文切換,核心要切換的是虛擬內存及一些通用寄存器。

進程切換虛擬內存,需要切換對應的 TLB 中的 ASID 及頁表,頁表也即不同進程的虛擬內存翻譯需要的 "map"。

進程的數據結構中,有一個間接字段 cpu_context 保存了通用寄存器的值,寄存器切換的本質就是將上一個進程的寄存器保存到 cpu_context 字段,然后再將下一個進程的 cpu_context 數據結構中的字段加載到寄存器中,至此完成進程的切換。

到此,關于“Linux進程調度的邏輯是什么”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

禹州市| 洛南县| 成武县| 崇明县| 阳高县| 平山县| 金沙县| 灌云县| 广汉市| 襄汾县| 巨野县| 宜城市| 平凉市| 铜山县| 临安市| 松潘县| 边坝县| 湟中县| 于田县| 隆化县| 台北市| 水富县| 临邑县| 河源市| 龙南县| 嘉定区| 塔城市| 岑溪市| 清苑县| 天峨县| 安平县| 青川县| 东山县| 宁明县| 赣州市| 麻江县| 桃园市| 贵港市| 冕宁县| 股票| 思茅市|