类似于Unix的OS开发-多任务处理和系统调用(7)

在上一篇文章中,我们学习了如何使用虚拟地址空间。 今天,我们将添加多任务支持。

目录


构建系统(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)。

多任务


您还记得,每个进程都应该有自己的地址空间。

为此,您需要计算该进程使用的页数。

因此,我们必须描述该过程的内存:

/* * Process memory description */ struct task_mem_t { void* pages; /* task physical pages */ u_int pages_count; /* task physical pages count */ void* page_dir; /* page directory */ void* page_table; /* page table */ }; 

流程必须以某种方式相互交换信息。 为此,我们引入消息的概念:

 struct message_t { u_short type; /* message type */ u_int len; /* data length */ u8 data[IPC_MSG_DATA_BUFF_SIZE]; /* message data */ }; 

之后,您需要将流程本身描述为循环列表的元素。

此处的循环列表很方便,因为在调度程序中,您需要从每个任务中选择下一个,直到列表为空。

这样,我们消除了退化的情况,这意味着我们简化了逻辑。

 /* * Process descriptor */ struct task_t { struct clist_head_t list_head; /* should be at first */ u_short tid; /* task id */ char name[8]; /* task name */ struct gp_registers_t gp_registers; /* general purpose registers */ struct op_registers_t op_registers; /* other purpose registers */ struct flags_t flags; /* processor flags */ u_int time; /* time of task execution */ bool reschedule; /* whether task need to be rescheduled */ u_short status; /* task status */ int msg_count_in; /* count of incomming messages */ struct message_t msg_buff[TASK_MSG_BUFF_SIZE]; /* task message buffer */ void* kstack; /* kernel stack top */ void* ustack; /* user stack top */ struct task_mem_t task_mem; /* task memory */ } attribute(packed); 

我们将为每个进程分配一个配额,即定时器中断的次数,之后将重新安排该进程。

 #define TASK_QUOTA 3 

接下来,您需要编写一个函数来创建一个新进程。

虽然我们没有特权级别,但是我们将使用内核选择器。

将来,我们将需要2个堆栈,其中一个用于用户模式,另一个用于内核。

我们将初始状态设置为TASK_UNINTERRUPTABLE,以使任务没有时间在完全初始化之前完成。

 /* * Api - Create new task */ extern bool task_create(u_short tid, void* address, struct task_mem_t *task_mem) { struct task_t* task; struct clist_head_t* entry; /* allocate memory */ 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); /* fill data */ 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)); /* set flags */ *(u32*)(&task->flags) = asm_get_eflags() | 0x200; /* set general purpose registers */ memset(&task->gp_registers, 0, sizeof(struct gp_registers_t)); /* set other purpose registers */ 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,则该进程将被调度程序删除。
删除时,我们只需要释放堆栈,为数据和代码段分配的页面,并销毁进程页面的目录即可。

通常,对于一个好的用户堆栈,您可以通过内存管理器进行分配,但是为了方便起见,调试时要在内核堆(始终在页面目录中)中实现它。

 /* * Api - Delete task by id */ extern void task_delete(struct task_t* task) { printf(MSG_SCHED_TID_DELETE, (u_int)task->tid); assert(task != null); /* free stack memory */ free(task->kstack); free(task->ustack); task->kstack = null; task->ustack = null; /* free user pages memory */ 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; } /* clear resources */ 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状态,则需要完成当前任务。
之后,我们为下一个任务准备堆栈框架并切换上下文。

 /* * Api - Schedule task to run */ extern void sched_schedule(size_t* ret_addr, size_t* reg_addr) { struct task_t* next_task = null; /* finish current task */ if (current_task != null) { /* update running time */ current_task->time += 1; /* check quota exceed */ if (current_task->time < TASK_QUOTA && !current_task->reschedule) { return; /* continue task execution */ } /* reset quota */ current_task->time = 0; current_task->reschedule = false; /* save task state */ current_task->op_registers.eip = *ret_addr; current_task->op_registers.cs = *(u16*)((size_t)ret_addr + 4); *(u32*)(&current_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(&current_task->gp_registers, (void*)reg_addr, sizeof(struct gp_registers_t)); } /* pick next task */ 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); /* whether should kill current task */ if (current_task && current_task->status == TASK_KILLING) { /* kill current task */ task_delete(current_task); } else { /* try to kill killing tasks */ struct task_t* task; task = task_find_by_status(TASK_KILLING); if (task) { task_delete(task); } } /* prepare context for the next 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)); /* update current task pointer */ current_task = next_task; /* run 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 

我们在流程之间实现最简单的消息传递机制。

当我们将消息发送到流程时,如果消息已冻结,则需要将其解冻,因为它正在等待消息。

但是,当我们收到一条消息时,如果消息队列为空,则必须冻结该过程。

之后,您需要将控制权转移到调度程序。

 /* * Api - Send message to task */ extern void ksend(u_short tid, struct message_t* msg) { struct task_t* task; /* get target task */ task = task_get_by_id(tid); /* put message to task buffer */ task_pack_message(task, msg); /* whether task has frozen */ if (task->status == TASK_INTERRUPTABLE) { /* defrost task */ task->status = TASK_RUNNING; } } /* * Api - Receive message to task * This function has blocking behaviour */ extern void kreceive(u_short tid, struct message_t* msg) { struct task_t* task_before; /* before yield */ struct task_t* task_after; /* after yield */ /* get current task */ task_before = sched_get_current_task(); assert(tid == task_before->tid); assert(task_before->status == TASK_RUNNING); /* whether task has not incomming messages */ if (task_before->msg_count_in == 0) { /* freeze task */ task_before->status = TASK_INTERRUPTABLE; } /* wait fot message */ sched_yield(); /* get current task */ task_after = sched_get_current_task(); assert(task_after == task_before); assert(tid == task_after->tid); assert(task_after->status == TASK_RUNNING); /* fetch message from buffer */ task_extract_message(task_after, msg); assert(msg != null); } 

现在,您需要编写系统调用以使用用户应用程序中的进程。

在那里,我们将引发系统调用以发送和接收消息。

 /* * Api - Syscall handler */ 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(); /* handle syscall */ switch (*function) { case SYSCALL_KSEND: { /* send message */ u_short tid = *(u_int*)params_addr; ksend(tid, *(struct message_t**)(params_addr + 4)); break; } case SYSCALL_KRECEIVE: { /* receive message */ u_short tid = *(u_int*)params_addr; kreceive(tid, *(struct message_t**)(params_addr + 4)); break; } case SYSCALL_KILL: { /* kill task */ 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: { /* exit task */ 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: { /* get tasks 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内核 开发过程的描述。

Source: https://habr.com/ru/post/zh-CN468057/


All Articles