对应版本v1.4
-
TSS:For each CPU which executes processes possibly wanting to do system calls via interrupts, one TSS is required. The only interesting fields are SS0 and ESP0. Whenever a system call occurs, the CPU gets the SS0 and ESP0-value in its TSS and assigns the stack-pointer to it. So one or more kernel-stacks need to be set up for processes doing system calls. and The actual loading of the TSS must take place in protected mode and after the GDT has been loaded.
-
TR:任务寄存器
-
GDTR和IDTR都是48位寄存器,高32位是基地址,低16位是限长。
-
段寄存器中最低两位是特权级,第三位如果是1表示LDT,如果是0表示GDT。第四位表示所选的LDT还是GDT中的第几个项。
-
TSS:任务状态段
-
创建好进程0后,并不代表进程0就可以使用,还需要将进程0的task_struct和LDT、TSS、GDT相挂接等操作才可以使用。
-
GDT中的每个进程都有一个TSS和LDT,LDT又包括了此进程的代码段和数据段描述符。Linux内核设计的艺术中
-
linux内核设计的艺术中,每个进程(task_union)如下
-
将TR寄存器指向TSS0,LDTR寄存器指向LDT0后,就可以通过TR、LDTR找到进程0的相关信息。
-
除了进程0之外,所有的进程都必须由一个在3特权级的进程创建,因此0进程最终要转变到特权级3上再创建子进程。
-
系统调用过程
以putchar
函数为例
用户在user.c
文件中调用putchar
函数,putchar
函数在user_lib.h
中声明,在user_lib.c
中定义,putchar
函数中调用_sys_putchar
函数,而_sys_putchar
函数中调用内联汇编,执行int 80
中断,将sys_putchar
符号给eax
寄存器来表明中断向量号,其他参数不考虑。
sys_putchar
符号在osdefs.h
中定义在枚举变量sys_call_tlb
中,在sys_call.c
文件中的interrupt_handler_0x80
函数中将中断向量号从eax寄存器中取出后,如果向量号是sys_putchar
,则会调用kern_putchar
内核函数,而这个kern_putchar
内核函数为了简化是在screen.c
文件中用c程序写的。
现在从用户调用putchar
系统调用函数到产生80中断有了,从80中断接收函数到路由到putchar
的内核函数kern_putchar
函数执行过程也有了,只剩下怎么从80中断到80中断的接收函数这个过程。这个关联是在init.c
中在初始化完GDT后初始IDT时,将interrupt_handler_0x80
函数和80中断绑定在一起了。这样就完成了从用户态执行系统调用函数到产生中断再到根据中断号找到对应的路由函数,再去执行这个内核函数代码的过程。
- 用户程序的加载
这里没有通过fork的方式产生1号进程,而是直接先写好用户的c程序,然后在内核main函数的load_user_program
将用户c程序加载到0x400000的用户段。然后通过move_to_user_task
函数跳转到user.c
中用户进程中去执行。
在init.c
文件中init_gdt
时初始化了kernel_task_tss
和user_task_tss
两个段描述符,并加到了gdt表中。然后创建了用户LDT也放到了gdt中。
这样在move_to_user_task
函数中,先将eax寄存器的值放40,也就是gdt[5]的地址,也就是kernel_task_tss
项的地址,通过ltr把tss加载到tr寄存器中,此时任务寄存器就是kernel_task_tss
的内容了。然后再跳转到user_task_tss
的0地址处。注意GDT中的表项中含有很多信息,其中就包括了段基址等。其中设置了用户数据段和代码段以及eip,这样jmp之后就可以执行Testmain函数了。
- labOS_version1.4中内存分配和GDT表示意图
- 运行效果图
对应版本v1.5
- 中断帧interrupt_frame结构体的作用
在https://gcc.gnu.org/onlinedocs/gcc-7.5.0/gcc/x86-Function-Attributes.html#x86-Function-Attributes
一文中提到struct interrupt_frame
结构体在有__attribute__((interrupt))
修饰属性的中断函数里必须是其中的一个参数,那这个结构体有什么作用呢?因为在中断指令执行时,cpu会自动压栈一些寄存器;而在iret中断返回指令执行时,cpu又会自动把这些压栈的值弹出栈。而压栈到出栈的这段时间内,这个结构体的地址就对应到将寄存器压栈的地址,这样通过这个结构体就可以取出刚才压栈的一些寄存器的值了。而这时还可以通过这个结构体来修改压栈的一些值,这样当iret的时候就会改变中断前寄存器的值了。而在我们的这个内核代码中就是通过修改这个结构体中eip和esp等变量的值来实现任务的切换操作的。
- 线性地址到物理地址的映射过程
- 分段分页的概念
x86体系结构分页机制时基于保护模式的,先打开CR0的pe才能打开pg。不存在没有ge的gp。保护模式是基于段的。那么这个过程就是先分好段,然后打开pe,然后分好页,然后打开pg。
一般来说,每个进程都要加载属于自己的代码和数据,这些代码和数据的寻址都是用段加偏移的形式,也就是逻辑地址形式表示的。cpu硬件是自动的将逻辑地址计算为cpu可寻址的线性地址,然后根据操作系统对页目录表和页表的设置,自动将线性地址计算为cpu可寻址的物理地址。