一个超简单的系统内核

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

这一周的作业是“完成一个简单的时间片轮转多道程序内核代码”,其实在接触linux之前曾在stm32平台上跑过ucos-ii,及ucos-iii虽然ucos的内核是抢占式的任务调度但也是一个多任务的操作系统也具有任务控制块(PCB)等概念其实在移植ucos过程中也需要编写一些汇编语言但自己都是参考手册直接照着写的,这个礼拜在上完课也算是大致明白如何通过中断使程序从一个任务跳到另一个任务了相信对我以后进一步学习ucosiii也有很大帮助,因为ucosiii也开始支持这种时间片轮转调度任务。


首先根据老师提供的网址https://github.com/mengning/mykernel配置实验环境,按照步骤下来很顺利。

运行内核 my_start_kernel在执行,同时my_timer_handler时钟中断处理程序周期性执行。

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

首先是PCB的定义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;  /* -1 unrunnable, 0*/
	char stack[KERNEL_STACK_SIZE];
	struct Thread thread;
	unsigned long task_entry;
	struct PCB *next;
}tPCB;
	
void my_schedule(void);

这里我犯了一个错误,起初没注意把栈大小设置为8 结果运行时出现了错误,很显然是栈爆掉了。。。但这也给我一个启示,栈的大小应该如何确定?

因为在栈里储存着局部变量以及任务的eip等内容,理论上局部变量越多应该分配更多的栈空间。


然后是初始化函数和任务函数mymain.c

#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;
	task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;
	task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE];/* Modfiy?*/
	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].state = -1;
		task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE];
		task[i].next = task[i-1].next;
		task[i-1].next = &task[i];
	}
	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)
		);
}
void my_process(void)
{
	int i = 0;
	while(1)
	{
		i++;
		if(i%100000000 == 0)
		{
			printk(KERN_NOTICE "this is process %d %d-\n", my_current_task->pid,i/100000000);
			if(my_need_sched == 1)
			{
				my_need_sched = -1;
				my_schedule();
			}
			printk(KERN_NOTICE "this is process %d +\n", my_current_task->pid);
		}
	}
}

在初始化中首先是对PCB内容的初始化,pid和state比较容易理解。之前一直不理解入口entry,以及esp的初始问题。在经过上一周对堆栈的理解以及本周所学的内核调度,我总结如下,任务(PCB的每一项)和函数不一定一一对应,很有可能是几个PCB执行一个函数(如本次实验),但是反复的调用的不是同一个函数,有点像同一个电影但是被多个放映机放映,每次只让一台放映机放映,这样一来他们之间的放映进度都是独立的。这里为了验证我的猜想我打印了i(局部变量)的值(因为变得太快了我除了1千万正好是进入里层if的次数)。在明白栈里存的是什么之后其实ebp初始值的问题也就明白了,当然是栈底了,但是根据同学在留言板上的留言指出新的栈必然会push esp 也就是上一个栈(其他进程的栈)的栈底因此初始值可以直接设为
KERNEL_STACK_SIZE
不必减一,避免浪费空间。我也很赞同~


可以看到process2算的数和peocess3是不一样的!!!

时间片调度方式是通过时钟中断进行的在myinterrupt.c中

#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(time_count%1000 == 0 && my_need_sched != 1)
	{
		printk(KERN_NOTICE ">>>my_timer_handler here<<<\n");
		my_need_sched =1;
	}
	time_count ++;
	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");

	next = my_current_task->next;
	prev = my_current_task;
	if(next->state == 0)
	{
		asm volatile(
			"push %%ebp\n\t"
			"movl %%esp, %0\n\t"
			"movl %2,%%esp\n\t"
			"movl $1f,%1\n\t"
			"pushl %3\n\t"
			"ret\n\t"
			"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);
		asm volatile(
			"push %%ebp\n\t"
			"movl %%esp, %0\n\t"
			"movl %2,%%esp\n\t"
			"movl %2,%%ebp\n\t"
			"movl $1f,%1\n\t"
			"pushl %3\n\t"
			"ret\n\t"
			: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
			: "m" (next->thread.sp),"m" (next->thread.ip)
		);							
	}
	return;
}

这里的难点在于 “调度” 也就是老师说的中断上下文和进程上下文的切换。这里的切换分为两种

1、新进程执行过,对于执行过的进程,只要保存好现在的现场(ebp存入堆栈中,esp(指向刚刚存进去的ebp)放入之前的thread结构体的sp中,eip放入thread结构体的ip中),恢复刚才的现场,也就是将新进程的eip,esp,ebp放出来。esp是直接从sp中赋给esp,eip从ip放入栈再通过ret弹出来(因为eip不能被直接修改),此时esp是指向刚才的ebp的(想不明白可以看esp存到sp里时指的是谁?)。但是弹出ebp这个过程要在ret之后,因为那个eip存的时候是指的1:,新的进程自然就会在它的1:处恢复

2、新进程未执行过,因为没执行过,所以进程的栈是空栈,要给他分配一个ebp而不是弹出来一个ebp因此有了

"movl %2,%%ebp\n\t"
少了

"popl %%ebp\n\t"

总结

在这个实验中所有的进程都很“自觉”,他们会主动去想被调度(my_schedule),但是my_schedule的调用条件还有my_need_sched,相当于多了一个锁头,这把锁头的钥匙就在time的interrupt上,每隔一段时间interrupt会打开一次这个锁头让进程被调度,这就是时间调度了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值