在上一篇文章中,我们学习了如何使用虚拟地址空间。 今天,我们将添加多任务支持。
目录
构建系统(make,gcc,gas)。 初始引导(多次引导)。 启动(qemu)。 C库(strcpy,memcpy,strext)。
C库(sprintf,strcpy,strcmp,strtok,va_list ...)。 以内核模式和用户应用程序模式构建库。
内核系统日志。 显存 输出到终端(kprintf,kpanic,kassert)。
动态内存,堆(kmalloc,kfree)。
内存和中断处理的组织(GDT,IDT,PIC,syscall)。 例外情况
虚拟内存(页面目录和页面表)。
过程。 策划人 多任务处理。 系统调用(kill,exit,ps)。内核(initrd),elf及其内部文件系统。 系统调用(执行)。
字符设备驱动程序。 系统调用(ioctl,fopen,fread,fwrite)。 C库(fopen,fclose,fprintf,fscanf)。
Shell作为内核的完整程序。
用户保护模式(ring3)。 任务状态段(tss)。
多任务
您还记得,每个进程都应该有自己的地址空间。
为此,您需要计算该进程使用的页数。
因此,我们必须描述该过程的内存:
struct task_mem_t { void* pages; u_int pages_count; void* page_dir; void* page_table; };
流程必须以某种方式相互交换信息。 为此,我们引入消息的概念:
struct message_t { u_short type; u_int len; u8 data[IPC_MSG_DATA_BUFF_SIZE]; };
之后,您需要将流程本身描述为循环列表的元素。
此处的循环列表很方便,因为在调度程序中,您需要从每个任务中选择下一个,直到列表为空。
这样,我们消除了退化的情况,这意味着我们简化了逻辑。
struct task_t { struct clist_head_t list_head; u_short tid; char name[8]; struct gp_registers_t gp_registers; struct op_registers_t op_registers; struct flags_t flags; u_int time; bool reschedule; u_short status; int msg_count_in; struct message_t msg_buff[TASK_MSG_BUFF_SIZE]; void* kstack; void* ustack; struct task_mem_t task_mem; } attribute(packed);
我们将为每个进程分配一个配额,即定时器中断的次数,之后将重新安排该进程。
#define TASK_QUOTA 3
接下来,您需要编写一个函数来创建一个新进程。
虽然我们没有特权级别,但是我们将使用内核选择器。
将来,我们将需要2个堆栈,其中一个用于用户模式,另一个用于内核。
我们将初始状态设置为TASK_UNINTERRUPTABLE,以使任务没有时间在完全初始化之前完成。
extern bool task_create(u_short tid, void* address, struct task_mem_t *task_mem) { struct task_t* task; struct clist_head_t* entry; entry = clist_insert_entry_after(&task_list, task_list.head); task = (struct task_t*)entry->data; task->kstack = malloc(TASK_KSTACK_SIZE); task->ustack = malloc(TASK_USTACK_SIZE); task->tid = tid; task->name[0] = '\0'; task->status = TASK_UNINTERRUPTABLE; task->msg_count_in = 0; task->time = 0; memcpy(&task->task_mem, task_mem, sizeof(struct task_mem_t)); *(u32*)(&task->flags) = asm_get_eflags() | 0x200; memset(&task->gp_registers, 0, sizeof(struct gp_registers_t)); task->op_registers.cs = GDT_KCODE_SELECTOR; task->op_registers.ds = GDT_KDATA_SELECTOR; task->op_registers.ss = GDT_KSTACK_SELECTOR; task->op_registers.eip = (size_t)address; task->op_registers.cr3 = (size_t)task_mem->page_dir; task->op_registers.k_esp = (u32)task->kstack + TASK_KSTACK_SIZE; task->op_registers.u_esp = (u32)task->ustack + TASK_USTACK_SIZE; printf(MSG_SCHED_TID_CREATE, tid, (u_int)address, task->kstack, task->ustack); return true; }
如果进程的状态为TASK_KILLING,则该进程将被调度程序删除。
删除时,我们只需要释放堆栈,为数据和代码段分配的页面,并销毁进程页面的目录即可。
通常,对于一个好的用户堆栈,您可以通过内存管理器进行分配,但是为了方便起见,调试时要在内核堆(始终在页面目录中)中实现它。
extern void task_delete(struct task_t* task) { printf(MSG_SCHED_TID_DELETE, (u_int)task->tid); assert(task != null); free(task->kstack); free(task->ustack); task->kstack = null; task->ustack = null; if (task->task_mem.pages_count > 0) { mm_phys_free_pages(task->task_mem.pages, task->task_mem.pages_count); task->task_mem.pages = null; task->task_mem.pages_count = 0; } if (task->task_mem.page_dir != null) { mmu_destroy_user_page_directory(task->task_mem.page_dir, task->task_mem.page_table); } clist_delete_entry(&task_list, (struct clist_head_t*)task); }
现在,您需要编写调度程序本身。
首先,我们需要了解当前任务是正在运行还是第一次运行。
如果正在执行任务,则需要检查其配额是否已用尽。
如果是这样,则需要通过显式指定启用中断标志来保存其状态。
之后,您需要以状态TASK_RUNNING查找下一个要执行的任务。
接下来,如果当前任务处于TASK_KILLING状态,则需要完成当前任务。
之后,我们为下一个任务准备堆栈框架并切换上下文。
extern void sched_schedule(size_t* ret_addr, size_t* reg_addr) { struct task_t* next_task = null; if (current_task != null) { current_task->time += 1; if (current_task->time < TASK_QUOTA && !current_task->reschedule) { return; } current_task->time = 0; current_task->reschedule = false; current_task->op_registers.eip = *ret_addr; current_task->op_registers.cs = *(u16*)((size_t)ret_addr + 4); *(u32*)(¤t_task->flags) = *(u32*)((size_t)ret_addr + 6) | 0x200; current_task->op_registers.u_esp = (size_t)ret_addr + 12; current_task->gp_registers.esp = current_task->op_registers.u_esp; memcpy(¤t_task->gp_registers, (void*)reg_addr, sizeof(struct gp_registers_t)); } if (current_task) { next_task = task_get_next_by_status(TASK_RUNNING, current_task); } else { next_task = task_get_by_status(TASK_RUNNING); tss_set_kernel_stack(next_task->kstack); } assert(next_task != null); if (current_task && current_task->status == TASK_KILLING) { task_delete(current_task); } else { struct task_t* task; task = task_find_by_status(TASK_KILLING); if (task) { task_delete(task); } } next_task->op_registers.u_esp -= 4; *(u32*)(next_task->op_registers.u_esp) = (*(u16*)(&next_task->flags)) | 0x200; next_task->op_registers.u_esp -= 4; *(u32*)(next_task->op_registers.u_esp) = next_task->op_registers.cs; next_task->op_registers.u_esp -= 4; *(u32*)(next_task->op_registers.u_esp) = next_task->op_registers.eip; next_task->gp_registers.esp = next_task->op_registers.u_esp; next_task->op_registers.u_esp -= sizeof(struct gp_registers_t); memcpy((void*)next_task->op_registers.u_esp, (void*)&next_task->gp_registers, sizeof(struct gp_registers_t)); current_task = next_task; printf(MSG_SCHED_NEXT, next_task->tid, next_task->op_registers.u_esp, *ret_addr, next_task->op_registers.eip); asm_switch_ucontext(next_task->op_registers.u_esp, next_task->op_registers.cr3); }
仍然需要编写任务上下文切换功能。
为此,您需要下载一个新的页面目录并还原所有常规寄存器,包括标志。
您还需要切换到下一个任务的堆栈。
接下来,您需要从中断中返回,因为为此我们形成了一个特殊的堆栈框架。
稍后我们将需要它来切换特权级别。
/* * Switch context (to kernel ring) * void asm_switch_kcontext(u32 esp, u32 cr3) */ asm_switch_kcontext: mov 4(%esp),%ebp # ebp = esp mov 8(%esp),%eax # eax = cr3 mov %cr0,%ebx # ebx = cr0 and $0x7FFFFFFF,%ebx # unset PG bit mov %ebx,%cr0 mov %eax,%cr3 or $0x80000001,%ebx # set PE & PG bits mov %ebx,%cr0 mov %ebp,%esp popal sti iretl
我们在流程之间实现最简单的消息传递机制。
当我们将消息发送到流程时,如果消息已冻结,则需要将其解冻,因为它正在等待消息。
但是,当我们收到一条消息时,如果消息队列为空,则必须冻结该过程。
之后,您需要将控制权转移到调度程序。
extern void ksend(u_short tid, struct message_t* msg) { struct task_t* task; task = task_get_by_id(tid); task_pack_message(task, msg); if (task->status == TASK_INTERRUPTABLE) { task->status = TASK_RUNNING; } } extern void kreceive(u_short tid, struct message_t* msg) { struct task_t* task_before; struct task_t* task_after; task_before = sched_get_current_task(); assert(tid == task_before->tid); assert(task_before->status == TASK_RUNNING); if (task_before->msg_count_in == 0) { task_before->status = TASK_INTERRUPTABLE; } sched_yield(); task_after = sched_get_current_task(); assert(task_after == task_before); assert(tid == task_after->tid); assert(task_after->status == TASK_RUNNING); task_extract_message(task_after, msg); assert(msg != null); }
现在,您需要编写系统调用以使用用户应用程序中的进程。
在那里,我们将引发系统调用以发送和接收消息。
extern size_t ih_syscall(u_int* function) { size_t params_addr = ((size_t)function + sizeof(u_int)); size_t result = 0; struct task_t *current = sched_get_current_task(); printf(MSG_SYSCALL, *function, current->tid); asm_lock(); switch (*function) { case SYSCALL_KSEND: { u_short tid = *(u_int*)params_addr; ksend(tid, *(struct message_t**)(params_addr + 4)); break; } case SYSCALL_KRECEIVE: { u_short tid = *(u_int*)params_addr; kreceive(tid, *(struct message_t**)(params_addr + 4)); break; } case SYSCALL_KILL: { u_short tid = *(u_int*)params_addr; struct task_t* task = task_find_by_id(tid); if (task != null) { assert(task->tid == tid); task->status = TASK_KILLING; task->reschedule = true; result = true; } else { result = false; } break; } case SYSCALL_EXIT: { int errno = *(int*)params_addr; u_int tid = current->tid; printf(MSG_TASK_FINISHED, tid, errno); current->status = TASK_KILLING; sched_yield(); break; } case SYSCALL_TASK_LIST: { result = (size_t)task_get_task_list(); break; } default: unreachable(); } printf(MSG_SYSCALL_RET, *function); asm_unlock(); return result; }
为了使用C库中的系统调用,我们需要编写一个导致内核中断的函数。 为简单起见,由于我们没有特权级别,因此我们将不使用特殊的Intel命令。 作为参数,我们将传递系统函数的编号及其所需的参数。
/* * Call kernel * void asm_syscall(unsigned int function, ...) */ asm_syscall: push %ebx push %ebp mov %esp,%ebp mov %ebp,%ebx add $8,%ebx # skip registers add $4,%ebx # skip return address push %ebx # &function int $0x80 mov %ebp,%esp pop %ebp pop %ebx ret
在没有保护环支持的情况下,这足以实现最简单的多任务处理。 现在打开
视频教程并按顺序观看所有内容!
参考文献
视频教程中的详细信息和说明。
git存储库中的源代码(您需要lesson7分支)。
参考文献
1.詹姆斯·莫洛伊(James Molloy)。 滚动自己的玩具UNIX克隆操作系统。
2.牙齿。 DOS,Windows,Unix的汇编器
3.卡拉什尼科夫。 汇编程序很简单!
4. Tanenbaum。 操作系统。 实施与开发。
5.罗伯特·洛夫(Robert Love)。 Linux内核 开发过程的描述。