“Linux内核分析”实验二报告
张文俊 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
一、第二周学习内容总结
1、计算机工作“三大法宝”
首先,计算机工作原理最重要的三个内容就是:存储程序计算机工作模型、中断机制和函数调用堆栈。
存储程序计算机工作模型是计算机系统最最基础性的逻辑结构;
中断机制是多道程序操作系统的基点,没有中断机制程序只能从头一直运行结束才有可能开始运行其他程序;
函数调用堆栈是高级语言得以运行的基础,有了高级语言及函数后,堆栈成为了计算机的基础功能,函数参数传递机制和局部变量存储。
2、详细介绍函数调用堆栈
首先,堆栈是C语言程序运行时必须的一个记录调用路径和参数的空间,即CPU内已经集成好了很多功能。
堆栈含以下元素:
函数调用框架、传递参数、保存返回地址、提供局部变量空间、C语言编译器对堆栈的使用有一套的规则。
了解堆栈存在的目的和编译器对堆栈使用的规则是理解操作系统一些关键性代码的基础。
其次,堆栈相关的寄存器常用的共有四种,esp、ebp、push、pop。详细解释如下:
esp,堆栈指针,指向栈顶;
ebp,基址指针,指向栈底,在C语言中用作记录当前函数调用基址;
push,栈顶地址减少4个字节,从低地址向高地址;
pop,栈顶地址增加4个字节,从高地址向低地址;
最后,课程介绍了函数参数的传递与框架——
(1)建立框架,即call指令
相当于
push %ebp
movl %esp,%ebp
cs:eip原来的值指向call下一条指令,该值被保存到栈顶cs:eip的值指向function的入口地址
(2)执行函数主体代码
(3)拆除框架
相当于
movl %ebp,%esp
pop %ebp
ret
函数的返回值通过eax寄存器传递
3、参数传递
这里以老师在课堂上举出的例子:
这里,使用变址寻址方式,将x+y的值赋给eax。
然后,我们观察main是如何传递参数给P2的:
和main中的局部变量:
可以看到,汇编代码中用变址寻址把y的值和x的值存放到堆栈中,然后进行局部变量调用。
4、C代码中嵌入汇编代码
____asm____
(
汇编语句模板:
输入部分:
输出部分:
破坏描述部分:
);
二、实验二内容
本次实验内容是在mykernel基础上构造一个简单的操作系统内核。
首先我们cd进入LinuxKernel/linux-3.9.4文件,执行qemu -kernel arch/x86/boot/bzImage,可以看到窗口弹出如下:
然后,我们查看mymain.c和myinterrrupt.c文件:
可以发现,mymain是系统中唯一的进程,函数主要部分是my_start_kernel。每循环10万次,就打印一次my_start_kernel here.
myinterrupt是时间中断处理程序,每进行一次就会发生一次时钟处理中断,每次时钟中断都调用printk并输出。
程序分析:
mypcb.h
#define MAX_TASK_NUM 4
#define KERNEL_STACK_SIZE 1024*8
/* CPU-specific state of this task */
struct Thread {
unsigned long ip;//保存eip
unsigned long sp;//保存esp
};
typedef struct PCB{
//用于表示一个进程,定义了进程管理相关的数据结构
int pid;
volatile long state; /* 定义进程的状态:-1 不可运行, 0 可运行, >0 停止 */
char stack[KERNEL_STACK_SIZE];
//内核堆栈
struct Thread thread;
unsigned long task_entry; //指定进程入口
struct PCB *next;//进程链表
}tPCB;
void my_schedule(void);//调用了my_schedule,表示调度器
mymain.c
void my_timer_handler(void)
{
#if 1
if(time_count%1000 == 0 && my_need_sched != 1)
//设置时间片的大小,时间片用完时设置一下调度标志。当时钟中断发生1000次,并且my_need_sched!=1时,把my_need_sched赋为1。当进程发现my_need_sched=1时,就会执行my_schedule。
{
printk(KERN_NOTICE ">>>my_timer_handler here<<<\n");
my_need_sched = 1;
}
time_count ++ ;
#endif return;
}
void my_schedule(void)
{
tPCB * next; //下一个进程
tPCB
* prev; //当前进程
if(my_current_task == NULL //task为空,即发生错误时返回
|| my_current_task->next == NULL)
{
return;
}
printk(KERN_NOTICE ">>>my_schedule<<<\n");
/* schedule */
next = my_current_task->next; //将当前进程的下一个进程赋给next
prev = my_current_task;//当前进程为prev
if(next->state == 0)
/* -1 unrunnable, 0 runnable, >0 stopped */
{
//在两个正在执行的进程之间做上下文切换
asm volatile( "pushl %%ebp\n\t"
/* 保存当前进程的ebp */
"movl %%esp,%0\n\t" /* 保存当前进程的esp */
"movl %2,%%esp\n\t" /* 重新记录要跳转进程的esp,将下一进程中的sp放入esp中 */
"movl $1f,%1\n\t" /* $1f指标号1:的代码在内存中存储的地址,即保存当前的eip */
"pushl %3\n\t" //将下一进程的eip压入栈,%3为 next->thread.ip
"ret\n\t" /* 记录要跳转进程的eip */
"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 /* 与上一段代码不同的是如果下一个进程为新进程时,就运用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" /* 保存当前进程的ebp */ "movl %%esp,%0\n\t" /* 保存当前进程的esp */ "movl %2,%%esp\n\t