从一个精简Linux内核分析操作系统的基本运行过程
2016-11-11 23:09
381 查看
郑德伦 原创作品转载请注明出处 《Linux内核分析》MOOC课程
http://mooc.study.163.com/course/USTC-1000029000
转自:http://blog.csdn.net/a363344923/article/details/44276715
STEP1:搭建实验环境
首先在自己的Linux系统中配置好实验的环境,依次输入以下的命令:
2
3
4
5
6
7
8
9
10
1
2
3
4
5
6
7
8
9
10
最后执行qemu -kernel arch/x86/boot/bzImage
STEP2:完成一个进程切换的内核
经过环境的配置,Linux内核经过我们修改,在qemu窗口中我们会看到,一个只有时钟中断的系统。如图所示
因为在mymian.c里面,只有一个函数在执行,my_start_kernel(void),循环输出一句话。
2
3
4
5
6
7
8
9
10
11
1
2
3
4
5
6
7
8
9
10
11
在myinterrupt.c文件里面只有一个处理时钟中断的函数
2
3
4
1
2
3
4
如果我们要完成可以进行进程调度的操作系统内核,需要将github中mykernel的myinterrupt.c mymain.c mypcb.h三个文件复制到linux内核文件夹下面,然后再进行make。
首先在github上面下载3个文件,在终端中输入如下命令:
http://mooc.study.163.com/course/USTC-1000029000
转自:http://blog.csdn.net/a363344923/article/details/44276715
STEP1:搭建实验环境
首先在自己的Linux系统中配置好实验的环境,依次输入以下的命令:
• sudo apt-get install qemu # install QEMU • sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu • wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.9.4.tar.xz # download Linux Kernel 3.9.4 source code • wget https://raw.github.com/mengning/mykernel/master/mykernel_for_linux3.9.4sc.patch # download mykernel_for_linux3.9.4sc.patch • xz -d linux-3.9.4.tar.xz • tar -xvf linux-3.9.4.tar • cd linux-3.9.4 • patch -p1 < ../mykernel_for_linux3.9.4sc.patch • make allnoconfig • make1
2
3
4
5
6
7
8
9
10
1
2
3
4
5
6
7
8
9
10
最后执行qemu -kernel arch/x86/boot/bzImage
STEP2:完成一个进程切换的内核
经过环境的配置,Linux内核经过我们修改,在qemu窗口中我们会看到,一个只有时钟中断的系统。如图所示
因为在mymian.c里面,只有一个函数在执行,my_start_kernel(void),循环输出一句话。
void __init my_start_kernel(void) { int i = 0; while(1) { i++; if(i%100000 == 0) printk(KERN_NOTICE "my_start_kernel here %d \n",i); } }1
2
3
4
5
6
7
8
9
10
11
1
2
3
4
5
6
7
8
9
10
11
在myinterrupt.c文件里面只有一个处理时钟中断的函数
void my_timer_handler(void) { printk(KERN_NOTICE "\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n"); }1
2
3
4
1
2
3
4
如果我们要完成可以进行进程调度的操作系统内核,需要将github中mykernel的myinterrupt.c mymain.c mypcb.h三个文件复制到linux内核文件夹下面,然后再进行make。
首先在github上面下载3个文件,在终端中输入如下命令:
wget https://raw.github.com/mengning/mykernel/master/mypcb.h wget https://raw.github.com/mengning/mykernel/master/myinterrupt.c wget https://raw.github.com/mengning/mykernel/master/mymain.c[/code]1 2
3
1
2
3
然后将三个文件拷贝到linux内核文件夹下面的mykernel文件夹下面,在终端中输入命令cp myinterrupt.c linux-3.9.4/mykernel/ cp mymain.c linux-3.9.4/mykernel/ cp mypch.h linux-3.9.4/mykernel/1
2
3
1
2
3
最后执行make重新编译内核
然后终端中输入qemu -kernel arch/x86/boot/bzImage就可以看到一个进程切换的内核运行了。
STEP3:分析代码,理解执行过程
首先打开mypcb.h文件,有两个结构体。
第一Thread结构体,保存一个任务的eip还有esp的信息。struct Thread { unsigned long ip; unsigned long sp; };1
2
3
4
1
2
3
4
第二个结构体PCB,保存进程的一些信息。
pid保存一个进程唯一的一个ID号
state表示一个进程的运行状态
stack数组表示一个进程的堆栈空间
Thread结构体记录eip和esp堆栈栈顶的信息
task_entry表示进程的执行入口地址
struct PCB* next则指向下一个进程,用链式储存结构将进程连在一起typedef struct PCB{ int pid; volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ char stack[KERNEL_STACK_SIZE]; /* CPU-specific state of this task */ struct Thread thread; unsigned long task_entry; struct PCB *next; }tPCB;1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9
然后打开mymain.c文件
volatile int my_need_sched = 0;
my_need_sched变量表示一个任务,是否可以进行调度。
然后分析kernel开始执行的函数。void __init my_start_kernel(void) { int pid = 0; int i; /* Initialize process 0*/ task[pid].pid = pid;/*将进程号初始化为0*/ task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */ task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process; /*把进程的入口和eip都初始化为my_process函数的入口地址*/ task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1]; /*将堆栈的栈顶地址保存为栈的最后一个元素,即最高的地址*/ task[pid].next = &task[pid]; /*next指针指向自己*/ /*fork more process */ for(i=1;i<MAX_TASK_NUM;i++) { memcpy(&task[i],&task[0],sizeof(tPCB)); /*将task[0]的信息全部复制到各个新进程中*/ task[i].pid = i;/*将新进程的pid设置为变量i*/ task[i].state = -1;/*新进程的状态为unrunnable*/ task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];/*将每个新进程的栈顶指针都指向堆栈数组的最后一个元素*/ /*使用尾插法创建链表形成一个任务链0->1->2->3->0这样的循环*/ 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];//将当前进程设置为0号进程 asm volatile( /*把0号进程的esp放入esp寄存器*/ "movl %1,%%esp\n\t" /* set task[pid].thread.sp to esp */ /*push0号进程的esp,因为此时栈为空,esp==ebp,所以此处等同于push ebp*/ "pushl %1\n\t" /* push ebp */ /*将0号进程的eip,即process函数的入口地址push到当前堆栈*/ "pushl %0\n\t" /* push task[pid].thread.ip */ /*将栈顶元素pop到eip然后跳转*/ "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*/ ); }1
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
1
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
my_start_kernel完成的任务主要是初始化进程链表,然后开始执行第一个进程。
执行完ret操作之后将会跳转到process函数中执行。
然后我们来分析process程序,process程序是一个无限循环,每循环100000000执行判断生效一次,输出KERN_NOTICE “this is process pid-,并且判断my_need_sched是否等于1,如果等于1的话,就把my_need_sched置0并且执行my_schedule()进行调度,最后执行KERN_NOTICE “this is process pid+void my_process(void) { int i = 0; while(1) { i++; if(i%100000000 == 0)// 没执行100000000次循环进入一次 { 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); } } }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
我们再来看看myinterrupt.c文件void my_timer_handler(void) { #if 1 /*函数每执行1000次,并且my_need_sched不等于1的时候执行if判断,然后把my_need_sched置为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; }1
2
3
4
5
6
7
8
9
10
11
12
13
1
2
3
4
5
6
7
8
9
10
11
12
13
分析一下函数my_timer_handler(void),#if 1 到#endif表示一个条件预编译指令,表示永远编译,一般用于测试代码中,把1改为0相当于注释掉代码段,方便调试。
my_timer_handler函数每执行1000次并且my_need_sched不为1的时候输出一段话,并且把my_timer_handler置为1.
到此我们就清楚了my_need_sched这个变量的变化过程(由process置0,由my_timer_handler置1,交替执行),和mymain.c里面的process函数结合就可以分析出进程调度的详细过程。
注:因为时间中断处理每隔一段时间就会进行一次,可能在0号进程执行过程中进行了多次时间中断处理,我们暂时把没有发生my_need_sched变量改变的时间中断处理时间忽略掉。
进程的调度过程如下:
0号进程启动,my_need_sched=0,当my_timer_handler将my_need_sched置1时,process执行调度函数my_schedule(),并将my_need_sched置0
->执行1号进程,my_need_sched=0,当my_timer_handler将my_need_sched置1时,process执行调度函数my_schedule(),并将my_need_sched置0
->执行2号进程,my_need_sched=0,当my_timer_handler将my_need_sched置1时,process执行调度函数my_schedule(),并将my_need_sched置0
->执行3号进程,my_need_sched=0,当my_timer_handler将my_need_sched置1时,process执行调度函数my_schedule(),并将my_need_sched置0
->执行0号进程,my_need_sched=0,当my_timer_handler将my_need_sched置1时,process执行调度函数my_schedule(),并将my_need_sched置0
整个调度过程就如上面所示,process循环执行,等待时钟中断来将允许调度的变量置1,然后完成调度。
具体的调度过程我们要分析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; /*next进程两种情况,运行和非运行要分情况处理*/ 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 */ /*将下面标号1:之后的语句popl ebp的地址放入prev的ip中保存*/ "movl $1f,%1\n\t" /* save eip */ /*将next的eip push到栈上,然后执行ret的话,就会从next的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的状态置为runnable*/ 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; }1
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
1
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
如果next进程为非runnable状态的时候执行else,runnable的状态执行if,整个过程就是先保存当前进程的ebp和esp,然后将next进程的esp和ebp复原,并且将prev进程的eip储存为标号1的位置,最后将next进程的eip push到栈上,再执行ret指令,返回到next的eip所指向的地方执行。
因为进程是用一个循环链表连接起来的,一直从prev向next切换,总会有一次再次切换到自己,也就是prev进程,变成next进程。如果下一次进入的时候会标号1:开始执行完成ebp的复原操作。
注:为了便于理解我们用0号进程和1号进程表示。
这个schedule函数分两次执行,第一次是从0号进程跳转到1号进程,而第二次进入则是执行popl ebp恢复现场,然后return函数,返回到原调用者也就是0号进程的process里面。
总结:
通过以上代码的分析,我们可以初步了解到了,linux内核是如何完成进程的切换的。我们可以认为一个进程相当于一个堆栈,每个进程有自己的堆栈空间。如果将ebp和esp修改为另一个进程的ebp和esp,并且完成一些寄存器的保存,就相当于完成的进程的切换。
相关文章推荐
- 从一个精简Linux内核分析操作系统的基本运行过程
- groovy分析脚本基本组成文件详解和运行过程中出错分析
- 在兼容机与IBM 8434型PC串操作系统的过程中,IBM PC硬盘被烧毁原因分析
- ASP.NET 2.0运行原理及其过程简要分析
- SpringMVC:DispatcherServlet代码分析及运行过程
- Loadrunner脚本回放 场景运行过程中常见错误分析
- ASP.NET 2.0运行原理及其过程简要分析
- Java的安装、配置和运行的基本过程及其原理!
- NAS DIY的设计和实施过程-7-打造属于我的NAS操作系统1-精简centos
- Linux内核进程创建过程分析
- linux非解压代码的启动过程分析 unicore head.S vmlinux解压后的代码运行 临时MMU的建立
- ASP.NET 2.0运行原理及其过程简要分析
- Linux内核情景分析读书笔记——存储管理之地址映射全过程
- [转]ASP.NET 2.0运行原理及其过程简要分析
- NAS DIY的设计和实施过程-7-打造属于我的NAS操作系统2-精简centos
- 从一个实例看TCLCL的运行过程
- ASP.NET 2.0运行原理及其过程简要分析
- ASP.NET 2.0运行原理及其过程简要分析
- Cocoa应用程序基本运行过程(图解)
- OO 中的继承分析:主要分析在编译和运行过程中 子类、父类 的字段和方法以及实例化时候在内存中分配 和 执行的先后,以及两个原则