操作系統的進程調度簡析

    陳鐵 + 原創作品轉載請註明出處 + 《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000 ”。特別說明,所有代碼出自孟寧老師的mykernel,也許出於練習的目的有所修改,也可忽略。


    學習的過程其實就是不斷的模仿,重複老師演示的內容,不斷地練習,直到成爲自己所能獨立表述的知識。自己實在很笨了,作業勉強完成,好在也算努力,花時間多些,畢竟是自己的辛苦學習的過程體現。所以擺出來給方家一笑,好歹也是自己學習的收穫。


   一、 實驗用的是實驗樓環境,虛擬機環境如下:Linux d0c756f6c18a 3.13.0-30-generic #55-Ubuntu SMP Fri Jul 4 21:40:53 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux。實驗開始使用簡單代碼,可以看見中斷調度演示。

cd LinuxKernel/linux-3.9.4

qemu -kernel arch/x86/boot/bzImage

   

二、將老師的代碼mypch.b,mymain.c,myinterrupt.c複製到mykernel目錄中。回到kernel目錄下:

make all

qemu -kernel arch/x86/boot/bzImage

    就可以看到進程調度的過程在虛擬機中體現出來。以下截圖:

wKioL1UCNgqRaXrtAAHEoAz_Ako469.jpg

wKiom1UCNOrRFLJiAAPPgY7b62w412.jpg

   

三、下面來分析一下代碼的執行過程,描述一下現代操作系統的工作機制。

    1.在linux核心中爲了實現高效執行,大量使用了內聯彙編,所以在此先介紹一下內聯彙編的相關知識。(1)雖然現代編譯器優化代碼,但仍比不過手寫的彙編代碼;(2)有些平臺相關的指令必須手寫,在C語言中沒有等價的語法,例如x86是端口I/O。

    gcc提供了一種擴展語法可以在C代碼中使用內聯彙編。最簡單的格式是__asm__("assembly code");,例如__asm__("nop");就只是執行一條空指令。執行多條彙編指令,則應該用\n\t將各條指令分隔開。

    內聯彙編要和C的變量建立關聯,使用完整的內聯彙編格式:

__asm__(assembler template 
	: output operands                  /* optional */
	: input operands                   /* optional */
	: list of clobbered registers      /* optional */
	);

這種格式由四部分組成,第一部分是彙編指令,第二部分和第三部分是約束條件,第二部分指示彙編指令的運算結果 要輸出到哪些C操作數中,C操作數應該是左值表達式,第三部分指示彙編指令需要從哪些C操作數獲得輸入,第四部分是在彙編指令中被修改過的寄存器列表,指示編譯器哪些寄存器的值在執行這條__asm__語句時會改變。後三個部分都是可選的,如果有就填寫,沒有就空着只寫個:號。

    2.mypcb.h代碼如下:

/*
* linux/mykernel/mypcb.h
*
* Kernel internal PCB types
*
* Copyright (C) 2013 Mengning
*
*/
#define MAX_TASK_NUM 4               //定義系統執行的最大進程數。
#define KERNEL_STACK_SIZE 1024*8     //內核堆棧大小
/* CPU-specific state of this task */
struct Thread {                     //定義結構體Thread
    unsigned long    ip;                //存儲指令指針和堆棧指針
    unsigned long    sp;
};
typedef struct PCB{               //結構體類型進程控制塊PCB
    int pid;                          //進程id
    volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ //進程狀態
    char stack[KERNEL_STACK_SIZE];        //進程堆棧
    /* CPU-specific state of this task */
    struct Thread thread;
    unsigned long    task_entry;    //入口
    struct PCB *next;               //形成鏈表,下一個進程
}tPCB;
void my_schedule(void);     //調度函數

    3.以下mymain.c主程序代碼

/*
* linux/mykernel/mymain.c
*
* Kernel internal my_start_kernel
*
* Copyright (C) 2013 Mengning
*
*/
#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];      //定義進程數組
tPCB * my_current_task = NULL;  //當前進程指針,從0號進程開始
volatile int my_need_sched = 0; //0號進程不需要調度
void my_process(void);

void __init my_start_kernel(void)    //內核創建進程,從0號進程開始初始化
{
    int pid = 0;
    int i;
    /* Initialize process 0*/
    task[pid].pid = pid;        
    task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */
    //指令指針指向自己
    task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process; 
    //堆棧指向定義的內核Stack
    task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
    task[pid].next = &task[pid];
    /*fork more process */
    for(i=1;i<MAX_TASK_NUM;i++)    //通過fork函數啓動更多的進程,本例0,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];
    }
    /* start process 0 by task[0] */ //啓動0號進程
    pid = 0;
    my_current_task = &task[pid];
    /*
        內聯彙編,%0,%1代表輸入輸出部分的變量"c"代表ECX,"d"代表EDX,"=m"表示內存
        %%reg表示寄存器。\n\t表示結束。
        以下彙編代碼不難理解,就是爲了效率。構建起CPU的運行環境,啓動了0號進程。
    */
    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*/
    );
    }
    /*以下是我們的簡單進程所執行的代碼,用來讓人類知道CPU執行了哪個進程。實際上很多操作系統進程
    只是在後臺執行,並不需要進行人機交互,但我們不要忽略了它們。
    */
    void my_process(void)
    {
        int i = 0;
        while(1)
        {
            i++;
        if(i%10000000 == 0)    //循環一千萬次,輸出一次進程id,主動調度,避免消息機制。
        {
            printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid);
            if(my_need_sched == 1)
            {
                my_need_sched = 0;
                my_schedule();
            }
            printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid);
        }
    }
}

        4.以下是myinterrupt.c的代碼及簡單說明:

/*
* linux/mykernel/myinterrupt.c
*
* Kernel internal my_timer_handler
*
* Copyright (C) 2013 Mengning
*
*/
#include <linux/types.h>
#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;    //時間計數已實現主動執行,我們的簡單代碼不接受輸入
/*
* Called by timer interrupt.
* it runs in the name of current running process,
* so it use kernel stack of current running process
*/
void my_timer_handler(void)
{
    #if 1
    //計數1000次並且沒有切換進程就輸出一行提醒
    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(my_current_task == NULL
    || my_current_task->next == NULL)
    {
        return;                //出錯處理
    }
    printk(KERN_NOTICE ">>>my_schedule<<<\n");
    /* schedule */
    next = my_current_task->next;
    prev = my_current_task;
    if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */
    {
    /* 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;
    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;
}

四、實驗總結,老師簡化的代碼還不難理解,但要自己編寫還沒有這個本事,所以直接抄下來自己理解一下,執行的過程沒有出現報錯。雖然是簡化代碼,但對於理解操作系統的工作機制還是很有幫助的。首先是內核的自舉,畢竟所有的程序都不過是內存中的代碼,內核不過是認爲指定了特權,0號進程,開始運行,自己建立自己所需要的環境。其次,操作系統畢竟是爲實際的程序服務的,接下來就要負責創建其他進程執行環境、資源分配,採用鏈表機制切換到新進程,並且執行。最後,內核要負責管理進程的狀態,利用中斷機制實現進程切換,控制程序的執行。總之,操作系統所作的就是中斷上下文的處理和進程切換上下文的處理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章