【操作系統】2.進程和線程

来源:https://www.cnblogs.com/stuxuan/archive/2023/09/01/17652782.html
-Advertisement-
Play Games

1.操作系統的多進程圖像 操作系統main函數中最後 if(!fork()) {init();} ,也就是main函數最後創建了第1個進程,init執行了shell(Windows)桌面。 操作系統管理和組織進程都使用PCB(Process Control Block),不同的程式的PCB放在不同的 ...


1.操作系統的多進程圖像

操作系統main函數中最後 if(!fork()) {init();} ,也就是main函數最後創建了第1個進程,init執行了shell(Windows)桌面。

操作系統管理和組織進程都使用PCB(Process Control Block),不同的程式的PCB放在不同的位置,用於記錄該進程運行時的狀態。操作系統對進程進行分類,例如等待執行的進程和等待某些事件完成的進程,例如等待磁碟讀寫。

 
  1. 新建態:系統完成創建進程的一系列工作。只能轉換到就緒態
  2. 就緒態:擁有除CPU之外的其他所需的所有資源。當擁有CPU時就可以轉換到運行態
  3. 運行態:用於CPU和所需的所有資源
    1. 當時間片到或者處理機被搶占了,就轉換到就緒態;
    2. 當進程用“系統調用”的方式申請某種系統資源或者請求等待某個事件的發生,則進入阻塞態(主動)
  1. 阻塞態:沒有所需要的資源。當所需要的資源得到分配時,進入就緒態(被動)
  2. 終止態:進程運行結束或者出現不可修複的錯誤時,由運行態轉到終止態

進程切換的三個部分:隊列操作+調度+切換

pCur.state = 'W';    // 啟動磁碟讀寫,將當前進程設置為阻塞狀態
schedule();         // 將pCur放到DiskWaitQueue

schedule()
{
    pNew = getNext(ReadyQueue);  // 從就緒隊列找到下一個進程,調度函數演算法非常複雜
    switch_to(pCur,pNew);  // 保存當前進程的現場,把下一個進程的現場恢復
}

把當前進程的現場保存到pCur中(PCB),把切換程式的pNew(PCB)讀取到寄存器中

多個進程同時存在於記憶體的問題:不同進程的地址可能影響其他進程的代碼,這可能導致其他進程的崩潰。操作系統需要維護一張映射表,將記憶體映射到實際的記憶體地址中,把不同的進程隔離開來保證進程的安全,下圖中同樣對記憶體100的操作分別映射到了記憶體地址780和記憶體地址1260。

但實際上多進程之間可能存在合作關係,比如印表機進程需要讀取word進程的內容來完成列印的工作,這時可以提交到共用緩衝區。但這裡可能存在一個問題,因為進程1和進程2是交替進行的,可能進程1首先讀取到空間7是空的,接下來切換到進程2也讀取到空間7是空的,開始向空間7寫入,接下來切換到進程1繼續在這裡寫入,會導致寫入緩衝區的內容是錯誤的。所以操作系統需要管理一個合理的進程推進順序。

2.用戶級線程

進程 = 資源(映射表) + 指令執行序列

線程是只切換指令,如PC和寄存器,而不切換映射表,這種切換保留了併發了優點,避免了進程切換的代價

舉例說明,對於瀏覽器來說,可以用一個線程接收伺服器數據,一個線程顯示文本,一個線程處理圖片,一個線程顯示圖片,它們不需要用多個映射表完全分離開,沒有必要用多個進程完成這些工作。我們需要的工作主要就是下麵看到的兩個部分,創建Create線程進行工作處理,使用Yield跳轉到另一個線程工作。

void WebExplorer(){
    char URL[] = "http://cms.hit.edu.cn";
    char buffer[1000];
    pthread_create(..., GetData, URL, buffer);
    pthread_create(..., Show, buffer);
 }

void GetData(char *URL, char *p) {...}
void Show(char *p) {...};

 線程切換的詳細過程:每個線程都有自己的棧。線程1執行過程中,首先調用函數B(),保護現場,將上一段程式的幀指針和函數B完成後PC應指向的地址壓入棧(參見【深入理解電腦系統】3.程式的機器級表示),接下來調用Yield()函數,保護現場,將之前的幀指針和Yield函數結束後的PC204壓入棧,接下來Yield函數將當前棧指針1000保存在TCB1中,並將棧指針切換到TCB2的棧指針2000,完成了線程間的切換。接下來線程2的Yield使得棧指針回到1000處,繼續上一個線程對應位置執行。下麵給出了用戶級線程的Create和Yield核心代碼。

void Yield(){
    TCB1.esp = esp;  //Thread Control Block
    esp = TCB2.esp;
}
void ThreadCreate(A){
  TCB *tcb=malloc();   //申請空間保存TCB
  *stack=malloc();    //申請空間保存棧
  *stack = A;       //向棧中壓入數據
  tcb.esp=stack;     //將棧和TCB建立聯繫
}

3.內核級線程

用戶級線程存在的問題,用戶級線程在請求下載數據的過程中,理想情況是下載了一些後跳轉到顯示文本的線程執行,但實際上內核級線程不知道這些事情,由於等待網卡IO會阻塞這個進程,最後導致瀏覽器沒有實現我們需要的功能。

所以引入內核級線程,ThreadCreate是系統調用,會進入內核,Yield的調度由系統決定。

接下來看一下多核和多CPU,可以看到多核CPU只有一套MMU(記憶體映射),也就是多核心CPU在執行進程的時候,也需要切換記憶體映射再執行,只有多處理器才能並行運行多個進程。但這個時候內核級線程的優勢就體現出來了,多核CPU可以並行的執行同一進程不同線程的代碼,因為這些代碼共用一套記憶體映射。

對於內核級線程,它與用戶級線程的區別是

用戶級線程在用戶棧執行,多個用戶級線程對應了多個用戶棧,1個TCB(用戶態)關聯1個用戶棧;

內核級線程在用戶棧和內核棧都需要執行和調用函數,所以多內核級線程實際上對應了多套棧(包括用戶棧和內核棧),1個TCB(內核態)關聯1個用戶棧和1個內核棧。

int中斷指令會引起內核棧的切換,內核棧中記錄了用戶棧用戶代碼兩部分內容。SS寄存器(棧頂段地址)和SP寄存器(偏移地址)的值,SS:SP是此時棧頂位置;PC記錄了用戶代碼程式運行的代碼位置,CS記錄了用戶代碼段基址

 內核級線程的切換包含5個階段

1.中斷入口(進入切換):系統中斷線程1從用戶態進入內核態,用戶態寄存器的值保存到內核棧

2.中斷處理(引發切換):調用schedule函數,引起TCB切換。這裡有可能啟動磁碟讀寫或時鐘中斷,內核會調用schedule找到下一個要執行的TCB,然後用next指針指向這個TCB

3.內核棧切換(switch_to):把當前ESP寄存器放在current指向的TCB中,然後把next指向的esp賦給寄存器,完成內核棧指向地址的切換,現在ESP指向了下一個線程的TCB地址

4.中斷返回(iret):把TCB存儲的內核棧現場恢復出來

5.用戶棧切換:切換回用戶態PC指針還有對應的用戶棧

4.內核級線程實現

首先從這段代碼開始,main函數開始,首先遇到函數A,用戶棧中壓入A的返回地址(也就是B的初始地址),在A函數執行中遇到fork()函數,首先將系統調用號__NR_fork移入%eax寄存器,然後調用INT 0x80中斷,執行這條指令時PC自動加1,此時PC指向下一行mov res,%eax。觸發INT 0x80中斷後,cpu立刻找到用戶棧對應的內核棧,將當前時刻的SS和SP壓入內核棧,接下來將返回地址CSIP壓入內核棧,也就是mov res,%eax這一行。接下來執行system_call

_system_call:
    cmpl $nr_system_calls-1,%eax # 調用號超出範圍就在eax設置-1並退出
    ja bad_sys_call
    push %ds %es %fs   # 保存原段寄存器值
    pushl %edx %ecx %ebx    # 一個系統調用最多帶3個參數,這裡存放了系統對應C語言函數調用的參數
    movl $0x10,%edx    # 設置ds和es到內核段
    mov %dx,%ds
    mov %dx,%ex    #edx的低16位賦值給ds和es指向內核數據段
    movl $0x17,%edx
    mov %dx,%fs
    call _sys_call_table(,%eax,4)
    pushl %eax  #系統調用返回值壓入棧

下圖為切換5段論的中斷入口和中斷出口。_system_call首先保護現場,將原段寄存器的值壓入棧,然後將調用的參數壓入棧,接下來調用sys_fork,他首先判斷判斷當前程式TCB是不是等於0,等於0說明已經就緒,如果不等於0說明線程阻塞,則應該重新調度reschedule(也就是切換5段論中間3段,切換TCB),完成後進行中斷返回ret_from_sys_call

下圖為切換5段論的中斷出口,對應入口的大量push壓入棧,出口把保存在TCB中的數據pop出棧

切換5段論中的switch_to使用的時TSS切換,是一個長跳轉。TR表示當前cpu對應的任務段,TR改變時會把寄存器中的內容全部保存到舊的TSS中,然後把新的TSS中所有內容都會載入到寄存器

創建一個線程最重要的就是做出可以切換的樣子。_sys_fork首先拷貝父進程的所有參數,這些參數都已經在中斷過程壓入內核棧,

copy_process的細節:創建棧。申請一頁記憶體用於保存PCB和內核棧,註意這裡內核棧重新創建,但ss和esp的棧與父進程一模一樣,也就是它可以和父進程用同樣的代碼同樣的棧,eip是int 0x80中斷的下一句話。最後如果創建了子進程,會把%eax置為0,所以從子進程返回到mov res,%eax的時候,res是0;但如果從父進程返回到mov res,%eax,res是非0,所以有一段經典代碼if(!fork()){子進程代碼段}else{父進程代碼段},這樣就實現了子進程和父進程都返回這個位置,但執行不同的代碼

如何讓子進程執行我們想要的代碼?下麵給出了更為詳細的代碼,如果非fork則執行代碼,如果是父進程則執行另一部分代碼。

5.CPU調度策略

吞吐量和響應時間之間有矛盾:響應時間小 -> 切換次數多 -> 系統內耗大 -> 吞吐量小

前臺任務和後臺任務的關註點不同:前臺任務關註響應時間(從提交到相應的時間間隔),後臺任務關註周轉時間(從提交到完成的時間間隔)

需要綜合考慮IO約束型任務和CPU約束型任務

 應該綜合考慮花費時間短的程式優先執行來降低周轉時間,劃分時間片來降低響應時間,同時也應該為前臺和後臺應用劃分優先順序

6.進程同步與信號量

不同進程需要合作,例如印表機的列印隊列與word文檔之間的合作,這種同步是通過信號量控制的

進程同步就是控制進程交替執行的過程,保證多進程合作合理有序

假設有3個生產者進程P,1個消費者進程C,1個緩衝區,用信號量來表示緩衝區的狀態,這些進程就可以通過信號量實現進程同步(也就是進程的等待和喚醒)

(1)緩衝區滿,P1執行,P1發現緩衝區滿所以sleep,設置sem=-1(有1個進程等待,緩衝區缺少1個位置)

(2)P2執行,P2 sleep,設置sem=-2(有2個進程等待)

(3)C執行,列印1份文件,緩衝區增加1個空間,wakeup P1,設置sem=-1

(4)C再執行,緩衝區又增加1個空間,wakeup P2,設置sem=0

(4)C再執行,不需要喚醒進程,設置sem=1(緩衝區盈餘1個位置)

(5)P3執行,因為緩衝區還有內容,直接執行,設置sem=0

信號量的臨界區保護

信號量是一個共有的變數,大家一起修改一起使用,多進程切換過程中可能存在問題。下麵生產者P1和P2會修改empty信號量,調用生產者P1或P2時,他們都會首先讀取現在的信號量,接下來將信號量-1,並把這個值賦回給公共的信號量。接下來右圖給出了一種可能的調度,由於生產者P1在信號量-1之後沒有將該信號量賦值給公共的信號量,此時發生調度轉到了生產者P2,這就導致本來應該兩個生產者使信號量-2,但實際上只-1

解決方法:寫共用變數empty時阻止其他進程訪問,即上鎖的思想

臨界區:一次只允許一個進程進入的該進程的那一段代碼,在這裡就是每個進程中修改empty的這段代碼,這裡最重要的工作就是找到進程臨界區的代碼。核心思想就是進程進入臨界區代碼時進行一些操作,退出臨界區後再進行一些操作,基本原則互斥進入,其次應該有空讓進,並且是有限等待的。

下麵是兩種臨界區控制的嘗試,分別為輪換法和標記法。

輪換法: 使用turn變數控制進入。首先看互斥進入,如果P0進入說明turn=0,如果P1進入說明turn=1,滿足互斥性,但是可能P0完成後將turn置為1,P1進程又在阻塞狀態,就導致P1進程不使用臨界區代碼,P0進程又無法進入臨界區代碼,不滿足有空讓進

標記法:如果進程想要進入自己的臨界區,就將自己的標記flag設置為true。首先看互斥性,如果P0進入說明flag[0]=true,flag[1]=false,如果P1進入說明flag[1]=true,flag[0]=false,滿足互斥性。接下來看有空讓進,兩個進程都會檢測對方是否想要進入臨界區,如果想要進入就謙讓,但有可能雙方同時調整了自己的標誌位,最後導致雙方互相謙讓,沒有人能進入臨界區,不滿足無限等待

這兩種標誌太對稱了,你也一樣我也一樣,最後卡死在這個地方

Peterson演算法:如果P0想要進入臨界區,修改P0的flag為true,並且修改turn下一次應該是進程1運行。

互斥性:

P0進入flag[0]=true,flag[1]=false或turn=0

P1進入flag[1]=true,flag[0]=false或turn=1

連起來看就是如果P0和P1同時進入時一定flag[0]=flag[1]=true,那麼只能turn=0=1,矛盾,滿足互斥性

有空讓進:P1不在臨界區時,出臨界區設置flag[1]=false,入臨界區前turn=0,P0都可以進入

無限等待:turn一定等於0或等於1,所以永遠有一個可以進入

多個進程進入臨界區的解決辦法:

1.麵包店演算法。仍然是標記和輪轉的結合,每個進程都會獲得一個序號,序號最小的進入,進程離開時序號為0,不為0的號就是標記。每個進入商店的客戶都會獲得一個號碼,號碼小的先得到服務。互斥進入一定滿足,因為大家號不一樣,有空讓進也滿足,最小序號的進入,有限等待也滿足,他是一個隊列。但代碼實現很複雜,有可能溢出,排號也很麻煩

2.硬體實現:最簡單的辦法實際上是阻止調度,臨界區出現問題的根本原因是調度,另一個進程操作了一個共有的變數。硬體提供了cli()關中斷和sti()開中斷,可以在cpu硬體中加一個標記,但多CPU不太好使

3.硬體原子指令法:鎖本質上就是一個變數,讓其他代碼不能同時執行這一段的代碼,也就是這段代碼不能因為調度被打斷。硬體提供了一種一次執行完畢的指令,如果x是true,則該指令返回true,在while處空轉;如果x是false,它會把x置為true,接下來返回false,進入臨界區執行,但其它代碼就無法進入了

7.信號量的代碼實現

8.實驗

1.嘗試體驗使用fork創建線程,main函數中實現了進程創建和執行不同的函數,cpuio_bound模擬了進程使用cpu和進行io

#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <sys/times.h>

#define HZ    100

void cpuio_bound(int last, int cpu_time, int io_time);

int main(int argc, char * argv[])
{
    pid_t n_proc[10];    //子進程的PID號
    int i;
    for(i=0;i<10;i++)
    {
        /*
         *fork()創建進程
         *返回值為0則創建子進程成功並從子進程返回
         *返回值為PID則是從父進程返回
         *返回值小於0表示進程創建失敗
         */
        n_proc[i] = fork(); 
        if(n_proc[i] == 0)
        {
            // 從子進程返回會進入下麵該代碼區
            // 執行函數結束後return0結束子進程
            cpuio_bound(20, 2*i, 20-2*i);
            return 0;
        else if(n_proc[i] < 0)
        {
            printf("Faild to fork child process %d!\n", i+1);
            return -1;
        }
    }
    // 父進程執行完創建子進程後會進入該代碼區列印子進程的PID
    for(i=0;i<10;i++)
    {
        printf("Child PID: %d\n", n_proc[i]);
    }
    wait(&i);
    return 0;
}

/*
 * 此函數按照參數占用CPU和I/O時間
 * last: 函數實際占用CPU和I/O的總時間,不含在就緒隊列中的時間,>=0是必須的
 * cpu_time: 一次連續占用CPU的時間,>=0是必須的
 * io_time: 一次I/O消耗的時間,>=0是必須的
 * 如果last > cpu_time + io_time,則往複多次占用CPU和I/O
 * 所有時間的單位為秒
 */
void cpuio_bound(int last, int cpu_time, int io_time)
{
    struct tms start_time, current_time;
    clock_t utime, stime;
    int sleep_time;

    while (last > 0)
    {
        /* CPU Burst */
        times(&start_time);
        /* 其實只有t.tms_utime才是真正的CPU時間。但我們是在模擬一個
         * 只在用戶狀態運行的CPU大戶,就像“for(;;);”。所以把t.tms_stime
         * 加上很合理。*/
        do
        {
            times(&current_time);
            utime = current_time.tms_utime - start_time.tms_utime;
            stime = current_time.tms_stime - start_time.tms_stime;
        } while ( ( (utime + stime) / HZ )  < cpu_time );
        last -= cpu_time;

        if (last <= 0 )
            break;

        /* IO Burst */
        /* 用sleep(1)模擬1秒鐘的I/O操作 */
        sleep_time=0;
        while (sleep_time < io_time)
        {
            sleep(1);
            sleep_time++;
        }
        last -= sleep_time;
    }
}                    

8.1實現進程的內核級切換 

 內核創建流程:通過 int0x80  中斷進入 system_call 彙編函數,根據 __NR_fork  號調用 sys_fork  函數,該函數中調用了 copy_process 函數來創建自己的內核棧並牽手父進程的用戶棧

 內核級線程切換流程: schedule 函數找到下一進程的PCB(進程式控制制塊)和LDT(局部描述符),調用 switch_to 彙編函數進行PCB和內核棧的切換,並彈出回用戶棧 

 Linux0.11中的 switch_to 是使用Intel提供的 ljmp 指令完成的,它將TSS中保存的寄存器映像完全覆蓋到CPU中實現進程切換,但這個指令大約需要200個時鐘周期,執行時間很長,本次實驗主要目的是:

  • (1)重寫 switch_to
  • (2)將重寫的 switch_to 和 schedule() 函數接在一起
  • (3)修改 fork()

現在不使用 TSS 進行切換,而是採用切換內核棧的方式來完成進程切換,所以在新的 switch_to 中將用到當前進程的 PCB、目標進程的 PCB、當前進程的內核棧、目標進程的內核棧等信息(內核棧中記錄了用戶棧和用戶代碼兩部分內容;PCB中記錄了進程相關信息,如進程狀態,PID,I/O等)。

Linux 0.11 進程的內核棧和該進程的 PCB 在同一頁記憶體上(一塊 4KB 大小的記憶體),其中 PCB 位於這頁記憶體的低地址,棧位於這頁記憶體的高地址;另外,由於當前進程的 PCB 是用一個全局變數 current 指向的,所以只要告訴新 switch_to()函數一個指向目標進程 PCB 的指針就可以了。同時還要將 next 也傳遞進去,雖然 TSS(next)不再需要了,但是 LDT(next)仍然是需要的,也就是說,現在每個進程不用有自己的 TSS 了,因為已經不採用 TSS 進程切換了,但是每個進程需要有自己的 LDT,地址分離地址還是必須要有的,而進程切換必然要涉及到 LDT 的切換。(整個系統中一個處理器只有一個GDT(全局描述符),每個程式對應一個LDT(局部描述符),包含其代碼、數據、堆棧等)

8.1.1修改switch_to彙編代碼

首先修改 kernel/system_call.s 中的 switch_to 這段彙編代碼

switch_to:
// 因為該彙編函數在c語言中調用,所以需要手動處理棧幀
    pushl %ebp
    movl  %esp,%ebp
    pushl %ecx
    pushl %ebx
    pushl %eax
    movl  8(%ebp),%ebx
    cmpl  %ebx,current
    je    1f

// --------pcb切換--------
    movl  %ebx,%eax
    xchgl %eax,current

// ----TSS中內核棧指針重寫-----
    movl tss,%ecx
    addl $4096,%ebx
    movl %ebx,ESP0(%ecx)


// -------切換內核棧-------
    movl %esp,KERNEL_STACK(%eax)
    movl 8(%ebp),%ebx
    movl KERNEL_STACK(%ebx),%esp


// --------切換LDT--------
    movl 12(%ebp),%ecx
    lldt %cx
    movl $0x17,%ecx
    mov  %cx,%fs
    cmpl %eax,last_task_used_math
    jne  1f
    clts

1:
    popl %eax
    popl %ebx
    popl %ecx
    popl %ebp
    ret

 switch_to這段代碼在 schedule 函數中進行調用,首先將當前ebp指向的幀指針地址壓入內核棧,之後將當前esp指向的地址賦值給幀指針,此時ebp指向的記憶體地址保存的是上一個幀指針的位置

接下來進行了三次壓棧操作,分別將三個寄存器的值保存到內核棧中

    pushl %ebp     movl  %esp,%ebp
    pushl %ecx     pushl %ebc          pushl %eax

第一行將ebp+8位置的值放到寄存器ebx中,然後對比ebx和全局變數current的值,如果相同則是同一進程,直接跳出該部分代碼(下圖第1列)

第二行代碼實現了ebx -> eax, eax和current交換,也就是此時ebx和current都指向下一進程的PCB,eax指向當前進程的pcb(下圖第2列)

第三行,雖然現在不使用TSS進行進程切換,但這種中斷機制還需要保持,我們在 sched.c 中定義了全變數 struct tss_struct *tss=&(init_task.task.tss) ,也就是0號進程的TSS,所有進程共用這個TSS(下圖第3列)

ebx指向PCB地址,Linux0.11中進程的內核棧和PCB放在一塊大小為4K的記憶體段中,高地址開始是內核棧,低地址開始是PCB,所以ebx+4096實際上就是內核棧的地址,其中 ESP0=4 ,因為TSS中內核棧指針esp0就放在偏移為4的地方,也就是我們將內核棧的地址賦給了TSS中的內核棧地址,實現了內核棧指針的重寫

    movl  8(%ebp),%ebx      cmpl  %ebx,current      je   1f
    movl  %ebx,%eax         xchgl %eax,current
    movl  tss,%ecx          addl $4096,%ebx         movl %ebx,ESP0(%ecx)
/* linux/sched.h */
struct tss_struct {
    long    back_link;
    long    esp0;
    /* ...... */
}

第一行完成了內核棧的切換。首先將當前進程esp保存到當前PCB的kernelstack中。此時ebx保存的是下一進程內核棧的地址,應該改成PCB地址,所以重新取ebp+8的位置放入ebx寄存器。接下來將ebx寄存器中保存的kernelstack地址讀入esp寄存器,實現內核棧esp的切換

第二行完成了LDT的切換。將下一進程的內核棧地址送入%ecx,載入LDT局部描述符等。

movl %esp,KERNEL_STACK(%eax)    movl 8(%ebp),%ebx    movl KERNEL_STACK(%ebx),%esp
movl 12(%ebp),%ecx              lldt %cx             movl $0x17,%ecx                 mov  %cx,%fs
/* linux/sched.h */
struct task_struct {
long state;
long counter;
long priority;
long kernelstack;    // 需要增加變數
/* ...... */
}

至此 switch_to 彙編代碼全部寫完,我們需要給他添加全局標識符以及定義用到的變數

 因為PCB結構增加了kernelstack,所以0號進程的PCB初始化時也應該改變,以及信號量對應的位置需要改變

8.1.2 修改fork.c代碼

我們修改完swtich_to彙編代碼實際上實現了內核級線程的切換,同樣我們創建線程的時候也需要構造出相同的樣子。第一段代碼是sys_fork系統調用函數,下麵是fork.c中copy_process()函數的完整代碼,調用了一個彙編函數。

sys_fork:
    call find_empty_process
    testl %eax,%eax
    js 1f
    push %gs
    pushl %esi
    pushl %edi
    pushl %ebp
    pushl %eax
    call copy_process
    addl $20,%esp
1:    ret
// 添加外部聲明, 這裡使用了一段彙編來實現彈出棧信息到寄存器
extern long first_return_from_kernel(void);

int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
              long ebx,long ecx,long edx,
              long fs,long es,long ds,
              long eip,long cs,long eflags,long esp,long ss)
{
    struct task_struct *p;
    int i;
    struct file *f;

    p = (struct task_struct *) get_free_page();

    if (!p)
        return -EAGAIN;
    task[nr] = p;

    *p = *current;    /* NOTE! this doesn't copy the supervisor stack */
    p->state = TASK_UNINTERRUPTIBLE;
    p->pid = last_pid;
    p->father = current->pid;
    p->counter = p->priority;
    p->signal = 0;
    p->alarm = 0;
    p->leader = 0;        /* process leadership doesn't inherit */
    p->utime = p->stime = 0;
    p->cutime = p->cstime = 0;
    p->start_time = jiffies;

    // 初始化內核棧 
    long* krnstack;
    // 這裡PAGE_SIZE是4096, p是PCB的地址,PCB地址加上4096就是內核棧地址
    krnstack = (long*) (PAGE_SIZE + (long)p);

    // ss和sp等都是 copy_process() 函數的參數,來自父進程內核棧
    *(--krnstack) = ss & 0xffff;
    *(--krnstack) = esp;
    *(--krnstack) = eflags;
    *(--krnstack) = cs & 0xffff;
    *(--krnstack) = eip;

    // “內核級線程切換五段論”中的最後一段切換,即完成用戶棧和用戶代碼的切換
    // 依靠的核心指令就是 iret,回到用戶態程式,當然在切換之前應該恢復一下執行現場,主要就是
    // eax,ebx,ecx,edx,esi,edi,gs,fs,es,ds 等寄存器的恢復.
    *(--krnstack) = ds & 0xffff;
    *(--krnstack) = es & 0xffff;
    *(--krnstack) = fs & 0xffff;
    *(--krnstack) = gs & 0xffff;
    *(--krnstack) = esi;
    *(--krnstack) = edi;
    *(--krnstack) = edx;

    // 處理 switch_to 返回,即結束後 ret 指令要用到的,ret 指令預設彈出一個 EIP 操作
    *(--krnstack) = (long)first_return_from_kernel;

    // swtich_to 函數中的 “切換內核棧” 後的彈棧操作
    *(--krnstack) = ebp;
    *(--krnstack) = ecx;
    *(--krnstack) = ebx;
    *(--krnstack) = 0;              

    // 存放在 PCB 中的內核棧指針 指向 初始化完成時內核棧的棧頂
    p->kernelstack = krnstack;
    if (last_task_used_math == current)
        __asm__("clts ; fnsave %0"::"m" (p->tss.i387));
    if (copy_mem(nr,p)) {
        task[nr] = NULL;
        free_page((long) p);
        return -EAGAIN;
    }
    for (i=0; i<NR_OPEN;i++)
        if ((f=p->filp[i]))
            f->f_count++;
    if (current->pwd)
        current->pwd->i_count++;
    if (current->root)
        current->root->i_count++;
    if (current->executable)
        current->executable->i_count++;
    set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
    set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
    p->state = TASK_RUNNING;    /* do this last, just in case */
    return last_pid;
}

8.1.3 修改sched.c函數

我們已經完成了關鍵的switch_to彙編代碼編寫,使得系統可以不使用TSS而是用內核級線程切換;同時我們也完成了fork.c函數的修改,使得我們創建的內核級線程對應了我們switch_to需要的樣子。

最後我們對sched.c進行修改,首先聲明外部函數switch_to,需要傳入當前PCB地址以及LDT地址,然後聲明我們在switch_to中需要的全局變數tss地址,最後修改schedule函數中的句子。


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

-Advertisement-
Play Games
更多相關文章
  • 來源:進擊雲原生 ### 1、檢測兩台伺服器指定目錄下的文件一致性 ``` #!/bin/bash ###################################### 檢測兩台伺服器指定目錄下的文件一致性 ##################################### #通過對 ...
  • 向ES發送請求時,如何創建請求對象呢?官方推薦的builder patter,在面對複雜的請求對象結構時還好用嗎?有沒有更加直觀簡潔的方法,盡在本文一網打盡 ...
  • # Bread.Mvc [Bread.Mvc](https://gitee.com/rizo/bread-mvc) 是一款完全支持 Native AOT 的 MVC 框架,搭配同樣支持 AOT 的 Avalonia,讓你的開發事半功倍。項目開源在 Gitee,歡迎 [Star](https://gi ...
  • ## 前言 **`Visual Studio`** 開發工具的熟練使用,能夠潛在的提升我們工作效率,而且一些開發技巧的使用,會讓我們的工作顯得那麼方便快捷。那麼你知道VS中有哪些你不知道的使用小技巧呢?接下來,我們就來探索VS中的**“任務列表”**的使用。 任務列表是使用 `TODO` 、 `HA ...
  • # .Net 6/Net Core Vue Element Uniapp前後端分離低代碼快速開發框架 這是一個能提高開發效率的開發框架,全自動生成PC與移動端(uniapp)代碼;支持移動ios/android/h5/微信小程式。 # 一、框架能做什麼 1、前後端分離項目 2、純後端項目 3、移動端 ...
  • ## 前言 在軟體系統中,當創建一個類的實例的過程很昂貴或很複雜,並且我們需要創建多個這樣類的實例時,如果我們用new操作符去創建這樣的類實例,這就會增加創建類的複雜度和創建過程與客戶代碼複雜的耦合度。如果採用工廠模式來創建這樣的實例對象的話,隨著產品類的不斷增加,導致子類的數量不斷增多,也導致了相 ...
  • # 高併發解決方法-LVS、LVS-NAT、LVS-DR ## 前言: 集群功能分類: 1.LB Load Balancing,負載均衡(增加處理能力),有一定高可用能力,但不是高可用集群,是以提高服務的**併發處理**能力為根本著重點。**LVS** 2.HA High Availability ...
  • 哈嘍大家好,我是鹹魚 在《[一臺伺服器上部署 Redis 偽集群》](https://mp.weixin.qq.com/s?__biz=MzkzNzI1MzE2Mw==&mid=2247486439&idx=1&sn=0b10317397ef3259dd98d493915dd706&chksm=c2 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...