构建64位操作系统-进程与线程

1.进程

1.1.论述

1.1.1.综合论述

        进程是执行片段,数据片段,栈段集合体,可以让处理器执行其代码片段。

        为了管理进程,需要引入进程管理对象,记录进程运行环境,进程占据资源等。

        系统存在多个进程时,需要通过调度策略,决定何时执行那个进程。执行进程变化时,涉及进程切换。

1.1.2.进程分类

        按照进程是执行片段,数据片段,栈段集合体的定义。

        操作系统本身是一个进程。操作系统这个进程用于完成Boot,Loader,启动阶段初始化等一系列工作。完成这些工作后,通过while(1) hlt();保持低功率运行。

        向操作系统进程存在一个特点,该进程只有内核层线性地址空间,没有用户层线性地址空间。这样的进程称为内核进程。

        对于既有内核层线性地址空间,又有内核层线性地址空间的进程称为用户进程。

1.1.3.进程控制结构

struct task_struct
{
	volatile long state;
	unsigned long flags;
	long preempt_count;
	long signal;
	long cpu_id;		//CPU ID
	struct mm_struct *mm;
	struct thread_struct *thread;
	struct List list;
	unsigned long addr_limit;	
	/*0x0000,0000,0000,0000 - 0x0000,7fff,ffff,ffff user*/
	/*0xffff,8000,0000,0000 - 0xffff,ffff,ffff,ffff kernel*/
	long pid;
	long priority;
	long vrun_time;
};

        进程本身是存在与磁盘上通过编译得到的代码段,数据段,栈段等集合体。

        进程加载就是将磁盘上上述数据加载到内存,然后让处理器从进程起始地址开始执行的过程。

        对于当前计算机系统,系统中同时可以存在多个进程,这些进程需要被cpu进程管理,通过进程调度来并发执行。

        所以,进程加载除了上述基础工作,我们还需为进程准备进程控制对象,用来为进程管理提供数据支持。

        上述进程控制对象各个字段含义如下:

        state:进程状态

        flags:进程标志位

        preempt_count:放在多核部分讲述

        signal:信号

        cpu_id:运行进程的cpu的编号

        mm:进程依赖的页表,进程各个段区域信息。

        thread:进程实时状态信息

        list:双向链表

        addr_limit:进程允许使用的线性区域信息

        pid:进程id

        priority:进程优先级

        vrun_time:虚拟运行时间

struct mm_struct
{
	pml4t_t *pgd;	//page table point
	unsigned long start_code,end_code;
	unsigned long start_data,end_data;
	unsigned long start_rodata,end_rodata;
	unsigned long start_brk,end_brk;
	unsigned long start_stack;	
};

        mm_struct描述了进程依赖的页表,进程内各个段的区域信息。

        这个结构的各个字段含义如下:

        pgd:这里存储的是进程依赖的cr3寄存器的值。cr3寄存器值包含了PML4页表的物理基地址及属性。

        start_code:进程代码段起始线性地址

        end_code:进程代码段尾后线性地址

        start_data:进程数据段起始线性地址

        end_data:进程数据段尾后线性地址

        start_rodata:进程只读数据段起始线性地址

        end_rodata:进程只读数据段尾后线性地址

        start_brk:进程可用堆区域起始线性地址

        end_brk:进程可用堆区域尾后线性地址

        start_stack:应用层栈基地址

struct thread_struct
{
	unsigned long rsp0;	//in tss
	unsigned long rip;
	unsigned long rsp;	
	unsigned long fs;
	unsigned long gs;
	unsigned long cr2;
	unsigned long trap_nr;
	unsigned long error_code;
};

        thread_struct结构用于保存进程的实时信息。thread_struct各个字段含义如下:

        rsp0:进程的特权级0栈的基地址

        rip:进程当前rip

        rsp:进程当前rsp

        fs:进程当前fs

        gs:进程当前gs

        cr2:进程当前cr2

        trap_nr:异常号

        error_code:异常错误码

        想象以下,进程正在执行,执行完当前指令时,遭遇异常/中断/进程切换,这时就需要先将此时的信息存储到thread_struct,以备后续进程恢复执行时快速定位到原来位置。

        前面我们知道:IA-32e模式下,TSS区域无需用来提供对任务切换时现场保存与恢复的支持。只需负责为不同特权级间的栈切换提供支持即可。且IA-32e下TSS支持两类栈切换模式。通过执行描述符跳转时,描述符中的信息来确定是采用IST机制,还是老的机制实现跳转时栈的管理。

        我们将TSS区域按数据结构来描述如下:

struct tss_struct
{
	unsigned int  reserved0;
	unsigned long rsp0;
	unsigned long rsp1;
	unsigned long rsp2;
	unsigned long reserved1;
	unsigned long ist1;
	unsigned long ist2;
	unsigned long ist3;
	unsigned long ist4;
	unsigned long ist5;
	unsigned long ist6;
	unsigned long ist7;
	unsigned long reserved2;
	unsigned short reserved3;
	unsigned short iomapbaseaddr;
}__attribute__((packed));

 1.2.实践

1.2.1.为内核进程构建进程管理对象

        通过论述部分,我们知道了,进程加载时,除了从磁盘加载到内存,并让cpu从进程起始地址开始执行指令。为了管理进程,我们还为每个进程安排一个进程控制对象,用来为对进程进行记录和管理提供数据支持。

        下面的实践部分,围绕为内核进程建立进程控制对象来展开。

// Kernel.lds单独规划出一个段区域用作内核进程栈段
. = ALIGN(32768);
.data.init_task : { *(.data.init_task) }

        接下来,我们为内核进程准备一个struct task_struct实例对象。

union task_union
{
	struct task_struct task;
	unsigned long stack[STACK_SIZE / sizeof(unsigned long)];
}__attribute__((aligned (8)));	//8Bytes align

union task_union init_task_union __attribute__((__section__ (".data.init_task"))) = 
{
	INIT_TASK(init_task_union.task)
};

        上述__attribute__((__section__(".data.init_task")))的意思是这个实例对象被放置在名字为.data.init_task的段内。

        这个task_union实例对象采用union,故而一个task_union实例对象占据尺寸为32KB。

         .data.init_task这个段通过.=ALIGN(32768)保证了这个段的起始线性地址可以被32KB整除。

        下面看实例对象初始化

#define INIT_TASK(tsk)	\
{			\
	.state = TASK_UNINTERRUPTIBLE,		\
	.flags = PF_KTHREAD,		\
	.preempt_count = 0,		\
	.signal = 0,		\
	.cpu_id = 0,		\
	.mm = &init_mm,			\
	.thread = &init_thread,		\
	.addr_limit = 0xffff800000000000,	\
	.pid = 0,			\
	.priority = 2,		\
	.vrun_time = 0		\
}

// task_init
list_init(&init_task_union.task.list);
init_task_union.task.preempt_count = 0;
init_task_union.task.state = TASK_RUNNING;
init_task_union.task.cpu_id = 0;

        state字段设置为先是TASK_UNINTERRUPTBLE(不可中断),后是TASK_RUNNING。

        flags的PF_KTHREAD表示这是一个内核线程。

        addr_limit的0xffff800000000000,表示这个进程只能访问[0xffff800000000000, 0xffffffff ffffffff]的内核线性区域。

        

        上述mm采用的是&init_mm,那么这个init_mm实例对象是如何初始化的呢?

init_mm.pgd = (pml4t_t *)Global_CR3;
init_mm.start_code = memory_management_struct.start_code;
init_mm.end_code = memory_management_struct.end_code;
init_mm.start_data = (unsigned long)&_data;
init_mm.end_data = memory_management_struct.end_data;
init_mm.start_rodata = (unsigned long)&_rodata; 
init_mm.end_rodata = (unsigned long)&_erodata;
init_mm.start_brk = 0;
init_mm.end_brk = memory_management_struct.end_brk;
init_mm.start_stack = _stack_start;

        这里为init_mm各个字段分别赋予有意义的数值,以便:

        pgd可以表示内核进程依赖的cr3数值

        start_code可以表示内核进程代码段起始线性地址

        end_code可以表示内核进程代码段尾后线性地址

        start_data可以表示内核进程数据段起始线性地址

        end_data可以表示内核进程数据段尾后线性地址

        start_rodata可以表示内核进程只读数据段起始线性地址

        end_rodata可以表示内核进程只读数据段尾后线性地址

        start_brk表示内核进程堆区域起始位置

        end_brk表示内核进程堆区域尾后位置。(在此位置之后区域,依然可用于动态内存分配)

        start_stack表示内核栈的基地址

// head.S
ENTRY(_stack_start)
.quad	init_task_union + 32768

// task.h
extern unsigned long _stack_start;

        这个_stack_start变量指向的位置是.data.init_task这个段的尾后位置。

        这样我们知道了,内核进程的栈段大小为32KB。内核栈区域起始处放置了一个task_struct对象,即进程控制对象。

        上述thread采用的是&init_thread,那么这个init_thread是如何初始化的呢?

struct thread_struct init_thread = 
{
	.rsp0 = (unsigned long)(init_task_union.stack + STACK_SIZE / sizeof(unsigned long)),
	.rsp = (unsigned long)(init_task_union.stack + STACK_SIZE / sizeof(unsigned long)),
	.fs = KERNEL_DS,
	.gs = KERNEL_DS,
	.cr2 = 0,
	.trap_nr = 0,
	.error_code = 0
};

        这里为init_thread各个字段赋予有意义的数值,以便:

        rsp0可以表示内核进程的特权级0栈的基地址

        rsp可以表示内核进程当前rsp内容

        fs,gs,cr2,trap_nr,error_code分别可表示内核进程当前状态下各个寄存器及状态的值。 

        为了使每个cpu可以正常处理中断、异常、调度,我们还需要为每个cpu准备一个TSS,并为此TSS区域注册GDT,且通知cpu。

// task.h
extern struct tss_struct init_tss[NR_CPUS];

// head.S
// 为GDT按照TSS描述符,按照在索引10位置
// TSS区域起始位置是init_tss--这是数组首个元素起始地址
setup_TSS64:
    leaq	init_tss(%rip),	%rdx
    xorq	%rax,	%rax
	xorq	%rcx,	%rcx
	movq	$0x89,	%rax
	shlq	$40,	%rax
	movl	%edx,	%ecx
	shrl	$24,	%ecx
	shlq	$56,	%rcx
	addq	%rcx,	%rax
	xorq	%rcx,	%rcx
	movl	%edx,	%ecx
	andl	$0xffffff,	%ecx
	shlq	$16,	%rcx
	addq	%rcx,	%rax
	addq	$103,	%rax
	leaq	GDT_Table(%rip),	%rdi
	movq	%rax,	80(%rdi)	//tss segment offset
	shrq	$32,	%rdx
	movq	%rdx,	88(%rdi)	//tss+1 segment offset

// task.c
struct tss_struct init_tss[NR_CPUS] = 
{ 
	[0 ... NR_CPUS-1] = INIT_TSS 
};

#define INIT_TSS \
{	.reserved0 = 0,	 \
	.rsp0 = (unsigned long)(init_task_union.stack + STACK_SIZE / sizeof(unsigned long)),	\
	.rsp1 = (unsigned long)(init_task_union.stack + STACK_SIZE / sizeof(unsigned long)),	\
	.rsp2 = (unsigned long)(init_task_union.stack + STACK_SIZE / sizeof(unsigned long)),	\
	.reserved1 = 0,	 \
	.ist1 = 0xffff800000007c00,	\
	.ist2 = 0xffff800000007c00,	\
	.ist3 = 0xffff800000007c00,	\
	.ist4 = 0xffff800000007c00,	\
	.ist5 = 0xffff800000007c00,	\
	.ist6 = 0xffff800000007c00,	\
	.ist7 = 0xffff800000007c00,	\
	.reserved2 = 0,	\
	.reserved3 = 0,	\
	.iomapbaseaddr = 0	\
}

        这样为运行内核进程的bsp的cpu准备的TSS区域里,rsp0,rsp1,rsp2均指向内核进程栈基地址。ist系列均指向0xffff800000007c00这个线性地址位置。

        这样,我们就完整讲述了如何为内核进程,准备TSS区域(其实是cpu级别),为内核进程准备进程控制对象及其初始化,为内核进程准备栈区域。

 1.2.2.在内核进程中创建线程

        其实线程,在linux实现中也是进程。也需要进程控制对象, 参与进程调度。

        不过,属于同一进程内各个线程,由于共享所属进程的进程级资源,且在磁盘上也以进程整体为单位存在。所以,将这些线程归为一组,这个组级别称为进程。具体到组内每个独立执行体,称为线程。

        如果一个进程只有一个独立执行体,那么这个进程只有一个线程。进程一开始就存在的独立执行体,称为进程的主线程。

unsigned long init(unsigned long arg)
{
	struct pt_regs *regs;
	color_printk(RED, BLACK, "init task is running,arg:%#018lx\n", arg);
	current->thread->rip = (unsigned long)ret_system_call;
	current->thread->rsp = (unsigned long)current + STACK_SIZE - sizeof(struct pt_regs);
	regs = (struct pt_regs *)current->thread->rsp;
	current->flags = 0;
	__asm__	__volatile__	(	"movq	%1,	%%rsp	\n\t"
					"pushq	%2		\n\t"
					"jmp	do_execve	\n\t"
					::"D"(regs),"m"(current->thread->rsp),"m"(current->thread->rip):"memory");

	return 1;
}

        在进程内开启新线程的第一步就是确定新线程的执行体--代码段。因为线程本身就是独立执行体。共享隶属进程的进程级资源。线程作为独立执行体也有各自独立的线程级资源。

        上述就是我们为要从内核进程开启的新线程的执行体。

        因为实现层面,线程就是进程。所以,进程控制对象,进程调度这些对于线程来说,完全按进程的来即可。

struct pt_regs
{
	unsigned long r15;
	unsigned long r14;
	unsigned long r13;
	unsigned long r12;
	unsigned long r11;
	unsigned long r10;
	unsigned long r9;
	unsigned long r8;
	unsigned long rbx;
	unsigned long rcx;
	unsigned long rdx;
	unsigned long rsi;
	unsigned long rdi;
	unsigned long rbp;
	unsigned long ds;
	unsigned long es;
	unsigned long rax;
	unsigned long func;
	unsigned long errcode;
	unsigned long rip;
	unsigned long cs;
	unsigned long rflags;
	unsigned long rsp;
	unsigned long ss;
};

         为了实现对进程的管理,除了上述介绍的task_struct,mm_struct,thread_struct,TSS这些结构。为了实现进程切换时,进程执行环境的保存与恢复,我们还需要为每个进程准备一个pt_regs实例对象。这个对象用于保存进程执行现场。

        在进程被换出前,将执行现场在进程的pt_regs中存储。在进程恢复时,再将进程的pt_regs恢复就可以实现执行现场的还原。

        

        对内核主线程,它的pt_regs放置在内核主线程的栈基地址向下偏移一个sizeof(pt_regs)尺寸的位置。

        对这里要为内核进程构建的新线程,我们为其临时构造一个pt_regs对象,并初始化。

struct pt_regs regs;
memset(&regs,0,sizeof(regs));
regs.rbx = (unsigned long)fn;
regs.rdx = (unsigned long)arg;
regs.ds = KERNEL_DS;
regs.es = KERNEL_DS;
regs.cs = KERNEL_CS;
regs.ss = KERNEL_DS;
regs.rflags = (1 << 9);
regs.rip = (unsigned long)kernel_thread_func;

        有了执行函数,pt_regs,下面我们只需继续为要构造的新线程准备task_struct,thread_struct,mm_struct即可。

        为了使得cpu可以进程进程调度,每个cpu有一个自己的就绪进程队列。这样我们只需把新构建线程的task_struct实例对象加入到隶属cpu的就绪进程队列即可。

        这样,只需等待后续进程调度时,cpu将其调度执行即可。

2.进程调度

2.1.论述

2.1.1.内核主线程,init线程切换视角

        进程切换的发生:

        1.线程执行中遭遇定时器中断

        2.定时器中断里通过rsp可以找到被中断线程的task_struct,为其设置调度标志

        3.中断返回时因为被中断线程有调度标志,执行进程调度

        4.进程调度里面取出目标线程,执行线程切换

        切换时,需要保存被切换者的上下文,当前执行位置。

        5.切换后开始恢复目标线程上下文,让目标线程从被中断位置继续执行

        情形1:

        1.目标线程执行中,再次被中断

        2.中断执行

        3.中断返回

        4.目标线程上下文恢复,继续从被中断位置执行

        情形2:

        1.目标线程执行中,再次被中断

        2.中断执行,设置被中断线程task_struct里的调度标志

        3.中断返回时因为被中断线程有调度标志,执行进程调度

        4.进程调度里面取出新的目标线程,执行线程切换。

        切换时,需要保存被切换者的上下文,当前执行位置。

        5.切换后开始恢复新的目标线程上下文,让新的目标线程从被中断位置继续执行

        情形3:

        1.目标线程执行

        2.目标线程执行完毕

        3.进程调度里面取出新的目标线程。执行线程切换。

        当前进程因为已经结束,需要进行记录。不再需要为其保存上下文,当前执行位置。

        4.切换后开始恢复新的目标线程上下文,让新的目标线程从被中断位置继续执行

       

        进程调度必须能完美契合上述情形

2.1.2.sysexit,sysenter

        sysexit:

        用于从特权级0转向特权级3。

        IA32_SYSENTER_CS:

        位于MSG的0x174。

        一个32位寄存器,用于索引特权级3下的代码段选择子,栈段选择子。

        代码段选择子为:

        IA-32e模式下,IA32_SYSENTER_CS[15:0]+32

        其他模式下,IA32_SYSENTER_CS[15:0]+16

        栈段选择子:

        代码段选择子+8

        RDX:

        保存着一个Canonical型地址。

        返回64位模式代码段时,将作为rip的值。

        返回非64位模式代码段时,取低32位作为rip的值。

        RCX:

        保存着一个Canonical型地址。

        返回64位模式代码段时,将作为rsp的值。

        返回非64位模式代码段时,取低32位作为rsp的值。

        

        SYSEXIT在64位模式下,默认操作数不是64位,如果要返回64位的应用层,需在指令前插入0x48前缀,让其使用64位操作数。

        sysenter:

        用于从特权级3跳转到特权级0。

        IA32_SYSENTER_CS:

        位于MSG的0x174。

        一个32位寄存器,用于索引特权级0下的代码段选择子,栈段选择子。

        代码段选择子:IA32_SYSENTER_CS[15:0]

        栈段选择子:代码段选择子+8

        IA32_SYSENTER_ESP:

        位于MSG的0x175。

        返回64位模式代码段时,将作为RSP的值。

        返回非64位模式代码段时,取低32位作为RSP的值。

        IA32_SYSENTER_EIP:

        位于MSG的0x176。

        返回64位模式代码段时,将作为RIP的值。

        返回非64位模式代码段时,取低32位作为RIP的值。

        sysenter执行后,会自动禁止外部中断。

2.2.实践

2.2.1.进程切换的发生--从内核主线程到内核新线程

        一开始只有内核主线程。为了线程切换,我们在内核主线程里面,产生一个新的内核线程。

kernel_thread(init, 10, CLONE_FS | CLONE_FILES | CLONE_SIGNAL);
int kernel_thread(unsigned long (* fn)(unsigned long), unsigned long arg, unsigned long flags)
{
	// 现象执行现场---对新线程而言,它的执行现场
	struct pt_regs regs;
	memset(&regs,0,sizeof(regs));
	regs.rbx = (unsigned long)fn;
	regs.rdx = (unsigned long)arg;
	regs.ds = KERNEL_DS;
	regs.es = KERNEL_DS;
	regs.cs = KERNEL_CS;
	regs.ss = KERNEL_DS;
	regs.rflags = (1 << 9);
	// 线程当前执行位置
	regs.rip = (unsigned long)kernel_thread_func;
	return do_fork(&regs, flags, 0, 0);
}

        每个线程有自己的执行现场,新内核线程的执行线程我们人为指定。人为指定时,我们让实时指令指针指向kernel_thread_func,这样新线程被恢复后,将从此位置继续执行。

unsigned long do_fork(struct pt_regs * regs, unsigned long clone_flags, unsigned long stack_start, unsigned long stack_size)
{
	struct task_struct *tsk = NULL;
	struct thread_struct *thd = NULL;
	struct Page *p = NULL;
	color_printk(WHITE, BLACK, "alloc_pages,bitmap:%#018lx\n", *memory_management_struct.bits_map);
	// 采用分配物理页的方式得到可用物理内存区域--因为前面我们对所有可分配物理页均做了页表注册
	// 所以,物理页对应的线性区域也是直接可以访问使用的。
	p = alloc_pages(ZONE_NORMAL, 1, PG_PTable_Maped | PG_Kernel);
	color_printk(WHITE, BLACK, "alloc_pages,bitmap:%#018lx\n", *memory_management_struct.bits_map);
	// 从物理页基地址得到对应的线性地址--因为做了页表映射,所以线性地址可以直接使用
	tsk = (struct task_struct *)Phy_To_Virt(p->PHY_address);
	color_printk(WHITE, BLACK, "struct task_struct address:%#018lx\n", (unsigned long)tsk);
	// 我们用分配的2M线性区域的初始部分放置task_struct实例对象
	memset(tsk, 0, sizeof(*tsk));
	// 隶属于一个进程的多个线程的task_struct中mm字段应该是一致的
	*tsk = *current;
	// 字段初始化
	list_init(&tsk->list);
	tsk->priority = 2;
	tsk->pid++;
	tsk->preempt_count = 0;
	// 一个进程内多个线程可以在不同cpu并发或并行运行。所以,cpu_id字段可能不一样。
	tsk->cpu_id = SMP_cpu_id();
	// 一个进程内多个线程可独立进程调度,所以状态独立。
	tsk->state = TASK_UNINTERRUPTIBLE;
	thd = (struct thread_struct *)(tsk + 1);
	tsk->thread = thd;

	// thread_struct实例对象初始化
	memset(thd, 0, sizeof(*thd));
	// 新线程特权级0的栈的栈基地址
	thd->rsp0 = (unsigned long)tsk + STACK_SIZE;
	// 新线程指令指针实时位置
	thd->rip = regs->rip;
	// 新线程栈指针实时位置
	thd->rsp = (unsigned long)tsk + STACK_SIZE - sizeof(struct pt_regs);
	// 新线程的fs,gs实时值
	thd->fs = KERNEL_DS;
	thd->gs = KERNEL_DS;
	// 如果新线程是用户级的,那么令线程下面执行位置指向ret_system_call
	if(!(tsk->flags & PF_KTHREAD))
		thd->rip = regs->rip = (unsigned long)ret_system_call;

	// 新线程的执行现场
	memcpy(regs, (void *)((unsigned long)tsk + STACK_SIZE - sizeof(struct pt_regs)), sizeof(struct pt_regs));
	// 新线程处于就绪状态
	tsk->state = TASK_RUNNING;
	insert_task_queue(tsk);
	return 1;
}

        我们继续为新线程分配内存区域-2M。起始部分存储新线程的task_struct实例对象。

        通过thd->rsp0我们设置新线程的特权级0的栈的基地址。

        通过thd->rsp我们设置新线程实时栈指针指向栈基指针向下偏移尺寸sizeof(struct pt_regs)位置,恰好指向新线程执行现场的struct pt_regs实例对象。

        因为我们的线程含PF_KTHREAD,所以,thd->rip,regs->rip指向的是kernel_thread_func。

extern void kernel_thread_func(void);
__asm__ (
"kernel_thread_func:	\n\t"
"	popq	%r15	\n\t"
"	popq	%r14	\n\t"	
"	popq	%r13	\n\t"	
"	popq	%r12	\n\t"	
"	popq	%r11	\n\t"	
"	popq	%r10	\n\t"	
"	popq	%r9	\n\t"	
"	popq	%r8	\n\t"	
"	popq	%rbx	\n\t"	
"	popq	%rcx	\n\t"	
"	popq	%rdx	\n\t"	
"	popq	%rsi	\n\t"	
"	popq	%rdi	\n\t"	
"	popq	%rbp	\n\t"	
"	popq	%rax	\n\t"	
"	movq	%rax,	%ds	\n\t"
"	popq	%rax		\n\t"
"	movq	%rax,	%es	\n\t"
"	popq	%rax		\n\t"
"	addq	$0x38,	%rsp	\n\t"
"	movq	%rdx,	%rdi	\n\t"
"	callq	*%rbx		\n\t"
"	movq	%rax,	%rdi	\n\t"
"	callq	do_exit		\n\t"
);

        通过insert_task_queue(tsk)我们将新线程加入cpu的调度队列。

// schedule.h
struct schedule
{
	long running_task_count;// 队列里面进程梳理
	long CPU_exec_task_jiffies;// 队列级时间片
	// 这个是哨兵节点
	struct task_struct task_queue;// 就绪进程队列
};

// 允许每个cpu有自己的调度队列
extern struct schedule task_schedule[NR_CPUS];

// schedule.c
struct schedule task_schedule[NR_CPUS];
void insert_task_queue(struct task_struct *tsk)
{
	struct task_struct *tmp = NULL;
	// cpu的内核进程无需作为有效元素插入链式结构
	if(tsk == init_task[SMP_cpu_id()])
		return ;

	tmp = container_of(list_next(&task_schedule[SMP_cpu_id()].task_queue.list), struct task_struct, list);
	if(list_is_empty(&task_schedule[SMP_cpu_id()].task_queue.list))
	{
	}
	else
	{
		// 寻找有效元素里面虚拟运行时间大于等于待插入元素的
		// 因为链式结构存在哨兵节点,且此节点的虚拟运行时间为超大值。所以,迭代次数必然是有限的。
		while(tmp->vrun_time < tsk->vrun_time)
			tmp = container_of(list_next(&tmp->list),struct task_struct,list);
	}

	// 在tmp前面插入新元素
	// 这样可以保证链式结构有效元素按虚拟运行时间有序组织---虚拟运行时间递增
	list_add_to_before(&tmp->list, &tsk->list);
	// 更新队列内有效元素数
	task_schedule[SMP_cpu_id()].running_task_count += 1;
}

        有了上述准备,我们只需等待调度被触发,然后执行即可。 

// 进程调度初始化
void schedule_init()
{
	int i = 0;
	memset(&task_schedule, 0, sizeof(struct schedule) * NR_CPUS);
	for(i = 0; i < NR_CPUS; i++)
	{
		// 哨兵节点的list初始化
		list_init(&task_schedule[i].task_queue.list);
		// 哨兵节点的虚拟运行时间为超大值
		task_schedule[i].task_queue.vrun_time = 0x7fffffffffffffff;
		// 队列元素数1--哨兵也考虑了(哨兵代表的是cpu的内核进程)
		task_schedule[i].running_task_count = 1;
		// 队列时间片--时间片消耗为0时,触发调度动作
		task_schedule[i].CPU_exec_task_jiffies = 4;
	}
}

        这里我们赋予cpu调度队列的CPU_exec_task_jiffies为4,意味着4s后,触发调度。

void HPET_handler(unsigned long nr, unsigned long parameter, struct pt_regs * regs)
{
	jiffies++;
	struct timer_list* lpTimeList = container_of(list_next(&timer_list_head.list), struct timer_list, list);
	// 索引为0的软中断触发时机:链表首个有效元素的触发时机得到满足
	if((lpTimeList->expire_jiffies <= jiffies))
		set_softirq_status(TIMER_SIRQ);
	
	// current可以正常工作要求rsp指向的栈是32KB区域。区域起存储了被中断进程的task_struct
	switch(current->priority)
	{
		case 0:
		case 1:
			// 每隔cpu有独立的进程调度队列--只有就绪状态进程才会出现在调度队列等待调度
			// 何时触发进程调度
			// 需要调度队列记录的时间片被消耗尽了
			task_schedule[SMP_cpu_id()].CPU_exec_task_jiffies--;
			// 更新当前进程的虚拟运行时间
			current->vrun_time += 1;
			break;
		case 2:
		default:
			task_schedule[SMP_cpu_id()].CPU_exec_task_jiffies -= 2;
			current->vrun_time += 2;
			break;
	}

	if(task_schedule[SMP_cpu_id()].CPU_exec_task_jiffies <= 0)
	{
		// 当cpu进程调度队列记录的时间片被消耗尽了。
		// 则需要发起进程调度操作。
		// 进程调度操作类似软中断,在中断处理返回阶段,检测到需要调度时,进入调度处理。
		current->flags |= NEED_SCHEDULE;
	}
}

        通过HPET部分的介绍,我们知道 HPET_handler是HPET定时器0的中断处理函数。每隔1s触发一次。

        在这个处理函数里,我们会递减cpu调度队列的CPU_exec_task_jiffies。当其变为0时,我们令被中断线程的task_struct包含NEED_SCHEDULE标志。

        为了厘清中断处理中current,我们需要回顾一下

// head.S
setup_TSS64:
	leaq	init_tss(%rip),	%rdx
	xorq	%rax,	%rax
	xorq	%rcx,	%rcx
	movq	$0x89,	%rax
	shlq	$40,	%rax
	movl	%edx,	%ecx
	shrl	$24,	%ecx
	shlq	$56,	%rcx
	addq	%rcx,	%rax
	xorq	%rcx,	%rcx
	movl	%edx,	%ecx
	andl	$0xffffff,	%ecx
	shlq	$16,	%rcx
	addq	%rcx,	%rax
	addq	$103,	%rax
	leaq	GDT_Table(%rip),	%rdi
	movq	%rax,	80(%rdi)	//tss segment offset
	shrq	$32,	%rdx
	movq	%rdx,	88(%rdi)	//tss+1 segment offset

        上述在GDT表索引10位置安装的TSS描述符,TSS区域起始位置init_tss(%rip)

// task.h
extern struct tss_struct init_tss[NR_CPUS];
// task.c
struct tss_struct init_tss[NR_CPUS] = 
{ 
	[0 ... NR_CPUS-1] = INIT_TSS 
};

         这样,我们知道了TSS区域的位置是init_tss首个元素

// task.h
extern union task_union init_task_union;

#define INIT_TSS \
{	.reserved0 = 0,	 \
	.rsp0 = (unsigned long)(init_task_union.stack + STACK_SIZE / sizeof(unsigned long)),	\
	.rsp1 = (unsigned long)(init_task_union.stack + STACK_SIZE / sizeof(unsigned long)),	\
	.rsp2 = (unsigned long)(init_task_union.stack + STACK_SIZE / sizeof(unsigned long)),	\
	.reserved1 = 0,	 \
	.ist1 = 0xffff800000007c00,	\
	.ist2 = 0xffff800000007c00,	\
	.ist3 = 0xffff800000007c00,	\
	.ist4 = 0xffff800000007c00,	\
	.ist5 = 0xffff800000007c00,	\
	.ist6 = 0xffff800000007c00,	\
	.ist7 = 0xffff800000007c00,	\
	.reserved2 = 0,	\
	.reserved3 = 0,	\
	.iomapbaseaddr = 0	\
}

// task.c
union task_union init_task_union __attribute__((__section__ (".data.init_task"))) = 
{
	INIT_TASK(init_task_union.task)
};

// kernel.lds
. = ALIGN(32768);
.data.init_task : { *(.data.init_task) }

        这个TSS的rsp0/rsp1/rsp2指向的是链接时划分出的一块32KB对齐的区域的尾后位置。

SetCurTR(10);

        启动阶段,执行上述,通知cpu-GDT中索引10存储的是TSS描述符。

set_tss64((unsigned int *)&init_tss[0],
		_stack_start, _stack_start, _stack_start, 
		0xffff800000007c00, 0xffff800000007c00, 0xffff800000007c00, 0xffff800000007c00, 0xffff800000007c00, 0xffff800000007c00, 0xffff800000007c00);

// task.h
extern unsigned long _stack_start;

// head.S
ENTRY(_stack_start)
.quad	init_task_union + 32768

        我们对TSS区域的rsp0/rsp1/rsp2进行修正。修正后依然指向链接时划分出的一块32KB对齐的区域的尾后位置。

unsigned char * ptr = NULL;
// TSS中IST机制的栈采用动态分配区域(之前是0x7c00)
// 将栈顶设置到TSS的IST域
ptr = (unsigned char *)kmalloc(STACK_SIZE, 0) + STACK_SIZE;
SetKernelBspTss_Ist((unsigned long)ptr);


void SetKernelBspTss_Ist(unsigned long ptr)
{
	init_tss[0].ist1 = (unsigned long)ptr;
	init_tss[0].ist2 = (unsigned long)ptr;
	init_tss[0].ist3 = (unsigned long)ptr;
	init_tss[0].ist4 = (unsigned long)ptr;
	init_tss[0].ist5 = (unsigned long)ptr;
	init_tss[0].ist6 = (unsigned long)ptr;
	init_tss[0].ist7 = (unsigned long)ptr;
}

        我们动态分配出尺寸为32KB区域,用此区域尾后位置设置TSS区域的ist1/ist2/~/ist7。

        这样,TSS的rsp0/rsp1/rsp2指向一个32KB区域的尾后位置,ist1/~ist7指向另一个32KB区域的尾后位置。

// trap.c
void sys_vector_init()
{
	set_trap_gate(0, 0, divide_error);
	set_trap_gate(1, 0, debug);
	set_intr_gate(2, 0, nmi);
	set_system_gate(3, 0, int3);
	set_system_gate(4, 0, overflow);
	set_system_gate(5, 0, bounds);
	set_trap_gate(6, 0, undefined_opcode);
	set_trap_gate(7, 0, dev_not_available);
	set_trap_gate(8, 0, double_fault);
	set_trap_gate(9, 0, coprocessor_segment_overrun);
	set_trap_gate(10, 0, invalid_TSS);
	set_trap_gate(11, 0, segment_not_present);
	set_trap_gate(12, 0, stack_segment_fault);
	set_trap_gate(13, 0, general_protection);
	set_trap_gate(14, 0, page_fault);
	set_trap_gate(16, 0, x87_FPU_error);
	set_trap_gate(17, 0, alignment_check);
	set_trap_gate(18, 0, machine_check);
	set_trap_gate(19, 0, SIMD_exception);
	set_trap_gate(20, 0, virtualization_exception);
}

        上述是我们为内部异常(向量号0~31)注册描述符。我们注册的描述符里面ist部分为0,意味着,内部异常发生时,打断执行线程,采用TSS区域的rsp0/rsp1/rsp2结合异常处理自身特权级,确认rsp。

        这样,我们知道内部异常发生时,对内核主线程,异常处理时rsp指向的栈区域,和内核主线程所使用的栈区域是同一个。

for(i = 32;i < 56;i++)
{
	set_intr_gate(i , 0 , interrupt[i - 32]);
}

        上述是我们为外部中断(向量号32~55)注册描述符。 我们注册的描述符里面ist部分为0,意味着,内部异常发生时,打断执行线程,采用TSS区域的rsp0/rsp1/rsp2结合异常处理自身特权级,确认rsp。

        这样,我们知道外部中断发生时,对内核主线程,异常处理时rsp指向的栈区域,和内核主线程所使用的栈区域是同一个。

         

        有了上述的基础,我们知道HPET_handler中,如果被中断的内核主线程,那么current得到的是内核主线程的task_struct实例对象位置。

ret_from_exception:
ENTRY(ret_from_intr)
	movq	$-1,	%rcx
	testq	softirq_status(%rip),	%rcx	check softirq
	jnz	softirq_handler

	GET_CURRENT(%rbx)
	movq	TSK_PREEMPT(%rbx),	%rcx		check preempt
    cmpq	$0,	%rcx
	jne		RESTORE_ALL

	movq	TSK_FLAGS(%rbx),	%rcx		try schedule
	testq	$2,	%rcx
	jnz	reschedule
	jmp	RESTORE_ALL	

reschedule:
	callq	schedule						do schedule
	jmp	RESTORE_ALL

#define GET_CURRENT(reg)	\
	movq	$-32768,reg;	\
	andq	%rsp,	reg

        在中断返回时,我们会判断被中断线程的task_struct是否存在调度标志。如果存在,执行schedule,触发进程调度。

        注意,触发进程调度时候,我们需要能保存被中断线程的执行现场到被中断线程的栈区域的struct pt_regs中,要能保存被中断线程的实时信息到被中断线程的栈区域的struct thread中。

#define switch_to(prev,next)			\
do{							\
	__asm__ __volatile__ (	"pushq	%%rbp	\n\t"	\
				"pushq	%%rax	\n\t"	\
				"movq	%%rsp,	%0	\n\t"	\
				"movq	%2,	%%rsp	\n\t"	\
				"leaq	1f(%%rip),	%%rax	\n\t"	\
				"movq	%%rax,	%1	\n\t"	\
				"pushq	%3		\n\t"	\
				"jmp	__switch_to	\n\t"	\
				"1:	\n\t"	\
				"popq	%%rax	\n\t"	\
				"popq	%%rbp	\n\t"	\
				:"=m"(prev->thread->rsp),"=m"(prev->thread->rip)		\
				:"m"(next->thread->rsp),"m"(next->thread->rip),"D"(prev),"S"(next)	\
				:"memory"		\
				);			\
}while(0)

        上述是进程切换被触发时,执行的代码。

        prev->thread->rsp存储了进程切换时的rsp。

        prev->thread->rip存储了lf(%%rip),也即上述汇编里面标号1:的位置。

        上述操作中包含将next->thread->rsp设置到rsp,对于我们要切换到的内核新线程,指向其struct pt_regs起始位置。 

        然后将next->thread->rip入栈,接着跳转到__switch_to继续执行

inline void __switch_to(struct task_struct *prev,struct task_struct *next)
{
	unsigned int color = 0;
	init_tss[SMP_cpu_id()].rsp0 = next->thread->rsp0;
	__asm__ __volatile__("movq	%%fs,	%0 \n\t":"=a"(prev->thread->fs));
	__asm__ __volatile__("movq	%%gs,	%0 \n\t":"=a"(prev->thread->gs));
	__asm__ __volatile__("movq	%0,	%%fs \n\t"::"a"(next->thread->fs));
	__asm__ __volatile__("movq	%0,	%%gs \n\t"::"a"(next->thread->gs));
	wrmsr(0x175, next->thread->rsp0);
	if(SMP_cpu_id() == 0)
		color = WHITE;
	else
		color = YELLOW;

	color_printk(color, BLACK, "prev->thread->rsp :%#018lx\n", prev->thread->rsp);
	color_printk(color, BLACK, "next->thread->rsp :%#018lx\n", next->thread->rsp);
}

        __switch_to里面,先是用next->thread->rsp0设置init_tss[SMP_cpu_id()].rsp0。这样设置后,新线程执行中遭遇中断,异常时,中断和异常使用的栈为next->thread->rsp0。即保持线程被异常,中断打断时,继续使用原来的栈。

        然后,保存fs,gs实时值到pre->thread中。恢复next->thread中fs,gs。

        然后,向MSR的0x175写入next->thread->rsp0。这个作用稍后解释。

        这个函数执行完毕后,继续执行时,将执行出栈并跳转到出栈所得位置。

        这样将从next->thread->rip指示的位置继续执行。同时,rsp此时为next->thread->rsp。

        对于内核新线程,它的next->thread->rip是

extern void kernel_thread_func(void);
__asm__ (
"kernel_thread_func:	\n\t"
"	popq	%r15	\n\t"
"	popq	%r14	\n\t"	
"	popq	%r13	\n\t"	
"	popq	%r12	\n\t"	
"	popq	%r11	\n\t"	
"	popq	%r10	\n\t"	
"	popq	%r9	\n\t"	
"	popq	%r8	\n\t"	
"	popq	%rbx	\n\t"	
"	popq	%rcx	\n\t"	
"	popq	%rdx	\n\t"	
"	popq	%rsi	\n\t"	
"	popq	%rdi	\n\t"	
"	popq	%rbp	\n\t"	
"	popq	%rax	\n\t"	
"	movq	%rax,	%ds	\n\t"
"	popq	%rax		\n\t"
"	movq	%rax,	%es	\n\t"
"	popq	%rax		\n\t"
"	addq	$0x38,	%rsp	\n\t"
"	movq	%rdx,	%rdi	\n\t"
"	callq	*%rbx		\n\t"
"	movq	%rax,	%rdi	\n\t"
"	callq	do_exit		\n\t"
);

        对于内核新线程,此时rsp为next->thread->rsp指向内核新线程的struct pt_regs起始位置。

        上述代码,

        先是恢复pt_regs中一系列寄存器。

        恢复了r15~rax的寄存器,对

unsigned long func;

unsigned long errcode;

unsigned long rip;

unsigned long cs;

unsigned long rflags;

unsigned long rsp;

unsigned long ss;

        采用的是直接跳过。 

        这样操作后,rsp此时指向栈区域尾后位置。

        然后执行

"    movq    %rdx,    %rdi    \n\t"
"    callq    *%rbx        \n\t"

       对于内核新线程,这样是调用init函数。参数是我们初始化时指定的10。

unsigned long init(unsigned long arg)
{
	struct pt_regs *regs;
	color_printk(RED, BLACK, "init task is running,arg:%#018lx\n", arg);
	// 再次设置新线程的thread_struct的rip,rsp
	// rip指向新位置
	current->thread->rip = (unsigned long)ret_system_call;
	// rsp再次指向线程的struct pt_regs
	current->thread->rsp = (unsigned long)current + STACK_SIZE - sizeof(struct pt_regs);
	regs = (struct pt_regs *)current->thread->rsp;
	// 取消当前线程任何标志
	current->flags = 0;
	__asm__	__volatile__	(	"movq	%1,	%%rsp	\n\t"
					"pushq	%2		\n\t"
					"jmp	do_execve	\n\t"
					::"D"(regs),"m"(current->thread->rsp),"m"(current->thread->rip):"memory");

	return 1;
}

        上述代码里面重新设置当前线程的thread->rip,thread->rsp,重置标志。

        然后,设置rsp。

        接着转去执行 do_execve,将regs作为参数。       

unsigned long do_execve(struct pt_regs * regs)
{
	// 为线性地址进行页表注册
	unsigned long addr = 0x800000;
	unsigned long * tmp;
	unsigned long * virtual = NULL;
	struct Page * p = NULL;
	// 更改线程执行现场
	regs->rdx = 0x800000;	//RIP
	regs->rcx = 0xa00000;	//RSP
	regs->rax = 1;	
	regs->ds = 0;
	regs->es = 0;
	
	// 为线性地址进行页表注册
	Global_CR3 = Get_gdt();
	tmp = Phy_To_Virt((unsigned long *)((unsigned long)Global_CR3 & (~ 0xfffUL)) + ((addr >> PAGE_GDT_SHIFT) & 0x1ff));
	virtual = kmalloc(PAGE_4K_SIZE,0);
	// 设置MPL4页表项
	set_mpl4t(tmp,mk_mpl4t(Virt_To_Phy(virtual),PAGE_USER_GDT));
	tmp = Phy_To_Virt((unsigned long *)(*tmp & (~ 0xfffUL)) + ((addr >> PAGE_1G_SHIFT) & 0x1ff));
	virtual = kmalloc(PAGE_4K_SIZE,0);
	// 设置PDPT页表项
	set_pdpt(tmp,mk_pdpt(Virt_To_Phy(virtual),PAGE_USER_Dir));
	tmp = Phy_To_Virt((unsigned long *)(*tmp & (~ 0xfffUL)) + ((addr >> PAGE_2M_SHIFT) & 0x1ff));
	p = alloc_pages(ZONE_NORMAL,1,PG_PTable_Maped);
	// 设置PDT页表项
	set_pdt(tmp,mk_pdt(p->PHY_address,PAGE_USER_Page));
	// 因为更改了页表,让TLB所有缓存失效
	flush_tlb();
	if(!(current->flags & PF_KTHREAD))
		current->addr_limit = 0xffff800000000000;

	color_printk(RED,BLACK,"do_execve task is running, addr_limit_%#018lx\n", current->addr_limit);
	// 设置线性区域0x800000的内容
	memcpy(user_level_function,(void *)0x800000,1024);
	return 1;
}

        do_execv执行完毕,将出栈并跳转执行,也即执行ret_system_call。

        执行时,rsp指向线程的struct pt_regs实例对象起始位置。

ENTRY(ret_system_call)	
	movq	%rax,	0x80(%rsp)		 
	popq	%r15				 
	popq	%r14				 	
	popq	%r13				 	
	popq	%r12				 	
	popq	%r11				 	
	popq	%r10	
	popq	%r9				 	
	popq	%r8				 	
	popq	%rbx				 	
	popq	%rcx				 	
	popq	%rdx				 	
	popq	%rsi				 	
	popq	%rdi				 	
	popq	%rbp				 	
	popq	%rax				 	
	movq	%rax,	%ds			 
	popq	%rax				 
	movq	%rax,	%es			 
	popq	%rax				 
	addq	$0x38,	%rsp
	sti	
	.byte	0x48		 
	sysexit	

        上述先是将 do_execv执行的返回值存储到struct pt_regs的rax。

        然后,恢复pt_regs中一系列寄存器。

        恢复了r15~rax的寄存器,对

unsigned long func;

unsigned long errcode;

unsigned long rip;

unsigned long cs;

unsigned long rflags;

unsigned long rsp;

unsigned long ss;

        采用的是直接跳过。 

        完成恢复后通过sti开启中断,以便可以响应外部中断。

        然后执行

.byte 0x48

sysexit

        

wrmsr(0x174, KERNEL_CS);

// head.S
.section .data
.globl GDT_Table

GDT_Table:
	.quad	0x0000000000000000			/*0	NULL descriptor		       	00*/
	.quad	0x0020980000000000			/*1	KERNEL	Code	64-bit	Segment	08*/
	.quad	0x0000920000000000			/*2	KERNEL	Data	64-bit	Segment	10*/
	.quad	0x0000000000000000			/*3	USER	Code	32-bit	Segment 18*/
	.quad	0x0000000000000000			/*4	USER	Data	32-bit	Segment 20*/
	.quad	0x0020f80000000000			/*5	USER	Code	64-bit	Segment	28*/
	.quad	0x0000f20000000000			/*6	USER	Data	64-bit	Segment	30*/
	.quad	0x00cf9a000000ffff			/*7	KERNEL	Code	32-bit	Segment	38*/
	.quad	0x00cf92000000ffff			/*8	KERNEL	Data	32-bit	Segment	40*/
	.fill	100,8,0					/*10 ~ 11 TSS (jmp one segment <9>) in long-mode 128-bit 50*/
GDT_END:

        这样,sysexit采用的

        代码段选择子:0x28,这是64位特权级3的代码段

        栈段选择子:0x30,这是64位特权级3的数据段

        RDX:因为do_execve中的处理,此时为0x800000,用其设置RIP。

        RCX:因为do_execve中的处理,此时为0xa00000,用其设置RSP。

        这样是用分配得到的2M物理页对应的线性区域的尾后位置作为特权级3的栈基地址了。

void user_level_function()
{
	long ret = 0;
	//	color_printk(RED,BLACK,"user_level_function task is running\n");
	char string[]="Hello World!\n";
	__asm__	__volatile__	(	"leaq	sysexit_return_address(%%rip),	%%rdx	\n\t"
					"movq	%%rsp,	%%rcx		\n\t"
					"sysenter			\n\t"
					"sysexit_return_address:	\n\t"
					:"=a"(ret):"0"(1),"D"(string):"memory");	

	//	color_printk(RED,BLACK,"user_level_function task called sysenter,ret:%ld\n",ret);
	while(1)
		;
}

// task_init
wrmsr(0x174, KERNEL_CS);
wrmsr(0x175, current->thread->rsp0);
wrmsr(0x176, (unsigned long)system_call);

// __switch_to
wrmsr(0x175, next->thread->rsp0);

        这样user_level_function中的内嵌汇编,将会从特权级3跳转到特权级0执行system_call,rsp被赋值为 next->thread->rsp0。

ENTRY(system_call)
	sti
	subq	$0x38,	%rsp			 
	cld;					 

	pushq	%rax;				 	
	movq	%es,	%rax;			 	
	pushq	%rax;				 	
	movq	%ds,	%rax;			 	
	pushq	%rax;				 	
	xorq	%rax,	%rax;			 	
	pushq	%rbp;				 	
	pushq	%rdi;				 	
	pushq	%rsi;				 	
	pushq	%rdx;				 	
	pushq	%rcx;				 
	pushq	%rbx;				 	
	pushq	%r8;				 	
	pushq	%r9;				 	
	pushq	%r10;				 
	pushq	%r11;				 
	pushq	%r12;				 	
	pushq	%r13;				 
	pushq	%r14;				 	
	pushq	%r15;				 	
	movq	$0x10,	%rdx;			 	
	movq	%rdx,	%ds;			 	
	movq	%rdx,	%es;			 
	movq	%rsp,	%rdi			 	
	callq	system_call_function		 

        上述先是执行运行现场保存到栈的struct pt_regs部分,然后调用 system_call_function。传入的参数是struct pt_regs位置。

unsigned long  system_call_function(struct pt_regs * regs)
{
	return system_call_table[regs->rax](regs);
}

        上述通过regs->rax作为索引调用函数。在user_level_function中我们将rax设置为1。

system_call_t system_call_table[MAX_SYSTEM_CALL_NR] = 
{
	[0] = no_system_call,
	[1] = sys_printf,
	[2 ... MAX_SYSTEM_CALL_NR-1] = no_system_call
};

unsigned long sys_printf(struct pt_regs * regs)
{
	color_printk(BLACK,WHITE,(char *)regs->rdi);
	return 1;
}

        这样将执行sys_printf。regs->rdi 在user_level_function中被我们设置为string。

        上述sys_printf执行完毕,sys_call_function也执行完毕,将回到这里继续执行

callq	system_call_function	

ENTRY(ret_system_call)	
	movq	%rax,	0x80(%rsp)		 
	popq	%r15				 
	popq	%r14				 	
	popq	%r13				 	
	popq	%r12				 	
	popq	%r11				 	
	popq	%r10	
	popq	%r9				 	
	popq	%r8				 	
	popq	%rbx				 	
	popq	%rcx				 	
	popq	%rdx				 	
	popq	%rsi				 	
	popq	%rdi				 	
	popq	%rbp				 	
	popq	%rax				 	
	movq	%rax,	%ds			 
	popq	%rax				 
	movq	%rax,	%es			 
	popq	%rax				 
	addq	$0x38,	%rsp
	sti	
	.byte	0x48		 
	sysexit		

        因为前面system_call_function开始执行是rsp指向struct pt_regs起始位置。所以, system_call_function结束时,依然指向struct pt_regs起始位置。

        这样,上述将函数返回值存储到rax。

        然后恢复pt_regs中一系列寄存器。

        恢复了r15~rax的寄存器,对

unsigned long func;

unsigned long errcode;

unsigned long rip;

unsigned long cs;

unsigned long rflags;

unsigned long rsp;

unsigned long ss;

        采用的是直接跳过。 

        完成恢复后通过sti开启中断,以便可以响应外部中断。

        然后执行

.byte    0x48         
sysexit        

         因为void user_level_function()中我们设置了rdx为sysexit_return_address(%%rip),rcx为rsp。所以执行sysexit执行流程将回到

void user_level_function()
{
	long ret = 0;
	color_printk(RED,BLACK,"user_level_function task is running\n");
	char string[]="Hello World!\n";
	__asm__	__volatile__	(	"leaq	sysexit_return_address(%%rip),	%%rdx	\n\t"
					"movq	%%rsp,	%%rcx		\n\t"
					"sysenter			\n\t"
					"sysexit_return_address:	\n\t"
					:"=a"(ret):"0"(1),"D"(string):"memory");	

	//	color_printk(RED,BLACK,"user_level_function task called sysenter,ret:%ld\n",ret);
	while(1)
		;
}

        这样,我们先是从特权级3进入特权级0执行了一个调用,然后又回到特权级3继续向下执行。

        后面,我们while(1);将流程卡在特权级3的代码段。 

        这样我们完整讲述了内核创建新线程,内核主线程执行中新线程被调度执行,新线程环境恢复,新线程如何从内核级代码段跳转到应用层代码段执行,如何在应用层代码段通过系统调用(先是从应用层进入内核层并调用处理过程,再从内核层返回应用层继续执行)享受系统服务的完整过程。

2.2.2.内核新线程响应外部中断再恢复

        2.2.1.完成了从内核主线程到内核新线程的切换。

        内核新线程运行。

        内核新线程运行中,如果发生外部中断,将进入中断处理。

        以HPET的定时器0的中断为例,该中断发生时,依据中断描述符信息,将使用TSS的特权级0的栈作为中断栈。

        前面切换到新线程的__switch_to中init_tss[SMP_cpu_id()].rsp0 = next->thread->rsp0;

        所以,中断栈指向新线程的特权级0的32KB区域尾后位置。

        这样,中断进入时,将保存线程执行现场到32KB的struct pt_regs部分。

        中断处理后,返回时,将依据struct pt_regs恢复寄存器,rsp,rip为被中断时候的值,继续新线程的执行。

2.2.3.内核新线程响应外部中断并在中断返回时,切换到内核主线程

        2.2.2.介绍了内核新线程在运行中被中断的表现。

        我们知道被中断时,内核新线程的特权级0的栈区域的struct pt_regs部分将负责保存新线程此时的执行现场以备后续恢复。

        如果被定时器0中断,且中断处理中,设置了新现场task_struct的调度标志。

        则中断返回时,将触发schedule进行调度。由于此时调度队列只有哨兵节点,所以将切换到内核主线程。并将内核新线程的task_struct加入调度队列。

#define switch_to(prev,next)			\
do{							\
	__asm__ __volatile__ (	"pushq	%%rbp	\n\t"	\
				"pushq	%%rax	\n\t"	\
				"movq	%%rsp,	%0	\n\t"	\
				"movq	%2,	%%rsp	\n\t"	\
				"leaq	1f(%%rip),	%%rax	\n\t"	\
				"movq	%%rax,	%1	\n\t"	\
				"pushq	%3		\n\t"	\
				"jmp	__switch_to	\n\t"	\
				"1:	\n\t"	\
				"popq	%%rax	\n\t"	\
				"popq	%%rbp	\n\t"	\
				:"=m"(prev->thread->rsp),"=m"(prev->thread->rip)		\
				:"m"(next->thread->rsp),"m"(next->thread->rip),"D"(prev),"S"(next)	\
				:"memory"		\
				);			\
}while(0)

        上述将在prev->thread->rsp中存储此时rsp,在prev->thread->rip中存储此时rip。

        然后,将恢复next->thread->rsp。

        然后,先执行__switch_to,
       

inline void __switch_to(struct task_struct *prev,struct task_struct *next)
{
	unsigned int color = 0;
	init_tss[SMP_cpu_id()].rsp0 = next->thread->rsp0;
	__asm__ __volatile__("movq	%%fs,	%0 \n\t":"=a"(prev->thread->fs));
	__asm__ __volatile__("movq	%%gs,	%0 \n\t":"=a"(prev->thread->gs));
	__asm__ __volatile__("movq	%0,	%%fs \n\t"::"a"(next->thread->fs));
	__asm__ __volatile__("movq	%0,	%%gs \n\t"::"a"(next->thread->gs));
	wrmsr(0x175, next->thread->rsp0);
	if(SMP_cpu_id() == 0)
		color = WHITE;
	else
		color = YELLOW;

	//color_printk(color, BLACK, "prev->thread->rsp :%#018lx\n", prev->thread->rsp);
	//color_printk(color, BLACK, "next->thread->rsp :%#018lx\n", next->thread->rsp);
	struct pt_regs *lpReg = (struct pt_regs*)(next->thread->rsp);
	//color_printk(color, BLACK, "__switch_to :%#018lx,rdx:%#018lx,rcx:%#018lx,init_Addr:%#018lx,rsp:%#018lx\n", 
	//	next->thread->rip, lpReg->rdx, lpReg->rcx, init, next->thread->rsp);
}

        __switch_to里面用新线程的thread->rsp0来设置init_tss[SMP_cpu_id()].rsp0。

        然后,存储prev的fs,gs。恢复next的fs,gs。

        __switch_to执行完毕,

        将在出栈并跳转执行next->thread->rip。

        因为前面从内核主线程切换到内核新线程时,我们在内核主线程的thread->rsp存储了当时的rsp,我们在内核主线程的thread->rip存储了当时的rip。

        所以,rip恢复到

"1:    \n\t"    \

        这样继续执行下,先是rax,rbp出栈恢复。

        然后结束schedule()---这里的schedule是之前调度时的函数

reschedule:
	callq	schedule						do schedule
	jmp	RESTORE_ALL

        再然后将执行RESTORE_ALL,因为rsp已经恢复了。

        所以,RESTORE_ALL将像执行中断恢复那样,恢复内核主线程前面执行时的寄存器状态,然后从之前被中断位置继续执行。 

2.2.4.内核主线程执行中响应外部中断并在中断返回时,切换到内核新线程

        我们知道内核主线程被中断时,内核主线程的特权级0的栈区域的struct pt_regs部分将负责保存新线程此时的执行现场以备后续恢复。

        如果被定时器0中断,且中断处理中,设置了新现场task_struct的调度标志。

        则中断返回时,将触发schedule进行调度。此时将切换到内核新线程。

#define switch_to(prev,next)			\
do{							\
	__asm__ __volatile__ (	"pushq	%%rbp	\n\t"	\
				"pushq	%%rax	\n\t"	\
				"movq	%%rsp,	%0	\n\t"	\
				"movq	%2,	%%rsp	\n\t"	\
				"leaq	1f(%%rip),	%%rax	\n\t"	\
				"movq	%%rax,	%1	\n\t"	\
				"pushq	%3		\n\t"	\
				"jmp	__switch_to	\n\t"	\
				"1:	\n\t"	\
				"popq	%%rax	\n\t"	\
				"popq	%%rbp	\n\t"	\
				:"=m"(prev->thread->rsp),"=m"(prev->thread->rip)		\
				:"m"(next->thread->rsp),"m"(next->thread->rip),"D"(prev),"S"(next)	\
				:"memory"		\
				);			\
}while(0)

        上述将在prev->thread->rsp中存储此时rsp,在prev->thread->rip中存储此时rip。

        然后,将恢复next->thread->rsp。

        然后,先执行__switch_to,

inline void __switch_to(struct task_struct *prev,struct task_struct *next)
{
	unsigned int color = 0;
	init_tss[SMP_cpu_id()].rsp0 = next->thread->rsp0;
	__asm__ __volatile__("movq	%%fs,	%0 \n\t":"=a"(prev->thread->fs));
	__asm__ __volatile__("movq	%%gs,	%0 \n\t":"=a"(prev->thread->gs));
	__asm__ __volatile__("movq	%0,	%%fs \n\t"::"a"(next->thread->fs));
	__asm__ __volatile__("movq	%0,	%%gs \n\t"::"a"(next->thread->gs));
	wrmsr(0x175, next->thread->rsp0);
	if(SMP_cpu_id() == 0)
		color = WHITE;
	else
		color = YELLOW;

	//color_printk(color, BLACK, "prev->thread->rsp :%#018lx\n", prev->thread->rsp);
	//color_printk(color, BLACK, "next->thread->rsp :%#018lx\n", next->thread->rsp);
	struct pt_regs *lpReg = (struct pt_regs*)(next->thread->rsp);
	//color_printk(color, BLACK, "__switch_to :%#018lx,rdx:%#018lx,rcx:%#018lx,init_Addr:%#018lx,rsp:%#018lx\n", 
	//	next->thread->rip, lpReg->rdx, lpReg->rcx, init, next->thread->rsp);
}

        __switch_to里面用新线程的thread->rsp0来设置init_tss[SMP_cpu_id()].rsp0。

        然后,存储prev的fs,gs。恢复next的fs,gs。

        __switch_to执行完毕,

        将在出栈并跳转执行next->thread->rip。

        因为前面从内核新线程切换到内核主线程时,我们在内核新线程的thread->rsp存储了当时的rsp,我们在内核新线程的thread->rip存储了当时的rip。

        所以,rip恢复到

 "1:    \n\t"    \

        这样继续执行下,先是rax,rbp出栈恢复。

        然后结束schedule()---这里的schedule是之前调度时的函数

reschedule:
	callq	schedule						do schedule
	jmp	RESTORE_ALL

        再然后将执行RESTORE_ALL,因为rsp已经恢复了。

        所以,RESTORE_ALL将像执行中断恢复那样,恢复内核新线程前面执行时的寄存器状态,然后从之前被中断位置继续执行。 

        

        

        

         

        

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

raindayinrain

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值