docker底層實現原理,《Linux內核分析》(二)——從一個簡單Linux內核分析進程切換原理

 2023-11-09 阅读 31 评论 0

摘要:轉載:https://blog.csdn.net/FIELDOFFIER/article/details/44280717 《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000?” 實驗環境:c+Linux64位 (32位系統可能結果會不同) 依照學術誠信條款,我保證此回答

轉載:https://blog.csdn.net/FIELDOFFIER/article/details/44280717

《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000?”
實驗環境:c+Linux64位 (32位系統可能結果會不同)
依照學術誠信條款,我保證此回答為本人原創,所有回答中引用的外部材料已經做了出處標記。


源代碼以及運行環境搭建請參考mykernel,其中提供了一個簡單的Linux內核源代碼,本文主要分析其中的三個文件:

  • mypcb.h
  • mymain.c
  • myinterrupt.c

通過對這三個文件的分析來理解進程的切換原理。

注意,雖然在源代碼中建立了4個進程并且進行循環的切換,但是為了簡便分析時假定只有兩個進程,編號0和1。另,在實際的Linux系統中每個進程都會有兩個堆棧:用戶態一個、內核態一個,但是在這個模擬的內核中每個進程只分配了一個堆棧。

首先,從mymain.c開始分析

docker底層實現原理?在進程切換中最為重要的是運行棧的切換和eip(即程序計數器)的正確跳轉,mymain.c中的函數my_start_kernel是最開始執行的代碼,因此從這個函數開始進行分析。my_start_kernel函數首先建立起了4個進程并且進行了初始化,如分配棧等,注意在剛建立的時候只有0號進程的狀態是runuable,其余的都是unrunnable。還有就是PCB結構中的threap.sp,每個進程對應一個棧,所以在這里thread.sp指向對應PCB內的char stack[KERNEL_STACK_SIZE - 1],即用這個字符數組作為運行棧,因為棧是由高地址向低地址增長,所以指向stack[KERNEL_STACK_SIZE - 1]。
接下來重點分析這段代碼:

/* start process 0 by task[0] */pid = 0;my_current_task = &task[pid];asm volatile("movl %1,%%esp\n\t"     /* set task[pid].thread.sp to esp */"pushl %1\n\t"          /* push ebp */"pushl %0\n\t"          /* push task[pid].thread.ip */"ret\n\t"               /* pop task[pid].thread.ip to eip */"popl %%ebp\n\t": : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)   /* input c or d mean %ecx/%edx*/);
  • 這段內嵌匯編代碼的功能是完成對0號進程的啟動,其運行時的棧的情況如下:
  • 開始之前的棧的情況:

    P1

"movl %1,%%esp\n\t"     /* set task[pid].thread.sp to esp */
  • 本條指令把0號進程當前的棧頂地址放入esp,因為初始化的時候task[0].thread.sp指向的是stack[KERNEL_STACK_SIZE-1],所以執行完這條指令之后esp指向task[0]的堆棧的棧頂處,如下圖:

2

"pushl %1\n\t"          /* push ebp */
  • 把task[0]的sp壓入棧,棧的示意圖如下:

3

"pushl %0\n\t"          /* push task[pid].thread.ip */
  • 把0號進程的ip,即my_process()函數的入口地址入棧,此時棧內情況如下圖:

4

"ret\n\t"               /* pop task[pid].thread.ip to eip */
  • 從棧中彈出剛剛放入的my_process()函數入口地址賦給eip,開始運行0號進程

centos6內核版本。5

my_process()在執行時將會判斷是否需要進行進程切換,由于我們假設內核中只有0和1兩個進程,我們假定切換條件已經滿足,在此直接分析從0號進程切換到1號進程的情況。
由于當前1號進程的pid[1].state是 “-1”(unrunnable),所以將會執行my_schedule()函數的else分支的內容。
my_interrupt.c文件的my_schedule()函數的else分支的內容如下:

else{next->state = 0;my_current_task = next;printk(KERN_NOTICE "switch from %d process to %d process\n \>>>process %d running!!!<<<\n\n\n",prev->pid,next->pid,next->pid);/* switch to new process */asm volatile(  "pushl %%ebp\n\t" /* save ebp */"movl %%esp,%0\n\t" /* save esp */"movl %2,%%esp\n\t" /* restore esp */"movl %2,%%ebp\n\t" /* restore ebp */"movl $1f,%1\n\t" /* save eip */   "pushl %3\n\t""ret\n\t" /* restore eip */: "=m" (prev->thread.sp),"=m" (prev->thread.ip): "m" (next->thread.sp),"m" (next->thread.ip));}
  • 在此重點分析其中內嵌的匯編代碼。由于在從函數my_start_kernel()切換到函數my_process()的時候會有保存現場的動作,函數my_process()調用函數my_schedule()也會保存現場,所以在上述代碼執行前的棧的情況應該是:

這里插入第六張圖

"pushl %%ebp\n\t" /* save ebp */
"movl %%esp,%0\n\t" /* save esp */
  • 保存0號進程的運行現場,即首先把0號進程的當前的棧底保存在棧中,然后把當前esp的值保存在0號進程的thread.sp中,這些現場值在切回到0號進程的時候是可以復原的,此時的棧的情況:

7

"movl %2,%%esp\n\t" /* restore esp */
"movl %2,%%ebp\n\t" /* restore ebp */
  • 開始進行棧切換,即從task[0].stack,切換到task[1].stack,此時的棧的情況:

8

"movl $1f,%1\n\t" /* save eip */
  • 這里的$1f是一個有特殊意義的數字,上述這條代碼的功能是當下一次進程切換回0號進程時,將會從這里繼續執行。
"pushl %3\n\t"
  • 把1號進程的進程入口地址(這里因為是1號進程第一次運行,所以就是my_process()函數的入口地址,若果不是第一次運行,由于執行過上一條代碼”movl $1f,%1\n\t”那么進程1將會按照被切換掉的時候的代碼繼續執行下去)入棧。此時棧中情況如圖:

9

"ret\n\t" /* restore eip */
  • 從當前棧task[1].stack中彈出剛剛送入的1號進程的入口地址,即執行my_process()函數,啟動1號進程,與0號進程的啟動類似,接下來就是判斷是否需要進程切換。在此同樣假設進程切換的條件都已經滿足,接下來分析從1號進程切換到0號進程的過程。

shell多線程。首先,經過了一系列的函數調用,1號進程的棧已經發生了變化,示意圖如下:

10

接下來開始分析從1號進程切換回0號進程的的流程

由于0號進程之前運行過,所以其狀態是runnable,則調度過程中將進入my_schedule()方法的if分支執行,即執行如下代碼:

if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */{//save current scene/* switch to next process */asm volatile(  "pushl %%ebp\n\t" /* save ebp */"movl %%esp,%0\n\t" /* save esp */"movl %2,%%esp\n\t" /* restore esp */"movl $1f,%1\n\t" /* save eip */   "pushl %3\n\t""ret\n\t" /* restore eip */"1:\t" /* next process start here */"popl %%ebp\n\t": "=m" (prev->thread.sp),"=m" (prev->thread.ip): "m" (next->thread.sp),"m" (next->thread.ip));my_current_task = next;//switch to the next taskprintk(KERN_NOTICE "switch from %d process to %d process\n>>>process %d running!!!<<<\n\n",prev->pid,next->pid,next->pid);}
  • 主要分析嵌入的匯編代碼
"pushl %%ebp\n\t" /* save ebp */
"movl %%esp,%0\n\t" /* save esp */
  • 實現1號進程的運行棧環境保存,具體做法是把當前ebp入棧,然后把棧頂指針esp存入1號進程的task[1].thread.sp,執行后的棧情況如圖:

11

"movl %2,%%esp\n\t" /* restore esp */
  • 此條匯編語句執行后,esp所指的棧已經改變,即由1號進程切換到0號進程,把進程0的thread.sp的值賦給esp,由于在從進程0切換到進程1的時候保存過thread.sp的值,所以此時的棧如圖:

12

    "movl $1f,%1\n\t" /* save eip */"pushl %3\n\t"
  • 這兩條代碼與之前切換時實現相同的功能,都是為下一步的切換做的準備,之后的棧如圖:

shell進程?13

"ret\n\t" /* restore eip */
  • 這是由1號進程,即當前進程調用的my_schedule()將執行最后一條指令,其后的語句將在下次進行進程切換,切換回1號進程時繼續執行。 此時棧情況如圖:

14

此時eip和esp都已經切換回了由進程0切換到進程1最后時刻的值,當eip繼續去指令時,得到的將是0號進程上一次被切換掉時的指令的下一條指令,如下:

/* start process 0 by task[0] */pid = 0;my_current_task = &task[pid];asm volatile("movl %1,%%esp\n\t"     /* set task[pid].thread.sp to esp */"pushl %1\n\t"          /* push ebp */"pushl %0\n\t"          /* push task[pid].thread.ip */"ret\n\t"               /* 進程0在這里被切換走 */"popl %%ebp\n\t" /* 接著應該執行這條指令 */: : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)   /* input c or d mean %ecx/%edx*/);
  • 也就是說接下來要執行的指令是:
"popl %%ebp\n\t"
  • 之后的棧的情況如圖:

15

在這之后,0號進程將順著執行流,完成對my_schedule()的調用,并且返回0號進程的my_process(),然后就是在進程0與進程1之間不斷的切換。


參考文獻:
https://github.com/mengning/mykernel/blob/master/mymain.c

Linux內核組成,https://github.com/ExiaHan/linuxKernelStudy/blob/master/secondWeekend/processSwitch.md

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/3/169312.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息