棧操作 內核堆棧 運行時 內容 void uri 任務 ini blog

linux內核分析學習筆記 ——第二章 操作系統是如何工作的


計算機的“三大法寶”

  • 程序存儲計算機 即馮諾依曼體系結構,基本上是所有計算機的基礎性的邏輯框架
  • 函數調用堆棧 高級語言可以運行的起點就是函數調用堆棧
  • 中斷機制

函數調用堆棧

  • 堆棧的具體作用
    • 記錄函數調用的框架
    • 傳遞函數參數
    • 保存返回值地址
    • 提供函數內部局部變量的存儲空間
  • 堆棧相關的寄存器
    • ESP:堆棧指針,指向堆棧棧頂
    • EBP:基址指針,指向堆棧棧底,在C語言中記錄當前函數調用基址
    • 技術分享圖片
  • 堆棧操作
    • push 棧頂地址減少4個字節,將操作數放入棧頂存儲單元
    • pop 將操作數從棧頂存儲單元移出,棧頂地址增加4個字節
    • 函數調用堆棧就是由多個邏輯上的棧堆疊起來的框架
  • 其他關鍵寄存器
    • CS:EIP 總是指向下一條指令地址 CS是代碼段寄存器 EIP是指向下一條指令的地址
      • 順序執行:總是指向地址連續的下一條指令
      • 轉跳/分支:CS:EIP會根據程序需要被修改
      • call:將CS:EIP壓入棧頂,隨後指向被調用函數的入口地址
      • ret:從棧頂彈出原來保存在這裏的CS:EIP的值,放入CS:EIP中
    • 函數調用堆棧框架
      • 技術分享圖片

      • 堆棧是C語言程序運行時必須記錄函數調用路徑和參數存儲的空間,pushl和popl指令用來進行出棧壓棧,enter和leave指令對函數調用堆棧框架的建立和拆除進行封裝,堆棧中最關鍵的就是函數調用堆棧框架

  • 堆棧用來傳遞函數的參數
    • 對32位的x86來說堆棧傳遞參數的方法是從左到右依次壓棧
  • 堆棧傳遞返回值
    • 程序用EAX保存返回值。
    • 如果有多個返回值,EAX返回一個內存地址,這個內存地址裏面可以指向很多返回數據。
  • 堆棧還提供局部變量的空間
    • 編譯器一般在函數開始執行時預留出足夠的棧空間來保存函數體的所有局部變量

C語言中內嵌匯編語言的寫法

內嵌匯編的語法如下:
    _asm_ _volatile_ (
                    匯編語句模版;
                    輸出部分;
                    輸入部分;
                    破壞描述部分;
                     );

其中,_asm_ 是GCC的關鍵字asm的宏定義,是內嵌匯編的關鍵字。
_volatile_是GCC的關鍵字,告訴編譯器不要優化代碼,匯編指令保留原樣。
同時,%作為轉義字符,寄存器前面會多一個轉義符號
%加一個數字代表輸入、輸入和破壞描述的編號。

匯編語言語法規則

#include <stdio.h>

int main()
{
    unsigned int val1 = 1;
    unsigned int val2 = 2;
    unsigned int val3 = 0;
    pritnf("val1:%d,val2:%d,val3:%d\n",val1,val2,val3);
    asm volatile(
        "movl $0,%%eax\n\t"
        "addl %1,%%eax\n\t"
        "addl %2,%%eax\n\t"
        "movl %%eax,%0\n\t"
        :"=m"(val3)
        :"c"(vall),"d"(val2)
    );
    pritnf("val1:%d,val2:%d,val3:%d\n",val1,val2,val3);

    return 0;
}
  • "movl $0,%%eax\n\t"將eax寄存器清零

  • "addl %1,%%eax\n\t"
    • %1 是指輸入輸出部分,從0開始編號,所以%1指的是val1,前面的c指的是寄存器ecx用來存儲val1的值
    • 這條語句的就是就是將ecx中存儲的val1的值與eax寄存器中的值相加,結果為1
  • "addl %2,%%eax\n\t"
    • %2 是指val2存在edx寄存器中
    • 這條語句就是將val2與寄存器eax中的值相加,結果為3
  • "movl %%eax,%0\n\t"
    • “=m”代表內存變量,而不是使用寄存器
    • 這條指令就是將val1+val2的值寫入到內存變量val3中去
  • 內嵌匯編當作一個函數來看的話,第二部分和第三部分輸入相當於函數的參數和返回值,第一部分則相當於函數內部具體的代碼。

虛擬一個x86的CPU硬件平臺

  • 中斷
    • 在沒有中斷機制之前,計算機只能一個一個程序執行,也就是批處理。
    • 中斷機制的CPU會把當前正在執行的程序的CS:EIP寄存器和ESP寄存器壓入內核堆棧,將CS:EIP指向中斷程序的入口,保存現場的工作,等重新回來再恢復現場,恢復CS:EIP寄存器和ESP寄存器。

在mykernel基礎上構造一個簡單的操作系統內核

  • 步驟
    • 在實驗樓系統中搭建平臺

技術分享圖片

技術分享圖片

- 在mykernel中查看mymain.c 和 myinterupt.c 代碼

技術分享圖片

技術分享圖片

- 運行結果展示

技術分享圖片

  • 將mykernel操作系統的代碼進行擴展
    • 添加 mypcb.h 的頭文件,用來定義進程控制塊
    • 修改 mymain.c 作為內核代碼的入口,負責初始化內核的各個組成部分
    • 修改 myinterrupt.c 增加進程切換代碼

代碼如下:

mypcb.h:

#define MAX_TASK_NUM  4
#define KERNEL_STACK_SIZE 1024*8
 
  struct Thread {
    unsigned long   ip;
    unsigned long   sp;
};

typedef struct PCB{ 
    int pid;                                        //進程的編號
    volatile long state;                            //進程的狀態
    char stack[KERNEL_STACK_SIZE];                  //進程的棧
    struct Thread thread;                           //Thread 結構體
    unsigned long   task_entry;                     //進程的起始入口地址
    struct PCB *next;                                //單鏈表鏈接每個進程
 }tPCB;

void my_schedule(void);                          //調度器
  • 頭文件一開始的定義,表示的是進程的數目、進程堆棧的大小。
  • 頭文件中結構體PCB表示一個進程結構體,其中包括進程編號、進程運行狀態、進程堆棧的大小、進程的兩個指針、進程入口以及指向一下個進程的指針next。

mymain.c

#include <linux/types.h>#include <linux/types.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/tty.h>
#include <linux/vmalloc.h>

#include "mypcb.h"


tPCB task[MAX_TASK_NUM];//定義4個進程
tPCB * my_current_task = NULL;
volatile int my_need_sched = 0;

void my_process(void);   //每10000000 來進行進程調度,調用my_schedule

void __init my_start_kernel(void)
{ 
    int pid = 0; 
    int i;
    task[pid].pid = pid;   //0號進程pid設為0
    task[pid].state = 0;   //0號進程state設為可運行
    task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;//0號進程的ip和入口地址設為my_process();
    task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
    task[pid].next = &task[pid];  //next指針指向自己
    for(i=1;i<MAX_TASK_NUM;i++)  //1,2,3號進程復制0號進程
        {
            memcpy(&task[i],&task[0],sizeof(tPCB));
            task[i].pid = i;
            task[i].state = -1;
            task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];
            task[i].next = task[i-1].next;
            task[i-1].next = &task[i];  //所有進程成為一個循環鏈表
        }
    pid = 0;
    my_current_task = &task[pid];   //當前運行的進程設為0號進程
     asm volatile(
        "movl %1,%%esp\n\t" //esp指向stack數組的末尾
        "pushl %1\n\t"  //將task[0].thread.sp壓棧
        "pushl %0\n\t"  //將task[0].thread.ip壓棧
        "ret\n\t"   //eip指向0進程起始地址,啟動0號進程
        "popl %%ebp\n\t"//釋放棧空間
        : 
        : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) 
    );
}   

void my_process(void)
    {
        int i = 0;
       while(1)
    {
        i++;
        if(i%10000000 == 0)
        {
            printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid);
            if(my_need_sched == 1)
        {
            my_need_sched = 0;
            my_shcedule();
        }
            printk(KERN_NOTICE"this is process %d +\n",my_current_task->pid);
        }
    }
}
  • mymain.c
    • void _init my_start_kernel(void) 函數用於初始化0號進程
    • task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];這一句可以看出來,進程的sp指針,代表的是堆棧的棧底,因為堆棧從高地址向低地址轉變,所以數組的最高位代表的就是堆棧的棧底。
    • for(i=1;i<MAX_TASK_NUM;i++)for循環用於生成其他的三個進程,並將它們連成單鏈表
    • 下面對內嵌匯編代碼詳細分析
      • movl %1,%%esp\n\t 表示將task[pid].thread.sp指針的指向存放在esp中,即esp指向0進程的堆棧棧底
      • pushl %1\n\t 表示將當前堆棧棧底地址入棧
      • pushl %0\n\t 表示當前進程的EIP入棧
      • ret\n\t 將進程的入口放入EIP寄存器中

技術分享圖片

技術分享圖片

技術分享圖片

技術分享圖片

接下來進程0啟動,開始執行my_process(void)函數代碼。

myinterrupt.c

#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/tty.h>
#include <linux/vmalloc.h>

#include "mypcb.h"


extern tPCB task[MAX_TASK_NUM];
extern *tPCB my_current_task;
extern volatile int my_need_sched;
volatile int time_count = 0;

void my_timer_handler(void)
{
    #if 1
    if(time_count%1000 == 0 && my_need_sched != 1)
    {
        printk(KERN_NOTICE ">>>my_timer_handler here<<<\n");
        my_need_sched = 1;
    } 
    time_count ++ ;  
    #endif
    return;  
}

void my_schedule(void)
{
    tPCB * next;
    tPCB * prev;
    
     if(next->state == 0)  //下一個進程可運行,執行進程切換
    {
        /* switch to next process */   
        asm volatile(
            "pushl %%ebp\n\t"   //保存當前進程的ebp
            "movl %%esp,%0\n\t" //將當前進程的esp儲存到當前進程的thread.sp
            "movl %2,%%esp\n\t" //esp指向下一個進程
            "movl $1f,%1\n\t"   //將1f存儲到thread.sp.$1f是“1:\t”處,再次調度到該進程時就會從1:開始執行
            "pushl %3\n\t"  //將下一個進程的thread.ip壓棧
            "ret\n\t"   //eip指向下一個進程的起始地址
            "1:\t"  
            "popl %%ebp\n\t"//待下一個進程執行完後釋放棧空間,恢復現場
            : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
            : "m" (next->thread.sp),"m" (next->thread.ip)
           ); 
        my_current_task = next; 
        printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);  
    else
    {
        next->state = 0;
        my_current_task = next;
        printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->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)
        );  
    }
    return;
}
  • 中斷函數計數達到1000產生一個中斷,將my_need_sched置1,此時my_process()將會執行my_shcedule()實現進程調度。
  • my_shcedule()有兩種情況,一種是next->state == 0表示下一個要切換的進程正在運行。
    • 下面是對應堆棧變化的圖解

技術分享圖片

技術分享圖片

技術分享圖片

技術分享圖片

技術分享圖片

問題

在畫進程切換堆棧時,我很疑惑的是這些堆棧是誰的堆棧,是進程自己的堆棧還是內核堆棧呢?查閱相關資料,以下是一些概念。

  • 進程的堆棧
    • 內核在創建進程時,會為進程創建堆棧,每個進程都有兩個堆棧,一個是用戶棧,一個是內核棧,分別存在於用戶空間和內核空間。
    • 當進程因為中斷或者系統調用而陷入內核態之時,進程所使用的堆棧也要從用戶棧轉到內核棧。
      • 當一個任務(進程)執行系統調用而陷入內核代碼中執行時,稱進程處於內核運行態(內核態)。
      • 當進程在執行用戶自己的代碼時,則稱其處於用戶運行態(用戶態)。
      • 參考http://www.cnblogs.com/Anker/p/3269106.html
        https://blog.csdn.net/lqygame/article/details/72898069
  • 處理器的運行狀態
    • 內核態 運行在進程上下文
    • 內核態 運行於終端上下文
    • 用戶態 運行於用戶空間
  • 我的理解
    • 我在上面畫出的堆棧都是在內核棧中的各個進程的內核堆棧,進程的用戶棧一般是存儲進程要處理的相關操作的內容。兩個堆棧的切換也是內核堆棧的切換。
      技術分享圖片
    不知道我的理解是不是正確,因為提到進程調度會涉及到用戶棧到內核棧的切換,剛開始畫圖一直在思考,這裏的代碼到底是對用戶棧的操作還是對內核棧的操作,後來發現這個代碼主要是以進程的調度為重點,所以調度代碼都是在內核棧的操作。

2018-2019-1 20189206 《Linux內核原理與分析》第三周作業