第一次作业:基于Orange's OS系统的进程模型分析与心得体会

Posted 夏栀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第一次作业:基于Orange's OS系统的进程模型分析与心得体会相关的知识,希望对你有一定的参考价值。

1一. 操作系统进程概念模型与进程控制块概念浅析

1. 什么是进程?

  

      图 1 - 1 (WIN10系统任务管理器对进程管理的图形化界面)

      计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

                                          ——百度百科

  应用程序的实例。对正在运行的程序的抽象。 

                                          ——《现代操作系统》

2. 什么是进程控制块?

  进程控制块(Processing Control Block),是操作系统核心中一种数据结构,主要表示进程状态。其作用是使一个在多道程序环境下不能独立运行的程序(含数据),成为一个能独立运行的基本单位或与其它进程并发执行的进程。

                                          ——百度百科

  为了实现进程模型,操作系统维护着一张表格(一个结构数组),即进程表(也称进程控制块)。

                                          ——《现代操作系统》

3. 什么是进程模型?

  在进程模型中,计算机上所有可运行的软件,通常也包括操作系统,被组织成若干顺序进程(sequential process),简称进程(process)。

                                          ——百度百科,《现代操作系统》

  《现代操作系统》补充内容:多道程序设计的设计对进程快速切换,这个过程通过进程模型来实现。

 

4. 我的理解

  1. 进程-CPU ——>伪并行概念 && 多处理器(程序的切换、多道程序系统)
  2. 进程模型
       进程运行在CPU上。CPU在各个进程之间来回切换。(进程的调度:多道程序设计与进程状态的保存)      
       代码+数据+堆栈表示一个进程 (其他细节暂时不考虑) + 进程调度
  3. CPU被若干进程共享(进程通信、进程切换与优先级比较)
  4. PCB可以理解为一个存储进程的所有信息的数据体集合。

运行的进程及一个软件双击打开后,这个程序在操作系统中的具体存在形式。程序是抽象的,进程是具体的。那么进程表也好理解了,管理多个PCB的表,能够实现多个进程在操作系统中的运行的管理与维护。(先不考虑实现方式)

二. Orange OS中进程的实现方式(ring的切换与进程表结构)

进程堆栈信息应该独立成块,防止进程表指针对进程数据/代码读取时破坏其他信息。   
esp的位置出现在3个不同的区域: 
- 进程栈–进程运行时自身的堆栈  
- 进程表–存储进程状态信息的数据结构  
- 内核栈–进程调度模块运行时使用的堆栈   

通过时钟中断来实现对进程的启动。

把大象放在冰箱需要三步,在orange os中,进程的创立也类似于此。其中esp指针的跳转实现了对进程表的管理。

1. 模拟时间中断的实现

1 ALIGN   16
2     hwint00:    ;   /*时间中断时,中断进程*/
3         iretd       /*中断返回*/

 

很简单的汇编代码。模拟时间中断时,通过iretd指令来跳转程序运行的指令。

2. 初始化工作的准备

           

                 图 2 - 1

一个进程刚开始时,指定各个寄存器,就可以直接运行。所以要先完成对寄存器的初始化。

同时,你必须对进程表这个数据结构进行定义。这个是以后要运行的进程的整体框架。

1) 进程结构体的定义

1 typedef struct s_proc{
2         STACK_FRAM regs;/*寄存器*/
3         u16 ldt_sel;/*LDT选择子*/
4         DESCRIPTOR  ldts[LDT_SIZE];/*LDTS*/
5         u32 pid;    /*进程id*/
6         char p_name[16];/*进程名*/
7     }PROCESS;

上面这个结构体中有对寄存器信息进行保存,这个对于以后进程的管理是相当有用的。

 1 /*准备进程体TestA()*/    
 2 /*充当一个进程执行体的功能函数*/
 3     void TestA(){
 4         int i = 0;
 5         while(1){
 6             disp_str("A");
 7             disp_int(i++);
 8             disp_str(".");
 9             delay(1);/*需要再delay中编写简单的运行函数*/
10         }
11     }

 

2) 进程表的定义与初始化

 1     PROCESS* p_proc=proc_table;
 2 
 3     p_proc--->ldt_sel=SELECTOR_LDT_FIRST;                                              
 4 
 5     //设置进程表中进程的ldt_sel,ldt_sel被赋值SELECTOR_LDT_FIRST,这个宏的定义在代码6-1-中
 6 
 7     memcpy(&p_proc->ldts[0], &gdt[SELECTOR_KERNEL_CS >> 3], sizeof(DESCRIPTOR));  //将SELECTOR_KERNEL_CS所指的描述符拷贝到进程PCB的ldts[0]处
 8     p_proc->ldts[0].attr1 = DA_C | PRIVILEGE_TASK << 5;                              // change the DPL
 9     memcpy(&p_proc->ldts[1], &gdt[SELECTOR_KERNEL_DS >> 3], sizeof(DESCRIPTOR)); //将SELECTOR_KERNEL_DS所指的描述符拷贝到进程PCB的ldts[1]处
10     p_proc->ldts[1].attr1 = DA_DRW | PRIVILEGE_TASK << 5;                          // change the DPL
11 
12     //LDT中共有两个描述符,分别被初始化为内核代码段和内核数据段,只是改变了一下DPL,以让其运行在低特权级下(2)
13     p_proc->regs.cs  = ((8 * 0) & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK;    //8*0和8*1是选择子,而且是进程PCB中的LDT描述符的选择子。
14 
15     //cs指向LDT中的第一个描述符 (3) 通过cs中的这个描述符跳转到内核代码段                              //LDT选择子是从0开始的。一个描述符相隔8个字节,所以要乘以8
16     p_proc->regs.ds  = ((8 * 1) & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK;    //根据选择子的属性确定是全局选择子还是LDT选择子。 
17     p_proc->regs.es  = ((8 * 1) & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK;    //本例会根据cs中的选择子自动到进程PCB中读取LDT描述符
18     p_proc->regs.fs  = ((8 * 1) & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK;    //本例比较特殊的地方是进程LDT描述符在进程PCB中,没有固定的LDT段
19     p_proc->regs.ss  = ((8 * 1) & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK;
20 
21     //ds,es,fs,ss指向LDT中的第二个描述符
22     p_proc->regs.gs  = (SELECTOR_KERNEL_GS & SA_RPL_MASK) | RPL_TASK;
23 
24     //gs指向显存,只是其RPL发生改变
25     p_proc->regs.eip = (u32)TestA;
26 
27     //eip指向TestA,这表明进程将从TestA的入口地址开始运行
28     p_proc->regs.esp = (u32) task_stack + STACK_SIZE_TOTAL;
29 
30     //esp指向单独的堆栈,堆栈大小为STACK_SIZE_TOTAL
31     p_proc->regs.eflags = 0x1202; // IF=1, IOPL=1, bit 2 is always 1.
32 
33     //eflags=0x1202,恰巧设置了IF位,并把IOPL设为1,这样,进程就可以使用I/O指令,并且中断会在iretd执行时,被打开。
34 
35     //代码中使用到的宏,基本在protect.h中
36 
37     p_proc_ready = proc_table; 

 

内容很多吧?你只要知道其中完成的工作,1. 寄存器 2. LDTSelector 3. LDT。配置之后,把我们刚才准备的TestA放入运行。

 

其次,必须了解进程表的定义。

 1 //进程表数据结构
 2     typedef struct s_stackframe {   /* proc_ptr points here             ↑ Low           */
 3     u32 gs;     /* ┓                        │           */
 4     u32 fs;     /* ┃                        │           */
 5     u32 es;     /* ┃                        │           */
 6     u32 ds;     /* ┃                        │           */
 7     u32 edi;        /* ┃                        │           */
 8     u32 esi;        /* ┣ pushed by save()               │           */
 9     u32 ebp;        /* ┃                        │           */
10     u32 kernel_esp; /* <- \'popad\' will ignore it            │           */
11     u32 ebx;        /* ┃                        ↑栈从高地址往低地址增长*/      
12     u32 edx;        /* ┃                        │           */
13     u32 ecx;        /* ┃                        │           */
14     u32 eax;        /* ┛                        │           */
15     u32 retaddr;    /* return address for assembly code save()  │           */
16     u32 eip;        /*  ┓                       │           */
17     u32 cs;     /*  ┃                       │           */
18     u32 eflags;     /*  ┣ these are pushed by CPU during interrupt  │           */
19     u32 esp;        /*  ┃                       │           */
20     u32 ss;     /*  ┛                       ┷High           */
21     }STACK_FRAME;
22 
23 
24     typedef struct s_proc {
25     STACK_FRAME regs;          /* process registers saved in stack frame */
26 
27     u16 ldt_sel;               /* gdt selector giving ldt base and limit */
28     DESCRIPTOR ldts[LDT_SIZE]; /* local descriptors for code and data */
29 
30     int ticks;                 /* remained ticks */
31     int priority;
32 
33     u32 pid;                   /* process id passed in from MM */
34     char p_name[16];           /* name of the process */
35     }PROCESS;

 

注意,进程结构和进程表结构完全是不同的概念。进程表用于管理进程。

恢复一个进程时,便将esp指向这个结构体的开始处,然后运行一系列的pop命令,读取寄存器的值。

3.准备GDT和TSS 

 1  restart:
 2         /*设置esp(堆栈指针)的值*/
 3         /*p_proc_ready是一个指向进程表的指针*/
 4         /*p_proc_ready内有s_proc,用于存放进程状态信息*/
 5         mov     esp,[p_proc_ready]  
 6         
 7         lldt    [esp + P_LDT_SEL]
 8         lea     eax,[esp + P_STACKTOP]
 9         mov     dword [tss + TSS3_S_SP0] , eax
10     restart_reenter:
11         dec     dword [k_reenter]
12         pop     gs
13         pop     fs
14         pop     es
15         pop     ds
16         poped
17         add     esp,4
18         iretd

a) 表示把堆栈指针和进程表指针这两者结合,意味着你可以把TestA在进程表中存在这个项目,便于运行。

b) 初始化GDT中的TSS和LDT , 初始化TSS。

  在init_prot中完成

c) 完成跳转,实现ring0->ring1  

  这个过程中需要为下一次ring1->ring0准备,用iretd返回之前要保证tss.esp()指向的地址正确。

 

总结

                

                            图 2 - 2

三. 进程的调度与状态转换

                    

                            图 3 - 1

1. 睡眠态 —— 运行态

所谓进程间切换最主要的就是,进程A运行时,另一个进程B想要占用此时的CPU,那就必须将进程A中止,并将其运行信息保存,这些信息保存到CPU寄存器中(能够恢复) , 然后把CPU的控制权交给进程B,并且通过CPU寄存器读取存放的进程B的信息。这个过程通过进程调度来实现。

esp指针可以决定将进程表中的某个进程占据cpu。此时这个进程从睡眠态->运行态。  

所以,动态把进程从睡眠态变为运行态,只需要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
    ;...

2. 运行态 —— 睡眠态

当让一个进程变为运行态时,原先运行的进程需要动态改变,让其睡眠。需要完成对这个进程的信息的保存。(保护机制的使用)  


所以,当时钟中断来临时,切换进程需要保存信息到寄存器中。

 1 hwint00:        ; Interrupt routine for irq 0 (the clock).
 2     sub esp, 4
 3     pushad      ; `.
 4     push    ds  ;  |
 5     push    es  ;  | 保存原寄存器值
 6     push    fs  ;  |
 7     push    gs  ; /
 8     mov dx, ss
 9     mov ds, dx
10     mov es, dx
11 
12     inc byte [gs:0]     ; 改变屏幕第 0 行, 第 0 列的字符
13     ;...
14     mov esp, StackTop       ; 切到内核栈
15     ;...
16     call    clock_handler
17     ;...
18 
19     mov esp, [p_proc_ready] ; 离开内核栈
20     lldt    [esp + P_LDT_SEL]
21     lea eax, [esp + P_STACKTOP]
22     mov dword [tss + TSS3_S_SP0], eax
23     ;...
24     pop gs  ; `.
25     pop fs  ;  |
26     pop es  ;  | 恢复原寄存器值
27     pop ds  ;  |
28     popad       ; /
29     add esp, 4
30 
31     iretd

 

这个时候时在cloc_handler之前,时钟调用之前,即进程切换之前,完成对运行->睡眠的这个进程的信息的保存。包括状态的保存,放到寄存器中。

 

四. 心得

从程序到进程,一个是抽象的,一个是具体的。

期间对于这些应用程序在操作系统中的运行与维护通过对进程的管理来实现。例如怎么停止一个进程的运行,怎么启动一个休眠态的进程。这些都是进程的状态的互相转换及调度来实现的。

进程控制块是进程表中一个相当重要的概念,只有有进程控制块,才能完成对运行的进程的管理。所谓的进程表即是多个进程的集合。

书上介绍的调度算法有多种,orange\'s os 用了最简单的表的管理的思想,称不上是高效,但是是最基本的进程管理与控制的思想。

不过在分析进程模型中,orange os有提到了ring的概念,操作系统内核,有点理解不能。

 

五. 参考资料

1. https://baike.baidu.com/item/%E8%BF%9B%E7%A8%8B/382503?fr=aladdin 百度百科进程词条

2. https://blog.csdn.net/begginghard/article/details/7371754

以上是关于第一次作业:基于Orange's OS系统的进程模型分析与心得体会的主要内容,如果未能解决你的问题,请参考以下文章

第一次作业:基于Linux系统深入源码分析进程模型

Orange Pi PC 2 远程桌面的坑

登录验证实现【学生作业管理系统】

2021年春季学期-信号与系统-第十一次作业参考答案-第三小题

第一次作业:基于Linux操作系统深入源码进程模型分析

第一次作业 基于Linux 0.12的进程模型分析