TSS
单核CPU想要实现多任务,唯一的方案就是多个任务共享同一个CPU,也就是让CPU在多个任务间轮转。
CPU执行任务时,需要把任务运行所需要的数据加载到寄存器、栈和内存中,因为CPU只能直接处理这些数据,这是CPU在设计上就直接决定的。任务的数据和指令是CPU的处理对象,任务的执行会占用寄存器和内存。
内存相对于CPU来说是低速设备,里面的数据往往被加载到高速寄存器之后被CPU处理,再将结果写到内存中。所以寄存器中的数据就是当前任务的最新状态。采用轮流使用CPU的方式运行多任务,在当前任务被换下CPU的时候,任务的状态应该被保存一份,TSS就是用来关联任务的。
TSS(任务状态段)是由程序员来提供,CPU进行维护。程序员提供是指需要我们定义一个结构体,里面存放任务要用的寄存器数据。CPU维护是指切换任务时,CPU会自动把旧任务的数据存放的结构体变量中,然后将新任务的TSS数据加载到相应的寄存器中
TSS和之前所说的段一样,本质上也是一片存储数据的内存区域,CPU用这块内存区域保存任务的最新状态。所以也需要一个描述符结构来表示它,这个描述符就是TSS描述符,它的结构如下
这个描述符主要关注于它type字段中的B位,B位为1时表示任务繁忙。
任务繁忙有两方面的意义,一是此任务正在CPU上运行,二是此任务嵌套调用了新的任务,CPU此时正在执行这个新的任务,此任务暂时挂起,等到新任务运行完了之后返回此任务继续执行
这个B位是由CPU自动维护的,任务被调到上CPU的时候,CPU自动将此B位置1,被换下CPU的时候,自动将其置0
前面说的TSS描述符是用来描述TSS的,TSS的结构如下图
TSS结构中的数据就是我们保存任务时需要存储的数据,我们提供的保存TSS的数据结构也要照着这个设计。
Linux中采用的任务切换方式
Linux为了提高任务切换的速度,通过如下方式来进行任务切换
一个CPU上的所有任务共享一个TSS,通过TR寄存器保存这个TSS,在使用ltr指令加载TSS之后,该TR寄存器永远指向同一个TSS,之后在进行任务切换的时候也不会重新加载TSS,只需要把TSS中的SS0和esp0更新为新任务的内核栈的段地址及栈指针。
在当初硬件厂商设计TSS的时候,本意是想让一个任务保存一份TSS,这样在切换任务的时候,重新从内存中加载TSS,让TR寄存器指向该TSS,从而实现任务切换。但是这种方式切换任务,每次都要从内存中加载数据,对于CPU来说,速度太慢了,而且切换的步骤也十分繁琐。
Linux的任务切换方式只需要修改TSS中的SS0和esp0,进行任务切换的速度当然是大幅度提升了。
初始化TSS
1 | /* 任务状态段tss结构 */ |
该TSS的结构完全是根据上面图中所需而定义的。
TSS的初始化工作主要是初始化TSS结构的ss0和esp0,然后将TSS描述符加载到全局描述符表中。
1 | void update_tss_esp(task_struct* pthread) |
通过上面的步骤TSS描述符就被加载到gdt中了,可以直接在模拟器中观察当前gdt的数据
实现用户进程
实现原理
实现进程的过程是在之前的线程基础上进行的,在当初创建线程的时候是将栈的返回地址指向了kernel_thread函数,通过该函数调用线程函数实现的,其执行流程如下
这里我们只需要把执行线程的函数换成创建进程的函数就可以啦。
进程与线程最大的区别就每个进程都拥有单独的4GB虚拟地址空间,所以,需要单独为每个进程维护一个虚拟地址池,用来标记该进程中哪些地址被分配了,哪些地址没有被分配
1 | typedef struct tag_task_struct |
该结构是进程线程通用的,是用来管理进程或线程数据的。有些数据是进程特用的,这样在线程使用该结构的时候只需要将这些数据置0即可。在这里将这个结构作为进程的pcb使用。
为用户进程创建页表
1 | // 在虚拟内存池中申请pg_cnt个虚拟页 |
进入3特权级
到目前为止我们一直工作在0级特权级下,这里既然是模仿操作系统的实现,用户进程肯定还是要工作在3特权级下的。这个kernel再简陋,基本的功能还是要有的。
一般情况下,CPU不允许从高特权级转向低特权级,只有从中断返回或者从调用门返回的情况下才可以。
这里采用从中断返回的方式进入3特权级。由于目前还没有用户进程,也就别谈从中断返回了,都没有进入中断何谈从中断返回呢,这里就只能再次欺骗一下CPU,就像之前创建线程一样,制造从中断返回的条件,执行iretd指令了。
iretd指令会用带栈中的数据作为返回地址,还会加载栈中的eflags的值到eflags寄存器,如果栈中的cs.rpl为更低的特权级,处理器的特权级检查通过之后会将栈中的cs载入到CS寄存器。从中断返回的过程就是进入中断的逆过程,所以我们只需要在栈中准备好数据,调用iretd指令即可。
构建用户进程初始上下文信息
1 | void start_process(void *filename_) |
激活页表
1 | /* 激活页表 */ |
创建用户进程的页目录表
1 | uint32_t *create_page_dir(void) |
创建用户进程1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/* 创建用户进程 */
void process_execute(void *filename, char *name)
{
/* pcb内核的数据结构,由内核来维护进程信息,因此要在内核内存池中申请 */
task_struct *thread = get_kernel_pages(1);
init_thread(thread, name, default_prio);
create_user_vaddr_bitmap(thread);
thread_create(thread, start_process, filename);
thread->pgdir = create_page_dir();
enum intr_status old_status = intr_disable();
ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));
list_append(&thread_ready_list, &thread->general_tag);
ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));
list_append(&thread_all_list, &thread->all_list_tag);
intr_set_status(old_status);
}
用户进程一般是由加载器将用户程序加载到内存中,再根据其文件格式解析里面的内容,将程序的段加载到相应的内存地址,CS:EIP指向程序的入口地址,程序便执行起来了。由于目前还没有实现文件系统,只能通过函数来模拟进程的执行,但是产生的效果是一样的。
进程的创建便完成了。由于目前只能模拟一下进程的运行,所以模拟运行的线程函数任然运行在内核空间中,但是进程该有的属性都有了,比如3特权级,自己单独的页表,3特权级栈等。拥有了这些属性就可以称之为一个用户进程
这是运行了进程函数之后cs的值,其低2位的值为11,也就是rpl=3,目前确实运行在3特权级下.