《Linux内核分析》之操作系统是如何工作的 实验总结
前言
实验阶段,由于学校网速等条件限制,未能在真机上搭建出实验环境。在实验楼中,将代码粘贴进去出现严重的缩进错位,最终未能完成编译新的。本文以分析关键代码为主。
环境搭建简易过程
此处为环境搭建的简易过程,详细的可以参考孟宁老师的github:mykernel,这里不再进行赘述。 [toggle hide=“yes” title=“环境搭建简易过程” color=“”] 1、创建(mkdir)工作区SG13225146 2、将linux-3.9.4文件夹剪切到刚创建工作区SG13225146 3、将mykernel_for_linux3.9.4sc.patch复制到工作区SG13225146 4、查看工作区内容 5、patch -p1 < …/mykernel_for_linux3.9.4sc.patch 6、make allnoconfig 复位 7、make 编译 8、安装qemu 9、使用qemu查看内核 10、结合网上所查资料,在mykernel文件夹中主要写入mypcb.h、mymain.c、myinterupt.c三个文件。之后再在linux-3.9.4文件夹中make 编译一下。 11、使用qemu再次查看内核,正常情况下应该可以看到更改后的。 [/toggle] 小总结:1-7步是编译linux内核过程,8-9为查看内核信息的过程,10-11为编写自己的简易内核过程。 [toggle hide=“yes” title=“相关图片” color=“”] [caption id=“” align=“aligncenter” width=“439”] mymain.c部分截图[/caption] [caption id=“” align=“aligncenter” width=“404”]
代码粘进去严重错位了= =[/caption] [caption id=“” align=“aligncenter” width=“327”]
linux原内核工作状态[/caption] [/toggle]
实验及总结
主要代码及分析
各文档所包含的头文件不在列出
mypcb.h
这个头文件主要定义了进程控制结构PCB [toggle hide=“yes” title=“mypcb.h” color=“”]
#define MAX_TASK_NUM 4
#define KERNEL_STACK_SIZE 1024*8
/*
*Thread用于存储eip和esp
*/
struct Thread{
unsigned long ip;
unsigned long sp;
};
typedef struct PCB{
int pid;/*进程id*/
volatile long state;/*进程状态 -1 unrunable,0 runable*/
char stack[KERNEL_STACK_SIZE];/*进程的堆栈*/
/*CPU -specific state of this task*/
struct Thread thread;
unsigned long task_entry;/*程序入口*/
struct PCB *next;/*下一个进程*/
}tPCB;
void my_schedule(void);/*调度器*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[/toggle]
mymain.c
这个文件主要是定义了启动N个进程的过程 [toggle hide=“yes” title=“mymain.c” color=“”]
tPCB task[MAX_TASK_NUM];/*task数组*/
tPCB *my_current_task = NULL;/*当前task的指针*/
volatile int my_need_sched = 0;/*是否需要调度*/
void my_process(void);
void __init my_start_kernel(void)
{
int pid =0;
int i;
/*Initialize process 0(初始化0号进程的数据结构)*/
task[pid].pid = pid;
task[pid].state = 0;/* -1 unrunable,0 runable,>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,%%espnt" /*set task[pid].thread.sp to esp */
"pushl %1nt" /*push ebp*/
"pushl %0nt" /*push task[pid].thread.ip*/
"retnt" /*pop task[pid].thread.ip to eip*/
"popl %%ebpnt"
:
:"c"(task[pid].thread.ip),"d"(task[pid].thread.sp) /*input %ecx/%edx*/
);
}
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);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
[/toggle]
代码解析
my_start_kernel可以看做操作系统的入口 [toggle hide=“yes” title=“my_start_kernel中主要过程” color=“”] 1、初始化循环体,初始一个单PCB循环链表 2、扩充循环链表,使用memcpy将task[0]初始状态复制到task[i]。该代码中到结束时形成一个有4个PCB的循环链表。 3、利用一段汇编代码,初始化堆栈esp、ebp、eip [/toggle]
汇编代码部分解析
/*start process 0 by task[0]*/
pid = 0;
my_current_task = &task[pid];
asm volatile(
"movl %1,%%espnt" /*set task[pid].thread.sp to esp */
"pushl %1nt" /*push ebp*/
"pushl %0nt" /*push task[pid].thread.ip*/
"retnt" /*pop task[pid].thread.ip to eip*/
"popl %%ebpnt"
:
:"c"(task[pid].thread.ip),"d"(task[pid].thread.sp) /*input %ecx/%edx*/
);
2
3
4
5
6
7
8
9
10
11
12
[infobg class=“tips” closebtn=“” color=“” bgcolor=“”] 1、将task[0]的栈顶(即task[pid].thread.sp)赋值给esp。 2、将task[pid].thread.sp(即栈顶)压栈,由于当前栈为空栈,故当前ebp压栈同时esp的值被修改,为以后的%ebp的复位使用。 3、将task[0].thread.ip(即程序入口my_process)压栈。 4、执行ret等价于pop task[pid].thread.ip to eip,即出栈,将 task[0].thread.ip(即程序入口my_process)赋给eip。 5、再次出栈,之前保存的sp赋给ebp寄存器。 [/infobg] 这些步骤完成之后0号进程正式启动。
进程的运行
{
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);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
此过程为从i=0开始,每运行一千万次,程序自己检测是否需要进行调度(是否需要调度由时钟中断函数决定),如果是,就执行调度函数,切换到下一个进程。
myinterupt.c
这个文件主要是时钟中断函数和进程调度函数的具体实现,通过该文件中的函数完成最终的进程调度。 [toggle hide=“yes” title=“myinterupt.c” color=“”]
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 interrupy
* it runs in the name of current running process
* so it use kernel stack of current running process
*/
/*设置时间片的大小,时间片用完时设置一下调度标志*/
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;
}
void my_schedule(void)
{
tPCB *next;
tPCB *prev;
if(my_current_task == NULL || my_current_task->next == NULL)
{
return;
}
printk(KERN_NOTICE ">>> my_schedule here <<<n");
/*schedule*/
next = my_current_task->next;
prev = my_current_task;
if(next->state == 0)/* -1 unrunable,0 runable,>0 stopped*/
{
/*switch to next process */
asm volatile(
"pushl %%ebpnt" /*save ebp*/
"movl %%esp,%0nt" /*save esp*/
"movl %2,%%espnt" /*restore esp*/
"movl $1f,%1nt" /*save eip*/ /*$1f是指接下来的标号1:的位置*/
"pushl %3nt"
"retnt" /*restore eip*/
"1:t" /*next process start here*/
"popl %%ebpnt"
: "=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 %%ebpnt" /*save ebp*/
"movl %%esp,%0nt" /*save esp*/
"movl %2,%%espnt" /*restore esp*/
"movl %2,%%ebpnt" /*restore ebp*/
"movl $1f,%1nt" /*save eip*/
"pushl %3nt"
"retnt" /*restore eip*/
: "=m"(prev->thread.sp),"=m"(prev->thread.ip)
: "m"(next->thread.sp),"m"(next->thread.ip)
);
}
return;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
[/toggle]
代码分析
函数void my_timer_handler(void)作为时钟中断函数主要功能为判断是否需要进行进程调度。通过设置时间片的大小,时间片用完时设置一下调度标志。 又注释中提到”该函数运行在当前进程的地址空间内,所以它使用当前进程的内核栈空间“。故每个进程中均有一个自己的time_count用来计算时间片。又此函数中time_count达到1000的倍数时my_need_sched才改变一次,故可知每个进程运行的时间是1000个CPU时钟。 当my_need_sched = 1时执行void my_schedule(void)函数,此时下一个进程状态一般分为正在执行和尚未执行。
下一个进程正在执行时
两个正在运行的进程进行上下文切换,此时执行if中的代码。
if(next->state == 0)/* -1 unrunable,0 runable,>0 stopped*/
{
/*switch to next process */
asm volatile(
"pushl %%ebpnt" /*save ebp*/
"movl %%esp,%0nt" /*save esp*/
"movl %2,%%espnt" /*restore esp*/
"movl $1f,%1nt" /*save eip*/ /*$1f是指接下来的标号1:的位置*/
"pushl %3nt"
"retnt" /*restore eip*/
"1:t" /*next process start here*/
"popl %%ebpnt"
: "=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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[infobg class=“primary” closebtn=“” color=“” bgcolor=“”] 1、保存当前进程的ebp 2、将当前进程的esp赋到当前进程的sp,即保存当前的esp 3、将新进程的sp放到esp中 4、保存eip,即将eip保存到prev的ip 5、将新进程的eip压栈 6、ret 出栈,将next的ip赋给eip 7、此时新进程开始运行 8、恢复ebp (注意这里已经切换了进程) [/infobg] 小结: 1.保存prev进程的ebp、esp和eip 2.恢复next进程的esp、ebp和eip
下一个进程未执行过时
切换到一个新进程时,此时执行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 %%ebpnt" /*save ebp*/
"movl %%esp,%0nt" /*save esp*/
"movl %2,%%espnt" /*restore esp*/
"movl %2,%%ebpnt" /*restore ebp*/
"movl $1f,%1nt" /*save eip*/
"pushl %3nt"
"retnt" /*restore eip*/
: "=m"(prev->thread.sp),"=m"(prev->thread.ip)
: "m"(next->thread.sp),"m"(next->thread.ip)
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[infobg class=“primary” closebtn=“” color=“” bgcolor=“”] 1、保存当前进程的ebp 2、将当前进程的esp赋到当前进程的sp,即保存当前的esp 3、将新进程的sp放到esp中 4、将新进程的sp放到ebp中 5、保存eip,即将eip保存到当前的ip 6、将当前进程的ip压栈(即将当前程序的入口保存) 7、ret 出栈,将prev进程的ip赋给eip [/infobg] 小结: 1.保存prev进程的ebp、esp和eip 2.设置新进程的eip、ebp和esp。 因为是新进程,所以ebp和esp相同,都是从存储的sp那里取值。
两种进程切换的不同之处
当切换到一个新进程时,新进程的ebp不再是从栈顶恢复,而是设置一个新的值。
总结
初始化好的CPU从my_start_kernel开始执行,时钟中断机制周期性性执行my_time_handler中断处理程序,执行完后中断返回总是可以回到my_start_kernel中断的位置继续执行。 即操作系统通过CUP执行进程的同时判断分配到的时间片是否用完,当用完时保存当前中断现场的相关信息并进行进程调度,开始另一个进程,当另一个进程的时间片用完时,再回到之前中断的地方恢复并继续执行后面的内容,如此循环的方法进行工作。
附录
C语言中嵌入汇编语言的格式: 1、基本格式 [infobg class=“success” closebtn=“” color=“” bgcolor=“”] __asm__( 汇编语句模板: 输出部分: 输入部分: 破坏描述部分); [/infobg] [caption id=“” align=“aligncenter” width=“464”] __asm__可写为asm[/caption] 2、%1等相当于函数中的参数
3、/*$1f是指接下来的标号1:的位置*/ windCoder原创作品转载请注明出处
参考资料
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
除特别注明外,本站所有文章均为 Windcoder网 原创,转载请注明出处来自: linux-nei-he-fen-xi-zhi-cao-zuo-xi-tong-shi-ru-he-gong-zuo-di-shi-yan-zong-jie

额,看起来有点复杂……
回复 @Funix: 这个。。。只有最后的总结可以算点干货
满篇都是高大上啊,所有的字母在单个的时候我都认识,组成团了我就不认得了,呵呵,单身的字母好认也好记啊。Linux我都是照着网上的教程才懂安装,还要是简单的。像这个Linux内核这样的,就更加的木知道了。
很长的技术文章,可惜无爱
回复 @恋羽: 嘻嘻,主要是用来分析汇编代码部分了,看了下确实有点长,就缩了一下
过来转转。Google Chrome浏览器下页面有错位。
回复 @wys.me: 恩,有时可能会出点小错误