此文仅用于MOOC
Linux内核分析
作业张依依+原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
以下为正文
一.运行精简的操作系统内核
二.时间片轮转多道程序
直接使用mykernel上的精简算法调度的代码。
替换原本的mymain.c
和myinterrupt.c
,加入头文件mypcb.h
重新编译内核。
通过qemu -kernel arch/x86/boot/bzImage
运行的结果(进程0~3相互轮换):
三.以下详细分析代码
1.进程的启动
a.初始化部分:
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].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];
}
/* 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*/
);
}
- 该函数中先初始化pid为0的进程。
- 并在for循环中初始化1~3的进程,注意这其中state均被赋值为-1,即
unrunnable
- 其中
task[i].next = task[i-1].next;task[i-1].next = &task[i];
语句实际上是创建一个循环的链表。 - 之后先启动进程号为0的进程,用内联汇编实现。关于Inline assembler的具体内容可查看:inline assembler
b. 内核中实现进程启动的关键代码实际上是这里的内联汇编:
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*/
);
- 在以上的内联汇编中,没有
output operands
,有两个input operands
.即线程的ip和sp - 通过
pushl %0\n\t
把task[pid].thread.ip压入栈,再ret\n\t
, 把task[pid].thread.ip的值赋给eip,使eip的值改变,指向my_process。
c. eip指向的my_process代码
void my_process(void)
{
int i = 0;
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
控制调度函数my_schedule()的运行。 - 这里的
my_need_sched
是通过时钟函数改变的(当发生时钟中断time_count%1000 == 0
时,把my_need_sched置位为1 这是my_process中if部分开始,把my_need_sched复位为0,并调用my_schedule)
2.函数调度
a.调度函数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 */
{
/* 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)
);
my_current_task = next;
printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);
}
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;
}
- 申明两个tPCB类型的变量,分别为目前的进程prev和下一个进程next
- 调度的核心同样在于两段内联汇编的代码
- if和else之间的两段inline assembler是相同的,区别只是在于需要调度的进程的状态state
b. 进程调度的内联函数详解
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)
- 在以上的内联汇编中,有两个
output operands
,即当前进程的sp和ip - 有两个
input operands
.即下一个要执行的线程的ip和sp - 可以推测到该汇编代码实际上就是将当前的ip和sp变为下个进程的ip和sp,并保存现场
- 在保持ebp后,
movl %%esp,%0\n\t
即把当前的esp保存到prev->thread.sp中 - 接下来的
movl %2,%%esp\n\t
,把next->thread.sp(下一个执行的进程的sp)赋予esp - 之后restore ebp
- 使用
movl $1f,%1\n\t
来保存当前的eip - 通过接下来的两句汇编代码实现改变eip的值,首先将eip要改为的值压入栈
(next->thread.ip),接着
ret\n\t
,使eip指向下一个进程的ip
总结
从分析进程的启动和进程的切换机制的过程中,可以看出,真正的启动和切换过程是用内联汇编代码实现的。如果从更深的角度来说,是通过改变eip
的值做到的。
在整个过程中,eip的值决定着程序执行的走向,而堆栈中ebp,esp的改变则负责保护现场数据,传递参数等功能。
一些值得注意的部分:
my_need_sched
参数负责控制调度函数my_schedule的执行。会被时钟函数所改变- my_start_kernel函数在初始化thread的堆栈时,sp设置为KERNEL_STACK_SIZE-1,即栈是向低地址扩展的数据结构
几个疑问:
-
state变量(-1 unrunnable, 0 runnable, >0 stopped)实际中的作用是什么?是不是因为代码中因为精简而删除了部分代码,所以操作state的过程被省略了。
-
希望能详细了解patch文件修改的代码,其中通过diff -Naur命令来修改内核的启动程序为mymain.c是怎么实现的?