分类:
2010-06-28 14:03:14
【进程续】 一、进程的启动: 现在我们已经熟悉了启动一个进程的需求和主要步骤,它的动作也就是: ×准备IDT、TSS、LDT(该LDT描述符在GDT中。但是LDT局部的描述符在进程表的ldts[LDT_SIZE]字段中.) 初始化IDT,也就是定义好GATE描述符。将其0门描述符对应IRQ0(VECTOR),也就是按386模式规定的中断异常处理机制。接着自己定义0x20向量对应8259A的主片IRQ0,0x28对应8259A的从片IRQ8.这需要自己对8259A进行初始化。写入ICW1-ICW4,并且设置可屏蔽中断寄存器OCW1,OCW2(是用作EOI表示中断处理完毕)。 初始化TSS比较简单,直接用0填充这个全局TSS结构体。然后设置tss.iobase=sizeof (tss)表示TSS没有IO许可位图,tss.ss0=SELECTOR_KERNEL_DS(因为堆栈也属性数据段).这是Ring0用的ss选择子。在时钟中断发生后就会用这个。 初始化LDT,首先要初始在GDT当中的LDT描述符。因为进程的LDT描述符是归GDT管理的。它描述着LDT局部描述符的段基地址和段界限。并且属性要加上DA_LDT。至于LDT内部的描述符就等在进程表中实初始化。因为它是属于进程表的一部分。 ×准备进程表 当IDT、TSS、以及LDT都准备好的时候,那么就来初始化进程表: 首先初始化进程的ldt_sel=INDEX_LDT_FIRST,它就是LDT在GDT中的选择子。当实现进程的局部代码与数据访问时候需要用到这个选择子。那么接着就来初始LDT内部的段,这里属于进程的句柄段就定义了两个:一个代码段、一个数据段、而且初始化方法是直接从GDT中复制了代码段、数据段描述符到&ldts[0],这就代表了LDT内部的代码段与数据段也是平坦的0-4GB模式。不过这里还要改一下属性,给代码段加上DA_C | DA_PRIVILEGE_TASK << 5,因为先前的代码段DPL是0。而这是LDT的代码段是属于进程的当然要比内核DPL要低一点了。 同理数据段也修改为DA_PRIVILEGE_TASK << 5. 上面是初始化了LDT内部描述符段。也就进程专用的代码、以及数据段描述符。那么如果想要使用上述的描述符当然少不了要将CPU段选择子对上LDT内部的描述符了。 接下来在进程表里初始化regs的段寄存器字段, (0*8表示的就是代码段在LDT中的位置,因为LDT不像GDT是以空descriptor开始的)。 //这个SA_RPL_MASK、SA_TI_MASK是掩码。其作用是将选择的子RPL、TI位清除掉。其他位当然全是1. regs.cs = ((8 * 0) & SA_RPL_MASK & SA_TI_MASK) |SA_TIL | RPL_TASK //以下也好理解,就是LDT的数据段,ds、es、fs、ss都是用这个平坦的数据段来访问数据。这个也不足为其了吧。那么进程的整个寻址方式除了gs访问显示器是用GDT以外,其他的可就都是LDT模式的了,进程的DPL,RPL都是TASK。要说明的是只有当执行了lldt ldt_sel指令后进程才会是知道LDT的具体位置。这个指令会在初始化进程表后执行的。 regs.ds = ((8 * 1) & SA_RPL_MASK & SA_TI_MASK) |SA_TIL | RPL_TASK regs.es = ((8 * 1) & SA_RPL_MASK & SA_TI_MASK) |SA_TIL | RPL_TASK regs.fs = ((8 * 1) & SA_RPL_MASK & SA_TI_MASK) |SA_TIL | RPL_TASK regs.ss = ((8 * 1) & SA_RPL_MASK & SA_TI_MASK) |SA_TIL | RPL_TASK regs.gs = SELECTOR_KERNEL_GS & SA_RPL_MASK | RPL_TASK //到这里已经把进程的选择子全部搞定了.那么你一定想到了,剩下的就是运行时需要的偏移寄存器了. regs.eip = TestA //这只是一个偏移值。它的CS段选择子对应的描述符有基址;需要注意的是现在CS是LDT代码段。虽然访问时候地址跟内核访问地址是相等的,但是它的DPL、CPL都变成TASK级别的了。 regs.esp = STACKTOP + STACK_SIZE_TOTAL //这个也是LDT的esp访问时对应regs.ss regs.eflags = 0x1202 //进程表中eflags的值是:IOPL=MASK(这样进程才能正常使用I/O指令).IF=1(开启时钟中断), p_proc_ready = proc_table //proc_table是整个进程表的数组的基址,那么上面的赋值就是第一个进程表的字段,现在将基址给了p_proc_ready. p_proc_ready是用来在中断时给esp赋值用的。那么它就是代表当前需要激活的进程的进程表栈尾。 ×启动第一个进程 restart() ; //这个函数就是启动第一个进程的关键。它就是将上面的进程表数据全部写入到CPU。要知道这个时候CPU还是运行在Ring0内核模式的,。因为现在正在启动第一个进程。那么代表处于是Ring0内核代码.经过restart()的最后一条 iretd后。那么就进入了进程任务模式了。这个时候会随着时钟中断的来临 压栈ss ...eip寄存器到Tss 的esp0中.那么我们在restart()函数里就把第一个进程表的regs栈顶付給了Tss 。mov esp,p_proc_ready ;第一个进程表的栈尾。 mov eax,[esp + P_STACK_TOP] mov dword [tss+ TSS3_S_ESP0],eax 那么现在从进程(Ring1)到时钟中断(Ring0),它就可以找到对应的esp0堆栈。并且保存ss...eip到进程表.再从iretd时钟中断返回时,又可以从当前的esp值逆操作回去?OK这是一个进程。其实没多少改变只是进程在运行时,总是有时钟中断来参与。 二、时钟中断处理 进程的重点就在时钟中断,下面就来细细的了解时钟中断: 在上一个程序时钟中断代码处理是: hwint00: iretd; 是这么的直接、武断。 可以看到时钟中断的代码就是直接iretd、那么此时已经经过了Ring1->Ring0,esp当然就是Tss 中指定的esp0了,中断发生时push ss...eip到esp、而现在又直接iretd。也没有发送0x20到8259A的0x20或者0xA0端口结束中断处理让8259A继续接收中断。那么现在的8259A系列的硬件中断就不会再产生了。也就是进程只被打扰了一次又继续执行死循环了。 现在我们就来改善一下时钟中断的处理,让时钟中断一直参与进程的运行。 hwint00: inc byte[gs:0] ;在显示器第0行0列的缓冲区ascii码++ mov al,0x20 out INT_M_CTL,al ; 告之8259A中断处理完毕,这个时候IF=1就可以继续接收8259A中断了. iretd 将中断改成这样后,当那么时钟中断就会参与到进程的运行了。当进程运行到某个时刻时钟中断发生!程序跳到hwint00 然后在第0行0列缓冲区++(ascii),接着告之8259A中断处理完毕,iretd返回到进程。那么这个时候又会发生时钟中断了。进程简单的实现了按时钟中断间隔无限的切换着自己的进程表。 可是到这里大家有没有发现我们改变了al的值,在中断处理代码里面我们还没有保存所有常用寄存器。只是保存了iretd要用到的寄存器。那么这样当然不完善了,毕竟我们还需要在内核进行调度实现多进程!下面就再改下代码: hwint00: inc BYTE [gs:0] push colck_krn_msg ;注意此时的esp已经是StackTop 内核专业的,这下可以放心的使用了 call disp_str add esp,4 ;-------------恢复到进程表栈-,准备转向下一个进程,初始化tss esp0----- ×中断重入问题 也就是中断嵌套的处理,这里处理起来比较简单: 先看时钟重入的问题:{ sti ;sti和cli包起来的代码就是时钟中断的嵌套,当sti (IF=1)那么时钟中断被打开,这个时候内核调度的代码在执行时有可能就会被打断. call delay ;如果这个时候调用一个延迟的函数,第二次时钟中断发生了,就会循环到sti,那这个时候,就会发生时钟中断的死循环、为了解决这个问题。我们设计一个全局变量来标志时钟中断是否嵌套. cli } 解决:{ inc dword [k_reenter] ;它的初始值是-1,当第一次发生中断后,就变成了0 cmp dword [k_reenter],0 jne .re_enter ;如果不是0表示那么就证明第一次中断还没完成又出现中断了。重入发现! mov esp,StackTop ;内核堆栈 sti cli .re_enter: dec dword [k_reenter] ;时钟中断嵌套后直接返回到时钟处理的代码。在时钟中断嵌套的时候使用的是内核堆栈,这个已经设计好了。当第一次中断时,sti前面就已经将esp 指向了内核栈。 ...... iretd } 三、添加一个进程: 第一个进程需要的是:进程表、进程体、GDT(LDT)、TSS,那么就可以: 定义一个给进程任务结构体(方便初始化进程表): typedef struct s_stack{ t_pf_task initial_eip; //typedef void(*t_pf_task)();如果想给initial_eip赋值,那么必需函数要是t_pf_task类型, int stacksize; //LDT里的堆栈大小 char name[32]; //进程的名字 }TASK; 初始化这个这个任务表: EXTERN TASK task_table[NR_TASKS] ={ TestA,STACK_SIZE_TESTA ,"TestA,\ TestB,STACK_SIZE_TESTB ,"TestB",\ };这里定义了两个任务体,其实也就是两个进程的函数入口与堆栈代码加进程名。 那么可以用这个结构体初始化进程表; p_proc=proc_table; p_task = task_table; //一个任务结构体对应一个进程表 char* p_task_stack=task_stack + STACK_SIZE_TOTAL; 堆栈自顶向下 t_16 selector_ldt = SELECTOR_LDT_FISR; for(int i=0;i { p_proc->ldt_sel = selector_ldt; .............. p_proc->regs.esp = p_task_stack; p_task_stack-= p_task->stacksize ;调整进程的堆栈。 selector_ldt += 1 << 3; p_proc++; p_task++; }那么进过了NR_TASKS次循环后。就把所有进程都给初始化了;这个时候再启动第一个进程: p_proc_raedy = proc_table; mov dword [k_reenter],-1 restart(); ;在时钟中断发生时需要调整p_proc_ready让它指向下一个需要运行的进程的进程表栈顶。因为进程的切换是随着时钟中断发生的、。 p_proc_ready++; if(p_proc_ready >= proc_table + NR_TASKS){ //如果已经是最后一个进程在运行了,那么让p_proc_ready指向第一; p_proc_ready =proc_table ;//这样就彻底解决了 进程的切换问题了; }借用前辈一句话:一个进程到两个进程就是质的飞跃,两个到三个是量的增加而以!!!. 四、认识8253可编程控制器PIT(programmable interval timer): ×IIntel8253时钟中断: 8253芯片有3个计数器(Counter).他们都是16位的.各有不同的作用。时钟中断是由Counter0产生的! 计数器的原理是这样的:它有一个输入率,在PC上是1193180Hz(1193180次/秒),在每一个时钟周期(CLK cycle),计数器值会减1.,当减到0时就会触发一个输出。由于计数器是16位最大值是65535,因此默认的时钟中断的发生频率是1193180 /65535==18.2Hz.(18.2次/s)。,值越小时间就越快,也就是说最小精确到1/1193180秒每次。(1193180次/秒)。如果想每10ms产生一次中断那么用这个公式n/1193180=10/1000(n就是次数).n=1193180*1/100;=11931次.。 我们可以用8253可编程控制来控制Counter0.需要操作的端口就是0x40,在操作Counter0之前需要先通过0x43端口写8253的模式控制寄存器。 out 0x43,0x34 ; //设置8253模式 out 0x40,(t_8)1193180/100; //将Counter0赋值 分别是低字节和高字节 out 0x40,(t_8)(1193180/100 >> 8);//当这里后时钟中断就是10ms一次了。 【总结】 ×时钟中断与进程: 经过不段改善设计成一套比较泛型的进程调度: 进程调度主要就是在时钟中断这块。就从启动第一个进程的时候开始讲: 以下的代码片段都是Kernel.asm里面的: hwint00: ;时钟中断发生的时候通过IDT GATE指定到这 call save ;push eip 非重入的时、 regs.retaddr=++eip 。 ; 这个时候的esp是指向内核栈的。并且[esp]是 (save) push的地址。 in al,INT_M_CTLMASK or al,1 out INT_M_CTLMASK,al ;这3条指令是将时钟中断屏蔽掉,不再接收时钟中断. mov al,EOI out INT_M_CTL,al ;让8259A继续接收其他没有屏蔽的中断,当IF=1时! sti push 0 call clock_handler ;这里进行调度。将p_proc_ready指向下一个要运行的进程. add esp,4 cli ; 把屏蔽的IRQ开打,已经处理完毕就不会重复了 in al,INT_M_CTLMASK and al,~0x01 out INT_M_CTLMASK,al ;这3条指令是将时钟中断屏蔽掉,不再接收时钟中断. ret ;返回到相应的函数进行切换进程或者原位返回/ save: 这个函数主要保存寄存器,判断中断重入问题 pushad push ds push es push fs push gs mov eax,esp ;临时保存进程表的开始位置。 inc dword[k_reenter] jnz .1 ;如果不等于0表示已经出现中断重入现象 mov esp,StackTop ;调度时要转换到内核栈 push restart jmp [eax + RETADR - P_STACKBASE] ;这个正是字段regs.retaddr .1: push restart_reenter ;将重入代码基址压到内核栈 jmp [eax + RETADR - P_STACKBASE] ;这个正是内核的regs.retaddr 偏移位置,并不是进程表的字段,不过这样也在内核栈模拟了进程表的内容。效果一样都是跳回call 下一条指令mov dword restart: ;非重入的时候,从这里一直往下走.,进行切换进程 mov esp,[p_proc_ready] ;指向下一个启动的进程表开始字段 lldt [esp + P_LDT_SEL] ;从进程表中获取LDT选择子 lea eax,[esp + P_STACKTOP] ;得到进程表的栈顶,指向regs.ss字段 mov [tss + TSS3_S_SP0],eax ;当中断再次发生时,就会将寄存器环境保存到esp0. restart_reenter: ;中断重入的时候,使用的是内核栈,那么现在就返回去。 dec dword [k_reenter] ;此时IF=1后就可以正常接收当前向量号的中断了。这样避免了重复的中断 pop ds pop es pop fs pop gs add esp,4 iretd ;切换到进程。 进程的切换方式也就是(都是在中断下进行的): 保存寄存器信息到进程表或者内核栈(中断重入)-->屏蔽当前处理的中断IRQ-->(sti)接收8259A其他的IRQ-->内核调度-->关闭中断(cli)-->切换进程或者返回继续指向中断(重入时,)这可以看做是一个循环!!! |