学号后三位:069
原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/
始臣之解牛之时,所见无非全牛者。
三年之后,未尝见全牛也。
mykernel是孟宁老师在2013年,在Linux内核繁杂的CPU初始化工作的基础上完成的一个虚拟、可编程的计算机硬件模拟环境。有了mykernel,稍有编程能力的学生就可以编写一个简单的时间片轮转调度的小型内核,并且能读懂代码,深刻理解如何在CPU的一个指令执行流上实现多个进程。
使用实验楼的虚拟机打开shell
依次输入如下指令:
cd LinuxKernel/linux-3.9.4
rm -rf mykernel
patch -p1 < ../mykernel_for_linux3.9.4sc.patch
make allnoconfig
make //编译时间大概10分钟
qemu -kernel arch/x86/boot/bzImage
在输入qemu -kernel arch/x86/boot/bzImage指令后,跳出如下界面。且有两类字符串不断交替输出。
实际上,
myinterrupt.c
中my_timer_handler
函数控制输出的mymain.c
中my_start_kernel
函数控制输出的由此,我们可以知道,在系统启动后会周期性的调用myinterrupt.c中my_timer_handler函数
以及mymain.c中my_start_kernel函数
。
本部分是要完成一个简单的时间片轮转多道程序内核代码,源代码来自孟宁老师的GitHub。https://github.com/mengning/mykernel
将以上文件下载到实验楼虚拟机中。
git clone https://github.com/mengning/mykernel
将这三个文件拷贝到LinuxKernel/linux-3.9.4下的mykernel中,覆盖原文件并增加新文件:mypcb.h
在LinuxKernel/linux-3.9.4下,执行下面指令:
patch -p1 < ../mykernel_for_linux3.9.4sc.patch //一定要打补丁
make allnoconfig //第二次编译之前,一定要make clean,不然会出错
make
输入如下指令后:
qemu -kernel arch/x86/boot/bzImage
弹出窗口,显示进程的运行及切换过程:
在上图中可以看到,进程切换的过程。⬆️
下面具体分析下这三个文件的代码:
源代码:https://github.com/mengning/mykernel/blob/master/mypcb.h
宏定义:
#define MAX_TASK_NUM 4
定义了最大任务数
#define KERNEL_STACK_SIZE 1024*2
定义了堆栈内核大小
线程结构体:
struct Thread {
unsigned long ip;
ip指令指针
unsigned long sp;
sp堆栈指针
};
此文件中最关键的部分是,下面的PCB结构体:
typedef struct PCB{
int pid;
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
unsigned long stack[KERNEL_STACK_SIZE];
/* CPU-specific state of this task */
struct Thread thread;
unsigned long task_entry;
struct PCB *next;
}tPCB;
其中,
pid
表示进程号,是进程的标识符
state
表示进程的状态,-1表示不可运行;0表示可运行等
stack[KERNEL_STACK_SIZE]
表示进程的堆栈空间
thread
表示进程的线程信息,包含ip、sp指针
task_entry
进程入口点
*next
PCB指针,指向下一个进程控制块
mypcb.h中,最后一部分是void my_schedule(void);
函数声明。此函数在myinterrput.c中定义,mymain.c中的进程会依据一个全局变量来决定是否调用此函数,来实现进程的调度切换。
源代码:https://github.com/mengning/mykernel/blob/master/mymain.c
全局变量:
volatile int my_need_sched = 0;
用来指示当前进程是否需要调度(值为1时,需要调度)
接下来是两个关键的函数:
my_start_kernel函数
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*/
);
}
my_start_kernel
是系统启动后,第一个调用的函数。在这个函数中,首先初始化了0号进程:
task[pid].pid = pid;
置其pid为0task[pid].state = 0
设置进程状态为可运行状态task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process
my_process
函数接下来的for
循环,则创建了3个进程,pid分别为1、2、3。并通过下面的代码,将PCB用指针链接了起来。
task[i].next = task[i-1].next;
task[i-1].next = &task[i];
接下来,启动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 */
:
: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/
);
汇编代码的作用,注释中都有解释。我下面就主要分析下,随着代码的执行过程,堆栈的内容变化。见下图:
从左到右依次是代码执行过程中,堆栈的内容变化。最后eip中存的就是my_process(void)
的函数位置。显而易见,接下来执行的就是my_process(void)
的函数。
my_process(void)函数
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_need_sched
,当其值为1时,就调用my_schedule()
函数,完成进程调度切换。
源代码:https://github.com/mengning/mykernel/blob/master/myinterrupt.c
这里首先定义/声明了一些全局变量:
extern tPCB task[MAX_TASK_NUM];
此变量在mymain.c文件中定义,此处为外部声明
extern tPCB * my_current_task;
同上
extern volatile int my_need_sched;
同上
volatile int time_count = 0;
此处定义了一个全局变量,用来模拟时间片
接下来实现了两个函数,my_timer_handler
函数和my_schedule
函数。下面依次来分析下这两个函数:
my_timer_handler函数
void my_timer_handler(void)
{
#if 1
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;
}
此函数的功能非常显然,时间片为1000的整数倍时,就将全局变量my_need_sched
的值置为1,这样mymain.c中的my_process
函数就可以调用my_schedule
函数来进行进程切换了。
my_schedule函数
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;
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;
}
首先定义了两个PCB指针:
tPCB * next;
指向下一个PCB
tPCB * prev;
指向当前PCB
如果下一个进程的state
是可运行的,那么就切换到下一个进程。具体实现由相应的内嵌汇编代码实现:
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)
);
简单来讲,以上汇编代码的功能就是在进程堆栈之前切换——保存当前进程的上下文环境、切换到另一进程的上下文环境;当切换回原来的进程时,也能够做到回复之前的上下文执行环境。
操作系统是如何工作的?
操作系统的工作主要依赖三大法宝:
-存储程序计算机
-函数嗲用堆栈机制
-中断支持
堆栈是C语言程序运行时必须使用的记录函数调用路径和参数存储的空间,堆栈的具体作用有:记录函数调用框架、传递函数参数、保存返回值的地址、提供内部局部变量的存储空间等。
而中断的支持也不容忽视,有了中断才有了多道处理程序,在没有中断机制之前,计算机智能一个程序一个程序的运行,也就是批处理,而无法实现并发执行。有了中断机制之后,当中断信号发生时,CPU把当前正在执行的程序的EIP、ESP寄存器的内容都压到堆栈当中进行保存。之后转而执行其他的程序,等执行过后还能依靠堆栈来恢复现场,恢复EIP、ESP寄存器的值,进而继续执行中断前的程序。