基于mykernel完成时间片轮询多道进程的简单内核

原创作品转载请注明出处+中科大孟宁老师的linux操作系统分析:https://github.com/mengning/linuxkernel/

操作系统概述

操作系统中,OS的任务有很多,比如内存管理、文件管理、进程管理等等,而这其中,进程的管理是至关重要的,因为只有实现了进程的管理,才能使得一个操作系统可以处理多种任务,通过进程的切换,可以从逻辑上实现任务的多个进程任务并行操作,使得操作系统可以更高效的服务于应用服务。本实验着重基于linuxkernel实现一个时间片轮转多道程序内核代码

实验环境

Ubuntu 18.04、vim、gcc、vscode、QEMU

实验步骤

首先安装QEMU工具,后面需要它显示内核执行的过程,可以UI展示

1
2
sudo apt-get install qemu
sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu

然后下载基于3.9.4版本的linux内核和孟老师基于内核裁剪出来的补丁(给孟宁老师实力点赞),然后进行解压和打补丁

1
2
3
4
5
6
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

然后就是编译源码,c语言里面可以通过强大的make实现编译,先预处理编译文件,在make编译

1
2
make allnoconfig
make

在这一步的时候,会报错,见下图,由于环境没有gcc7,通过网络,定位可以通过将linux-3.9.4/include/linux/下面的

1
2
cd /include/linux/
cp compiler-gcc4.h compiler-gcc5.h

然后在编译成功,如下图

我们再使用qemu工具来显示代码执行的UI过程

1
qemu -kernel arch/x86/boot/bzImage

可见我们的内核已经启动成功了,接下来我们就基于孟宁老师的项目代码去实现一个时间轮转多道程序。
我们先git clone git@github.com:mengning/mykernel.git代码到本地,然后将其中的myinterrupt.c,mymain.c,mypcb.h三个代码替换上面mykernel文件夹下对于的三个,替换前最好先像cp mymain.c mymain-bak.c这样备份一下,其中要把mypcb.h中的一行代码修改一下:

1

替换完之后,在make重新编译一下,这一次编译是让替换之后的代码重新编译,编译速度也会比上次快很多,因为是增量编译。

编译完之后,重新执行一下,输入qemu -kernel arch/x86/boot/bzImage
结果见下图

上图可以看出,时间片轮询多道程序已经实现了,总共有4个进程,pid分别为0,1,2,3;
下一节我们来分析代码,分析一下孟宁老师给出的简洁高效的实现时间片轮询多道进程的代码,基本深入浅出的阐明了基本工作原理。

源码分析

mypcb.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define MAX_TASK_NUM 10 // max num of task in system
#define KERNEL_STACK_SIZE (unsigned) long1024*8
#define PRIORITY_MAX 30 //priority range from 0 to 30
struct Thread {
unsigned long ip;//point to cpu run address
unsigned long sp;//point to the thread stack's top address
//todo add other attrubte of system thread
};
//PCB Struct
typedef struct PCB{
int pid; // pcb id
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
char stack[KERNEL_STACK_SIZE];// each pcb stack size is 1024*8
/* CPU-specific state of this task */
struct Thread thread;
unsigned long task_entry;//the task execute entry memory address
struct PCB *next;//pcb is a circular linked list
unsigned long priority;// task priority ////////
//todo add other attrubte of process control block
}tPCB;
//void my_schedule(int pid);
void (void);

首先是mypcb.h,其中定义了两个结构和一个函数。第一个是结构Thread,里面有两个变量,ip和sp用于保存现场。
第二个是结构PCB,PCB结构定义了进程管理块,包括6各变量:
(1)pid进程标识符;
(2)state状态,-1表示不可运行,0表示可运行,>0表示停止;
(3)定义了一个栈空间;
(4)一个Thread变量;
(5)任务入口点;
(6)下一个PCB的指针;
还定义了一个my_schedule函数,以及两个宏定义。

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
tPCB task[MAX_TASK_NUM];
tPCB * my_current_task = NULL;
volatile int my_need_sched = 0;
void my_process(void);
unsigned long get_rand(int );
void sand_priority(void)
{
int i;
for(i=0;i<MAX_TASK_NUM;i++)
task[i].priority=get_rand(PRIORITY_MAX);
}
void __init my_start_kernel(void)
{
int pid = 0;
/* Initialize process 0*/
task[pid].pid = pid;
task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */
// set task 0 execute entry add 大专栏  基于mykernel完成时间片轮询多道进程的简单内核ress to 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];
task[pid].next = &task[pid];
/*fork more process */
for(pid=1;pid<MAX_TASK_NUM;pid++)
{
memcpy(&task[pid],&task[0],sizeof(tPCB));
task[pid].pid = pid;
task[pid].state = -1;
task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
task[pid].priority=get_rand(PRIORITY_MAX);//each time all tasks get a random priority
}
task[MAX_TASK_NUM-1].next=&task[0];
printk(KERN_NOTICE "nnnnnn system begin :>>>process 0 running!!!<<<nn");
/* 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 c or d mean %ecx/%edx*/
);
}
void my_process(void)
{
int i = 0;
while(1)
{
i++;
if(i%10000000 == 0)
{
if(my_need_sched == 1)
{
my_need_sched = 0;
sand_priority();
my_schedule();
}
}
}
}//end of my_process
//produce a random priority to a task
unsigned long get_rand(max)
{
unsigned long a;
unsigned long umax;
umax=(unsigned long)max;
get_random_bytes(&a, sizeof(unsigned long ));
a=(a+umax)%umax;
return a;
}

首先定义了3个全局变量,两个PCB结构,一个是所有的进程集合,一个是当前的进程。然后是两个函数,my_process和my_start_kernel。
我们重点来看一下my_start_kernel函数,函数分为三部分:
第一部分,是初始化进程0。pid代表了进程号,0是第一个。state代表运行状态,初始化为可运行。Thread的ip就是进程入口点,其实就是进程运行的起点。sp实际上是定义了一段进程的栈空间。最后定义了下一个PCB的链接先指向自己。
第二部分,是根据第一个进程0初始化余下的进程。因为我们设置最大进程数为4,所以这里实际上是设置了进程1-3的数据结构的值。
最后一个部分,是从进程0号开始运行。这里使用了内联汇编编程,实际上就是将进程0的thread.sp的值赋给esp,将当前运行的地址保存到栈中,这样如果切换的话就可以保证下一个进程结束时回到原来的位置执行。总而言之,my_start_kernel函数实现了定义进程数组,并运行第一个进程。
对于my_process函数来说:
就是建立一个循环不断运行进程,并输出表明进程正在运行的语句。这里注意有一个my_schedule()函数,实际上这个函数是在myinterrupt.c中实现的,主要作用是切换进程。

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
void my_schedule(void)
{
tPCB * next;
tPCB * prev;
// if there no task running or only a task ,it shouldn't need schedule
if(my_current_task == NULL
|| my_current_task->next == NULL)
{
printk(KERN_NOTICE " time out!!!,but no more than 2 task,need not schedulen");
return;
}
/* schedule */
next = get_next();
prev = my_current_task;
printk(KERN_NOTICE " the next task is %d priority is %un",next->pid,next->priority);
if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */
{//save current scene
/* 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 */
"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;//switch to the next task
printk(KERN_NOTICE " switch from %d process to %d processn >>>process %d running!!!<<<nn",prev->pid,next->pid,next->pid);
}
else
{
next->state = 0;
my_current_task = next;
printk(KERN_NOTICE " switch from %d process to %d processn >>>process %d running!!!<<<nnn",prev->pid,next->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;
}//end of my_schedule

首先定义了一些全局变量。然后主要实现了两个函数:my_time_handler和my_schedule,其中my_time_handler实现了中断,而my_schedule实现了中断之后进程的切换。
my_time_hander()这个函数也很简单,就是每1000毫秒的时候产生一个中断,产生中断之后把my_need_sched设置为1,这样mymain.c中的my_process函数就会调用my_schedule函数来进行进程切换。
my_schedule()实现了时间片轮转的中断处理过程。首先是初始化next和prev两个PCB结构。然后是循环运行代码,就是当下一个进程的state状态是可运行时,说明这个进程之前已经在运行了,此时可以继续执行,就切换到下一个进程,中间有一段内联汇编,实现了保存栈地址和栈指针,这样进程切换回来的时候就可以正常运行。然后根据之前保存的栈地址恢复执行。
当下一个进程的state不为0时,那么也就是说下一个进程还从来都没有执行过,所以这一段内联汇编的作用是开始执行一个新进程。总而言之,在上述函数的作用下,成功地实现了时间片轮转的中断处理内核的功能。通过分析源码,我们了解到时间片轮转算法的具体方法,即每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。这是一种最古老,最简单,最公平且使用最广的算法。

实验分析

当mykernel自制操作系统启动后,即qemu命令执行后,系统会首先调用mymain产生一个进程,该进程内while循环条件恒为1所以永远执行输出“my_start_kernel here”+累计循环次数,没有结束的时候。此时由系统时钟中断触发调用myinterrupt产生一个新进程,并输出“my_timer_handler here”,结束后返回mymain产生的进程继续执行,等待下一次系统时钟中断触发调用myinterrupt产生一个新进程,myinterrupt产生的新进程和上一次的属于不同的进程,而mymain由于循环没有终止的时候,所以永远是原来的那个进程。通过观察可以发现,当执行myinterrupt后返回mymain时,终端输出的累计循环次数是连续的,并没有中断或重置,说明CPU和内核代码共同实现了保存现场和恢复现场的功能,会将一些重要的寄存器,比如eip、ebp、esp等保存下来,等待切换回来的时候继续执行。

实验总结

通过该实验操作,成功实现了一个简单的时间片轮转多道程序,通过一个精简的操作系统内核,完成了一个简单的操作系统功能。
计算机有三个法宝:存储程序计算机、函数调用堆栈、中断操作系统
有两把宝剑:中断上下文、进程上下文切换由于CPU只有一套寄存器,同一时间只能处理一个进程(暂且只考虑单核CPU),所以理论上只能单任务顺序执行
但基于三个法宝和两把宝剑,操作系统得以实现多任务操作。

基于mykernel完成时间片轮询多道进程的简单内核的更多相关文章

  1. 分享一个基于长连接+长轮询+原生的JS及AJAX实现的多人在线即时交流聊天室

    实现网页版的在线聊天室的方法有很多,在没有来到HTML5之前,常见的有:定时轮询.长连接+长轮询.基于第三方插件(如FLASH的Socket),而如果是HTML5,则比较简单,可以直接使用WebSoc ...

  2. 基于HTTP的长轮询简单实现

    Web客户端与服务器之间基于Ajax(http)的常用通信方式,分为短连接与长轮询. 短连接:客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接. 在长轮询机制中,客户端像传统轮 ...

  3. 基于Ajax的长轮询(long-polling)方式

    如 图 1 所示,AJAX 的出现使得 JavaScript 可以调用 XMLHttpRequest 对象发出 HTTP 请求,JavaScript 响应处理函数根据服务器返回的信息对 HTML 页面 ...

  4. 基于AJAX的长轮询(long-polling)方式实现简单的聊天室程序

    原理: 可以看:http://yiminghe.javaeye.com/blog/294781 AJAX 的出现使得 JavaScript 可以调用 XMLHttpRequest 对象发出 HTTP ...

  5. 基于PHP实现一个简单的在线聊天功能(轮询ajax )

    基于PHP实现一个简单的在线聊天功能(轮询ajax ) 一.总结 1.用的轮询ajax 二.基于PHP实现一个简单的在线聊天功能 一直很想试着做一做这个有意思的功能,感觉复杂的不是数据交互和表结构,麻 ...

  6. Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE

    1. 前言 Web端即时通讯技术因受限于浏览器的设计限制,一直以来实现起来并不容易,主流的Web端即时通讯方案大致有4种:传统Ajax短轮询.Comet技术.WebSocket技术.SSE(Serve ...

  7. Web 通信 之 长连接、长轮询(转)

    Web 通信 之 长连接.长轮询(long polling) 基于HTTP的长连接,是一种通过长轮询方式实现"服务器推"的技术,它弥补了HTTP简单的请求应答模式的不足,极大地增强 ...

  8. Web 通信 之 长连接、长轮询(long polling)

    基于HTTP的长连接,是一种通过长轮询方式实现"服务器推"的技术,它弥补了HTTP简单的请求应答模式的不足,极大地增强了程序的实时性和交互性. 一.什么是长连接.长轮询? 用通俗易 ...

  9. Web 通信 之 长连接、长轮询(long polling)(转)

    基于HTTP的长连接,是一种通过长轮询方式实现"服务器推"的技术,它弥补了HTTP简单的请求应答模式的不足,极大地增强了程序的实时性和交互性. 一.什么是长连接.长轮询? 用通俗易 ...

随机推荐

  1. 12. docker 网络 docker network (docker 网络)

    1. 环境准备 编写 Vagrantfile 为 # -*- mode: ruby -*- # vi: set ft=ruby : Vagrant.require_version "> ...

  2. leetcode腾讯精选练习之最长公共前缀(九)

    最长公共前缀 题目 编写一个函数来查找字符串数组中的最长公共前缀. 如果不存在公共前缀,返回空字符串 "". 示例 1: 输入: ["flower"," ...

  3. NSPredicate 应用

    //查询单词里面包含“ang”的字符串 NSArray *array = [[NSArray alloc]initWithObjects:@"beijing",@"sha ...

  4. python 并发执行

    并发执行, 精简代码. 适用python2 和python3 # -*- encoding:utf-8 -*- from threading import Thread from multiproce ...

  5. 在拖放文件的同时检测shift键的状态

    老板要给原来文件拖放的功能加个扩展分类,于是想在文件拖放时判断shift键的状态来区分. 一般通过keydown和keyup来判断按下与否,但这都是需要控件事件触发,而在拖放的时候是没法触发key事件 ...

  6. Opencv笔记(二十)——直方图(二)

    直方图均衡化 原理: 想象一下如果一副图像中的大多是像素点的像素值都集中在一个像素值范围之内会怎样呢?例如,如果一幅图片整体很亮,那所有的像素值应该都会很高.但是一副高质量的图像的像素值分布应该很广泛 ...

  7. FPGA 状态机-序列检测器verilog

    实现功能:检测出串行输入数据4位Data二进制序列0101,当检测到该序列的时候,out=1,否则out=0 (1)给出状态编码,画出状态图 (2)门电路实现 (3)verilog实现 首先规定Q3Q ...

  8. Linux基础篇三:文件系统

    /bin      实际上是  /usr/bin /sbin    实际上是  /usr/sbin /usr/bin 里面的命令其实是依赖  /lib64  或者    /lib32 ldd  /us ...

  9. 吴裕雄--天生自然python学习笔记:python 建立 Firebase 数据库连接

    Python 程序通过 python-firebase 包可以存取 Firebase 数据库. 使用 python-firebase 包 首先必须安装 python-firebase 包,安装方法如下 ...

  10. 系统学习javaweb4----CSS层叠样式表(结束)

    摘要:这几天临近过年,事情有点多,学习总是段段续续的,今天总算完成了CSS的基本知识学习. 学习笔记: 西瓜学习javaweb 1.css简述. 1.1 css是什么?有什么作用? HTML----- ...