可以简单的将进程管理理解为计算机多个执行流逻辑之间切换,具体实现原理利用了堆栈的自平衡去跳转逻辑执行流,在定时器的中断服务函数中进行进程调度,调度策略采用的是简单的"
轮转"
,都是属于链表的简单操作。
内核雏形
在添加完进程管理之后我们的玩具内核已经初具规模,至此已经实现了一个简单的可以运转的内核模型:
1.中断管理:完成了中断描述符的初始化,注册中断服务函数
2.内存管理:物理内存,虚拟内存的映射以及堆内存的动态分配
3.进程管理:简单的任务调度
4.设备管理:vga字符模式下的显卡驱动
关于堆栈的说明
堆和栈在使用时相向生长,栈向上生长,即向小地址生长,而堆向下生长,即向大地址方向,其间剩余部分是自由空间
栈帧一般包含如下几个方面的信息:
1、函数的返回地址和参数
2、临时变量
比如说,如果调用SubRouting(var1,var2,var3),编译之后的最终代码可能是
push var3
push var2
push var1
call SubRouting ;call指令把返回地址压入堆栈 4byte
add esp,12 ;修正堆栈
esp是堆栈指针,无法暂借使用,所以一般用ebp存取堆栈
下面是个汇编求值的小例子:
完成第一个参数减去第二个参数的子程序,它的定义是
MyProc proto Var1,Var2 ;有两个参数
local lVar1,lVar2 ;有两个局部变量
MyProc proc
push ebp
mov ebp,esp
sub esp,8
mov eax,dword ptr[ebp+8]
sub eax,dword ptr[ebp+0xc]
add esp,8
pop ebp
ret 8
ret 8 <==> ret / add esp,8
代码分析
创建进程
将进程控制块信息pcb放置在进程栈的最低端
//进程标识符
pid_t glb_pid = 0;
//创建内核线程
int create_thread(thread_handler handler,void *args)
{
//线程栈
pcb_t *thread_new = (pcb_t *)mallock(STACK_SIZE);
thread_new->pid = glb_pid++;
thread_new->state = ready;
thread_new->stack = thread_new;
thread_new->mm = NULL; //使用内核页表
uint32_t *stack_top = (uint32_t *)((uint32_t)thread_new + STACK_SIZE);
*(--stack_top) = (uint32_t)args;
*(--stack_top) = (uint32_t)handler;
thread_new->context.esp = (uint32_t)thread_new + STACK_SIZE - sizeof(uint32_t) * 2;
thread_new->context.ebp = (uint32_t)thread_new;
// 设置新任务的标志寄存器未屏蔽中断,很重要
thread_new->context.eflags = 0x200;
//循环链表
thread_new->next = running_list;
pcb_t *tail = running_list;
if (tail == 0)
{
return -1;
}
while(tail->next != running_list)
{
tail = tail->next;
}
tail->next = thread_new;
return thread_new->pid;
}
初始化调度器
主要就是初始化三个进程队列以及保存内核的当前执行流,将进程控制块放在内核的栈底
//循环链表
pcb_t *ready_list; //就绪队列
pcb_t *blocked_list; //阻塞队列
pcb_t *running_list; //运行队列
//初始化线程调度器
void init_sched()
{
//为当前执行流创建进程控制块,放在内核的栈底
ready_list = (pcb_t *)(kern_stack);
ready_list->pid = glb_pid++;
ready_list->state = ready;
ready_list->stack = kern_stack;
ready_list->mm = NULL; //使用内核页表
//循环链表
ready_list->next = ready_list;
running_list = ready_list;
}
进程调度
对运行队列进行"
轮转"
调度
//线程调度函数
void schedule()
{
pcb_t *switch_thread,*switch_prev;
//调度策略
if (running_list)
{
switch_prev = running_list;
switch_thread = running_list->next;
}
if (switch_thread != running_list)
{
running_list = switch_thread;
switch_to(&(switch_prev->context),&(switch_thread->context));
}
}
进程切换
保存当前进程的上下文contex,进程切换的汇编实现,这里的进程之间切换比较隐秘,是通过切换进程之间的进程栈,然后利用ret指令将要切换进程的下一条指令的地址利用堆栈平衡弹出到eip中执行
[global switch_to]
; 具体的线程切换操作,重点在于寄存器的保存与恢复
switch_to:
mov eax, [esp+4]
mov [eax+0], esp
mov [eax+4], ebp
mov [eax+8], ebx
mov [eax+12], esi
mov [eax+16], edi
pushf
pop ecx
mov [eax+20], ecx
mov eax, [esp+8]
mov esp, [eax+0]
mov ebp, [eax+4]
mov ebx, [eax+8]
mov esi, [eax+12]
mov edi, [eax+16]
mov eax, [eax+20]
push eax
popf
ret
一些建议
1.项目编译问题
修改完文件重新编译的时候如果你确信程序没有问题一定要先进行make clean,clean,clean,重要的事情说三遍,要不然怎么死的都不知道,尤其是修改的头文件
2.关于调试
遇到问题多画一画,写一写,想一想,看代码,不需要gdb去调试debug,尽早实现printk一切都解决了
3.本地使用的数据,函数尽量都用static修饰起来,防止外界调用
一些忠告
1.颜色属性的attribute一定要定义成16位的,要不然左移8位之后就全变为0了,然后得到的颜色值就是黑色,和背景色颜色相同,根本看不到字符输出。
2.记住console_clear()之后cursor_y和cursor_x都已经复位为0,如果紧挨着执行console_putc(c)的话是直接输出在屏幕的第一个字符。
3.static声明函数的时候一定不要将函数的声明放在头文件里,想一下static的作用就好了,static本来就是将函数限制在本地文件,防止其他文件之间的调用,所以肯定不能将声明放在对应的头文件里。
如static void move_cursor();
声明与函数实现static void move_cursor(){...}
肯定是放在同一个文件下的。切记!!!
4.c 语言float精度问题
1.059实际存储为1.058000 32615661621093750
类型 | 比特(位)数 | 有效数字 | 数值范围 |
---|---|---|---|
float | 32 | 6~7 | -3.410^38~+3.410^38 |
double | 64 | 15~16 | -1.710^-308~1.710^308 |
long double | 128 | 18~19 | -1.210^-4932~1.210^4932 |
5.console_write_dec
这里一定要判断精度是否为0,而不是判断除10取余是否为0,要不然遇到100这个函数就完了!!
6.cli对int软中断没有影响
7.asm和c的文件放在一个文件夹下不要同名,如果输出文件名一样的话就只会有一个输出文件!!
8.计时器中断,在没有配置8254的情况下打开irq_enable()不停的32号中断,因为定时器还在计数,只不过频率和溢出率不知道,不是你所期望的,可以禁用那个中断
9.一定要注意定时器的分频系数不能超过16位所表示的大小
10.在初始化内核函数栈之前一定不要有函数调用,这时内核栈还没有创建,函数调用一定会用到栈帧,会发生意想不到的后果!!
11.Makefile一定要正确,修改.h文件后要先make clean
12.段描述符表和中断描述符表为空或者没有初始化正确内核老是重启,因为里面数据是0
13.console_write_dec(025)与console_write_hex(025)输出的数据都等于21,这里不是程序错误,是因为025会当成八进制数据这里就是21
14.页目录、页表里面存储的是物理地址!!!而不是内核代码所在的虚拟地址