結合中斷上下文切換和進程上下文切換分析Linux內核的一般執行過程

来源:https://www.cnblogs.com/ustca/archive/2020/06/15/13126859.html
-Advertisement-
Play Games

實驗內容: 結合中斷上下文切換和進程上下文切換分析Linux內核一般執行過程 以fork和execve系統調用為例分析中斷上下文的切換 分析execve系統調用中斷上下文的特殊之處 分析fork子進程啟動執行時進程上下文的特殊之處 以系統調用作為特殊的中斷,結合中斷上下文切換和進程上下文切換分析Li ...


實驗內容:

  • 結合中斷上下文切換和進程上下文切換分析Linux內核一般執行過程
  • 以fork和execve系統調用為例分析中斷上下文的切換
  • 分析execve系統調用中斷上下文的特殊之處
  • 分析fork子進程啟動執行時進程上下文的特殊之處
  • 以系統調用作為特殊的中斷,結合中斷上下文切換和進程上下文切換分析Linux系統的一般執行過程

實驗環境:

VMWare虛擬機下的Ubuntu18.04.4,實驗採用的內核版本為linux-5.4.34。

1 基礎概念

CPU工作狀態

CPU的工作狀態分為系統態(管態)和用戶態(目態)。
引入這兩個工作狀態的原因是為了避免用戶程式錯誤地使用特權指令,保護操作系統不被用戶程式破壞。

當CPU處於用戶態時,不允許執行特權指令;當CPU處於系統態時,可執行包括特權指令在內的一切機器指令。

中斷與系統調用

  • 系統調用

    程式員或系統管理員通常並非直接和系統調用打交道。在實際應用中,程式員通過調用函數(或稱應用程式介面、API),管理員則使用更高層次的系統命令。

    操作系統為每個系統調用在標準C函數庫中構造一個具有相同名字的封裝函數,由它來屏蔽下層的複雜性,負責把操作系統提供的服務介面(即系統調用)封裝成應用程式能夠直接調用的函數(庫函數)

  • 中斷

    所謂中斷是指CPU對系統發生的某個事件做出的一種反應,CPU暫停正在執行的程式,保留現場後自動地轉去執行相應的處理程式,處理完該事件後再返回斷點繼續執行被“打斷”的程式。

    中斷概念主要分為三類

    • 外部中斷,如I/O中斷,時鐘中斷,控制臺中斷等。
    • 異常,如CPU本身故障(電源電壓或頻率),程式故障(非法操作碼、地址越界、浮點溢出等),即CPU的內部事件或程式執行中的事件引起的過程。
    • 陷入(陷阱),在程式中使用了請求系統服務的系統調用而引發的過程。
  • 中斷與系統調用

    外部中斷與異常通常都稱作中斷,它們的產生往往是無意、被動的。

    陷入是有意和主動的,系統調用本身是一種特殊的中斷。

進程上下文與中斷上下文

  • 進程上下文

    用戶空間的應用程式,通過系統調用進入內核空間。用戶空間的進程需要傳遞變數、參數的值給內核,在內核態運行時也要保存用戶進程的一些寄存器值、變數等。進程上下文,可以看作是用戶進程傳遞給內核的這些參數以及內核要保存的那一整套的變數、寄存器值和當時的環境等。

    相對於進程而言,就是進程執行時的環境。具體來說就是各個變數和數據,包括所有的寄存器變數、進程打開的文件、記憶體信息等。一個進程的上下文可以分為三個部分:用戶級上下文、寄存器上下文以及系統級上下文。

  • 中斷上下文

    為了在 中斷執行時間儘可能短 和 中斷處理需完成大量工作 之間找到一個平衡點,Linux將中斷處理程式分解為兩個半部:頂半部和底半部。頂半部完成儘可能少的比較緊急的功能,它往往只是簡單地讀取寄存器中的中斷狀態並清除中斷標誌後就進行“登記中斷”的工作。“登記中斷”意味著將底半部處理程式掛到該設備的底半部執行隊列中去。這樣,頂半部執行的速度就會很快,可以服務更多的中斷請求。

    對於中斷而言,內核調用中斷處理程式,進入內核空間。這個過程中,硬體的一些變數和參數也要傳遞給內核,內核通過這些參數進行中斷處理,中斷上下文就可以理解為硬體傳遞過來的這些參數和內核需要保存的一些環境,主要是被中斷的進程的環境。

2 fork系統調用

Linux中通過fork系統調用來處理進程創建的任務。
對於進程的創建,sys_clone, sys_vfork,以及sys_fork系統調用的內部都使用了do_fork函數。

在sys_clone,sys_vfork和sys_fork處打下斷點,運行系統,在sys_clone處停下:

sys_clone源碼:

SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
         int __user *, parent_tidptr,
         int, tls_val,
         int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
         int __user *, parent_tidptr,
         int __user *, child_tidptr,
         int, tls_val)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
        int, stack_size,
        int __user *, parent_tidptr,
        int __user *, child_tidptr,
        int, tls_val)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
         int __user *, parent_tidptr,
         int __user *, child_tidptr,
         int, tls_val)
#endif
{
    return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}
#endif

代碼最終調用do_fork函數,轉到do_fork執行,其他創建進程函數調用過程與此類似,do_fork函數如下:

long do_fork(unsigned long clone_flags,
          unsigned long stack_start,
          unsigned long stack_size,
          int __user *parent_tidptr,
          int __user *child_tidptr)
{
    struct task_struct *p;
    int trace = 0;
    long nr;

    /*
     * Determine whether and which event to report to ptracer.  When
     * called from kernel_thread or CLONE_UNTRACED is explicitly
     * requested, no event is reported; otherwise, report if the event
     * for the type of forking is enabled.
     */
    if (!(clone_flags & CLONE_UNTRACED)) {
        if (clone_flags & CLONE_VFORK)
            trace = PTRACE_EVENT_VFORK;
        else if ((clone_flags & CSIGNAL) != SIGCHLD)
            trace = PTRACE_EVENT_CLONE;
        else
            trace = PTRACE_EVENT_FORK;

        if (likely(!ptrace_event_enabled(current, trace)))
            trace = 0;
    }

    p = copy_process(clone_flags, stack_start, stack_size,
             child_tidptr, NULL, trace);
    /*
     * Do this prior waking up the new thread - the thread pointer
     * might get invalid after that point, if the thread exits quickly.
     */
    if (!IS_ERR(p)) {
        struct completion vfork;
        struct pid *pid;

        trace_sched_process_fork(current, p);

        pid = get_task_pid(p, PIDTYPE_PID);
        nr = pid_vnr(pid);

        if (clone_flags & CLONE_PARENT_SETTID)
            put_user(nr, parent_tidptr);

        if (clone_flags & CLONE_VFORK) {
            p->vfork_done = &vfork;
            init_completion(&vfork);
            get_task_struct(p);
        }

        wake_up_new_task(p);

        /* forking complete and child started to run, tell ptracer */
        if (unlikely(trace))
            ptrace_event_pid(trace, pid);

        if (clone_flags & CLONE_VFORK) {
            if (!wait_for_vfork_done(p, &vfork))
                ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
        }

        put_pid(pid);
    } else {
        nr = PTR_ERR(p);
    }
    return nr;
}

do_fork會進行一些pcb的拷貝工作。

在調用copy_process函數時,會進行一些實際內容的拷貝:複製當前進程產生子進程,並且傳入關鍵參數為子進程設置響應進程上下文。具體過程為:先通過調用 dup_task_struct 複製一份task_struct結構體,作為子進程的進程描述符。再初始化與調度有關的數據結構,調用sched_fork,將子進程的state設置為TASK_RUNNING。之後複製所有的進程信息,包括fs、信號處理函數、信號、記憶體空間(包括寫時複製)等。最終調用copy_thread,設置子進程的堆棧信息, 為子進程分配一個pid。

在調用wake_up_new_task函數時,主要任務是將子進程放入調度隊列中,從而使CPU有機會調度並得以運行。

3 execve系統調用

execve系統調用的作用是運行另外一個指定的程式。它會把新程式載入到當前進程的記憶體空間內,當前的進程會被丟棄,它的堆、棧和所有的段數據都會被新進程相應的部分代替,然後會從新程式的初始化代碼和 main 函數開始運行。同時,進程的 ID 將保持不變。

與fork系統調用不同,從一個進程中啟動另一個程式時,通常是先 fork 一個子進程,然後在子進程中使用 execve變為運行指定程式的進程。 例如,當用戶在 Shell 下輸入一條命令啟動指定程式時,Shell 就是先 fork了自身進程,然後在子進程中使用 execve來運行指定的程式。

execve系統調用的函數原型為:

int execve(const char *filename, char *const argv[], char *const envp[]);

filename 用於指定要運行的程式的文件名,argv 和 envp 分別指定程式的運行參數和環境變數。除此之外,該系列函數還有很多變體(execl、execlp、execle、execv、execvp、execvpe),它們執行大體相同的功能,區別在於需要的參數不同,但都是通過execve系統調用進入內核。

execve系統調用的過程:首先,執行__x64_sys_execve系統調用,進入內核態後調用do_execve載入可執行文件,之後再通過調用search_binary_handler覆蓋當前進程的可執行程式。

static int exec_binprm(struct linux_binprm *bprm)
{
    pid_t old_pid, old_vpid;
    int ret;

    /* Need to fetch pid before load_binary changes it */
    old_pid = current->pid;
    rcu_read_lock();
    old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));
    rcu_read_unlock();

    ret = search_binary_handler(bprm);
    if (ret >= 0) {
        audit_bprm(bprm);
        trace_sched_process_exec(current, old_pid, bprm);
        ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
        proc_exec_connector(current);
    }

    return ret;
}

最後將IP設置為新的進程的入口地址,然後返回用戶態,繼續執行新進程。最終舊進程的上下文被完全替換,但進程pid 不變,調用返回新進程。

4 Linux系統的一般執行過程

當前linux系統中正在運行用戶態進程X,需要切換到用戶態進程Y的時候,會執行以下過程:

  1. 用戶態進程X正在運行

  2. 運行的過程當中,發生了中斷

  3. 中斷上下文切換,swapgs指令保存現場後,再載入當前進程內核堆棧棧頂地址到RSP寄存器,由進程X的用戶態轉到進程X的內核態。

  4. 中斷處理過程中或中斷返回前調用schedule函數,完成進程調度演算法。

  5. switch_to調用__switch_to_asm彙編代碼,完成關鍵的進程上下文切換。

  6. 中斷上下文恢復。

  7. 繼續運行用戶態進程Y

Linux一般切換流程中有CPU的上下文的切換和內核中的進程上下文的切換。中斷和中斷返回有中斷上下文的切換,CPU和內核代碼中斷處理程式入口的彙編代碼結合起來完成中斷上下文的切換。進程調度過程中有進程上下文的切換,而進程上下文的切換完全由內核完成。

幾種特殊情況

(1)通過中斷處理過程中的調度時機,用戶態進程與內核線程之間互相切換和內核線程之間互相切換,與最一般的情況非常類似,只是內核線程運行過程中發生中斷沒有進程用戶態和內核態的轉換。

(2)內核線程主動調用schedule(),只有進程上下文的切換,沒有發生中斷上下文的切換,與最一般的情況略簡略。

(3)創建子進程的系統調用在子進程中的執行起點及返回用戶態,如fork。

(4)載入一個新的可執行程式後返回到用戶態的情況,如execve。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1.複製 JSON對象字元串 { "Name": "Robot", "Sex": "Man", "Describe": "aaa - JsConfig", "Message": "Hello World - JsConfig - 啊!" } 2.在 cs尾碼類文件 中,點擊 編輯/選擇性粘貼/將JS ...
  • AppSetting.json { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "Allow ...
  • 前言 上一篇【.Net Core微服務入門全紀錄(一)——項目搭建】講到要做到服務的靈活伸縮,那麼需要有一種機制來實現它,這個機制就是服務註冊與發現。當然這也並不是必要的,如果你的服務實例很少,並且很穩定,那麼就沒有必要使用服務註冊與發現。 服務註冊與發現 服務註冊:簡單理解,就是有一個註冊中心,我 ...
  • 所謂OOM就是當系統上的應用申請記憶體資源時,發現申請不到記憶體,這個時候Linux內核就會啟動OOM,內核將給系統上的所有進程進行評分,通過評分得分最高的進程就會被系統第一個幹掉,從而騰出一些記憶體空間,如果騰出的記憶體空間還是不夠該應用使用,它會繼續殺得分第二高的,直到應用有足夠的記憶體使用;一旦發生O... ...
  • windows下將tomcat註冊為服務 進入tomcat/bin 目錄下 輸入:service.bat install(remove) 修改服務名稱,為修改service.bat rem Set default Service name set SERVICE_NAME=Tomcat6qd set ...
  • Docker鏡像加速配置;Docker鏡像常用操作;Dcoker容器常用操作。 ...
  • 1.首先上傳安裝包,這裡我以 redis-5.0.8.tar.gz 為例子。 Linux下載redis地址:wget http://download.redis.io/releases/redis-5.0.8.tar.gz 先在opt目錄下建立一個軟體包上傳文件夾 mkdir /opt/softwa ...
  • 我們在進行伺服器配置的時候,經常要查看伺服器的某個埠是否已經開放。如果伺服器只有一兩台的話,那很好辦,只需要使用 nc 命令一個個查看即可。 但是,如果你的伺服器是個集群,有很多台呢?那如果還一個個手動去檢查的話,效率肯定是無比低下的,年底裁員名單里肯定有你。 在這種情況下,我們完全可以使用 Sh ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...