Orange'S:一个操作系统的实现

进程

Posted by BINBIN Blog on November 6, 2019

介绍

进程示意图

进程涉及到进程调度! 书上是说一个进程只干一件事,有很多种进程,也就是CPU 只能运行一个进程,其他进程正在等待!

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

实际上进程和进程调度室运行在不同的层级上。

书上举例都是让任务运行在ring1,进程切换运行在ring0.

典型的就是发生时钟中断,时钟中断发生时,中断处理程序会将控制权交给进程调度模块。这时候系统认为应该进行进程切换,进程切换就发生了!当前的进程的状态就被保存起来,队列中的下一个进程将被恢复执行。同一时刻,只能有一个进程处在运行态!(注意,并非每个时钟中断都一定会发生进程切换)。

作者提到是参考minix系统简化的,基本都是拿来主义的!

有兴趣可以直接看minix!

最简单的进程

情况是 一个进程运行中,发生了时钟中断,特权级从ring1 到 ring0 ,开始执行时钟中断处理程序,中断处理程序这时调用进程调度模块,指定下一个应该运行的进程,当中断处理程序结束时,下一个进程准备就绪并开始运行,特权级从ring0 跳回ring1

  • 进程A 运行
  • 时钟中断发生,ring1-> ring0 ,时钟中断处理程序启动。
  • 进程调度,下一个应运行的进程(假设进程B)被指定。
  • 进程B被恢复,ring0-> ring1 。
  • 进程B 运行中!

根据要求,需要实现的功能如下:

  • 时钟中断处理程序
  • 进程调度模块
  • 两个进程

首先要保存CPU 寄存器的值! 不但有push 也有pushad 一条指令可以保存许多寄存器的值!这些代码放在时钟中断最顶端,以便于中断发生时马上被执行! 自然恢复时pop ,执行iretd就回到进程B了

进程表

PCB 进程表简称,我们会有很多进程

进程表用来描述进程的,必须独立于进程之外的! 当把寄存器值压到进程表内的时候,已经处在进程管理模块之中了!

当寄存器的值已经被保存到进程表内,进程调度就开始执行了,那么esp指向何处呢?

因为进程调度模块会用到堆栈,寄存器的值被压到进程表之后,esp是指向进程表某个位置的。如果接下来的堆栈操作,会破坏进程表的值,从而下一次进程恢复时产生严重错误!

为解决这个问题,书上说是将esp指向专门的内核栈区域。这样,短短的进程切换过程中,esp的位置出现在3个不同的区域!

  • 进程栈
  • 进程表
  • 内核栈

具体编写代码,一定要弄清楚当前使用时哪个堆栈!

特权级变换 ring1-> ring0

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

书上说,开始的第一个进程,使用iretd 来实现ring0 - ring1 的转移,一旦转移成功,就认为在一个进程中运行了!

代码在chapter6/r/kernel/kernel.asm


354	; ====================================================================================
355	;				    restart
356	; ====================================================================================
357	restart:
358		mov	esp, [p_proc_ready]
359		lldt	[esp + P_LDT_SEL]
360		lea	eax, [esp + P_STACKTOP]
361		mov	dword [tss + TSS3_S_SP0], eax
362	restart_reenter:
363		dec	dword [k_reenter]
364		pop	gs
365		pop	fs
366		pop	es
367		pop	ds
368		popad
369		add	esp, 4
370		iretd

进程对我的确是新鲜的事物,本人按照书上,进行复述!

在kernel文件下,有一个main.c,里面有一个函数kernel_main(),函数的后面有一个restart(); 就是上面的那段代码!

它是进程的一部分,也是我们操作系统启动第一个进程时的入口!

分析代码

第358行 358 mov esp, [p_proc_ready] 设置了esp的值,

p_proc_ready 是指向进程表的指针,存放的便是下一个要启动进程表的地址。

其中内容必然是以图所示的顺序进行存放

这样才会使pop和popad指令执行后各寄存器的内容更新一遍!

查看代码可得知,p_proc_ready 在global.h定义,是结构类型指针,struct s_proc*,再打开proc.h,可以看到s_proc这个结构体第一个成员也是一个结构,s_stackframe。

原来的进程状态都被存放在s_proc这个结构体中,而且位于前部的是所有相关寄存器的值,s_proc这个结构应该是我们提到进程表。

当恢复一个进程时,便将esp指向这个结构体的开始处,然后运行一系列的pop命令将寄存器弹出。进程表的开始位置结构图如图所示!

在第359行 ` 359 lldt [esp + P_LDT_SEL] `

lldt是设置ldtr的,esp等同于p_proc_ready,那么esp+ P_LDT_SEL一定是s_proc的一个成员, 对比sconst.inc中P_LDT_SEL和结构体s_proc可知,esp+P_LDT_SEL恰好就是中的成员ldt_sel.

所以在执行restart()之前,做了ldt_sel的初始化工作,以便于lldt可以正确执行!

第360-361行的作用就是把s_proc这个结构中的第一个结构体成员regs的末地址赋给TSS中ring0堆栈指针域(esp)。

在下一次中断发生时,esp将变成regs的末地址,然后进程ss和esp两个寄存器的值,以及eflags,还有cs eip 这几个寄存器值将以此被压栈,放在regs这个结构体最后面(记住,堆栈是从高地址向低地址方向增长的哦)

s_stackframe这个结构定义时候,发现最末端的成员果然便是这5个,这恰好验证了我们的想法!

后面的代码 363 dec dword [k_reenter]就是讲k_reenter的值减1,另外就是讲esp加4.

结合s_stackframe的结构定义不难发现,其实esp加4恰好跳过retaddr这个成员,以便于执行iretd指令,之前堆栈内恰好是eip cs eflags esp ss的值。

那retaddr是干啥? k_reenter是干啥的?

时钟中断处理程序

书上说是从简单开始做起!


150	ALIGN	16
151	hwint00:		; Interrupt routine for irq 0 (the clock).
152		iretd

这个代码是中断例程,里面是没有动作!

进程表的各个值进行初始化,列出寄存器初始化的东西:

cs ds es fs gs ss esp eip eflags

Loader代码中吧gs对应的描述符DPL 设置为3,所以程序的代码有权限访问显存的,我们让其他段寄存器对应的描述符基地址和段界限与先前的段寄存器对应的描述符基地址和段界限相同,只是改变它们的RPL 和 TI ,以表示他们运行的特权级!

cs ds 等段寄存器对应的将是LDT中而不再是GDT了。所以我们的另一个任务就是初始化局部描述符表。

可以把它放置进程表中,从逻辑上看,由于LDT 是进程的一部分,所以这样也可以的!

同时我们还必须在GDT中增加相应的描述符,并在合适的时间将相应的选择子加载给ldtr。

另外由于我们用到了任务状态段,所以我们还必须初始化TSS,并在GDT中添加一个描述符,对应的选择子加载到tr这个寄存器,目前我们能用到的TSS 的也就是TSS ring0 的ss esp,初始化这两个就够了!

核心就是一个进程表和相关的TSS 等内容!

化整为零,分为四个部分,那就是进程表、进程体、GDT和TSS。他们之间的关系大致分为3部分!

  • 1.进程表和GDT 。进程表内的LDT Select对应GDT中的一个描述符,而这个描述符所指向的内存空间就存在进程表内。
    1. 进程表和进程 。 进程表是进程的描述,进程运行过程如果被终端,各个寄存器的值都会被保存进进程表中。但是,在我们的第一个进程开始之前,不需要初始化那么多的内容,只需要知道进程的入口地址就足够了。同时还需要使用堆栈,堆栈不够程序本身控制的,所以还需要事先制定esp。
  • GDT和TSS。 GDT中需要有一个描述符来对应TSS ,需要事先初始化这个描述符。

现在做这4个部分的初始化工作。

第一步,首先来准备小小的进程。代码只有10行。


48	/*======================================================================*
49								   TestA
50	 *======================================================================*/
51	void TestA()
52	{
53		int i = 0;
54		while(1){
55			disp_str("A");
56			disp_int(i++);
57			disp_str(".");
58			delay(1);
59		}
60	}
61

可以看出这个是函数,如果你是做嵌入式的,就会发现,这个跟os中的task很像!! 很简单的功能,每次循环一次打印一个字符和一个数字,和延时!

我们要把程序跳转kerne_main()这个函数,函数在main.c中。


17	/*======================================================================*
18								kernel_main
19	 *======================================================================*/
20	PUBLIC int kernel_main()
21	{
22		disp_str("-----\"kernel_main\" begins-----\n");
23
...
43		restart();
44
45		while(1){}
46	}

在kernel.asm 里面有执行跳转函数!


133
134		;sti
135		jmp	kernel_main
136
137		;hlt

进程A中的delay函数,也是一个循环!


63	/*======================================================================*
64								   delay
65	 *======================================================================*/
66	PUBLIC void delay(int time)
67	{
68		int i, j, k;
69		for (k = 0; k < time; k++) {
70			for (i = 0; i < 10; i++) {
71				for (j = 0; j < 10000; j++) {}
72			}
73		}
74	}
75

运行的时候如果发现两次打印之间的间隔不理想,可以调整这里的循环的次数!

第二步,初始化进程表

要初始化进程表,的有进程表结构的定义,代码如图所示,结构体STACK_FRAMEde 定义如下:



typedef struct s_stackframe {
	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 addr for kernel.asm::save()   */
	u32	eip;		/* \                                    */
	u32	cs;		/* |                                    */
	u32	eflags;		/* | pushed by CPU during interrupt     */
	u32	esp;		/* |                                    */
	u32	ss;		/* /                                    */
}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 */
	u32 pid;                   /* process id passed in from MM */
	char p_name[16];           /* name of the process */
}PROCESS;


有了结构体,在global.c声明一个进程表

PUBLIC PROCESS proc_table[NR_TASKS];

其中,NR_TASKS定义了最大允许进程,我们把他设置1,因为目前我们只实验1个进程!

书上说建议对照上图来理清思路!

初始化进程表在main.c中!


20	PUBLIC int kernel_main()
21	{
22		disp_str("-----\"kernel_main\" begins-----\n");
23
24		PROCESS* p_proc	= proc_table;
25
26		p_proc->ldt_sel	= SELECTOR_LDT_FIRST;
27		memcpy(&p_proc->ldts[0], &gdt[SELECTOR_KERNEL_CS>>3], sizeof(DESCRIPTOR));
28		p_proc->ldts[0].attr1 = DA_C | PRIVILEGE_TASK << 5;	// change the DPL
29		memcpy(&p_proc->ldts[1], &gdt[SELECTOR_KERNEL_DS>>3], sizeof(DESCRIPTOR));
30		p_proc->ldts[1].attr1 = DA_DRW | PRIVILEGE_TASK << 5;	// change the DPL
31
32		p_proc->regs.cs	= (0 & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK;
33		p_proc->regs.ds	= (8 & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK;
34		p_proc->regs.es	= (8 & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK;
35		p_proc->regs.fs	= (8 & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK;
36		p_proc->regs.ss	= (8 & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK;
37		p_proc->regs.gs	= (SELECTOR_KERNEL_GS & SA_RPL_MASK) | RPL_TASK;
38		p_proc->regs.eip= (u32)TestA;
39		p_proc->regs.esp= (u32) task_stack + STACK_SIZE_TOTAL;
40		p_proc->regs.eflags = 0x1202;	// IF=1, IOPL=1, bit 2 is always 1.
41
42		p_proc_ready	= proc_table;

从代码可以看出,主要初始化三个部分:

寄存器、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,看一下eflags 寄存器得知,0x1202恰好设置了IF 位并把IOPL设置为1。这样进程就可以使用IO 指令了,并且中断会在iretd执行时被打开 ,sti指令已经被注释掉了!

代码的宏定义都在protect.h中!

代码就不附上了!

要记得LDT 跟 GDT 是联系一起的,别忘了填充GDT中进程的LDT 描述符!

填充GDT中的进程LDT 的描述符


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

这段代码在init_port()中。init_descriptor和init_idt_desc有些类似!


144	/*======================================================================*
145							   init_descriptor
146	 *----------------------------------------------------------------------*
147	 初始化段描述符
148	 *======================================================================*/
149	PRIVATE void init_descriptor(DESCRIPTOR *p_desc,u32 base,u32 limit,u16 attribute)
150	{
151		p_desc->limit_low	= limit & 0x0FFFF;
152		p_desc->base_low	= base & 0x0FFFF;
153		p_desc->base_mid	= (base >> 16) & 0x0FF;
154		p_desc->attr1		= attribute & 0xFF;
155		p_desc->limit_high_attr2= ((limit>>16) & 0x0F) | (attribute>>8) & 0xF0;
156		p_desc->base_high	= (base >> 24) & 0x0FF;
157	}
158

seg2phys的定义


133	/*======================================================================*
134							   seg2phys
135	 *----------------------------------------------------------------------*
136	 由段名求绝对地址
137	 *======================================================================*/
138	PUBLIC u32 seg2phys(u16 seg)
139	{
140		DESCRIPTOR* p_dest = &gdt[seg >> 3];
141		return (p_dest->base_high<<24 | p_dest->base_mid<<16 | p_dest->base_low);
142	}

vir2phys是一个宏! 也是定义在protect.h中的!

第三步,准备GDT 和 TSS

剩下没有初始化的只有TSS了!,在init_port(), 填充TSS 以及对应的描述符!


99 		/* 填充 GDT 中 TSS 这个描述符 */
100		memset(&tss, 0, sizeof(tss));
101		tss.ss0 = SELECTOR_KERNEL_DS;
102		init_descriptor(&gdt[INDEX_TSS],
103				vir2phys(seg2phys(SELECTOR_KERNEL_DS), &tss),
104				sizeof(tss) - 1,
105				DA_386TSS);
106		tss.iobase = sizeof(tss); /* 没有I/O许可位图 */
107

TSS 准备好了,需要添加tr的代码, tr就是任务寄存器


130		xor	eax, eax
131		mov	ax, SELECTOR_TSS
132		ltr	ax


35 	typedef struct s_tss {
36 		u32	backlink;
37 		u32	esp0;	/* stack pointer to use during interrupt */
38 		u32	ss0;	/*   "   segment  "  "    "        "     */
39 		u32	esp1;
40 		u32	ss1;
41 		u32	esp2;
42 		u32	ss2;
43 		u32	cr3;
44 		u32	eip;
45 		u32	flags;
46 		u32	eax;
47 		u32	ecx;
48 		u32	edx;
49 		u32	ebx;
50 		u32	esp;
51 		u32	ebp;
52 		u32	esi;
53 		u32	edi;
54 		u32	es;
55 		u32	cs;
56 		u32	ss;
57 		u32	ds;
58 		u32	fs;
59 		u32	gs;
60 		u32	ldt;
61 		u16	trap;
62 		u16	iobase;	/* I/O位图基址大于或等于TSS段界限,就表示没有I/O许可位图 */
63 	}TSS;

iretd

关于提到的restart函数,可以直接复制到我们kernel.asm ,我们要实现ring0 到 ring1 的跳转,所以写了简化版的!


291	; ====================================================================================
292	;                                   restart
293	; ====================================================================================
294	restart:
295		mov	esp, [p_proc_ready]
296		lldt	[esp + P_LDT_SEL] 
297		lea	eax, [esp + P_STACKTOP]
298		mov	dword [tss + TSS3_S_SP0], eax
299
300		pop	gs
301		pop	fs
302		pop	es
303		pop	ds
304		popad
305
306		add	esp, 4
307
308		iretd
309
310

p_proc_ready是指向进程表结构的指针!

EXTERN PROCESS* p_proc_ready;

P_LDT_SEL、 P_STACKTOP、TSS3_S_SP0 和 SELECTOR_TSS 都定义在sconst.inc中。

一定要注意选择子一定要protect.h中的值保持一致!

进程的各个寄存器值已经在进程表里面都保存好了,我们只需要让esp指向栈顶,然后将各个值弹出就行。最后一句iretd执行后,eflags会被改变成pProc->regs.eflags的值。我们事先置了IF位,所以程序开始运行的时候,终端其实已经被打开了,虽然暂时来讲这没有意义,但是了解一下很重要!

回到kernel_main(),添加两行代码:


42		p_proc_ready	= proc_table;
43		restart();

进程启动,make一下! 发现编译报错!

https://bbs.csdn.net/topics/350133222 有回复问题解决办法

也就是在 在CFLAGS后面加上了-fno-stack-protector

注意也要把lib\kliba.asm 显示代码bug也修改完毕!


#########################
# Makefile for Orange'S #
#########################

# Entry point of Orange'S
# It must have the same value with 'KernelEntryPointPhyAddr' in load.inc!
ENTRYPOINT	= 0x30400

# Offset of entry point in kernel file
# It depends on ENTRYPOINT
ENTRYOFFSET	=   0x400

# Programs, flags, etc.
ASM		= nasm
DASM		= ndisasm
CC		= gcc
LD		= ld
ASMBFLAGS	= -I boot/include/
ASMKFLAGS	= -I include/ -f elf
CFLAGS		= -m32 -c -I include/  -fno-builtin -fno-stack-protector
LDFLAGS		= -s -Ttext $(ENTRYPOINT)
DASMFLAGS	= -u -o $(ENTRYPOINT) -e $(ENTRYOFFSET)

# This Program
ORANGESBOOT	= boot/boot.bin boot/loader.bin
ORANGESKERNEL	= kernel.bin
OBJS		= kernel/kernel.o kernel/start.o kernel/main.o\
			kernel/i8259.o kernel/global.o kernel/protect.o\
			lib/kliba.o lib/klib.o lib/string.o
DASMOUTPUT	= kernel.bin.asm

# All Phony Targets
.PHONY : everything final image clean realclean disasm all buildimg

# Default starting position
everything : $(ORANGESBOOT) $(ORANGESKERNEL)

all : realclean everything

final : all clean

image : final buildimg

clean :
	rm -f $(OBJS)

realclean :
	rm -f $(OBJS) $(ORANGESBOOT) $(ORANGESKERNEL)

disasm :
	$(DASM) $(DASMFLAGS) $(ORANGESKERNEL) > $(DASMOUTPUT)

# We assume that "a.img" exists in current folder
buildimg :
	dd if=boot/boot.bin of=a.img bs=512 count=1 conv=notrunc
	sudo mount -o loop a.img /mnt/floppy/
	sudo cp -fv boot/loader.bin /mnt/floppy/
	sudo cp -fv kernel.bin /mnt/floppy
	sudo umount /mnt/floppy

boot/boot.bin : boot/boot.asm boot/include/load.inc boot/include/fat12hdr.inc
	$(ASM) $(ASMBFLAGS) -o $@ $<

boot/loader.bin : boot/loader.asm boot/include/load.inc boot/include/fat12hdr.inc boot/include/pm.inc
	$(ASM) $(ASMBFLAGS) -o $@ $<

$(ORANGESKERNEL) : $(OBJS)
	$(LD) -m elf_i386  $(LDFLAGS) -o $(ORANGESKERNEL) $(OBJS)

kernel/kernel.o : kernel/kernel.asm include/sconst.inc
	$(ASM) $(ASMKFLAGS) -o $@ $<

kernel/start.o: kernel/start.c include/type.h include/const.h include/protect.h include/string.h include/proc.h include/proto.h \
			include/global.h
	$(CC) $(CFLAGS) -o $@ $<

kernel/main.o: kernel/main.c include/type.h include/const.h include/protect.h include/string.h include/proc.h include/proto.h \
			include/global.h
	$(CC) $(CFLAGS) -o $@ $<

kernel/i8259.o: kernel/i8259.c include/type.h include/const.h include/protect.h include/proto.h
	$(CC) $(CFLAGS) -o $@ $<

kernel/global.o: kernel/global.c include/type.h include/const.h include/protect.h include/proc.h \
			include/global.h include/proto.h
	$(CC) $(CFLAGS) -o $@ $<

kernel/protect.o: kernel/protect.c include/type.h include/const.h include/protect.h include/proc.h include/proto.h \
			include/global.h
	$(CC) $(CFLAGS) -o $@ $<

lib/klib.o: lib/klib.c include/type.h include/const.h include/protect.h include/string.h include/proc.h include/proto.h \
			include/global.h
	$(CC) $(CFLAGS) -o $@ $<

lib/kliba.o : lib/kliba.asm
	$(ASM) $(ASMKFLAGS) -o $@ $<

lib/string.o : lib/string.asm
	$(ASM) $(ASMKFLAGS) -o $@ $<



因为时间有限! 直接测试书上代码! 注意b章节的代码可以选择 make stage1 或者 stage2

改变屏幕地0行、第0列字符来说明终端例程正在运行! 本来这个位置是Boot的首字母“B”,如果发生中断,它会不断变化!运行一下!

b节代码,输入make final 运行会看到不断的出现的字符 “ ^ “ ,说明函数disp_str运行正常,而且没有影响中断处理的其他部分以及进程A。 之所以在两次字符A的打印中间有多个” ^ “ ,是因为我们的执行体加入了delay()函数。在此函数的执行过程中发生了中断!

c节代码,使用make stage1 编译!

打印了A0x0之后就不停打印”^” ,再也进不去进程里面了!! 这是因为中断还未处理完,又一次中断发生了!

这次make stage2, 如图所示,字符A和相应的数字又在不停的出现了,而且屏幕左上角的字母跳动速度快,而字符”^”打印速度慢,说明有很多时候程序在执行了 inc byte [gs:0]之后并没有执行disp_str,这也说明中断重入的确发生了。 后面就需要解决中断重入的问题!

make stage3

多进程!

d章节 显示我们打印的”#”

e章节,实现交替的A B 还有不断增加的数字 ,这表明我们第2个进程运行成功了!

f章节

l章节代码 可以看到+ 加号出现在A 之前

m章节

这个是ticks 第一次打印是A0x0 ,第二次打印的A0x9

n章节,不太精确的延迟! 代码已经把两次时钟中断的间隔改成了10ms,运行程序看到在很多的时间打印很多# 这说明中断发生快了很多。 原来一秒钟18.2次中断,大约55ms发生一次,现在一秒钟100次,10ms发生一次,所以区别才会这么明显。

编写新的延时函数,10ms中断发生一次,修改后效果如图所示:

o章节 A B C 三个进程分别延迟300、 900、 1500ms。

p章节 增加了优先级策略

q章节

在前面基础上打印当前的tick

r章节

在一个进程的ticks还没有变成0之前,其他进程就不会有机会获得执行!

优先级调度总结