项目简介
目前我们已经完成了对操作系统内核的加载,在进入操作系统内核执行后,我们设置了 IDT 表完成了对中断与异常的处理,同时我们设置了时钟中断,中断周期为 10ms 。在本节我们会完成对进程的管理。目前系统的执行流程如下:
x86架构下的任务切换
x86架构下的任务切换借助TSS任务段完成,TSS段中保存了当前任务的上下文信息等,包括通用寄存器(EAX、EBX、ECX、EDX等)、指令指针(EIP)、堆栈指针(ESP)以及程序状态寄存器(EFLAGS)。
我们将其抽象为数据结构如下
//TSS
typedef struct tss_t
{
uint32_t pre_link;
uint32_t esp0, ss0, esp1, ss1, esp2, ss2;
uint32_t cr3;
uint32_t eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi;
uint32_t es, cs, ss, ds, fs, gs;
uint32_t ldt;
uint32_t iomap;
}tss_t;
- pre_link: 指向前一个TSS的链接字段,用于链式TSS结构,通常未使用。
- esp0, ss0: 用于特权级别0(即内核模式)的堆栈指针(esp0)和堆栈段选择器(ss0)。
- esp1, ss1: 用于特权级别1的堆栈指针和段选择子。
- esp2, ss2: 用于特权级别2的堆栈指针和段选择子。
- cr3: 存储页目录基地址寄存器,用于分页机制。
- eip: 存储中断或任务切换时的指令指针。
- eflags: 存储程序状态标志寄存器,包括CPU的状态标志如中断使能等。
- eax, ecx, edx, ebx, esp, ebp, esi, edi: 存储通用寄存器的状态。
- es, cs, ss, ds, fs, gs: 存储段寄存器的值,分别对应额外段、代码段、堆栈段、数据段、附加段、F段寄存器、G段寄存器。
- ldt: 存储局部描述符表(Local Descriptor Table)的段选择器,项目中我们不会使用LDT表。
- iomap: 存储I/O位图的基地址,用于控制对I/O端口的访问,项目中我们不会使用。
TSS任务段的描述符保存在GDT表中,TSS段描述符结构如下:
结构与GDT表描述符相同,我们通过设置TYPE字段标识其为TSS段。
TR寄存器指向GDT表中的当前运行任务的TSS段描述符。
我们可以使用 far jump 切换任务,far jump 指令的操作数是一个内存地址,内存地址中存储的是段寄存器选择子与段偏移量。在切换任务时我们传入的是TSS段的选择子,偏移量为 0 。
在切换任务时,cpu会自动保存当前任务的上下文到TR寄存器指向的TSS段,同时加载要切换的任务的上下文到CPU。
Linux下的任务切换
实际上使用TSS段这种硬件切换任务是非常耗时的,大概需要几十到几百个时钟周期。Linux系统只使用了一个TSS任务段,Linux内核使用栈来保存任务的上下文,每个任务在Linux内核中都有一个内核栈,在切换任务时,它的上下文被保存在内核栈上。这种方法相比较之下更加复杂,简单起见在我们的项目中使用硬件切换。
进程控制块
为了方便管理进程与进程切换,我们定义了一个任务控制块。
//任务状态
enum task_state{TASK_CREATED,TASK_RUNNING,TASK_SLEEP,TASK_READY,TASK_WAITTING};
typedef struct task_t
{
enum task_state state;
int time_ticks;
int slice_ticks;
int sleep_ticks;
char name[TASK_NAME_SIZE];
list_node run_node; //readylise
list_node all_node; //task_list
list_node wait_node;
tss_t tss; //TSS任务段
int tss_sel; //TSS选择子
}task_t;
在任务控制块中,我们定义了三个 list_node 的链表结点,关于这部分代码,不是我们本节的重点,大家可以在代码仓库中查看。这部分的代码定义在 tools 文件夹中。这三个结点用于插入不同的链表队列中。
同时我们定义了一个枚举类型,这个类型用来标识进程的状态。在进程控制块中,我们还有三个整形,time_ticks定义了程序运行的时钟周期数,在进程控制块初始化时我们将 slice_ticks 设置为 time_ticks,在每次定时中断我们会将当前任务的 slice_ticks 减一,当减到零时我们会进行任务切换。sleep_ticks 用于sys_sleep函数,在之后我们会将该函数设置为系统调用。
tss 与 tss_sel 则是任务段与选择子。
在进程控制块初始化时,我们传入四个参数:进程控制块指针,进程名,进程的执行入口,进程的栈。
int tss_init(task_t* task,uint32_t entry,uint32_t esp)
{
int tss_sel=search_gdt_empty();
task->tss_sel=tss_sel;
if(tss_sel < 0)
{
print_log("alloc tss failed.\n");
return -1;
}
//设置全局段描述符表
set_global_segment(tss_sel>>3,(uint32_t)&task->tss,sizeof(tss_t),SEG_P | SEG_DPL0 | SEG_TYPE_TSS);
kernel_memset(&task->tss,0,sizeof(tss_t));
task->tss.eip=entry;
task->tss.esp=task->tss.esp0=esp;
task->tss.ss=task->tss.ss0 = KERNEL_DS << 3;
task->tss.es=task->tss.ds=task->tss.fs=task->tss.gs= KERNEL_DS << 3;
task->tss.cs= KERNEL_CS << 3;
task->tss.eflags= EFLAGES_IF | EFLAGES_DEFAULT;
uint32_t page_dir = memory_create_uvm();
if(page_dir == 0)
{
//释放描述符
gdt_free_sel(tss_sel);
return -1;
}
//创建成功
task->tss.cr3 = page_dir;
return 0;
}
int task_init(task_t* task,const char* name,uint32_t entry,uint32_t esp)
{
ASSERT(task!=0);
tss_init(task,entry,esp);
kernel_strncpy(task->name,name,TASK_NAME_SIZE);
task->state=TASK_CREATED;
task->time_ticks=TASK_TIME_SLICE_DEFAULT;
task->slice_ticks=task->time_ticks;
task->sleep_ticks=0;
list_node_init(&task->all_node);
list_node_init(&task->run_node);
list_node_init(&task->wait_node);
task_set_ready(task);
list_insert_last(&task_mananger.task_list,&task->all_node);
//uint32_t* pesp=(uint32_t*)esp;
//if(pesp)
//{
// *(--pesp)=entry;
// *(--pesp)=0;
// *(--pesp)=0;
// *(--pesp)=0;
// *(--pesp)=0;
// task->stack=pesp;
//}
return 0;
}
进程管理器
为了方便管理进程,我们定义了一个数据结构用于管理进程。
//任务管理器
typedef struct task_mananger_t
{
task_t* cur_task;
list ready_list; //就绪队列
list task_list; //所有队列
list sleep_list; //睡眠队列
task_t first_task;
task_t idle_task;
}task_mananger_t;
在任务管理器中我们定义了三个链表队列,分别为就绪队列,所有队列,睡眠队列。
就绪队列中是所有准备执行的任务,在时钟中断的处理函数中,我们切换的任务就是就绪队列的队头任务。所有队列中存储的是所有的任务,睡眠队列是执行 sys_sleep 设置定时睡眠的任务。
first_task与idle_task分别为系统内核任务与空任务,在所有进程都加入睡眠队列后,系统会切换到空任务中执行。空任务执行的是停机指令 hlt 。
执行这条指令后CPU会进入挂起的低功耗状态,这种状态下CPU不会消耗太多性能。但它仍然能够响应外部中断。一旦CPU接收到中断信号,他会从挂起状态唤醒,处理中断,然后继续执行程序。
同时我们维护一个指针,指针指向当前正在运行任务的进程控制块。
进程管理器的初始化
//任务管理器
static task_mananger_t task_mananger;
//空白进程的栈
static uint32_t idle_task_stack[1024];
//空白进程
static void idle_task_entry()
{
while(1)
{
hlt();
}
}
//进程控制器初始化
void task_mananger_init()
{
list_init(&task_mananger.ready_list);
list_init(&task_mananger.task_list);
list_init(&task_mananger.sleep_list);
task_mananger.cur_task=(task_t*)0;
task_init(&task_mananger.idle_task,"idle_task",(uint32_t)idle_task_entry,(uint32_t)idle_task_stack+1024);
}
任务切换函数
在定时中断处理函数中我们需要对任务进行切换,在任务切换函数中我们下一个应该运行的任务是就绪队列的队头任务。如果下一个要运行的任务不是当前正在运行的任务,我们进行任务切换。我们将任务管理器的当前任务指针更新为下一个要运行的任务。
然后我们将新任务的状态设置为 TASK_RUNNING,表示它现在正在运行。 完成上述任务后我们调用 task_switch_from_to 函数,执行实际的任务切换。
void task_dispatch()
{
task_t * to=task_next_run();
if(to != task_mananger.cur_task)
{
task_t* from= task_mananger.cur_task;
task_mananger.cur_task=to;
to->state=TASK_RUNNING;
task_switch_from_to(from,to);
}
}
//进程切换
void task_switch_from_to(task_t* from,task_t* to)
{
switch_to_tss(to->tss_sel);
}
void switch_to_tss(int tss_sel)
{
far_jump(tss_sel,0);
}
sys_sleep函数
我们会实现一个 sys_sleep 函数,在之后我们会将其封装为系统调用。这个函数很简单,他将当前任务从就绪队列移除,然后加入睡眠队列,同时设置进程控制块的 sleep_ticks。
完成上述工作后我们立即切换任务。
void task_set_sleep(task_t* task,uint32_t ticks)
{
if(ticks==0)
{
return;
}
task->sleep_ticks=ticks;
task->state=TASK_SLEEP;
list_insert_last(&task_mananger.sleep_list,&task->run_node);
}
void task_set_wakeup(task_t* task)
{
list_remove(&task_mananger.sleep_list,&task->run_node);
}
void sys_sleep(uint32_t ms)
{
task_set_block(task_mananger.cur_task);
task_set_sleep(task_mananger.cur_task,(ms+(OS_TICKS_MS))/OS_TICKS_MS);
//切换
task_dispatch();
}
定时中断处理
在完成以上所有工作后 ,我们需要在中断处理函数中,加入对就绪队列和睡眠队列的处理。
我们会通过 task_manager.cur_task 获取当前正在执行的任务的指针。然后我们将当前任务的 slice_ticks 减 1。如果 slice_ticks 减至 0,我们将当前任务的时间片重置为 cur_task->time_ticks。
同时调用 task_set_block(cur_task) 将当前任务从就绪队列移除,然后调用 task_set_ready(cur_task) 将当前任务加入队尾,准备被调度。 然后我们调用 task_dispatch() 进行任务切换,这将触发上下文切换到另一个任务。
同时我们会遍历 task_manager.sleep_list 中的任务,对于每个睡眠任务,如果 sleep_ticks(睡眠剩余计数)减至 0,表示任务的睡眠时间已经结束,我们会调用 task_set_wakeup(task) 唤醒任务。调用 task_set_ready(task) 将任务插入队尾,使其可以被调度。
void do_handler_time(exception_information* frame)
{
sys_tick++;
pic_send_eoi(IRQ0_TIMER);
//必须在后面
task_time_tick();
}
void task_time_tick()
{
task_t* cur_task=task_mananger.cur_task;
if(--cur_task->slice_ticks == 0)
{
cur_task->slice_ticks=cur_task->time_ticks;
task_set_block(cur_task);
task_set_ready(cur_task);
task_dispatch();
}
//唤醒睡眠进程
list_node* cur = task_mananger.sleep_list.first;
while (cur)
{
list_node* next= cur->next;
task_t* task=list_node_parent(cur,task_t,run_node);
if(--task->sleep_ticks==0)
{
task_set_wakeup(task);
task_set_ready(task);
}
cur=next;
}
task_dispatch();
}
这里 task_time_tick() 函数必须都在发送 EOI 信号后调用,否则下一个时钟中断无法被抵达。
测试
这里我们简单定义两个任务测试一下
//栈空间
uint32_t init_task_stack[1024];
static task_t init_task;
void init_task_entry()
{
int count = 0;
while(1)
{
sem_wait(&sem);
print_log("init_task");
//sys_sleep(1000);
//task_switch_from_to(&init_task,task_first());
//sys_sched_yield();
}
}
void init_main()
{
task_init(&init_task,"init_main",(uint32_t)init_task_entry,(uint32_t)&init_task_stack[1024]);
//task_init(&first_task,0,0); //再切换后 0,0会被自动改写。
task_first_init();
sem_init(&sem,0);
open_global_int();
while (1)
{
print_log("init_main");
//task_switch_from_to(task_first(),&init_task);
//sys_sched_yield();
sem_notify(&sem);
sys_sleep(1000);
}
}
执行效果如下:
因为篇幅问题,很多代码,细节 ,目录结构等没有解释,有兴趣的读者可以在gitee上查看完整代码。
代码仓库
x86_os: 从零手写32位操作系统 (gitee.com)