linux简单时间片轮转内核分析

操作系统是如何工作的

学号531
本博客围绕操作系统是如何工作的这一问题来进行相关的简单内核编写与分析的实验。
原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/

实验过程

使用实验楼的虚拟机打开shell
实验楼链接: https://www.shiyanlou.com/courses/195

  1. 打开Linux-3.9.4,编译,运行命令如下:
cd LinuxKernel/linux-3.9.4
rm -rf mykernel
patch -p1 < ../mykernel_for_linux3.9.4sc.patch
make allnoconfig
make #编译内核请耐心等待
qemu -kernel arch/x86/boot/bzImage
  1. 运行结果显示如下:
    linux简单时间片轮转内核分析

  2. 打开linux-3.9.4\mykernel,可以看到mykernel.c和myinterrupt.c文件,打开文件如下,初始化是一个无限循环,每十万次显示一次;
    linux简单时间片轮转内核分析

  3. 修改mykernel.c和myinterrupt.c文件,新建一个mypcb.h文件,
    文件代码参考自https://github.com/mengning/linuxkernel/;
    再次编译执行后如下:
    linux简单时间片轮转内核分析
    上图截图显示了进程1切换到进程2。

实验中时间片轮转的内核代码分析

  1. mypcb.h代码分析:
#define MAX_TASK_NUM        4/*最大进程数*/
#define KERNEL_STACK_SIZE   1024*2 /*内核栈大小*/
struct Thread {
    unsigned long		ip;
    unsigned long		sp;
};
typedef struct PCB{
    int pid;
    volatile long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
    unsigned long stack[KERNEL_STACK_SIZE];
    /* */
    struct Thread thread;
    unsigned long	task_entry;
    struct PCB *next;
}tPCB;
void my_schedule(void);

mypcb.h定义了进程控制块结构体;
其中tPCB用于保存进程信息:
thread:线程,其中ip指向下一个线程地址,sp指向栈底;
pid:进程代号;
state:为0时进程可运行,-1时进程不可运行,大于0进程挂起;
stack:进程堆栈;
task_entry:进程入口;
next:指向下一个PCB。

  1. mymain.c主要代码分析:
#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;
volatile int my_need_sched = 0;

void my_process(void);

void __init my_start_kernel(void)
{
    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;
    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++)
    {
        memcpy(&task[i],&task[0],sizeof(tPCB));
        task[i].pid = i;
	//*(&task[i].stack[KERNEL_STACK_SIZE-1] - 1) = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-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] */
    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 */
    	: 
    	: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)	/* input c or d mean %ecx/%edx*/
	);
} 

int i = 0;

void my_process(void)
{    
    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_schedule();
        	}
        	printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid);
        }     
    }
}

系统启动时,首先调用 my_start_kernel ,即先初始化进程0,其中task_entry指向 my_process(进程运行函数),sp指针指向进程栈尾地址,next指针指向自己;
然后初始化更多进程,并且把新的进程加入到链表中;
接着的汇编代码表示将sp赋给esp,sp与ip压栈,ip给eip;
最后my_process开始循环。

  1. myinterrupt.c主要代码分析:
void my_timer_handler(void)
{
#if 1
	if (time_count % 1000 == 0 && my_need_sched != 1)
	{
		printk(KERN_NOTICE ">>>my_timer_handler here<<<\n\n\n\n\n\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 */
	{
		my_current_task = next;
		unsigned ct = 1000000;
		while (ct)
		{
			ct--;
		}
		printk(KERN_NOTICE ">>>switch %d to %d<<<\n", prev->pid, next->pid);
		/* 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));
	}
	return;
}

my_timer_handler用于时钟相应,通过my_time_handler()函数定时地不断向cpu发出中断,从而实现了时间片轮转。每调用1000次,就去将全局变量my_need_sched的值修改为1,通知正在执行的进程执行调度程序my_schedule。
my_schedule用于完成进程的切换,ebp压栈保存,将esp的值保存到prev->thread.sp中,esp指向next->thread.sp,将1f赋值给prev->thread.ip,next->thread.ip压栈,(第一次执行会指向myprocess()函数而不是1f),next->thread.ip出栈并赋给eip,复位(第一次时执行my_process),即完成进程切换。

实验总结

总的来说,操作系统的工作需要进行进程的启动与切换,切换时需要保存上下文,切换的程序完成后需要恢复上下文,进程切换通过时钟中断进行控制。