开始
完成一个简单的时间片轮转多道程序内核代码
环境搭建
首先,我们要模拟一个Linux内核平台,步骤如下:
在root权限下执行以下命令:1
2
3
4
5
6
7
8
9
10
11~$ sudo apt-get install qemu #安装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 #下载Linux内核代码
~$ wget https://raw.github.com/mengning/mykernel/master/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 #所有的功能都屏蔽了,之后你需要什么加什么
~$ make #编译链接
~$ qemu -kernel arch/x86/boot/bzImage #运行
得到如下运行结果:
字符串my_start_kernel here表示运行的进程,而my_timer_handler_here表示进程被中断。CPU就是不停的运行进程,然后中断被进程,如此循环的。
结果分析
此时mykernel下面一共有两个文件,一个是mymain.c,一个是myinterrupt.c。下面分别分析里面的代码:
mymain.c里面的代码如下:
就是每循环多少次,输出一个my_start_kernel here,该进程是一直在执行的。
myinterrupt.c里面的代码如下:
这是一个时钟中断进程,它每隔一段时间就会执行一次,打断当前的进程。
复杂代码进阶
使用命令:1
git clone https://github.com/mengning/mykernel.git
将原来的mykernel文件进行替换,替换之后的mykernel里面的文件为mymain.c、mypcb.c、myinterrupt.c。这时操作系统能够完成模拟四个任务的执行过程。
然后输入命令:1
2~$ make #编译链接
~$ qemu -kernel arch/x86/boot/bzImage #运行
得到如下结果:
可见是四个进程换起来执行。
代码过程分析
以下是mypcb.c的内容:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* CPU-specific state of this task */
struct Thread {
unsigned long ip; //本进程指向的下一个指令
unsigned long sp; //存放进程的栈底地址
};
typedef struct PCB{
int pid; //定义进程id
volatile long state; /* -1 没有运行, 0 已运行, >0 停止 */ //进程状态
char stack[KERNEL_STACK_SIZE]; //内核堆栈
/* CPU-specific state of this task */
struct Thread thread; //进程结构体
unsigned long task_entry; //保存进程的入口函数地址
struct PCB *next; //将PCB用链表链起来,指向下一个PCB
}tPCB;
//声明任务调度器,其地址赋给PCB中的task_entry字段
void my_schedule(void);
它定义了一个线程结构体和一个PCB结构体。
以下是mymain.c的内容: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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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;
/*初始化0号进程*/
task[pid].pid = pid;
task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */
//把第一个进程的入口定义为my_process
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];
//目前只有一个0号进程,next指向自己
task[pid].next = &task[pid];
/*初始化其它三个进程(1号,2号,3号)*/
for(i=1;i<MAX_TASK_NUM;i++)
{
//先将已经初始化好的0号进程控制块拷贝过来, 再基于此进行修改
memcpy(&task[i],&task[0],sizeof(tPCB));
// 1号、2号、3号进程的PID分别为1、2、3
task[i].pid = i;
// 进程状态置为未运行(待运行)
task[i].state = -1;
// 堆栈指针指向自己进程堆栈的栈底
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];
}
/*启动0号进程*/
pid = 0;
my_current_task = &task[pid];
//第一个进程初始化堆栈和eip
//volatile用于指示编译器不要优化代码,后面的指令保持原样;
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"
//输出部分为空
:
//输入部分:分别对应%0和%1
: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %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);
//这里需要等时钟中断程序将my_need_sched的值改为1, 才能触发一次my_schedule函数
if(my_need_sched == 1)//1是需要调度,0是不需要调度
{
my_need_sched = 0;
my_schedule();//调度函数
}
printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid);
}
}
}
以下是myinterrupt.c的内容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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
extern tPCB task[MAX_TASK_NUM];
extern tPCB * my_current_task;
extern volatile int my_need_sched;
volatile int time_count = 0;
/*
操作系统每隔一段时间会收到一次时钟中断事件,
收到该事件后,无论当前在执行什么进程,
操作系统都会调用my_timer_handler()函数
*/
void my_timer_handler(void)
{
if(time_count%1000 == 0 && my_need_sched != 1)
{
printk(KERN_NOTICE ">>>my_timer_handler here<<<\n");
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<<<\n");
/*进程调度*/
next = my_current_task->next;
prev = my_current_task;
//next结点的初始状态为-1
if(next->state == 0)/*判断下一个进程是不是可以运行*/
{//next进程第二次及以后所有次走该分支
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 */
"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)
);
}
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 */
"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;
}
总结
操作系统启动后首先初始化若干进程,并执行第一个进程,之后由进程调度机制执行调度,使各个进程都能执行。并且是由CPU和内核代码共同实现保存现场和恢复现场的。
【版权声明】
本文首发于戚名钰的博客,欢迎转载,但是必须保留本文的署名戚名钰(包含链接)。如您有任何商业合作或者授权方面的协商,请给我留言:qimingyu.security@foxmail.com
欢迎关注我的微信公众号:科技锐新