《一个操作系统的实现》笔记(6)--进程
来源:互联网 发布:ipad不下载软件 编辑:程序博客网 时间:2024/06/06 21:15
我们可以把一个单独的任务所用到的所有东西封装在一个LDT中,这种思想是多任务处理的雏形。
多任务所用的段类型如下图,使用LDT来隔离每个应用程序任务的方法,正是关键保护需求之一:
进程示意:
我们需要一个数据结构记录一个进程的状态,在进程要被挂起的时候,进程信息就被写入这个数据结构,等到进程重新启动的时候,这个信息重新被读出来。
最简单的进程
进程切换的过程:
- 1.进程A运行中
- 2.时钟中断发生,ring1->ring0,时钟中断处理器启动
- 3.进程调度,下一个应运行的进程B被指定
- 4.进程B恢复,ring0->ring1
- 5.进程B运行中
进程表
保存进程信息的数据结构称为进程表,或叫进程控制块,即PCB。
进程栈和内核栈
esp的位置出现在3个不同的区域:
- 进程栈–进程运行时自身的堆栈
- 进程表–存储进程状态信息的数据结构
- 内核栈–进程调度模块运行时使用的堆栈
第1步–ring0->ring1
开始第一个进程,我们使用iretd
指令来实现由ring0到ring1的转移,转移成功后,就可以认为A进程在运行了。
进程表数据结构
typedef struct s_stackframe { /* proc_ptr points here ↑ Low */ u32 gs; /* ┓ │ */ u32 fs; /* ┃ │ */ u32 es; /* ┃ │ */ u32 ds; /* ┃ │ */ u32 edi; /* ┃ │ */ u32 esi; /* ┣ pushed by save() │ */ u32 ebp; /* ┃ │ */ u32 kernel_esp; /* <- 'popad' will ignore it │ */ u32 ebx; /* ┃ ↑栈从高地址往低地址增长*/ u32 edx; /* ┃ │ */ u32 ecx; /* ┃ │ */ u32 eax; /* ┛ │ */ u32 retaddr; /* return address for assembly code save() │ */ u32 eip; /* ┓ │ */ u32 cs; /* ┃ │ */ u32 eflags; /* ┣ these are pushed by CPU during interrupt │ */ u32 esp; /* ┃ │ */ u32 ss; /* ┛ ┷High */}STACK_FRAME;typedef struct s_proc { STACK_FRAME regs; /* process registers saved in stack frame */ u16 ldt_sel; /* gdt selector giving ldt base and limit */ DESCRIPTOR ldts[LDT_SIZE]; /* local descriptors for code and data */ int ticks; /* remained ticks */ int priority; u32 pid; /* process id passed in from MM */ char p_name[16]; /* name of the process */}PROCESS;
当要恢复一个进程时,便将esp指向这个结构体的开始处,然后运行一系列的pop命令将寄存器值弹出。
实现ring0->ring1
而堆栈的信息也不外乎ss和esp两个寄存器。
由于要为下一次ring1->ring0做准备,所以用iretd返回之前要保证tss.esp0是正确的。
restart: mov esp, [p_proc_ready] lldt [esp + P_LDT_SEL] lea eax, [esp + P_STACKTOP] mov dword [tss + TSS3_S_SP0], eaxrestart_reenter: dec dword [k_reenter] pop gs pop fs pop es pop ds popad add esp, 4 iretd
进程表及相关数据结构对应关系:
第一个进程的启动过程:
第2步–丰富中断处理程序
赋值tss.esp0
由ring0到ring1时,推展的切换直接在指令iretd被执行时就完成了,目标代码的cs、eip、ss、esp等都是从堆栈中得到的,这很简单。但ring1到ring0切换时就免不了用到TSS了。
而堆栈的信息也不外乎ss和esp两个寄存器。
由于要为下一次ring1->ring0做准备,所以用iretd返回之前要保证tss.esp0是正确的。
现在的中断例程:
在中断发生的开始,esp的值是刚刚从TSS里面渠道的进程表A中的regs的最高地址,然后个寄存器的值被压栈入进程表,然后esp指向regs的最低地址处,然后设置tss.esp0的值,准备下一次进程被中断时使用。
内核栈
mov esp, StackTop ; 切到内核栈 ;... mov esp, [p_proc_ready] ; 离开内核栈
中断重入
中断程序是被动的。
为了避免这种嵌套现象的发生,我们必须想一个办法让中断程序知道自己是不是在嵌套执行。
只要设置一个全局变量就可以了。
目前我们的处理,如果发现当前是嵌套的,则直接跳到最后,结束中断处理程序的执行。
多进程
从进程A切换到进程B之前,如何保留和恢复现场(即各寄存器的值)?
后面会提到。
一个进程只要有一个进程体和堆栈就可以运行了。
typedef void (*task_f) ();typedef struct s_task { task_f initial_eip; int stacksize; char name[32];}TASK;TASK task_table[NR_TASKS] = {{TestA, STACK_SIZE_TESTA, "TestA"},{TestB, STACK_SIZE_TESTB, "TestB"}};void TestA(){ //...}void TestB(){ //...}
初始化到proc_table
中,从TASK结构中读取不同任务入口地址、堆栈栈顶和进程名,然后赋值给相应的进程表项。
for(i=0;i<NR_TASKS;i++){ strcpy(p_proc->p_name, p_task->name); // name of the process p_proc->pid = i; // pid p_proc->ldt_sel = selector_ldt; memcpy(&p_proc->ldts[0], &gdt[SELECTOR_KERNEL_CS >> 3], sizeof(DESCRIPTOR)); p_proc->ldts[0].attr1 = DA_C | PRIVILEGE_TASK << 5; memcpy(&p_proc->ldts[1], &gdt[SELECTOR_KERNEL_DS >> 3], sizeof(DESCRIPTOR)); p_proc->ldts[1].attr1 = DA_DRW | PRIVILEGE_TASK << 5; p_proc->regs.cs = ((8 * 0) & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK; //... p_proc->regs.eip = (u32)p_task->initial_eip; p_proc->regs.esp = (u32)p_task_stack; p_task_stack -= p_task->stacksize; p_proc++; p_task++; selector_ldt += 1 << 3; }
LDT
填充 GDT 中进程的 LDT 的描述符。
int i; PROCESS* p_proc = proc_table; u16 selector_ldt = INDEX_LDT_FIRST << 3; for(i=0;i<NR_TASKS;i++){ init_descriptor(&gdt[selector_ldt>>3], vir2phys(seg2phys(SELECTOR_KERNEL_DS), proc_table[i].ldts), LDT_SIZE * sizeof(DESCRIPTOR) - 1, DA_LDT); p_proc++; selector_ldt += 1 << 3; }
每个进程都会在GDT中对应一个LDT描述符。
每个进程都有自己的LDT.所以当进程切换时需要重新加载ldtr
多进程的实现–交替执行A和B进程
一个进程如何由“睡眠”态变成“运行”态?
无非是将esp指向进程表项的开始处,然后在执行lldt
之后经历一系列pop
指令恢复各个寄存器的值。一切的信息都包含在进程表中。所以,要想恢复不同的进程,只需要将esp指向不同的进程表就可以了。
在离开内核栈时给esp赋值。
;... mov esp, StackTop ; 切到内核栈 ;... call clock_handler ;... mov esp, [p_proc_ready] ; 离开内核栈 lldt [esp + P_LDT_SEL] lea eax, [esp + P_STACKTOP] mov dword [tss + TSS3_S_SP0], eax ;...
一个进程如何由“运行”态变成“睡眠”态?
当CPU不再执行该进程的代码指令时,就可以认为这个进程已经睡眠了。
那么这个寄存器的值是怎么保存的呢?肯定是保存在该进程的进程表里的,因为由“睡眠”态变成“运行”态就是从这里获取的信息。
保护的时机就在进程调度切换之前。
我们在时钟中断切换进程时这样写:
hwint00: ; Interrupt routine for irq 0 (the clock). sub esp, 4 pushad ; `. push ds ; | push es ; | 保存原寄存器值 push fs ; | push gs ; / mov dx, ss mov ds, dx mov es, dx inc byte [gs:0] ; 改变屏幕第 0 行, 第 0 列的字符 ;... mov esp, StackTop ; 切到内核栈 ;... call clock_handler ;... mov esp, [p_proc_ready] ; 离开内核栈 lldt [esp + P_LDT_SEL] lea eax, [esp + P_STACKTOP] mov dword [tss + TSS3_S_SP0], eax ;... pop gs ; `. pop fs ; | pop es ; | 恢复原寄存器值 pop ds ; | popad ; / add esp, 4 iretd
简单来说,在调用clock_handler
之前,
我们保存的是进程A的寄存器到esp所指向的堆栈,也就是进程表A(从ring1跳到ring0,esp的值变成TSS中夜色少的ring0下的esp值)。
之后esp被切换成进程B的堆栈,所以pop
出来的就是保存在进程表B里的寄存器值了。
系统调用
用户进程因为特权级的关系,无法访问某些权限更高的内存区域,
只能通过系统调用来实现,它是应用程序和操作系统之间的桥梁。
用中断可以方便地实现系统调用。
实现一个简单的系统调用
操作系统给应用程序提供一个get_ticks()
的系统调用,用来获得当前总共发生了多少次时钟中断。
系统调用的过程:
- 1、“问”,告诉操作系统自己要什么;
- 2、操作系统“找”,即处理;
- 3、“回答”,也就是把结果返回给进程。
;syscall.asm_NR_get_ticks equ 0 ; 要跟 global.c 中 sys_call_table 的定义相对应!INT_VECTOR_SYS_CALL equ 0x90global get_ticks ; 导出符号bits 32[section .text]get_ticks: mov eax, _NR_get_ticks int INT_VECTOR_SYS_CALL ret
sys_call_table
是一个函数指针数组,每一个成员都指向一个函数,用以处理相应的系统调用。注意:sys_call
是内核调用的,如果要把返回值告诉应用进程的话,需要把函数的返回值放在进程表eax的位置,以便进程P被恢复执行时eax中装的是正确的返回值。
;kernel.asmsys_call: call save sti call [sys_call_table + eax * 4] mov [esi + EAXREG - P_STACKBASE], eax ; 把函数的返回值放在进程表eax的位置,以便进程P被恢复执行时eax中装的是正确的返回值。 cli ret
进程调度
进程优先级调度
在中断发生时,我们要优先级选择下一个要执行的进程时。
PUBLIC void schedule(){ PROCESS* p; int greatest_ticks = 0; while (!greatest_ticks) { for (p = proc_table; p < proc_table+NR_TASKS; p++) { if (p->ticks > greatest_ticks) { greatest_ticks = p->ticks; p_proc_ready = p; } } if (!greatest_ticks) { for (p = proc_table; p < proc_table+NR_TASKS; p++) { p->ticks = p->priority; } } }}
- 《一个操作系统的实现》笔记(6)--进程
- 一个操作系统的实现(8):进程
- 《一个操作系统的实现》学习笔记6
- 一个操作系统的实现笔记
- 一个操作系统的实现(10):进程间通信
- 一个操作系统的实现——进程
- 《Orange'S:一个操作系统的实现》学习笔记(一)
- 《Orange'S:一个操作系统的实现》学习笔记(二)
- 《Orange'S:一个操作系统的实现》学习笔记(四)
- 《Orange'S:一个操作系统的实现》学习笔记(四)
- 《Orange'S:一个操作系统的实现》学习笔记(1)
- 《一个操作系统的实现》笔记(2)--保护模式
- 《一个操作系统的实现》笔记(4)-- Boot&Loader
- 《一个操作系统的实现》笔记(5)--内核雏形
- 学习笔记:一个操作系统的实现--前言
- 《orange'S一个操作系统的实现》 笔记
- 《oranges:一个操作系统的实现》阅读笔记
- 《一个操作系统的实现》学习笔记
- Python3+phantomjs+selenium配置
- 对于超前,滞后,超前滞后使用范围
- 自学linux笔记
- Spring 4.x 配置类学习笔记
- SpringBoot创建一个最基本的项目
- 《一个操作系统的实现》笔记(6)--进程
- 关于求最大公约数经典算法---辗转相除法的思考
- 9月1-10月7日阶段小结
- 第三章运算符,表达式十个问题
- python 中的counter 用法
- 三、ESP8266之 TCP服务器(基于LUA开发)
- 《一个操作系统的实现》笔记(7)--输入/输出系统(I/O)
- 零基础入门深度学习(7)
- 获取ubuntu键盘按键记录