操作系统的进程调度简析

    陈铁 + 原创作品转载请注明出处 + 《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

    就可以看到进程调度的过程在虚拟机中体现出来。以下截图:

技术分享

技术分享

   

三、下面来分析一下代码的执行过程,描述一下现代操作系统的工作机制。

    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号进程,开始运行,自己建立自己所需要的环境。其次,操作系统毕竟是为实际的程序服务的,接下来就要负责创建其他进程执行环境、资源分配,采用链表机制切换到新进程,并且执行。最后,内核要负责管理进程的状态,利用中断机制实现进程切换,控制程序的执行。总之,操作系统所作的就是中断上下文的处理和进程切换上下文的处理。

本文出自 “StudyPark” 博客,请务必保留此出处http://swordautumn.blog.51cto.com/1485402/1619999

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。