初學Linux中進程調度與進程切換過程

第八講 進程的切換和系統的一般執行過程

@2015.04

一、理論知識

Linux系統的一般執行過程

最一般的情況:正在運行的用戶態進程X切換到運行用戶態進程Y的過程

1. 正在運行的用戶態進程X
2. 發生中斷——save cs:eip/esp/eflags(current) to kernel stack, then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).
3. SAVE_ALL //保存現場,這裡是已經進入內核中斷處裡過程
4. 中斷處理過程中或中斷返回前調用了schedule(),其中的switch_to做了關鍵的進程上下文切換
5. 標號1之後開始運行用戶態進程Y(這裡Y曾經通過以上步驟被切換出去過因此可以從標號1繼續執行)
6. restore_all //恢復現場
7. iret - pop cs:eip/ss:esp/eflags from kernel stack
8. 繼續運行用戶態進程Y

幾種特殊情況

  • 通過中斷處理過程中的調度時機,用戶態進程與內核線程之間互相切換和內核線程之間互相切換,與最一般的情況非常類似,只是內核線程運行過程中發生中斷沒有進程用戶態和內核態的轉換;
  • 內核線程主動調用schedule(),只有進程上下文的切換,沒有發生中斷上下文的切換,與最一般的情況略簡略;
  • 創建子進程的系統調用在子進程中的執行起點及返回用戶態,如fork;
  • 加載一個新的可執行程序後返回到用戶態的情況,如execve;

進程的調度時機與進程的切換

操作系統原理中介紹了大量進程調度算法,這些算法從實現的角度看僅僅是從運行隊列中選擇一個新進程,選擇的過程中運用了不同的策略而已。

對於理解操作系統的工作機制,反而是進程的調度時機與進程的切換機制更為關鍵。

進程調度的時機

  • 中斷處理過程(包括時鐘中斷、I/O中斷、系統調用和異常)中,直接調用schedule(),或者返回用戶態時根據need_resched標記調用schedule();比如sleep,就可能直接調用了schedule
  • 內核線程可以直接調用schedule()進行進程切換,也可以在中斷處理過程中進行調度,也就是說內核線程作為一類的特殊的進程可以主動調度,也可以被動調度;
  • 用戶態進程無法實現主動調度,僅能通過陷入內核態後的某個時機點進行調度,即在中斷處理過程中進行調度。用戶態進程只能被動調度。

進程的切換

  • 為了控制進程的執行,內核必須有能力掛起正在CPU上執行的進程,並恢復以前掛起的某個進程的執行,這叫做進程切換、任務切換、上下文切換;即進程上下文切換!
  • 掛起正在CPU上執行的進程,與中斷時保存現場是不同的,中斷前後是在同一個進程上下文中,只是由用戶態轉向內核態執行;
  • 進程上下文包含了進程執行需要的所有信息 用戶地址空間:包括程序代碼,數據,用戶堆棧等 控制信息:進程描述符,內核堆棧等 硬件上下文(注意中斷也要保存硬件上下文只是保存的方法不同)
  • schedule()函數選擇一個新的進程來運行,並調用context_switch進行上下文的切換,這個宏調用switch_to來進行關鍵上下文切換
    • next = pick_next_task(rq, prev);//進程調度算法都封裝這個函數內部
    • ontext_switch(rq, prev, next);//進程上下文切換
    • switch_to利用了prev和next兩個參數:prev指向當前進程,next指向被調度的進程
#define switch_to(prev, next, last)                     \
{ \
    /*                               \

    * Context-switching clobbers all registers, so we clobber   \
    * them explicitly, via unused output variables.      \
    * (EAX and EBP is not listed because EBP is saved/restored   \
    * explicitly for wchan access and EAX is the return value of    \
    * __switch_to())                      \
    */                                 \

    unsigned long ebx, ecx, edx, esi, edi; \
    asm volatile("pushfl\n\t"      /* save    flags */    \
    "pushl %%ebp\n\t"        /* save    EBP   */  \
    "movl %%esp,%[prev_sp]\n\t"  /* save    ESP   */  \
    "movl %[next_sp],%%esp\n\t"  /* restore ESP   */  \
    "movl $1f,%[prev_ip]\n\t"    /* save    EIP   */  \
    "pushl %[next_ip]\n\t"   /* restore EIP   */     \
    __switch_canary                    \
    "jmp __switch_to\n"  /* regparm call  */  \
    "1:\t" /*下一個進程開始執行的地方!*/                        \
    "popl %%ebp\n\t"     /* restore EBP   */     \
    "popfl\n"         /* restore flags */   \
    /* output parameters */                 \
    : [prev_sp] "=m"(prev->thread.sp),      \
    [prev_ip] "=m"(prev->thread.ip),         \
    "=a"(last),                  \
    /* clobbered output registers: */      \
    "=b"(ebx), "=c"(ecx), "=d"(edx),       \
    "=S"(esi), "=D"(edi)              \
    __switch_canary_oparam                 \
    /* input parameters: */                 \
    : [next_sp]  "m"(next->thread.sp),         \
    [next_ip]  "m"(next->thread.ip),        \
    /* regparm parameters for __switch_to(): */   \
    [prev]     "a"(prev),               \
    [next]     "d"(next)                \
    __switch_canary_iparam                 \
    : /* reloaded segment registers */            \
    "memory");
} while (0)

二、實驗

實驗內容

1.理解Linux系統中進程調度的時機,可以在內核代碼中搜索schedule()函數,看都是哪裡調用了schedule(),判斷我們課程內容中的總結是否準確;
2.使用gdb跟蹤分析一個schedule()函數 ,驗證您對Linux系統進程調度與進程切換過程的理解;推薦在實驗樓Linux虛擬機環境下完成實驗。
3.特別關注並仔細分析switch_to中的彙編代碼,理解進程上下文的切換機制,以及與中斷上下文切換的關係;

實驗步驟

cd LinuxKernel
rm menu -rf
git clone https://github.com/mengning/menu.git
cd menu
mv test_exec.c test.c
make rootfs

效果如下:

  • 關閉QEMU窗口,在shell窗口中,cd LinuxKernel回退到LinuxKernel目錄,使用下面的命令啟動內核並在CPU運行代碼前停下以便調試:
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S

接下來,我們就可以水平分割一個新的shell窗口出來,依次使用下面的命令啟動gdb調試

gdb
(gdb) file linux-3.18.6/vmlinux
(gdb) target remote:1234

並在系統調用

關閉QEMU窗口,在shell窗口中,cd LinuxKernel回退到LinuxKernel目錄,使用下面的命令啟動內核並在CPU運行代碼前停下以便調試:

qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S 接下來,我們就可以水平分割一個新的shell窗口出來,依次使用下面的命令啟動gdb調試

gdb
(gdb) file linux-3.18.6/vmlinux
(gdb) target remote:1234

並在內核函數schedule的入口處設置斷點,接下來輸入c繼續執行,則系統即可停在該函數處,接下來我們就可以使用命令n或者s逐步跟蹤,可以詳細瀏覽pick_next_task,switch_to等函數的執行過程,有圖為證: