《linux内核原理与分析》第三周作业
一、实验分析
- 实验内容
- 分析并运行实验楼提供的内核代码
- 修改并实现时间片轮转的多道程序调度
1.实验楼原内核代码分析
my_start_kernel为该简短内核的入口,与之相对应的,真正的linux内核的入口为start_kernel函数其存在于linux内核中的main.c文件中,这部分内容将在下一章中详细介绍,在这儿就不过多赘述。该入口中实现了一个无限循环函数,模拟真正的内核一直在运行的过程,同时每当运行100000次后就会进行一次输出。my_timer_handler函数实在模拟真正内核中的时间中断函数,每当调用一次该函数就进行一次输出。因此,使用qemu在模拟的硬件环境中运行该内核的结果就是无限循环,每当出现时间中断时输出一次中断处理函数中的信息。OK,根据上述分析,我利用已有的内核启动和中断处理和所学的函数调用堆栈知识来构建我们自己的多道程序内核。
2.实现时间片轮转的多道程序调度
首先为了模拟进程,将先定义出我们自己的进程控制块即PCB
mypcb.h
#define MAX_TASK_NUM 4
#define KERNEL_STACK_SIZE 1024*2
struct Thread {
unsigned long ip;
unsigned long sp;
};
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;
该段代码预先定义了四个进程,struct PCB中包含了进程运行所需的进程标识pid、进程状态state、进程的用户栈stack这里用数组来表示、进程的当前栈顶(sp)的和进程当前的执行位置(ip)、进程的入口task_entry以及指向下一个进程的pcb指针(用来进行进程调度)
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;
void my_timer_handler(void)
{
if(time_count%1000 == 0 && my_need_sched != 1)
{
printk(KERN_NOTICE ">>>my_timer_handler here<<<
");
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<<<
");
/* 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<<<
",prev->pid,next->pid);
/* switch to next process */
asm volatile(
"pushl %%ebp
" /* save rbp of prev */
"movl %%esp,%0
" /* save rsp of prev */
"movl %2,%%esp
" /* restore rsp of next */
"movl $1f,%1
" /* save rip of prev */
"pushl %3
"
"ret
" /* restore rip of next */
"1: " /* next process start here */
"popl %%ebp
"
: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
: "m" (next->thread.sp),"m" (next->thread.ip)
);
}
return;
}
该段代码对时间中断函数进行了修改,并添加了每次时间中断发生时采取的动作,即my_schedule函数。该函数内嵌了一段汇编代码,该段代码的作用是将现在正在运行的进程的ip即当前执行位置和ebp栈顶入该进程的用户栈(入栈的目的是为了方便ret命令弹出),最后通过ret指令将后一个进程的eip弹出到寄存器eip中,接下来cpu将自动执行下一个进程。
mymain.c
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;/* -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].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
" /* set task[pid].thread.sp to rsp */
"pushl %1
" /* push rbp */
"pushl %0
" /* push task[pid].thread.ip */
"ret
" /* pop task[pid].thread.ip to rip */
:
: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/
);
}
int i = 0;
void my_process(void)
{
while(1)
{
i++;
if(i%10000000 == 0)
{
printk(KERN_NOTICE "this is process %d -
",my_current_task->pid);
if(my_need_sched == 1)
{
my_need_sched = 0;
my_schedule();
}
printk(KERN_NOTICE "this is process %d +
",my_current_task->pid);
}
}
}
在内核出口函数里面,先初始化了四个进程,并将其连接起来作为调度程序序列
二、问题
以前一直认为用户栈是由系统分配,经过这次实验发现是由进程先申请的,并借此详细对用户堆栈和内核堆栈进行了一个区分
-
内核栈和用户栈区别
- 1.栈是系统运行在内核态的时候使用的栈,用户栈是系统运行在用户态时候使用的栈。
当进程由于中断进入内核态时,系统会把一些用户态的数据信息保存到内核栈中,当返回到用户态时,取出内核栈中得信息恢复出来,返回到程序原来执行的地方。
用户栈就是进程在用户空间时创建的栈,比如一般的函数调用,将会用到用户栈。 - 2.内核栈是属于操作系统空间的一块固定区域,可以用于保存中断现场、保存操作系统子程序间相互调用的参数、返回值等。
用户栈是属于用户进程空间的一块区域,用户保存用户进程子程序间的相互调用的参数、返回值等。 - 3.每个Windows 都有4g的进程空间,系统栈使用进程空间的地段部分,用户栈是高端部分如果用户要直接访问系统栈部分,需要有特殊的方式。
- 1.栈是系统运行在内核态的时候使用的栈,用户栈是系统运行在用户态时候使用的栈。
-
为何要设置两个不同的栈?
- 共享原因:内核的代码和数据是为所有的进程共享的,如果不为每一个进程设置对应的内核栈,那么就不能实现不同的进程执行不同的代码。
- 安全原因:如果只有一个栈,那么用户就可以修改栈内容来突破内核安全保护。