當前位置:首頁 » 操作系統 » linuxswitchto

linuxswitchto

發布時間: 2023-07-20 05:26:30

linux內核怎麼調度系統

1.調度器的概述

多任務操作系統分為非搶占式多任務和搶占式多任務。與大多數現代操作系統一樣,Linux採用的是搶占式多任務模式。這表示對CPU的佔用時間由操作系統決定的,具體為操作系統中的調度器。調度器決定了什麼時候停止一個進程以便讓其他進程有機會運行,同時挑選出一個其他的進程開始運行。

2.調度策略

在Linux上調度策略決定了調度器是如何選擇一個新進程的時間。調度策略與進程的類型有關,內核現有的調度策略如下:

#define SCHED_NORMAL 0#define SCHED_FIFO 1#define SCHED_RR 2#define SCHED_BATCH 3/* SCHED_ISO: reserved but not implemented yet */#define SCHED_IDLE 5

0: 默認的調度策略,針對的是普通進程。
1:針對實時進程的先進先出調度。適合對時間性要求比較高但每次運行時間比較短的進程。
2:針對的是實時進程的時間片輪轉調度。適合每次運行時間比較長得進程。
3:針對批處理進程的調度,適合那些非交互性且對cpu使用密集的進程。
SCHED_ISO:是內核的一個預留欄位,目前還沒有使用
5:適用於優先順序較低的後台進程。
註:每個進程的調度策略保存在進程描述符task_struct中的policy欄位

3.調度器中的機制

內核引入調度類(struct sched_class)說明了調度器應該具有哪些功能。內核中每種調度策略都有該調度類的一個實例。(比如:基於公平調度類為:fair_sched_class,基於實時進程的調度類實例為:rt_sched_class),該實例也是針對每種調度策略的具體實現。調度類封裝了不同調度策略的具體實現,屏蔽了各種調度策略的細節實現。
調度器核心函數schele()只需要調用調度類中的介面,完成進程的調度,完全不需要考慮調度策略的具體實現。調度類連接了調度函數和具體的調度策略。

  • 武特師兄關於sche_class和sche_entity的解釋,一語中的。

  • 調度類就是代表的各種調度策略,調度實體就是調度單位,這個實體通常是一個進程,但是自從引入了cgroup後,這個調度實體可能就不是一個進程了,而是一個組

  • 4.schele()函數

    linux 支持兩種類型的進程調度,實時進程和普通進程。實時進程採用SCHED_FIFO 和SCHED_RR調度策略,普通進程採用SCHED_NORMAL策略。
    preempt_disable():禁止內核搶占
    cpu_rq():獲取當前cpu對應的就緒隊列。
    prev = rq->curr;獲取當前進程的描述符prev
    switch_count = &prev->nivcsw;獲取當前進程的切換次數。
    update_rq_clock() :更新就緒隊列上的時鍾
    clear_tsk_need_resched()清楚當前進程prev的重新調度標志。
    deactive_task():將當前進程從就緒隊列中刪除。
    put_prev_task() :將當前進程重新放入就緒隊列
    pick_next_task():在就緒隊列中挑選下一個將被執行的進程。
    context_switch():進行prev和next兩個進程的切換。具體的切換代碼與體系架構有關,在switch_to()中通過一段匯編代碼實現。
    post_schele():進行進程切換後的後期處理工作。

    5.pick_next_task函數

    選擇下一個將要被執行的進程無疑是一個很重要的過程,我們來看一下內核中代碼的實現
    對以下這段代碼說明:
    1.當rq中的運行隊列的個數(nr_running)和cfs中的nr_runing相等的時候,表示現在所有的都是普通進程,這時候就會調用cfs演算法中的pick_next_task(其實是pick_next_task_fair函數),當不相等的時候,則調用sched_class_highest(這是一個宏,指向的是實時進程),這下面的這個for(;;)循環中,首先是會在實時進程中選取要調度的程序(p = class->pick_next_task(rq);)。如果沒有選取到,會執行class=class->next;在class這個鏈表中有三種類型(fair,idle,rt).也就是說會調用到下一個調度類。

  • static inline struct task_struct *pick_next_task(struct rq *rq){ const struct sched_class *class; struct task_struct *p; /*

  • * Optimization: we know that if all tasks are in

  • * the fair class we can call that function directly:

  • *///基於公平調度的普通進程

  • if (likely(rq->nr_running == rq->cfs.nr_running)) {

  • p = fair_sched_class.pick_next_task(rq); if (likely(p)) return p;

  • }//基於實時調度的實時進程

  • class = sched_class_highest; for ( ; ; ) {

  • p = class->pick_next_task(rq); //實時進程的類

  • if (p) return p; /*

  • * Will never be NULL as the idle class always

  • * returns a non-NULL p:

  • */

  • class = class->next; //rt->next = fair; fair->next = idle

  • }

  • }

  • 在這段代碼中體現了Linux所支持的兩種類型的進程,實時進程和普通進程。回顧下:實時進程可以採用SCHED_FIFO 和SCHED_RR調度策略,普通進程採用SCHED_NORMAL調度策略。
    在這里首先說明一個結構體struct rq,這個結構體是調度器管理可運行狀態進程的最主要的數據結構。每個cpu上都有一個可運行的就緒隊列。剛才在pick_next_task函數中看到了在選擇下一個將要被執行的進程時實際上用的是struct rq上的普通進程的調度或者實時進程的調度,那麼具體是如何調度的呢?在實時調度中,為了實現O(1)的調度演算法,內核為每個優先順序維護一個運行隊列和一個DECLARE_BITMAP,內核根據DECLARE_BITMAP的bit數值找出非空的最高級優先隊列的編號,從而可以從非空的最高級優先隊列中取出進程進行運行。
    我們來看下內核的實現

  • struct rt_prio_array {

  • DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* include 1 bit for delimiter */

  • struct list_head queue[MAX_RT_PRIO];

  • };

  • 數組queue[i]裡面存放的是優先順序為i的進程隊列的鏈表頭。在結構體rt_prio_array 中有一個重要的數據構DECLARE_BITMAP,它在內核中的第一如下:

  • define DECLARE_BITMAP(name,bits)

  • unsigned long name[BITS_TO_LONGS(bits)]

  • 5.1對於實時進程的O(1)演算法

    這個數據是用來作為進程隊列queue[MAX_PRIO]的索引點陣圖。bitmap中的每一位與queue[i]對應,當queue[i]的進程隊列不為空時,Bitmap的相應位就為1,否則為0,這樣就只需要通過匯編指令從進程優先順序由高到低的方向找到第一個為1的位置,則這個位置就是就緒隊列中最高的優先順序(函數sched_find_first_bit()就是用來實現該目的的)。那麼queue[index]->next就是要找的候選進程。
    如果還是不懂,那就來看兩個圖

    由結果可以看出當nice的值越小的時候,其睡眠時間越短,則表示其優先順序升高了。

    7.關於獲取和設置優先順序的系統調用:sched_getscheler()和sched_setscheler

  • #include <sched.h>#include <stdlib.h>#include <stdio.h>#include <errno.h>#define DEATH(mess) { perror(mess); exit(errno); }void printpolicy (int policy){ /* SCHED_NORMAL = SCHED_OTHER in user-space */


  • if (policy == SCHED_OTHER) printf ("policy = SCHED_OTHER = %d ", policy); if (policy == SCHED_FIFO) printf ("policy = SCHED_FIFO = %d ", policy); if (policy == SCHED_RR) printf ("policy = SCHED_RR = %d ", policy);

  • }int main (int argc, char **argv){ int policy; struct sched_param p; /* obtain current scheling policy for this process */

  • //獲取進程調度的策略

  • policy = sched_getscheler (0);

  • printpolicy (policy); /* reset scheling policy */


  • printf (" Trying sched_setscheler... ");

  • policy = SCHED_FIFO;

  • printpolicy (policy);

  • p.sched_priority = 50; //設置優先順序為50

  • if (sched_setscheler (0, policy, &p))

  • DEATH ("sched_setscheler:"); printf ("p.sched_priority = %d ", p.sched_priority); exit (0);

  • }

  • 輸出結果:

  • [root@wang schele]# ./get_schele_policy policy = SCHED_OTHER = 0


  • Trying sched_setscheler...

  • policy = SCHED_FIFO = 1

  • p.sched_priority = 50

  • 可以看出進程的優先順序已經被改變。

Ⅱ linux 怎樣安裝軟體

Linux下軟體安裝方法

對於Linux初學者來說,安裝一個很小的軟體恐怕都是一件很讓人頭疼的事,因為在Linux下安裝軟體不像在 Windows中那樣簡單。在Linux中大多數軟體提供的是源代碼,而不是現成的可執行文件,這就要求用戶根據自己系統的實際情況和自身的需要來配置、編譯源程序後,軟體才能使用。多數初學者往往不知道該如何進行配置和編譯就盲目地運行一些有執行屬性的文件或者機械地運行「make」、「make install」之類的命令。結果呢?是軟體沒裝成,自己急出一身汗,後果嚴重的還會破壞系統的穩定性。下面筆者將安裝軟體方面的一些規律寫出來與大家分享。

目前流行的軟體包有兩種比較常見的形式,一種是RPM包的形式,另一種是壓縮成*.tar.gz的形式。本文將討論這兩種形式的軟體包在文本環境和圖形環境(X Window)下不同的安裝方法。

文本環境下的軟體安裝

一、安裝簡便的RPM包

RPM是RedHat Package Manager的縮寫,它只能使用在安裝了RPM軟體的系統中,RedHat Linux和Turbo Linux中已經使用了它。這種結構的包使用起來還是很方便的,只要記住幾條簡單的命令和參數就可以方便地使用:

#〉rpm [options] filename.rpm

其中常用的options包括:

-i: 安裝軟體

-e: 卸載軟體

-q:查看軟體安裝的信息和狀態

-U: 升級現有軟體

例如安裝軟體時,可以使用如下命令:「rpm -i filename.rpm」,軟體安裝在什麼地方、是怎麼安裝的都不需要用戶操心,RPM可以幫助用戶管理。由於RPM使用方便,很多軟體都有RPM版本。如果想使用RPM形式的軟體就要首先下載一個RPM管理軟體。在ftp://ftp.rpm.org/pub/rpm/dist/rpm- version網站可以下載最新的版本——rpm-3.-.4.i386。該軟體有RPM包和.tar.gz包兩種形式,如果你的系統中已經有了RPM管理軟體,你可以下載RPM形式的包來升級現有軟體,否則就必須了解.tar.gz包的安裝方法。

二、安裝需要編譯的.tar.gz包

由於RPM包使用的局限性(必須安裝RPM),目前更多的軟體使用的是源代碼形式的.tar.gz包。這種軟體的安裝通常要經過解壓縮、軟體配置、軟體編譯及安裝的過程。

解壓縮通常有兩種命令方法:一種是「gunzip filename-VERSION-OS.tar.gz | tar xvf -」,它實質是兩條命令「gunzip filename-VERSION-OS.tar.gz」和「tar xvf filename-VERSION-OS.tar」;另一種是「tar xzvf filename-VERSION-OS.tar.gz」。一般來說在軟體解壓縮後會生成一個目錄filename-VERSION-OS。

軟體的配置、編譯、安裝是最讓初學者望而生畏的事了,但筆者認為掌握一些規律還是不難的。一般來說,在解壓縮生成的目錄中都會有名為Readme、 Rnstall或Readme.install之類的文件。這些文件通常會對軟體的功能、特性、版權許可、安裝以及相關知識加以介紹,並且會提到關於安裝的方法和步驟。舉例來說:在apache_1.3.6的install文件中說明了如下內容(此處只列出總的條目,具體內容省略):

Installing the Apache 1.3 HTTP server with APACI

==============================

1.Overview for the impatient(概括說明配置的全過程)

$./configure--prefix=PREFIX

$make

$make install

$PREFIX/bin/apachectl start

2.Requirements(需要的條件)

3.Configuring the source tree(配置的參數說明)

4.Building the package(編譯軟體的方法)

5.Installing the package(安裝軟體的方法)

6.Testing the package(軟體測試)

理解並能熟練使用這些說明文件後,就可以利用一些規律來安裝大多數的軟體。對於那些沒有說明文件的軟體(當然這種情況比較少見),這些規律通常也是適用的。一般來說,與安裝軟體有直接關系的文件只有兩個:configure 、Makefile。

其中,configure文件具有可執行的屬性,是用來配置軟體的,它的參數比較多,用法也比較靈活。當然,不同的軟體參數也不相同,這時候就需要藉助它的help參數,運行下面的命令就會讓你感到豁然開朗:

#〉 ./configure -help

Usage: configure [options]

Options: [defaults in brackets after descriptions]

General options:

--quiet, --silent do not print messages

--verbose,-v print even more messages

--sha [=DIR] switch to a shadow tree (under DIR) for building

Stand-alone options:

--help,-h print this message

--show-layout print installation path layout (check and debug)


Installation layout options:

--with-layout=[F:]ID use installation path layout ID (from file F)

--target=TARGET install name-associated files using basename TARGET

……

接下來,就可以運行「./configure [options]」來配置該軟體。注意,命令行中的「./」非常重要,它告訴系統要運行的命令就在當前目錄下(否則系統就會到$path變數指定的路徑下去執行命令)。執行命令後可以生成Makefile文件或者修改已有的文件配置。

Makefile文件通常是用來編譯和安裝軟體的。運行make命令時系統會自動根據Makefile文件中的設置對軟體進行編譯和安裝。make命令有時還可以帶一些參數,如:all、build、config、install等。具體要帶哪個參數可以參看Makefile文件。在Linux中絕大部分文件是文本文件,Makefile就是一個shell程序(Linux中shell程序與DOS中的批處理文件有很多相似之處,當然功能要強得多),很容易讀懂,尤其是編譯時可帶的參數都會明確寫出,例如:

##========================

## Targets

##========================

# default target

all: build

##------------------------

## Build Target

##------------------------

# build the package

build:

……

# the non-verbose variant for package maintainers

build-quiet:

@$(MAKE) -f $(TOP)/$(MKF) $(MFLAGS) $(MFWD) QUIET=1 build

# build the additional support stuff

build-support:

……

##------------------------

## Installation Targets

## -----------------------

# the install target for installing the complete Apache

# package. This is implemented by running subtargets for the

# separate parts of the installation process.

install:

……

# the non-verbose variant for package maintainers

install-quiet:

@$(MAKE) -f $(TOP)/$(MKF) $(MFLAGS) $(MFWD) QUIET=1 install

# create the installation tree

install-mktree:

……

上面這段代碼是apache_1.3.6的Makefile文件的一部分,從這段程序可以看出all參數表示完全編譯(預設參數)。此外,編譯時還可以帶 build、build-quiet、build-surpport等參數;安裝時可以帶install、install-quiet、install- surpport等參數。它們的功能分別在「#」表示的注釋中進行了說明。需要額外說明的是,有些軟體(例如Linux的內核升級程序)不用 configure命令來配置軟體,而是用make config來完成這項工作,所以,具體使用哪種方法要具體問題具體分析。

圖形界面下的軟體安裝

在圖形環境下,同樣可以彈出一個模擬終端以文本的方法來安裝軟體,但那樣就太笨了,因為在X Window中有一些簡單的方法可以幫您完成軟體安裝。下面筆者以RedHat 6.0為例做介紹。

一、圖形界面下安裝.tar.gz包

在X Window下,安裝這種形式的包簡化程度並不大,只是在解壓縮軟體時方便一些。用滑鼠左鍵雙擊要安裝的軟體包,系統就會自動生成一個目錄—— filename.tar.gz#utar,在這個目錄下就有你要解壓縮的內容——filename目錄。將該目錄拷貝到你要解壓縮的目錄下,解壓縮的工作就這樣簡單地完成了。不過,剩下的工作還要彈出一個模擬終端以文本的方法來完成。

二、圖形界面下安裝RPM包

在X Window中你要安裝、升級、卸載和查詢一個RPM軟體包實在是太容易了。以Redhat 6.0為例,它的X Window中有一個Gnome RPM軟體可以完成上面提到的一切工作。

點擊「Start」→「System」→「GnomeRPM」,可以運行該軟體。它將安裝好的RPM包形式的軟體按照功能分在Amusements、 Application、Development、Document、Extension、Extentions、Networking、System Environment、UserInterface、X11等幾個樹形目錄中,每個目錄中有相應的文件圖標和名稱。要安裝或升級軟體,只要點擊工具欄的相應按鈕就會彈出打開文件的對話框,選中你要安裝的RPM文件,單擊「OK」按鈕,一切大功告成;卸載軟體時,需要根據分類找到該軟體的圖標,點擊右鍵,選Uninstall就可以了。如果你會在Win 95/98中查找文件的話,在Linux中查找已經安裝的軟體也就不難了。這個軟體可以到下面的站點下載: ftp.gnome.org/pub/GNOME/stable/

sources/gnorpm。

幾點注意事項

一、安裝方法的適用范圍

上面提到的軟體安裝方法並不是在任何版本的Linux上都適用。筆者認為,.tar.gz包的安裝方法適用於各種版本的Linux,而RPM包則有一定的局限性。

目前常見的各種Linux發行版本中,如:Redhat 6.0、Turbo Linux 3.0.2、Xteam 3.0等都支持RPM包。如果你想知道你使用的Linux是否支持RPM包,只要運行一下「rpm --help」命令就知道了。不過,對於不支持RPM包的版本,可以安裝一個RPM管理軟體。

二、容易出現的問題

在安裝軟體時,一定要保證你對用到的軟體包有訪問許可權。當然如果你是root就沒問題了。但如果你真的是root你就需要注意另一個問題,由於root的許可權過高,所以在安裝軟體時,要防止對系統其它軟體造成誤操作(在使用rm 等「危險」命令時,尤其要注意)。

另一個容易出現的問題是,在卸載RPM包的軟體時要慎重,因為很多軟體之間是相互關聯的,你卸載的軟體很可能是其它軟體要用到的,要防止由於卸載了一個軟體而影響另一個軟體的正常使用。所以筆者建議,初學者對於與系統運行有關的軟體盡量不要刪除(對於游戲、應用軟體一類的軟體不必有太多顧慮)。等你成為一名經驗豐富的系統管理員時,你就能靈活地處理這些問題了。

Ⅲ 一文讀懂Linux任務間調度原理和整個執行過程

在前文中,我們分析了內核中進程和線程的統一結構體task_struct,並分析進程、線程的創建和派生的過程。在本文中,我們會對任務間調度進行詳細剖析,了解其原理和整個執行過程。由此,進程、線程部分的大體框架就算是介紹完了。本節主要分為三個部分:Linux內核中常見的調度策略,調度的基本結構體以及調度發生的整個流程。下面將詳細展開說明。

Linux 作為一個多任務操作系統,將每個 CPU 的時間劃分為很短的時間片,再通過調度器輪流分配給各個任務使用,因此造成多任務同時運行的錯覺。為了維護 CPU 時間,Linux 通過事先定義的節拍率(內核中表示為 HZ),觸發時間中斷,並使用全局變數 Jiffies 記錄了開機以來的節拍數。每發生一次時間中斷,Jiffies 的值就加 1。節拍率 HZ 是內核的可配選項,可以設置為 100、250、1000 等。不同的系統可能設置不同的數值,可以通過查詢 /boot/config 內核選項來查看它的配置值。

Linux的調度策略主要分為實時任務和普通任務。實時任務需求盡快返回結果,而普通任務則沒有較高的要求。在前文中我們提到了task_struct中調度策略相應的變數為policy,調度優先順序有prio, static_prio, normal_prio, rt_priority幾個。優先順序其實就是一個數值,對於實時進程來說,優先順序的范圍是 0 99;對於普通進程,優先順序的范圍是 100 139。數值越小,優先順序越高。

實時調度策略主要包括以下幾種

普通調度策略主要包括以下幾種:

首先,我們需要一個結構體去執行調度策略,即sched_class。該類有幾種實現方式

普通任務調度實體源碼如下,這裡麵包含了 vruntime 和權重 load_weight,以及對於運行時間的統計。

在調度時,多個任務調度實體會首先區分是實時任務還是普通任務,然後通過以時間為順序的紅黑樹結構組合起來,vruntime 最小的在樹的左側,vruntime最多的在樹的右側。以CFS策略為例,則會選擇紅黑樹最左邊的葉子節點作為下一個將獲得 CPU 的任務。而這顆紅黑樹,我們稱之為運行時隊列(run queue),即struct rq。

其中包含結構體cfs_rq,其定義如下,主要是CFS調度相關的結構體,主要有權值相關變數、vruntime相關變數以及紅黑樹指針,其中結構體rb_root_cached即為紅黑樹的節點

對結構體dl_rq有類似的定義,運行隊列由紅黑樹結構體構成,並按照deadline策略進行管理

對於實施隊列相應的rt_rq則有所不同,並沒有用紅黑樹實現。

下面再看看調度類sched_class,該類以函數指針的形式定義了諸多隊列操作,如

調度類分為下面幾種:

隊列操作中函數指針指向不同策略隊列的實際執行函數函數,在linux/kernel/sched/目錄下,fair.c、idle.c、rt.c等文件對不同類型的策略實現了不同的函數,如fair.c中定義了

以選擇下一個任務為例,CFS對應的是pick_next_task_fair,而rt_rq對應的則是pick_next_task_rt,等等。

由此,我們來總結一下:

有了上述的基本策略和基本調度結構體,我們可以形成大致的骨架,下面就是需要核心的調度流程將其拼湊成一個整體,實現調度系統。調度分為兩種,主動調度和搶占式調度。

說到調用,逃不過核心函數schele()。其中sched_submit_work()函數完成當前任務的收尾工作,以避免出現如死鎖或者IO中斷等情況。之後首先禁止搶占式調度的發生,然後調用__schele()函數完成調度,之後重新打開搶占式調度,如果需要重新調度則會一直重復該過程,否則結束函數。

而__schele()函數則是實際的核心調度函數,該函數主要操作包括選取下一進程和進行上下文切換,而上下文切換又包括用戶態空間切換和內核態的切換。具體的解釋可以參照英文源碼注釋以及中文對各個步驟的注釋。

其中核心函數是獲取下一個任務的pick_next_task()以及上下文切換的context_switch(),下面詳細展開剖析。首先看看pick_next_task(),該函數會根據調度策略分類,調用該類對應的調度函數選擇下一個任務實體。根據前文分析我們知道,最終是在不同的紅黑樹上選擇最左節點作為下一個任務實體並返回。

下面來看看上下文切換。上下文切換主要干兩件事情,一是切換任務空間,也即虛擬內存;二是切換寄存器和 CPU 上下文。關於任務空間的切換放在內存部分的文章中詳細介紹,這里先按下不表,通過任務空間切換實際完成了用戶態的上下文切換工作。下面我們重點看一下內核態切換,即寄存器和CPU上下文的切換。

switch_to()就是寄存器和棧的切換,它調用到了 __switch_to_asm。這是一段匯編代碼,主要用於棧的切換, 其中32位使用esp作為棧頂指針,64位使用rsp,其他部分代碼一致。通過該段匯編代碼我們完成了棧頂指針的切換,並調用__switch_to完成最終TSS的切換。注意switch_to中其實是有三個變數,分別是prev, next, last,而實際在使用時,我們會對last也賦值為prev。這里的設計意圖需要結合一個例子來說明。假設有ABC三個任務,從A調度到B,B到C,最後C回到A,我們假設僅保存prev和next,則流程如下

最終調用__switch_to()函數。該函數中涉及到一個結構體TSS(Task State Segment),該結構體存放了所有的寄存器。另外還有一個特殊的寄存器TR(Task Register)會指向TSS,我們通過更改TR的值,會觸發硬體保存CPU所有寄存器在當前TSS,並從新的TSS讀取寄存器的值載入入CPU,從而完成一次硬中斷帶來的上下文切換工作。系統初始化的時候,會調用 cpu_init()給每一個 CPU 關聯一個 TSS,然後將 TR 指向這個 TSS,然後在操作系統的運行過程中,TR 就不切換了,永遠指向這個 TSS。當修改TR的值得時候,則為任務調度。

更多Linux內核視頻教程文本資料免費領取後台私信【 內核大禮包 】自行獲取。

在完成了switch_to()的內核態切換後,還有一個重要的函數finish_task_switch()負責善後清理工作。在前面介紹switch_to三個參數的時候我們已經說明了使用last的重要性。而這里為何讓prev和last均賦值為prev,是因為prev在後面沒有需要用到,所以節省了一個指針空間來存儲last。

至此,我們完成了內核態的切換工作,也完成了整個主動調度的過程。

搶占式調度通常發生在兩種情況下。一種是某任務執行時間過長,另一種是當某任務被喚醒的時候。首先看看任務執行時間過長的情況。

該情況需要衡量一個任務的執行時間長短,執行時間過長則發起搶占。在計算機裡面有一個時鍾,會過一段時間觸發一次時鍾中斷,通知操作系統時間又過去一個時鍾周期,通過這種方式可以查看是否是需要搶占的時間點。

時鍾中斷處理函數會調用scheler_tick()。該函數首先取出當前CPU,並由此獲取對應的運行隊列rq和當前任務curr。接著調用該任務的調度類sched_class對應的task_tick()函數進行時間事件處理。

以普通任務隊列為例,對應的調度類為fair_sched_class,對應的時鍾處理函數為task_tick_fair(),該函數會獲取當前的調度實體和運行隊列,並調用entity_tick()函數更新時間。

在entity_tick()中,首先會調用update_curr()更新當前任務的vruntime,然後調用check_preempt_tick()檢測現在是否可以發起搶占。

check_preempt_tick() 先是調用 sched_slice() 函數計算出一個調度周期中該任務運行的實際時間 ideal_runtime。sum_exec_runtime 指任務總共執行的實際時間,prev_sum_exec_runtime 指上次該進程被調度時已經佔用的實際時間,所以 sum_exec_runtime - prev_sum_exec_runtime 就是這次調度佔用實際時間。如果這個時間大於 ideal_runtime,則應該被搶佔了。除了這個條件之外,還會通過 __pick_first_entity 取出紅黑樹中最小的進程。如果當前進程的 vruntime 大於紅黑樹中最小的進程的 vruntime,且差值大於 ideal_runtime,也應該被搶佔了。

如果確認需要被搶占,則會調用resched_curr()函數,該函數會調用set_tsk_need_resched()標記該任務為_TIF_NEED_RESCHED,即該任務應該被搶占。

某些任務會因為中斷而喚醒,如當 I/O 到來的時候,I/O進程往往會被喚醒。在這種時候,如果被喚醒的任務優先順序高於 CPU 上的當前任務,就會觸發搶占。try_to_wake_up() 調用 ttwu_queue() 將這個喚醒的任務添加到隊列當中。ttwu_queue() 再調用 ttwu_do_activate() 激活這個任務。ttwu_do_activate() 調用 ttwu_do_wakeup()。這裡面調用了 check_preempt_curr() 檢查是否應該發生搶占。如果應該發生搶占,也不是直接踢走當前進程,而是將當前進程標記為應該被搶占。

由前面的分析,我們知道了不論是是當前任務執行時間過長還是新任務喚醒,我們均會對現在的任務標記位_TIF_NEED_RESCUED,下面分析實際搶占的發生。真正的搶占還需要一個特定的時機讓正在運行中的進程有機會調用一下 __schele()函數,發起真正的調度。

實際上會調用__schele()函數共有以下幾個時機

從系統調用返回用戶態:以64位為例,系統調用的鏈路為do_syscall_64->syscall_return_slowpath->prepare_exit_to_usermode->exit_to_usermode_loop。在exit_to_usermode_loop中,會檢測是否為_TIF_NEED_RESCHED,如果是則調用__schele()

內核態啟動:內核態的執行中,被搶占的時機一般發生在 preempt_enable() 中。在內核態的執行中,有的操作是不能被中斷的,所以在進行這些操作之前,總是先調用 preempt_disable() 關閉搶占,當再次打開的時候,就是一次內核態代碼被搶占的機會。preempt_enable() 會調用 preempt_count_dec_and_test(),判斷 preempt_count 和 TIF_NEED_RESCHED 是否可以被搶占。如果可以,就調用 preempt_schele->preempt_schele_common->__schele 進行調度。

   本文分析了任務調度的策略、結構體以及整個調度流程,其中關於內存上下文切換的部分尚未詳細敘述,留待內存部分展開剖析。

1、調度相關結構體及函數實現

2、schele核心函數

Ⅳ 研究生課題 linux內核怎麼樣 2015

圖2-1顯示了基於x86計算機Linux系統的啟動順序。第一步是BIOS從啟動設備中導入主引導記錄(MBR),接下來MBR中的代碼查看分區表並從活動分區讀取GRUB、LILO或SYSLINUX等引導裝入程序,之後引導裝入程序會載入壓縮後的內核映像並將控制權傳遞給它。內核取得控制權後,會將自身解壓縮並投入運轉。
基於x86的處理器有兩種操作模式:實模式和保護模式。在實模式下,用戶僅可以使用1 MB內存,並且沒有任何保護。保護模式要復雜得多,用戶可以使用更多的高級功能(如分頁)。CPU 必須中途將實模式切換為保護模式。但是,這種切換是單向的,即不能從保護模式再切換回實模式。
內核初始化的第一步是執行實模式下的匯編代碼,之後執行保護模式下init/main.c文件(上一章修改的源文件)中的 start_kernel()函數。start_kernel()函數首先會初始化CPU子系統,之後讓內存和進程管理系統就位,接下來啟動外部匯流排和 I/O設備,最後一步是激活初始化(init)程序,它是所有Linux進程的父進程。初始化進程執行啟動必要的內核服務的用戶空間腳本,並且最終派生控制台終端程序以及顯示登錄(login)提示。

圖2-1 基於x86硬體上的Linux的啟動過程
本節內的3級標題都是圖2-2中的一條列印信息,這些信息來源於基於x86的筆記本 電腦的Linux啟動過程。如果在其他體系架構上啟動內核,消息以及語義可能會有所不同。

2.1.1 BIOS-provided physical RAM map
內核會解析從BIOS中讀取到的系統內存映射,並率先將以下信息列印出來:
BIOS-provided physical RAM map:
BIOS-e820: 0000000000000000 - 000000000009f000 (usable)
...
BIOS-e820: 00000000ff800000 - 0000000100000000 (reserved)
實模式下的初始化代碼通過使用BIOS的int 0x15服務並執行0xe820號函數(即上面的BIOS-e820字元串)來獲得系統的內存映射信息。內存映射信息中包含了預留的和可用的內存,內核將隨後使用這些信息創建其可用的內存池。在附錄B的B.1節,我們會對BIOS提供的內存映射問題進行更深入的講解。

圖2-2 內核啟動信息
2.1.2 758MB LOWMEM available
896 MB以內的常規的可被定址的內存區域被稱作低端內存。內存分配函數kmalloc()就是從該區域分配內存的。高於896 MB的內存區域被稱為高端內存,只有在採用特殊的方式進行映射後才能被訪問。
在啟動過程中,內核會計算並顯示這些內存區內總的頁數。

2.1.3 Kernel command line: ro root=/dev/hda1
Linux的引導裝入程序通常會給內核傳遞一個命令行。命令行中的參數類似於傳遞給C程序中main()函數的argv[]列表,唯一的不同在於它們是傳遞給內核的。可以在引導裝入程序的配置文件中增加命令行參數,當然,也可以在運行過程中修改引導裝入程序的提示行[1]。如果使用的是GRUB 這個引導裝入程序,由於發行版本的不同,其配置文件可能是/boot/grub/grub.conf或者是/boot/grub/menu.lst。如果使用的是LILO,配置文件為/etc/lilo.conf。下面給出了一個grub.conf文件的例子(增加了一些注釋),看了緊接著title kernel 2.6.23的那行代碼之後,你會明白前述列印信息的由來。
default 0 #Boot the 2.6.23 kernel by default
timeout 5 #5 second to alter boot order or parameters
title kernel 2.6.23 #Boot Option 1
#The boot image resides in the first partition of the first disk
#under the /boot/ directory and is named vmlinuz-2.6.23. 'ro'
#indicates that the root partition should be mounted read-only.
kernel (hd0,0)/boot/vmlinuz-2.6.23 ro root=/dev/hda1
#Look under section "Freeing initrd memory:387k freed"
initrd (hd0,0)/boot/initrd
#...
命令行參數將影響啟動過程中的代碼執行路徑。舉一個例子,假設某命令行參數為bootmode,如果該參數被設置為1,意味著你希望在啟動過程中列印一些調試信息並在啟動結束時切換到runlevel的第3級(初始化進程的啟動信息列印後就會了解runlevel的含義);如果bootmode 參數被設置為0,意味著你希望啟動過程相對簡潔,並且設置runlevel為2。既然已經熟悉了init/main.c文件,下面就在該文件中增加如下修改:
static unsigned int bootmode = 1 ;
static int __init
is_bootmode_setup( char * str)
{
get_option( & str, & bootmode);
return 1 ;
}

/* Handle parameter "bootmode=" */
__setup( " bootmode= " , is_bootmode_setup);

if (bootmode) {
/* Print verbose output */
/* ... */
}

/* ... */

/* If bootmode is 1, choose an init runlevel of 3, else
switch to a run level of 2 */
if (bootmode) {
argv_init[ ++ args] = " 3 " ;
} else {
argv_init[ ++ args] = " 2 " ;
}

/* ... */
請重新編譯內核並嘗試運行新的修改。

2.1.4 Calibrating delay...1197.46 BogoMIPS (lpj=2394935)
在啟動過程中,內核會計算處理器在一個jiffy時間內運行一個內部的延遲循環的次數。jiffy的含義是系統定時器2個連續的節拍之間的間隔。正如所料,該計算必須被校準到所用CPU的處理速度。校準的結果被存儲 在稱為loops_per_jiffy的內核變數中。使用loops_per_jiffy的一種情況是某設備驅動程序希望進行小的微秒級別的延遲的時候。
為了理解延遲—循環校準代碼,讓我們看一下定義於init/calibrate.c文件中的calibrate_ delay()函數。該函數靈活地使用整型運算得到了浮點的精度。如下的代碼片段(有一些注釋)顯示了該函數的開始部分,這部分用於得到一個 loops_per_jiffy的粗略值:
loops_per_jiffy = ( 1 << 12 ); /* Initial approximation = 4096 */
printk(KERN_DEBUG 「Calibrating delay loop...「);
while ((loops_per_jiffy <<= 1 ) != 0 ) {
ticks = jiffies; /* As you will find out in the section, 「Kernel
Timers," the jiffies variable contains the
number of timer ticks since the kernel
started, and is incremented in the timer
interrupt handler */

while (ticks == jiffies); /* Wait until the start of the next jiffy */
ticks = jiffies;
/* Delay */
__delay(loops_per_jiffy);
/* Did the wait outlast the current jiffy? Continue if it didn't */
ticks = jiffies - ticks;
if (ticks) break ;
}

loops_per_jiffy >>= 1 ; /* This fixes the most significant bit and is
the lower-bound of loops_per_jiffy */
上述代碼首先假定loops_per_jiffy大於4096,這可以轉化為處理器速度大約為每秒100萬條指令,即1 MIPS。接下來,它等待jiffy被刷新(1個新的節拍的開始),並開始運行延遲循環__delay(loops_per_jiffy)。如果這個延遲循環持續了1個jiffy以上,將使用以前的loops_per_jiffy值(將當前值右移1位)修復當前loops_per_jiffy的最高位;否則,該函數繼續通過左移loops_per_jiffy值來探測出其最高位。在內核計算出最高位後,它開始計算低位並微調其精度:
loopbit = loops_per_jiffy;

/* Graally work on the lower-order bits */
while (lps_precision -- && (loopbit >>= 1 )) {
loops_per_jiffy |= loopbit;
ticks = jiffies;
while (ticks == jiffies); /* Wait until the start of the next jiffy */
ticks = jiffies;

/* Delay */
__delay(loops_per_jiffy);

if (jiffies != ticks) /* longer than 1 tick */
loops_per_jiffy &= ~ loopbit;
}
上述代碼計算出了延遲循環跨越jiffy邊界時loops_per_jiffy的低位值。這個被校準的值可被用於獲取BogoMIPS(其實它是一個並非科學的處理器速度指標)。可以使用BogoMIPS作為衡量處理器運行速度的相對尺度。在1.6G Hz 基於Pentium M的筆記本 電腦上,根據前述啟動過程的列印信息,循環校準的結果是:loops_per_jiffy的值為2394935。獲得BogoMIPS的方式如下:
BogoMIPS = loops_per_jiffy * 1秒內的jiffy數 * 延遲循環消耗的指令數(以百萬為單位)
= ( 2394935 * HZ * 2 ) / ( 1000000 )
= ( 2394935 * 250 * 2 ) / ( 1000000 )
= 1197.46 (與啟動過程列印信息中的值一致)
在2.4節將更深入闡述jiffy、HZ和loops_per_jiffy。

2.1.5 Checking HLT instruction
由於Linux內核支持多種硬體平台,啟動代碼會檢查體系架構相關的bug。其中一項工作就是驗證停機(HLT)指令。
x86處理器的HLT指令會將CPU置入一種低功耗睡眠模式,直到下一次硬體中斷發生之前維持不變。當內核想讓CPU進入空閑狀態時(查看 arch/x86/kernel/process_32.c文件中定義的cpu_idle()函數),它會使用HLT指令。對於有問題的CPU而言,命令行參數no-hlt可以禁止HLT指令。如果no-hlt被設置,在空閑的時候,內核會進行忙等待而不是通過HLT給CPU降溫。
當init/main.c中的啟動代碼調用include/asm-your-arch/bugs.h中定義的check_bugs()時,會列印上述信息。
2.1.6 NET: Registered protocol family 2
Linux套接字(socket)層是用戶空間應用程序訪問各種網路 協議的統一介面。每個協議通過include/linux/socket.h文件中定義的分配給它的獨一無二的系列號注冊。上述列印信息中的Family 2代表af_inet(互聯網協議)。
啟動過程中另一個常見的注冊協議系列是AF_NETLINK(Family 16)。網路鏈接套接字提供了用戶進程和內核通信 的方法。通過網路鏈接套接字可完成的功能還包括存取路由表和地址解析協議(ARP)表(include/linux/netlink.h文件給出了完整的用法列表)。對於此類任務而言,網路鏈接套接字比系統調用更合適,因為前者具有採用非同步機制、更易於實現和可動態鏈接的優點。
內核中經常使能的另一個協議系列是AF_Unix或Unix-domain套接字。X Windows等程序使用它們在同一個系統上進行進程間通信。
2.1.7 Freeing initrd memory: 387k freed
initrd是一種由引導裝入程序載入的常駐內存的虛擬磁碟映像。在內核啟動後,會將其掛載為初始根文件系統,這個初始根文件系統中存放著掛載實際根文件系統磁碟分區時所依賴的可動態連接的模塊。由於內核可運行於各種各樣的存儲控制器硬體平台上,把所有可能的磁碟驅動程序都直接放進基本的內核映像中並不可行。你所使用的系統的存儲設備的驅動程序被打包放入了initrd中,在內核啟動後、實際的根文件系統被掛載之前,這些驅動程序才被載入。使用 mkinitrd命令可以創建一個initrd映像。
2.6內核提供了一種稱為initramfs的新功能,它在幾個方面較initrd更為優秀。後者模擬了一個磁碟(因而被稱為 initramdisk或initrd),會帶來Linux塊I/O子系統的開銷(如緩沖);前者基本上如同一個被掛載的文件系統一樣,由自身獲取緩沖 (因此被稱作initramfs)。
不同於initrd,基於頁緩沖建立的initramfs如同頁緩沖一樣會動態地變大或縮小,從而減少了其內存消耗。另外,initrd要求你的內核映像包含initrd所使用的文件系統(例如,如果initrd為EXT2文件系統,內核必須包含EXT2驅動程序),然而initramfs不需要文件系統支持。再者,由於initramfs只是頁緩沖之上的一小層,因此它的代碼量很小。
用戶可以將初始根文件系統打包為一個cpio壓縮包[1],並通過initrd=命令行參數傳遞給內核。當然,也可以在內核配置過程中通過 INITRAMFS_SOURCE選項直接編譯進內核。對於後一種方式而言,用戶可以提供cpio壓縮包的文件名或者包含initramfs的目錄樹。在啟動過程中,內核會將文件解壓縮為一個initramfs根文件系統,如果它找到了/init,它就會執行該頂層的程序。這種獲取初始根文件系統的方法對於嵌入式系統而言特別有用,因為在嵌入式系統中系統資源非常寶貴。使用mkinitramfs可以創建一個initramfs映像,查看文檔 Documentation/filesystems/ramfs- rootfs-initramfs.txt可獲得更多信息。
在本例中,我們使用的是通過initrd=命令行參數向內核傳遞初始根文件系統cpio壓縮包的方式。在將壓縮包中的內容解壓為根文件系統後,內核將釋放該壓縮包所佔據的內存(本例中為387 KB)並列印上述信息。釋放後的頁面會被分發給內核中的其他部分以便被申請。
在嵌入式系統開發過程中,initrd和initramfs有時候也可被用作嵌入式設備上實際的根文件系統。
2.1.8 io scheler anticipatory registered (default)
I/O調度器的主要目標是通過減少磁碟的定位次數來增加系統的吞吐率。在磁碟定位過程中,磁頭需要從當前的位置移動到感興趣的目標位置,這會帶來一定的延遲。2.6內核提供了4種不同的I/O調度器:Deadline、Anticipatory、Complete Fair Queuing以及NOOP。從上述內核列印信息可以看出,本例將Anticipatory 設置為了默認的I/O調度器。

2.1.9 Setting up standard PCI resources
啟動過程的下一階段會初始化I/O匯流排和外圍控制器。內核會通過遍歷PCI匯流排來探測PCI硬體,接下來再初始化其他的I/O子系統。從圖2-3中我們會看到SCSI子系統、USB控制器、視頻 晶元(855北橋晶元組信息中的一部分)、串列埠(本例中為8250 UART)、PS/2鍵盤 和滑鼠 、軟碟機 、ramdisk、loopback設備、IDE控制器(本例中為ICH4南橋晶元組中的一部分)、觸控板、乙太網控制器(本例中為e1000)以及PCMCIA控制器初始化的啟動信息。圖2-3中 符號指向的為I/O設備的標識(ID)。

圖2-3 在啟動過程中初始化匯流排和外圍控制器
本書會以單獨的章節討論大部分上述驅動程序子系統,請注意如果驅動程序以模塊的形式被動態鏈接到內核,其中的一些消息也許只有在內核啟動後才會被顯示。
2.1.10 EXT3-fs: mounted filesystem
EXT3文件系統已經成為Linux事實上的文件系統。EXT3在退役的EXT2文件系統基礎上增添了日誌層,該層可用於崩潰後文件系統的快速恢復。它的目標是不經由耗時的文件系統檢查(fsck)操作即可獲得一個一致的文件系統。EXT2仍然是新文件系統的工作引擎,但是EXT3層會在進行實際的磁碟改變之前記錄文件交互的日誌。EXT3向後兼容於EXT2,因此,你可以在你現存的EXT2文件系統上加上EXT3或者由EXT3返回到EXT2 文件系統。
EXT3會啟動一個稱為kjournald的內核輔助線程(在接下來的一章中將深入討論內核線程)來完成日誌功能。在EXT3投入運轉以後,內核掛載根文件系統並做好「業務」上的准備:
EXT3-fs: mounted filesystem with ordered data mode
kjournald starting. Commit interval 5 seconds
VFS: Mounted root (ext3 filesystem).

2.1.11 INIT: version 2.85 booting
所有Linux進程的父進程init是內核完成啟動序列後運行的第1個程序。在init/main.c的最後幾行,內核會搜索一個不同的位置以定位到init:
if (ramdisk_execute_command) { /* Look for /init in initramfs */
run_init_process(ramdisk_execute_command);
}

if (execute_command) { /* You may override init and ask the kernel
to execute a custom program using the
"init=" kernel command-line argument. If
you do that, execute_command points to the
specified program */
run_init_process(execute_command);
}

/* Else search for init or sh in the usual places .. */
run_init_process( " /sbin/init " );
run_init_process( " /etc/init " );
run_init_process( " /bin/init " );
run_init_process( " /bin/sh " );
panic( " No init found. Try passing init= option to kernel. " );
init會接受/etc/inittab的指引。它首先執行/etc/rc.sysinit中的系統初始化腳本,該腳本的一項最重要的職責就是激活對換(swap)分區,這會導致如下啟動信息被列印:
Adding 1552384k swap on /dev/hda6
讓我們來仔細看看上述這段話的意思。Linux用戶進程擁有3 GB的虛擬地址空間(見2.7節),構成「工作集」的頁被保存在RAM中。但是,如果有太多程序需要內存資源,內核會釋放一些被使用了的RAM頁面並將其存儲到稱為對換空間(swap space)的磁碟分區中。根據經驗法則,對換分區的大小應該是RAM的2倍。在本例中,對換空間位於/dev/hda6這個磁碟分區,其大小為1 552 384 KB。
接下來,init開始運行/etc/rc.d/rcX.d/目錄中的腳本,其中X是inittab中定義的運行級別。runlevel是根據預期的工作模式所進入的執行狀態。例如,多用戶文本模式意味著runlevel為3,X Windows則意味著runlevel為5。因此,當你看到INIT: Entering runlevel 3這條信息的時候,init就已經開始執行/etc/rc.d/rc3.d/目錄中的腳本了。這些腳本會啟動動態設備命名子系統(第4章中將討論 udev),並載入網路、音頻、存儲設備等驅動程序所對應的內核模塊:
Starting udev: [ OK ]
Initializing hardware... network audio storage [Done]
...
最後,init發起虛擬控制台終端,你現在就可以登錄了。

Ⅳ 有沒有懂linux內核源碼中的匯編代碼的#define switch_to(n){struct {long a, b;}__tmp; __asm__("cmpl "

用的at&t匯編,也就是Linux下的匯編語言,跟Intel x86匯編翻譯成i386指令是一樣的,就是寫法和符號不同。
裡面應該是__asm__()後面跟的是一個字元串,包含大量轉義字元,你把轉移字元翻譯成對於的格式再看.

Ⅵ Linux 進程管理之進程調度與切換

我們知道,進程運行需要各種各樣的系統資源,如內存、文件、列印機和最

寶貴的 CPU 等,所以說,調度的實質就是資源的分配。系統通過不同的調度演算法(Scheling Algorithm)來實現這種資源的分配。通常來說,選擇什麼樣的調度演算法取決於資源分配的策略(Scheling Policy)。

有關調度相關的結構保存在 task_struct 中,如下:

active_mm 是為內核線程而引入的,因為內核線程沒有自己的地址空間,為了讓內核線程與普通進程具有統一的上下文切換方式,當內核線程進行上下文切換時,讓切換進來的線程的 active_mm 指向剛被調度出去的進程的 active_mm(如果進程的mm 域不為空,則其 active_mm 域與 mm 域相同)。

在 linux 2.6 中 sched_class 表示該進程所屬的調度器類有3種:

進程的調度策略有5種,用戶可以調用調度器里不同的調度策略:

在每個 CPU 中都有一個自身的運行隊列 rq,每個活動進程只出現在一個運行隊列中,在多個 CPU 上同時運行一個進程是不可能的。

運行隊列是使用如下結構實現的:

tast 作為調度實體加入到 CPU 中的調度隊列中。

系統中所有的運行隊列都在 runqueues 數組中,該數組的每個元素分別對應於系統中的一個 CPU。在單處理器系統中,由於只需要一個就緒隊列,因此數組只有一個元素。

內核也定義了一下便利的宏,其含義很明顯。

Linux、c/c++伺服器開發篇-------我們來聊聊進程的那些事

Linux內核 進程間通信組件的實現

學習地址:C/C++Linux伺服器開發/後台架構師【零聲教育】-學習視頻教程-騰訊課堂

需要C/C++ Linux伺服器架構師學習資料加qun812855908獲取(資料包括 C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg 等),免費分享

在分析調度流程之前,我們先來看在什麼情況下要執行調度程序,我們把這種情況叫做調度時機。

Linux 調度時機主要有。

時機1,進程要調用 sleep() 或 exit() 等函數進行狀態轉換,這些函數會主動調用調度程序進行進程調度。

時機2,由於進程的時間片是由時鍾中斷來更新的,因此,這種情況和時機4 是一樣的。

時機3,當設備驅動程序執行長而重復的任務時,直接調用調度程序。在每次反復循環中,驅動程序都檢查 need_resched 的值,如果必要,則調用調度程序 schele() 主動放棄 CPU。

時機4 , 如前所述, 不管是從中斷、異常還是系統調用返回, 最終都調用 ret_from_sys_call(),由這個函數進行調度標志的檢測,如果必要,則調用調用調度程序。那麼,為什麼從系統調用返回時要調用調度程序呢?這當然是從效率考慮。從系統調用返回意味著要離開內核態而返回到用戶態,而狀態的轉換要花費一定的時間,因此,在返回到用戶態前,系統把在內核態該處理的事全部做完。

Linux 的調度程序是一個叫 Schele() 的函數,這個函數來決定是否要進行進程的切換,如果要切換的話,切換到哪個進程等。

從代碼分析來看,Schele 主要完成了2個功能:

進程上下文切換包括進程的地址空間的切換和執行環境的切換。

對於 switch_mm 處理,關鍵的一步就是它將新進程頁面目錄的起始物理地址裝入到寄存器 CR3 中。CR3 寄存器總是指向當前進程的頁面目錄。

switch_to 把寄存器中的值比如esp等存放到進程thread結構中,保存現場一邊後續恢復,同時調用 __switch_to 完成了堆棧的切換。

在進程的 task_struct 結構中有個重要的成分 thread,它本身是一個數據結構 thread_struct, 裡面記錄著進程在切換時的(系統空間)堆棧指針,取指令地址(也就是「返回地址」)等關鍵性的信息。

關於__switch_to 的工作就是處理 TSS (任務狀態段)。

TSS 全稱task state segment,是指在操作系統進程管理的過程中,任務(進程)切換時的任務現場信息。

linux 為每一個 CPU 提供一個 TSS 段,並且在 TR 寄存器中保存該段。

linux 中之所以為每一個 CPU 提供一個 TSS 段,而不是為每個進程提供一個TSS 段,主要原因是 TR 寄存器永遠指向它,在任務切換的適合不必切換 TR 寄存器,從而減小開銷。

在從用戶態切換到內核態時,可以通過獲取 TSS 段中的 esp0 來獲取當前進程的內核棧 棧頂指針,從而可以保存用戶態的 cs,esp,eip 等上下文。

TSS 在任務切換過程中起著重要作用,通過它實現任務的掛起和恢復。所謂任務切換是指,掛起當前正在執行的任務,恢復或啟動另一任務的執行。

在任務切換過程中,首先,處理器中各寄存器的當前值被自動保存到 TR(任務寄存器)所指定的任務的 TSS 中;然後,下一任務的 TSS 被裝入 TR;最後,從 TR 所指定的 TSS 中取出各寄存器的值送到處理器的各寄存器中。由此可見,通過在 TSS 中保存任務現場各寄存器狀態的完整映象,實現任務的切換。

因此,__switch_to 核心內容就是將 TSS 中的內核空間(0級)堆棧指針換成 next->esp0。這是因為 CPU 在穿越中斷門或者陷阱門時要根據新的運行級別從TSS中取得進程在系統空間的堆棧指針。

thread_struct.esp0 指向進程的系統空間堆棧的頂端。當一個進程被調度運行時,內核會將這個變數寫入 TSS 的 esp0 欄位,表示這個進程進入0級運行時其堆棧的位置。換句話說,進程的 thread_struct 結構中的 esp0 保存著其系統空間堆棧指針。當進程穿過中斷門、陷阱門或者調用門進入系統空間時,處理器會從這里恢復期系統空間棧。

由於棧中變數的訪問依賴的是段、頁、和 esp、ebp 等這些寄存器,所以當段、頁、寄存器切換完以後,棧中的變數就可以被訪問了。

因此 switch_to 完成了進程堆棧的切換,由於被切進的進程各個寄存器的信息已完成切換,因此 next 進程得以執行指令運行。

由於 A 進程在調用 switch_to 完成了與 B 進程堆棧的切換,也即是寄存器中的值都是 B 的,所以 A 進程在 switch_to 執行完後,A停止運行,B開始運行,當過一段時間又把 A 進程切進去後,A 開始從switch_to 後面的代碼開始執行。

schele 的調用流程如下:





熱點內容
安卓主板哪裡有賣 發布:2025-03-15 19:26:10 瀏覽:29
Q9源碼 發布:2025-03-15 19:24:21 瀏覽:175
芬蘭編程教育 發布:2025-03-15 18:59:46 瀏覽:426
網際網路的伺服器地址 發布:2025-03-15 18:53:01 瀏覽:892
手機實體店什麼配置好 發布:2025-03-15 18:32:35 瀏覽:168
攜帶型電腦的原始密碼是什麼 發布:2025-03-15 18:25:52 瀏覽:798
壓縮空間小 發布:2025-03-15 18:14:05 瀏覽:848
env的腳本 發布:2025-03-15 18:01:24 瀏覽:730
圖片上傳雲端 發布:2025-03-15 17:37:26 瀏覽:460
郵件伺服器ip池 發布:2025-03-15 17:31:51 瀏覽:398