mykernel原生代码的编译过程以及运行截图:
另外,若出现无法关闭qemu的情况,可用下面这条命令:
ps -A | grep qemu | awk '{print $1}' | xargs sudo kill -9
原生代码中mymain.c的核心部分,Linux内核其实也是一个死循环
void __init my_start_kernel(void)
{
int i = 0;
while(1)
{
i++;
if(i%100000 == 0)
printk(KERN_NOTICE "my_start_kernel here %d \n",i);
}
}
myinterrupt.c的核心部分,这个应该是时钟中断的中断处理函数
/*
* Called by timer interrupt.
*/
void my_timer_handler(void)
{
printk(KERN_NOTICE "\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n");
}
可以看到这两段代码都是比较简单的,只是简单的打印几句话而已。
至于内核是如何运行起来的,硬件是如何初始化的,my_start_kernel之前的代码都帮我们处理好了。在接下来的学习中,希望自己能搞明白从启动到内核运行起来的流程。
按照实验的要求,是希望我们在这两个文件的基础上实现一个简单的操作系统内核:
运行mykernel后就可以写一个自己的时间片轮转调度内核了
目前自己还未能完全理解这所有的机制,所以是把实例代码拷贝到自己的实验环境中去,然后重新make,观察这一整套的运行过程,其次的深入理解才是最重要的。
在实验楼的环境里面,我只是把mymain.c、myinterrupt.c更改为git上的代码,然后增加了mypcb.c,make运行之后,等待bzImage的生成。
实验截图如下:
需要学习的是:patch的生成以及如何打一个补丁。
可以参考以下链接:Linux打Patch的方法、补丁(patch)的制作与应用
如果之前接触过其他嵌入式系统的内核,会比较好理解这个内核的运行过程。
/* CPU-specific state of this task */
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;
- 上面这段是一个任务控制块应该有的一些关键变量,如:任务运行时的sp指向,eip所指向的命令的地址,还有任务的id号,任务状态,任务堆栈以及真正的任务入口。
- 当然,作为一个可调度的内核,肯定要有一个双向任务链表,自然就会有next和prev指针。
但是目前这个实现的内核中,并没有prev指针,应该还是有改善的余地的。
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;
//一般是向下增长,故而sp指向最大的地址
task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
//只有一个任务的时候next指针只能指向自己
task[pid].next = &task[pid];
当前期的软硬件环境已经准备妥当之后,程序在/init/main.c中运行到my_start_kernel,这时内核开始初始化,pid = 0。
- 首先是维护出进程的各种属性,如pid号,运行状态(个人理解这个变量指示的是该任务是否已经可以运行),以及这个进程的真正的运行代码入口地址(此内核中,0号idle任务的入口函数自然就是my_process)
- 当然还少不了每个进程都需要的堆栈所需的堆栈指针(这里面有个KERNEL_STACK_SIZE,应该是在配置文件中预先定义好的,当然,具体的等下还要查tPCB结构体的定义)。最开始的时候,只有一个任务,所以task的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];
//相当于有一个任务链表(单向循环链表),把所有的任务串起来,从0到MAX_TASK_NUM
task[i].next = task[i-1].next;
task[i-1].next = &task[i];
}
根据我们系统的需要,对需要的MAX_TASK_NUM个任务进行初始化,实际上,这就相当于初始化出来一个任务链表(如果是双向链表的话,会不会更好?),只是暂时没有实际分配任务的入口地址。
/* 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 */
"popl %%ebp\n\t"
:
: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/
);
- 这一大段的汇编代码,实际上模拟的正是任务栈帧形成的过程,因为%1代表的是第2个参数,即把当前要运行的任务的sp指针(
(unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];
)恢复到CPU的寄存器esp中,然后把sp压栈(一般每个独立的栈都会有自己的栈帧结构,即ebp和esp范围内的),故相当于ebp = esp。在函数调用的时候,最开始时的pushl %ebp; movl %esp, %ebp;
也是同样的道理。 - 把初始化时候的eip(
task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;
)压栈,之后迅速ret弹栈到eip(因为x86不允许直接修改eip指针,只好采用这种迂回策略),到此pid=0的线程开始运行。 - 由于任务是不允许退出的,最后一条popl的指令永远不会执行。
void my_process(void)
{
int i = 0;
while(1)
{
i++;
//10000000次循环之后才检查是否有任务需要调度
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);
}
}
myprocess是一段比较简单的代码,纯粹根据myinterrupt中的tick次数打印出消息,以及判断是否需要调度。
my_timer_handler则是系统的中断处理函数
irqreturn_t timer_interrupt
调用的,该函数对系统tick次数,计数次数到了,置位调度标志变量。调度代码的核心在于my_schedule,之前讲到,my_need_sched == 1的时候,my_schedule会被调用,执行一次调度。调度的算法有很多,但是这个就比较简单,仅仅是在有任务可调度的情况下,调度下一个任务。
next = my_current_task->next;
prev = my_current_task;
- 如果下一个任务控制块可用,且能运行(当然能运行了,因为我们在初始化tcb链表的时候,可以把state 初始化为 0,因为state似乎目前未体现出价值),则开始进行一次上下文切换。
- 首先保存当前任务的上下文(各种寄存器、堆栈等),然后恢复需要调度的任务到CPU的执行上下文环境中。
至于什么是当前任务的上下文,我的理解就是,任务的堆栈状态以及当前相关的硬件寄存器(esp,eip,eflags等这些),有解释如下:
当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的上下文,以便在再次执行该进程时,能够返回到切换时的状态执行下去。在LINUX中,当前进程上下文均保存在进程的任务数据结构中。
所以在设计任务tcb的时候,就考虑到这些问题了,为每个任务保存了esp,eip,state,stack空间等等。
由于我们采用的调度算法是:如果下一个可执行,那么调度到下一个。真正执行任务切换的时候,需要将当前任务的esp到堆栈中去(保存ebp似乎没看到必要性?如果没有必要性的话,最后的"popl %%ebp\n\t"
似乎也没有必要,这样的话,代码中的"1:\t"
只是起到了一个路引的作用,当被调度的任务返回的时候,返回到此处,继续执行被中断的任务),保存eip(当前任务要运行的下一条指令)到tcb中的ip变量中去(似乎没必要直接用到ip变量,可以直接保存在堆栈中),然后恢复要运行任务的上下文(恢复next thread的esp,同样迂回的办法恢复eip指针,并直接运行),任务调度完成,执行被调度的任务。
值得注意的是,之前所有的假设,都值得尝试,配置到自己的代码中,试运行。
经过尝试,果然,不保存ebp是没问题的。
else中代码就不需要分析了,因为在整个内核的逻辑中,除了初始化外,我们就没有维护到state的值,所以else根本执行不到。
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; //prev指针当然指向当前的任务控制块
if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */
{
//可运行的话,直接开始任务切换
my_current_task = next;
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"
// %0 %1
: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
// %2 %3
: "m" (next->thread.sp),"m" (next->thread.ip)
);
}
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;
}
总结:在这所有的代码中,其中的三段汇编代码才是理解的核心,这里面的机制,为何要保存这些寄存器,保存寄存器的顺序,如何恢复一个新的任务,这些机制搞清楚的话,对其他的代码理解应该帮助特别大。