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(®s,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(®s,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(®s, 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将像执行中断恢复那样,恢复内核新线程前面执行时的寄存器状态,然后从之前被中断位置继续执行。