• 操作系统开发系列—13.a.进程 ●


    进程的切换及调度等内容是和保护模式的相关技术紧密相连的,这些代码量可能并不多,但却至关重要。

    我们需要一个数据结构记录一个进程的状态,在进程要被挂起的时候,进程信息就被写入这个数据结构,等到进程重新启动的时候,这个信息重新被读出来。

    在很多情况下,进程和进程调度是运行在不同的层级上的。这里本着简单的原则,我们让所有任务运行在ring1,而让进程切换运行在ring0.

     诱发进程切换的原因不只一种,比较典型的情况是发生了时钟中断。但并非在每一次时钟中断时都一定会发生进程切换,不过这里为了容易理解和实现,每次中断都切换一次进程。

    下面介绍一下进程切换时的情形,如下图所示:

    1.进程A运行中。

    2.时钟中断发生,ring1—>ring0,时钟中断处理程序启动。

    3.进程调度,下一个应运行的进程(假设为进程B)被指定。

    4.进程B被恢复,ring0—>ring1.

    5.进程B运行中。

    只有可能被改变的才有保存的必要。所以我们要把寄存器的值统统保存起来,准备进程被恢复执行时使用。

    一条pushad指令可以保存许多寄存器值。

    进程栈和内核栈如下图:

    进程栈——进程运行时自身的堆栈。

    进程表——存储进程状态信息的数据结构。

    内核栈——进程调度模块运行时使用的堆栈。

    对于有特权级变换的转移,如果由外层向内层转移时,需要从TSS中取得从当前TSS中取出内层ss和esp作为目标代码的ss和esp。所以我们必须事先准备好TSS。由于每个进程相对独立,我们把涉及到的描述符放在局部描述符表LDT中,所以我们还需要为每个进程准备LDT。

    整个程序的大致流程是:

    	sgdt	[gdt_ptr]	; cstart() 中将会用到 gdt_ptr
    	call	cstart		; 在此函数中改变了gdt_ptr,让它指向新的GDT
    	lgdt	[gdt_ptr]	; 使用新的GDT
    
    	lidt	[idt_ptr]
    
    	jmp	SELECTOR_KERNEL_CS:csinit
    

    首先走kernel.asm的_start——这里调用start.c的cstart函数

    cstart函数——主要是将loader中的GDT复制到新的GDT中

    紧接着调用init_prot()函数——init_prot函数在protect.c中,主要是初始化8259A和全部中断门,此函数的最后是填充GDT中TSS这个描述符,紧接着填充GDT中进程的LDT的描述符。

    此时cstart结束。

    然后执行csinit——先加载ltr(TSS),然后进入kernel_main

    kernel_main函数——是在main.c中定义的,此函数首先初始化进程的进程表的各个属性,进程表的定义在proc.h中,然后最后执行restart函数

    restart函数在kernel.asm中定义,如下:

    restart:
    	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

    restart是进程调度的一部分,同时也是我们的操作系统启动第一个进程时的入口。

    首先让esp指向将要运行的进程的进程表,然后加载ldt指向的是进程表的ldt_selector,restart最后两行的作用是将s_proc这个结构中第一个结构体成员regs的末地址赋给TSS中ring0堆栈指针域(esp)。我们可以想象,在下一次中断发生时,esp将变成regs的末地址,然后进程ss和esp两个寄存器值,以及eflags还有cs、eip这几个寄存器值将依次被压栈,放到regs这个结构的最后面(不要忘记堆栈是从高地址向低地址生长的),最后通过iretd指令执行并进入进程TestA。

    对于p_proc_ready,编译器在编译时会产生一个符号表,记录了符号名和它的地址。对于指针变量,符号表里记录的是指针的地址,通过该地址取到所指变量的真实地址,最后取到的才是所指变量的值。

    IRETD 指令先弹出一个32位的EIP值,然后再弹出一个32位值并将最低的2个字节值传入CS寄存器,最后再弹出一个32位的标志寄存器值

    lea——

    比如: LEA AX,BUF
    就是将存储器中BUF所指的地址传送给AX.
    区别MOV传送指令:
    MOV传送的是地址所指的内容,而LEA只是地址。

    还有从低特权级到高特权级转移的时候,需要用到TSS

    p_proc_ready应该是一个指向进程表的指针,存放的便是下一个要启动进程的进程表的地址。而且其中的内容必然是以下图所示的顺序进行存放,这样才会使pop和popad指令执行后各寄存器的内容更新一遍。

    p_proc_ready是一个结构类型指针:struct s_proc*。s_proc这个结构体的第一个成员也是一个结构s_stackframe,它的内容安排与我们的推断完全一致。

    进程的状态统统被存放在s_proc这个结构体中,s_proc这个结构就应该是我们提到过的“进程表”。当要恢复一个进程时,便将esp指向这个结构体的开始处,然后运行一系列的pop命令将寄存器值弹出。进程表的开始位置结构图如下图所示:

    接下来lldt这个指令是设置ldtr的。esp + P_LDT_SEL是s_proc中的成员ldt_sel。restart最后两行的作用是将s_proc这个结构中第一个结构体成员regs的末地址赋给TSS中ring0堆栈指针域(esp)。

     一个进程开始之前,必须初始化的寄存器列表:cs、ds、es、fs、gs、ss、esp、eip、eflags。

    我们在Loader中就把gs对应的描述符DPL设为3,所以进程中的代码是有权限访问显存的。

    在第一个进程正式开始之前,其核心内容便是一个进程表以及与之相关的TSS等内容。如下图所示:

    这个图看起来有点复杂,但是如果将其化整为零,可以分为4个部分,那就是进程表、进程体、GDT和TSS。它们之间的关系大致分为三个部分:

    1.进程表和GDT。进程表内的LDT Selector对应GDT中的一个描述符,而这个描述符所指向的内存空间就存在于进程表内。

    2.进程表和进程。进程表是进程的描述,进程运行过程中如果被中断,各个寄存器的值都会被保存进进程表中。但是在我们的第一个进程开始之前,并不需要初始化太多内容,只需要知道进程的入口地址就足够了。另外由于程序免不了用到堆栈,而堆栈是不受程序本身控制的,所以还需要事先指定esp。

    3.GDT和TSS。GDT中需要有一个描述符来对应TSS,需要事先初始化这个描述符。

    第一步,首先来准备一个小的进程体。

    void TestA()
    {
    	int i = 0;
    	while(1){
    		disp_str("A");
    		disp_int(i++);
    		disp_str(".");
    		delay(1);
    	}
    }
    

    在之前我们调用指令sti打开中断之后就用hlt指令让程序停止以等待中断的发生。但在这里我得把hlt注释掉。还有由于在完成进程的编写之前,要让程序停住,所以我们用一个死循环作为它的结束。

    PUBLIC int kernel_main()
    {
    ...
    	while(1){}
    }
    

    第二步,初始化进程表。

    要初始化进程表,首先要有进程表结构的定义,proc.h的STACK_FRAME。global.c的NR_TASKS定义了最大允许进程,我们把它设为1.初始化进程表的代码在main.c的kernel_main()函数。

    进程表需要初始化的主要有3个部分:寄存器、LDT Selector和LDT。LDT Selector被赋值为SELECTOR_LDT_FIRST,LDT里面共有两个描述符,为简化起见,分别被初始化成内核代码段和内核数据段,只是改变了一下DPL以让其运行在低的特权级下。

    要初始化的寄存器比较多,cs指向LDT中第一个描述符,ds、es、fs、ss都设为指向LDT中的第二个描述符,gs仍然指向显存,只是其RPL发生改变。

    接下来eip指向TestA,这表明进程将从TestA的入口地址开始运行。另外esp指向了单独的栈,栈的大小为STACK_SIZE_TOTAL。

    最后一行是设置eflags,0x1202恰好设置了IF位并把IOPL设为1.这样,进程就可以使用I/O指令,并且中断会在iretd执行时被打开(kernel.asm中的sti指令已经被注释掉了)。

    一定要记得LDT跟GDT是联系在一起的,别忘了填充GDT中进程的LDT的描述符。如下:

    	/* 填充 GDT 中进程的 LDT 的描述符 */
    	init_descriptor(&gdt[INDEX_LDT_FIRST],
    		vir2phys(seg2phys(SELECTOR_KERNEL_DS), proc_table[0].ldts),
    		LDT_SIZE * sizeof(DESCRIPTOR) - 1,
    		DA_LDT);
    

    最后再初始化填充TSS以及对应的描述符:

    	/* 填充 GDT 中 TSS 这个描述符 */
    	memset(&tss, 0, sizeof(tss));
    	tss.ss0 = SELECTOR_KERNEL_DS;
    	init_descriptor(&gdt[INDEX_TSS],
    			vir2phys(seg2phys(SELECTOR_KERNEL_DS), &tss),
    			sizeof(tss) - 1,
    			DA_386TSS);
    	tss.iobase = sizeof(tss); /* 没有I/O许可位图 */
    
    
    ------------加载-----------------
    	xor	eax, eax
    	mov	ax, SELECTOR_TSS
    	ltr	ax
    

    由于进程的各寄存器值如今已经在进程表里面保存好了,现在我们只需要让esp指向栈顶,然后将各个值弹出就行了。最后一句iretd执行以后,eflags会被改变成pProc->regs.eflags的值。我们事先置了IF位,所以进程开始运行之时,中断其实也已经被打开了,这对以后的程序很重要。

    restart:
    	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
    

    启动进程(main.c),restart实现ring0->ring1的跳转:

    	p_proc_ready	= proc_table;
    	restart();
    

    运行如下:

    源码

  • 相关阅读:
    laravel-13-笔记-1
    laravel-14-笔记-2
    supervisor监听器-linux安装配置
    laravel-12-artisan命令创建view文件
    linux修改主机名
    laravel-11-laravel 模型Eloquent ORM
    laravel-composer安装laravel
    laravel-10-laravel collection集合
    laravel-8-laravel数据填充
    laravel-9-laravel数据库查询 查询组件
  • 原文地址:https://www.cnblogs.com/joey-hua/p/5422477.html
Copyright © 2020-2023  润新知