一个简单的时间片轮转多道程序内核代码

Casualet + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”

 

本文将基于linux 内核3.9.4 来分析一个简单的时间片轮转的多道程序的代码, 并总结其中的原理. 

首先,我们要搭建一个实验环境,方便我们来运行代码.本文采用的主机环境是ubuntu14.04.1, 搭建环境的方法可以参考 https://github.com/mengning/mykernel . 总体上分为以下几个步骤:

1.sudo apt-get install qemu

  该软件模拟了x86 cpu 的运行,可以用它来运行内核代码

2.sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu

  安装完上面的软件以后, 我们可以通过qemu-system-i386 来使用. 但是这个名字太长, 所以我们建立一个名字比较短的链接.这样就可以通过qemu来调用.

3.wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.9.4.tar.xz

  获得linux内核代码

4.wget https://raw.github.com/mengning/mykernel/master/mykernel_for_linux3.9.4sc.patch

  获得内核的补丁,应该是对内核进行了修改, 是的我们可以使用这个环境.

5. xz -d linux-3.9.4.tar.xz

  tar -xvf linux-3.9.4.tar

  cd linux-3.9.4   

  对压缩包进行解压, 然后进入解压目录.

6.patch -p1 < ../mykernel_for_linux3.9.4sc.patch

  使用下载的补丁修改内核文件.

7.make allnoconfig

   make

这一步完成后, 经过一段时间的编译, 就可以获得如下图所示的结果,注意倒数第四行的BUILD arch/x86/boot/bzImage, 这个是我们下面要使用的文件.

我们发现, 当前目录下多了一个mykernel 文件夹, 里面有我们后面需要修改的代码mymain.c 以及myinterrupt.c. 我们先按现有的运行命令:

qemu -kernel arch/x86/boot/bzImage

可以发现, myinterrupt.c 的代码开始执行,效果如下:

这样,我们有了一个基础的运行环境, 以后只要修改mymain.c 以及myinterrupt.c 然后使用上面的方法, 重新编译,就可以看到自己的程序的运行效果. 接下来, 我们在这个的基础上, 对

mykernel中的代码进行修改, 重新编译, 运行, 并解释过程中的原理.

---------------------------------------分割线------------------------------------------------------------------

首先, 进程是运行的一段代码, 那么是什么代码呢? 我们在这里指定一个自己写的c函数, 作为所有要调度的进程的代码. 这个函数是my_process, 他的作用是, 打印出当前进程的id, 然后在适当的时候调用schedule函数, 进行调度.
为了管理这么多的进程, 我们需要进程控制块. 进程控制块是一个结构体, 用来管理进程. 那么这个结构体包含什么内容呢? 首先, 为了标识不同的进程, 我们需要有进程id, 这个id可以用一个简单的int变量来表示. 进程有自己的代码, 必须要有一直指针指向代码的位置. 进程还得有自己的状态信息. 这样, 我们可以写一个进程空置块的结构体描述:
struct Thread {
    unsigned long        ip;
    unsigned long        sp;
};
typedef struct PCB{
    int pid;
    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;
然后, 我们会初始化进程控制块数组信息, 并且启动进程0, 先给出带有注释的代码.

/*
 *  linux/mykernel/mymain.c
 *
 *  Kernel internal my_start_kernel
 *
 *  Copyright (C) 2013  Mengning
 *
 */
#include <linux/types.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/kernel.h>
#include <linux/syscalls.h>
#include <linux/stackprotector.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/delay.h>
#include <linux/ioport.h>
#include <linux/init.h>
#include <linux/initrd.h>
#include <linux/bootmem.h>
#include <linux/acpi.h>
#include <linux/tty.h>
#include <linux/percpu.h>
#include <linux/kmod.h>
#include <linux/vmalloc.h>
#include <linux/kernel_stat.h>
#include <linux/start_kernel.h>
#include <linux/security.h>
#include <linux/smp.h>
#include <linux/profile.h>
#include <linux/rcupdate.h>
#include <linux/moduleparam.h>
#include <linux/kallsyms.h>
#include <linux/writeback.h>
#include <linux/cpu.h>
#include <linux/cpuset.h>
#include <linux/cgroup.h>
#include <linux/efi.h>
#include <linux/tick.h>
#include <linux/interrupt.h>
#include <linux/taskstats_kern.h>
#include <linux/delayacct.h>
#include <linux/unistd.h>
#include <linux/rmap.h>
#include <linux/mempolicy.h>
#include <linux/key.h>
#include <linux/buffer_head.h>
#include <linux/page_cgroup.h>
#include <linux/debug_locks.h>
#include <linux/debugobjects.h>
#include <linux/lockdep.h>
#include <linux/kmemleak.h>
#include <linux/pid_namespace.h>
#include <linux/device.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/signal.h>
#include <linux/idr.h>
#include <linux/kgdb.h>
#include <linux/ftrace.h>
#include <linux/async.h>
#include <linux/kmemcheck.h>
#include <linux/sfi.h>
#include <linux/shmem_fs.h>
#include <linux/slab.h>
#include <linux/perf_event.h>
#include <linux/file.h>
#include <linux/ptrace.h>
#include <linux/blkdev.h>
#include <linux/elevator.h>

#include <asm/io.h>
#include <asm/bugs.h>
#include <asm/setup.h>
#include <asm/sections.h>
#include <asm/cacheflush.h>

#ifdef CONFIG_X86_LOCAL_APIC
#include <asm/smp.h>
#endif
#include "mypcb.h"
tPCB task[MAX_TASK_NUM];//所有参加调度的进程的控制信息数组
tPCB * my_current_task = NULL;//指向当前进程,初始是0号
volatile int my_need_sched = 0;//一个共享的变量
void my_process(void);
#define MAIN_FREQUENCE 100000000

void __init my_start_kernel(void)
{
    int pid = 0;
    int i;
    task[pid].pid = pid;
    task[pid].state = 0;
    task[pid].task_entry = task[pid].thread.ip =(unsigned long)my_process;
    task[pid].thread.sp = (unsigned long) &task[pid].stack[KERNEL_STACK_SIZE-1];
    task[pid].next = &task[pid];
    /*以上代码实现了第0个进程控制块信息的初始化工作, 设置了pid=0;
     * state=0;然后设置了函数my_process的位置作为ip的位置,并且
     * 设置了sp执行自己申请的stack空间的最后一个字节的地址.
     *next指针指向了自己, 这个是暂时的,后面会做修改.
     */

    for(i=1;i<MAX_TASK_NUM;i++){
        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-1].next = &task[i];
    }
    //task[MAX_TASK_NUM -1].next = &task[0];
    /*
     * 这里使用了循环, 初始化了整个进程控制的数组. 基本的想法是,
     * 使用memcpy()函数, 复制task[0]的内容, 然后根据各个task的实际
     * 情况,进行定制. 定制的内容有:
     * pid, state,sp. 并且利用next指针使得整个的控制信息形成了一个
     * 链式的结构.
    */
    pid = 0;
    my_current_task = &task[pid];
    asm volatile(
          "movl %1,%%esp\n\t"
          "pushl %1\n\t"
          "pushl %0\n\t"
          "ret\n\t"
          "popl %%ebp\n\t"
          :
          :"c"(task[pid].thread.ip),"d"(task[pid].thread.sp)
    );
    /*
     * 上面使用了嵌入式汇编, 启动了进程0的代码,开始执行进程0的my_process
     * 代码. 其基本过程是:
     * 首先,初始化的时候, sp指向了task空间的最后一位,所以,要开始
     * 进入task[0]的环境,需要使得esp寄存器只想正确的位置. 在这个时候,
     * 通过movl是的栈的最后一个字节地址赋给esp, 以后每次压栈,esp都会减小,
     * 这样进程0就可以用这个栈空间了.
     * 然后由于这个时候,初始状态,esp=ebp,所以我们通过pushl等效达到了
     * 保存ebp值的目的. 后面的push 和 ret 是一起用的, 先push ip的值,
     * 然后ret是的ip的值放到eip寄存器中. 由于ip中包含了my_process函数的地址
     * 所以,这里可以实现跳转到my_process函数进行执行的效果.
     *
     */
}

void my_process(void){
    int i = 0;
    while(1){
        i++;
        if(i%MAIN_FREQUENCE == 0){
            int j = 0;
            for(;j<3;j++){
            my_delay(1000);
            printk(KERN_NOTICE "this is process %d time=%d-\n",my_current_task->pid,j);
            }
            if(my_need_sched ==1){
                my_need_sched = 0;
                printk("start schedule in main pid= %d",my_current_task->pid);
                my_schedule();
            }
            printk(KERN_NOTICE "\nPID=%d, after",my_current_task->pid);
        }
    }
}
/*对这个函数进行少量的修改, 并且从第一次的进程0开始讲起.
 *进程0通过上面的机制跳进了这个函数开始执行, 这是一个无限循环.每次
 if(i%**)判断成功, 就会循环打印30次的信息, 然后判断my_need_shed是不是
 =1.如果=1就会调用schedule函数, 跳到其他进程. 其他进程由于PCB的信息中ip
 也是指向这个函数, 所以也会执行这段代码, 但是执行的状态是不一样的.
 *
 */

void my_delay(int n){
    int i,j;
    volatile int temp;
    for(i=0;i<n;i++)
        for(j=0;j<300000;j++)
            temp = j;


}
接下来就是中断,始终中断设置了一个变量的值, 是的调度能够进行, 现给出代码以及注释.

/*
 *  linux/mykernel/myinterrupt.c
 *
 *  Kernel internal my_timer_handler
 *
 *  Copyright (C) 2013  Mengning
 *
 */
#include <linux/kernel_stat.h>
#include <linux/export.h>
#include <linux/interrupt.h>
#include <linux/percpu.h>
#include <linux/init.h>
#include <linux/mm.h>
#include <linux/swap.h>
#include <linux/pid_namespace.h>
#include <linux/notifier.h>
#include <linux/thread_info.h>
#include <linux/time.h>
#include <linux/jiffies.h>
#include <linux/posix-timers.h>
#include <linux/cpu.h>
#include <linux/syscalls.h>
#include <linux/delay.h>
#include <linux/tick.h>
#include <linux/kallsyms.h>
#include <linux/irq_work.h>
#include <linux/sched.h>
#include <linux/sched/sysctl.h>
#include <linux/slab.h>

#include <asm/uaccess.h>
#include <asm/unistd.h>
#include <asm/div64.h>
#include <asm/timex.h>
#include <asm/io.h>

#define CREATE_TRACE_POINTS
#include <trace/events/timer.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 = 1;
#define INTERRUPT_FREQUENCE 1000

/*
 * Called by timer interrupt.
 */
void my_timer_handler(void){
    if(time_count%INTERRUPT_FREQUENCE==0 && my_need_sched !=1){
         printk(KERN_NOTICE "\n>>>>>>>time_count = %d,let me chage my_nee_sched to 1!\n\n",time_count++);
        my_need_sched = 1;
        time_count = 0;
    }
    time_count++;
}
/*这里的my_time_handler函数, 每次时钟中断产生的时候, 就会被调用.
 *调用的时候, time_count会字增, 如果达到了INTERRUPT_FREQUENCE且
 当前的my_need_sched==0, 则把my_need_sched=1, 从而正在运行的
 my_process函数可以检测到这个变化, 调用my_schedule函数.
 */
void my_schedule(void){
    tPCB * next;
    tPCB * prev;
    if(my_current_task == NULL || my_current_task->next == NULL){
        printk(KERN_NOTICE "ERROR STATE");
        return;
    }
    next = my_current_task->next;
    prev = my_current_task;
    if(next->state == 0){
    printk(KERN_NOTICE "not first switch from prev=%d to next=%d",prev->pid, next->pid);
   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;

    }else{
        next->state = 0;
        my_current_task = next;
        printk(KERN_NOTICE "FIRST START switch from prev=%d to next=%d",prev->pid, next->pid);
        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)
        );
    }
    printk(KERN_NOTICE,"\nfinish my schedule prev=%d, next=%d\n",prev->pid,next->pid);
    return;
}

总结:

这个程序展示了如何写一个时间片轮转的调度的程序. 首先,我们有一个基础, 就是有一个入口函数void __init my_start_kernel(void), 我们在这里初始化了进程控制块信息, 并且通过push 和ret 跳转到了进程0的执行代码.

然后在进程0中, 有一个判断:

if(my_need_sched ==1){
                my_need_sched = 0;
                printk("start schedule in main pid= %d",my_current_task->pid);
                my_schedule();
            }

这个变量的值可以通过时钟中断处理函数来完成设置, 这样, 每个进程运行一段时间, 就检查一次变量, 如果符合条件, 就调用schedule函数进行进程切换. 所谓的进程切换, 就是从一个进程的eip切换到另外一个进程的eip, 同时要保持一些必要的信息, 如esp,ebp等, 这些信息都是保持在进程对应的结构体中的, 这些结构体是一个链表的结构, 有一个current 指针只想当前的pcb, 要进行切换, 只要通过next指针跳到下一个就可以了.

最后附上运行的截图:

 

 


 

 

posted @ 2016-03-06 22:04  Casualet  阅读(492)  评论(0编辑  收藏  举报