王雪 原创作品转载请注明出处 《Linux内核分析》MOOC课程 http://mooc.study.163.com/course/USTC-1000029000
一、理论知识
1.计算机是如何工作的?——总结关键的三点
(1)存储计算机模型——计算机系统中最最基础的数据结构
(2)函数调用堆栈——高级语言得以运行的基础
堆栈完成了计算机的基本功能:函数的参数传递机制和局部变量存取
(3)中断:多道程序操作系统的基点
2.堆栈:C语言运行时必须记得调用路径和调用参数空间
(1)堆栈的基本功能:函数调用框架、传递参数(32位)、保存返回地址(如eax保存返回值/内存地址)、提供局部变量空间
(2)与堆栈相关的寄存器:esp和ebp
与堆栈相关的操作:push(入栈时esp指针会减4)、pop(出栈时esp指针会加4)
(3)CS:eip总是指向下一条指令的地址
3.中断机制的工作
(1)函数堆栈的形成过程
call xxx
在执行call时,cs:eip原来的值指向call的下一条指令,该值被保存到栈顶(push %eip),然后eip指向 xxx的入口地址(movl xxx,%eip)
堆栈的变化情况:
可以通过objdump -S得到反汇编代码
4.C代码中内嵌汇编代码
(1)内嵌汇编代码的语法:
asm(汇编语句模板:
输出部分:
输入部分:
破坏描述部分);
asm 也可以用asm替代,使用asm volatile(….);代表编译器不能优化此代码,后面的指令原样保留
例:该函数实现val3 = val1+val2(注意内嵌汇编的使用)
#include <stdio.h>
int main()
{
unsigned int val1 = 1;
unsigned int val2 = 2;
unsigned int val3 = 0;
printf("val1:%d,val2:%d,val3:%d\n",val1,val2,val3);
__asm__ volatile(
"movl $0,%%eax\n\t" //将eax寄存器清0
"addl %1,%%eax\n\t"
//将%1(%val1)+eax-->eax
"addl %2,%%eax\n\t"
//将%2(%val2)+eax-->eax
"movl %%eax,%0\n\t"
//将eax-->%0(val3)
:"==m"(val3) //输出部分 val3(%0)
:"c"(val1),"d"(val2)
//输入部分 val1(%1),val2(%2)
);
printf("val1:%d+val2:%d = val3:%d\n",val1,val2,val3);
return 0;
}
内嵌汇编限制符
5.操作系统的两个“法宝”:(1)保存现场和恢复现场
(2)进程上下文切换
二、出现函数嵌套调用时堆栈的变化
分析三级函数调用程序的堆栈
通过指令
gcc -g simple_sched.c
//simple_sched.c 文件名,生成可执行的a.out
objdump -S a.out //生成反汇编代码
生成的反汇编代码比较长,截取有用部分。
函数从main开始执行:在main函数中调用p2的函数并传递了两个参数,在p2中,定义了一个变量,并调用了p1的函数,并传递了一个参数c,在p1中调用printf库函数打印输出。
函数堆栈情况:
注意代码中传入参数的压栈顺序以及函数返回时ebp的位置
三、实验内容
——通过编一个简单的时间片轮转多道程序内核代码理解操作系统的进程上下文切换等
(1)实验过程、代码以及截图
1.实验步骤:在实验楼的虚拟机中打开shell,执行
cd LinuxKernel/linux-3.9.4
rm -rf mykernel //删除已存在的旧文件
patch -p1 < ../mykernel_for_linux3.9.4sc.patch
//为内核打补丁,新增自己添加的功能
make allnoconfig
make //编译过程时间较长
//编译成功后执行
qemu -kernel arcj/x86/boot/bzImage
实验效果:
可以看到每当i增加100000会执行(printf函数输出my_ start_ kernel _ here …)时会触发一次时钟中断,在由时钟中断处理函数输出(>..>>my_timer_handler<<…<)
代码实现:
myinterrupt.c中实现了时钟处理函数
mymain.c中
(2)设计一个简单的时间片轮转多道程序
这个实验简单的实现了一个操作系统的进程调度
实验代码及过程
首先,创建一个mypcb.h的头文件,该头文件用于声明会用到的结构体以及函数
//最多任务数量
#define MAX_TASK_NUM 4
//内核堆栈大小
#define KERNEL_STACK_SIZE 1024*8
//定义结构体储存信息
struct Thread
{
unsigned int ip;//用于记录eip的值
unsigned int sp;//用于记录esp的值
};
//定义进程控制块,用于记录进程的相关信息
typedef struct PCB
{
unsigned int pid;//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();
修改mymain.c(只要是修改__init my _start _kernel()函数)文件,完成基本的创建0号线程,创建其他线程并赋予线程信息
#include "mypcb.h"
tPCB task[TASK_MAX_NUM];
tPCB *my_current_task;//当前任务的指针
volatile int my_need_sched = 0;
void my_process(void);
void __init my_start_kernel(void)
{
//初始化当前0号进程
int pid = 0;
int i;
task[pid].state=0;
//正在运行的eip
task[pid].thread.ip=(unsigned int)my_process
task[pid].thread.sp=(unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
task[pid].task_entry = &task[pid].thread.ip;
task[pid].next=&task[pid];
//创建其他进程
for(i=1;i<MAX_TASK_NUM;i++)
{
//复制0号进程的信息
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];
//链接,尾部插入
task[i].next=task[i-1].next;
task[i-1].next = task[i];
}
//启动0号进程
pid=0;
my_current_task=&task[pid];
//内嵌汇编代码,执行保护现场和恢复现场
asm volatile(
"movl %1,%%esp\n\t" //保存thread.sp----->esp
"pushl %1\n\t"//将thread.sp压栈
"pushl %0\n\t"//将thread.ip入栈
"ret\n\t" //将eip弹出
//当前的eip为task[pid].thread.ip=(unsigned int)my_process,所以执行my_process()
"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%10000000 == 0)
{
//active schedule
//every 10000000 execute
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);
}
}
}
注意:
0号进程的启动:(0号进程是第一个启动的进程)将esp置为当前进程的esp,然后将esp的值压栈,与ebp形成0号进程的自己的堆栈空间,将0号进程的task[pid].thread.ip也压栈,ret后,此时0号进程开始执行。
当0号进程启动时,my_ current _ task指向task[0],此时要执行0号进程的 task_ entry,task[pid].task _ entry = &task[pid].thread.ip,当前的eip为task[pid].thread.ip=(unsigned int)my _ process,所以执行my _ process(), 在my_ process中有一个while(1)循环,my _ need _ sched的初始值是0。接着看my_interrupt.c函数,找到调度何时会发生。
修改my_interrupt.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;//计时器
/*
* Called by timer interrupt.
*/
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;//此时置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");
next = my_cueernt_task->next;
prev = my_current_task;
if(next.state==0)//两个正在运行的进城之间进行切换
{
my_current_task = next;
printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);
//切换进程,内嵌汇编代码
asm volatile(
"pushl %%ebp\n\t" //save ebp
"movl %%esp,$0\n\t" //save esp,prev执行到此,将当前的esp保存到prev的thread的sp
"movl $2,%%esp" //将next的esp移入esp
"movl $1f,$1" //将prev的eip压栈
"pushl %3\n\t"
"ret\n\t"
"1:\t"
:"=m"(prev->thread.sp),"=m"(prev->thread.ip)
:"m"(next->thread.sp),"m"(next->thread.ip)
}
else//next 是一个新进程,重来没有执行过,所以初始的堆栈是空的
{
next.state=0
my_current_task = next;
printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);
//切换进程,内嵌汇编代码
asm volatile(
"pushl %%ebp\n\t" //save ebp
"movl %%esp,$0\n\t" //save esp,prev执行到此,将当前的esp保存到
"movl $2,%%esp" //将next的esp移入esp
"movl $2,%%ebp" //第一次执行,next的堆栈是空的
"movl $1f,$1" //将prev的eip压栈
"pushl %3\n\t"
"ret\n\t"
"1:\t"
:"=m"(prev->thread.sp),"=m"(prev->thread.ip)
:"m"(next->thread.sp),"m"(next->thread.ip)
}
}
到此,完成了程序的基本功能–操作系统的进程切换,进程切换的关键代码位置在my _ interrupt.c的my _ schedule()函数内嵌汇编部分!
分析:在进程(prev)切换到下一个进程(next)时,可以分为两种情况:
(1)next进程的运行状态为0(正在运行),觉得此处可以理解为,起初,next已经执行部分,在next的执行时发生过切换,切换给prev运行,此时我们想将next切换回来(两个正在进行的进程进行切换)。
所以,将正在执行的my_ current _task改为next,在切换前保存现场,将ebp压栈,将当前的esp(这里的esp我觉得是prev执行到的位置,在切换之前要保存下来,保存到prev->thread.sp中),将next->thread.sp赋值到esp中(可以理解为接着next上次执行停下来的位置接着执行),保存prev->thread.ip,将next->thread.ip压栈,在ret,接着执行的就是nexr的eip,完成了两个正在运行的进程间的切换。
(2)next重来没有执行过,此处可以理解为next是一个新进程,它还没有自己的运行堆栈等信息,所以与上的主要区别就在于next->thread.sp要同时赋值给next的esp和ebp,用于创建next的运行堆栈,其余相似。同样在ret后开始执行next的eip。
现在来看何时会发生调度?发生调度有两个if,第一个if要求i%10000000 == 0,第二个if要求my_ need _ sched == 1,在my_ timer _ handler()中,time_ count%1000 == 0 && my_ need_ sched != 1会将my_ need_ sched置1,所以一定会有某个进程进入调度函数。调度的主要目的就是完成上下文的切换,不同的进程task的pid是不同的,eip为task[pid].thread.ip=(unsigned int)my _ process,my_process中输出不同的pid,所以可以查看进程切换的结果。
实验结果:
可以看到进程的调度信息!
四、实验总结
1.操作系统是如何工作的:操作系统是计算机管理硬件和软件的核心,管理着整个计算机的资源。
操作系统主要具有五大功能:
(1)作业管理:包括任务、界面管理、人机交互、图形界面、语音控制和虚拟现实等;
(2)文件管理:又称为信息管理;
(3)存储管理:实质是对存储“空间”的管理,主要指对主存的管理;
(4)设备管理:实质是对硬件设备的管理,其中包括对输入输出设备的分配、启动、完成和回收;
(5)进程管理:实质上是对处理机执行“时间”的管理,即如何将CPU真正合理地分配给每个任务。
其中进程切换是操作系统最主要的功能。
进行进程切换就是从正在运行的进程中收回处理器,然后再使待运行进程来占用处理器。这里所说的从某个进程收回处理器,实质上就是把进程存放在处理器 的寄存器中的中间数据找个地方存起来,从而把处理器的寄存器腾出来让其他进程使用。让进程来占用处理器,实质上是把某个进程存放在私有堆栈中寄存器的数据(前一次本进程被中止时的中间数据)再恢复到处理器的寄存器中去,并把待运行进程的断点送入处理器的程序指针PC,于是待运行进程就开始被处理器运行了,也就是这个进程已经占有处理器的使用权了。
在切换时,一个进程存储在处理器各寄存器中的中间数据叫做进程的上下文,所以进程的切换实质上就是被中止运行进程与待运行进程上下文的切换。
2.从上面的叙述可知,调度器进程切换的代码应有如下功能:
●保存处理器eip寄存器的值到被中止进程的私有堆栈;
●保存处理器SP寄存器的值到被中止进程的进程控制块;
●保存处理器其他寄存器的值到被中止进程的私有堆栈;
●自待运行进程的进程控制块取SP值并存入处理器的寄存器SP;
●自待运行进程的私有堆栈恢复处理器各寄存器的值;
●自待运行进程的私有堆栈中弹出PC值并送入处理器的eip
所以,操作系统根据调度算法进行进程间切换,合理利用计算机资源。是计算机重要的功能!
这次实验使我收获很多,重点在于要深刻理解操作系统进行上下文切换到过程,理解内嵌汇编代码的使用!感谢为我们答疑解惑的老师~!