李述铜老师的手写操作系统,复习笔记。
特权级的转换
应用程序如果想调用操作系统的接口,需要切换特权级,拿printf_msg举例
static inline void printmsg(const char *msg,int arg0){
syscall_args_t args;
args.id = SYS_printmsg;
args.arg0=msg;
args.arg1=arg0;
return sys_call(&args);
}
调用这个函数之后,sys_call是这样的
/**
* 执行系统调用
*/
static inline int sys_call (syscall_args_t * args) {
const unsigned long sys_gate_addr[] = {0, SELECTOR_SYSCALL | 0}; // 使用特权级0
int ret;
// 采用调用门, 这里只支持5个参数
// 用调用门的好处是会自动将参数复制到内核栈中,这样内核代码很好取参数
// 而如果采用寄存器传递,取参比较困难,需要先压栈再取
// 这个地方,进入到内核,特权级从3到0,权限升高
__asm__ __volatile__(
"push %[arg3]\n\t"
"push %[arg2]\n\t"
"push %[arg1]\n\t"
"push %[arg0]\n\t"
"push %[id]\n\t"
"lcalll *(%[gate])\n\n"
:"=a"(ret)
:[arg3]"r"(args->arg3), [arg2]"r"(args->arg2), [arg1]"r"(args->arg1),
[arg0]"r"(args->arg0), [id]"r"(args->id),
[gate]"r"(sys_gate_addr));
return ret;
}
接下来,会进入到汇编
exception_handler_systemcall:
// 保存前一任务的状态
pusha
push %ds
push %es
push %fs
push %gs
pushf
// 使用内核段寄存器,避免使用应用层的
mov $(KERNEL_SELECTOR_DS), %eax
mov %eax, %ds
mov %eax, %es
mov %eax, %fs
mov %eax, %gs
// 调用处理函数
mov %esp, %eax
push %eax
call do_handler_syscall
add $4, %esp
// 再切换回来
popf
pop %gs
pop %fs
pop %es
pop %ds
popa
// 5个参数,加上5*4,不加会导致返回时ss取不出来,最后返回出现问题
retf $(5*4) // CS发生了改变,需要使用远跳转
保存当前的状态,然后进入到内核,内核再根据id执行相应的函数
void do_handler_syscall(syscall_frame_t *frame){
// 这里是调用门的处理函数,特权级是0,已经经过了切换
if(frame && frame->func_id< (sizeof(sys_table)/sizeof(syscall_handler_t))){
syscall_handler_t handler=sys_table[frame->func_id];
if(handler){
//调用函数
int ret=handler(frame->arg0,frame->arg1,frame->arg2,frame->arg3);
//frame里的eax,充当函数的返回值
frame->eax=ret;
return;
}
}
// 不支持的系统调用,打印出错信息
task_t * task = get_task_current();
log_printf("task: %s, Unknown syscall: %d", task->name, frame->func_id);
frame->eax = -1; // 设置系统调用的返回值,由eax传递
};
这里的handler是类似于一个函数指针的集合,用来指明函数的入口地址,类似于c的函数指针
static const syscall_handler_t sys_table[]={
[SYS_msleep]=(syscall_handler_t )sys_sleep,
[SYS_getpid]=(syscall_handler_t)sys_getpid,
[SYS_printmsg]=(syscall_handler_t)sys_print_msg,
[SYS_fork]=(syscall_handler_t)sys_fork,
};
这样,就保证了特权级的切换,类似于fork这样有返回值的系统调用函数,把返回值放到eax里即可。汇编就直接抄老师的代码了。
getpid()
这个函数返回当前进程唯一的序列号,要是从0开始,直接定义一个static uint32_t即可,加个锁就好了。这里采用的是,取页表的地址,保存在task_t 结构里
typedef struct _task_t{
enum {
TASK_CREATED,
TASK_RUNNING,
TASK_SLEEP,
TASK_READY,
TASK_WAITING,
TASK_BLOCK,
}state;
char name[TASK_NAME_SIZE]; // 任务名字
int slice_ticks_default; // 时间片
int slice_ticks; // 递减时间片计数, 一个代表一个系统周期,这里是0.1ms
int sleep_ticks; // 睡眠时间片计数
tss_t tss; // 任务的TSS段
uint16_t tss_sel; // tss选择子
list_node_t list_node; // 运行相关结点
list_node_t wait_node; // 存储在信号量中,用于唤醒的节点
uint32_t pid; // 进程pid
struct _task_t *parent_task; // 父进程的task
}task_t;
调用的时候,直接返回即可。
fork
fork 将当前的进程状态都复制一遍,形成第二个进程。
1、创建一个新的task_t
task_t *parent_task=get_task_current();
task_t *child_task=task_alloc();
if(!child_task){
log_printf("alloc task fails");
return -1;
}
task_alloc,就是定义了一个全局的task_t的表,然后在里面找一个空闲的task_t。
static task_t * task_alloc(){
// 在空闲进程表格里分配一个进程
mutex_lock(&task_table_mutex);
task_t *target=(task_t*)0;
for(int i=0;i<sizeof(task_table)/sizeof(task_t);i++){
if(task_table[i].name[0]=='\0'){
target=&task_table[i];
break;
}
}
mutex_unlock(&task_table_mutex);
return target;
}
2、初始化任务的结构,包括tss初始化,在gdt表加一项,将新加入的task_t 放到任务管理器里。
syscall_frame_t * frame = (syscall_frame_t *)(parent_task->tss.esp0 - sizeof(syscall_frame_t));
int err=task_init(child_task, "fork program", 0, frame->eip,
frame->esp + sizeof(uint32_t)*SYSCALL_PARAM_COUNT);
3、复制父进程属性
tss_t * tss = &child_task->tss;
tss->eax = 0; // 子进程返回0
tss->ebx = frame->ebx;
tss->ecx = frame->ecx;
tss->edx = frame->edx;
tss->esi = frame->esi;
tss->edi = frame->edi;
tss->ebp = frame->ebp;
tss->cs = frame->cs;
tss->ds = frame->ds;
tss->es = frame->es;
tss->fs = frame->fs;
tss->gs = frame->gs;
tss->eflags = frame->eflags;
child_task->parent_task = parent_task;
child_task->tss.cr3 =parent_task->tss.cr3;
这个tss是进程切换用的,父进程和子进程不一样,然后这里tss结构的eax用于返回值。
4、复制运行空间,栈,代码段,之类的,操作系统的代码只需要复制一级页表即可,二级页表都是一样的。
uint32_t memory_copy_uvm (uint32_t page_dir){
// 复制一份page_dir 返回新页表的地址
pde_t * dest_pde =(pde_t *)memory_create_uvm();
if(!dest_pde){
goto copy_uvm_failed;
}
// 这个是取操作系统最大内存的页表项, 之后的,就是需要复制的代码
uint32_t user_pde_start=pde_index(MEMORY_TASK_BASE);
//PDE_CNT是一级页表的最大项,4096/4
pde_t *current_pde=(pde_t*)page_dir+user_pde_start;
for(int i=user_pde_start;i<PDE_CNT;i++,current_pde++){
if(current_pde->present==0){
continue;//当前页表无效
}
pte_t *origin_pte=(pte_t *)pde_paddr(current_pde);
for(int j=0;j<PTE_CNT;j++,origin_pte++){
if (!origin_pte->present) {
continue;
}
// 分配物理内存
uint32_t page = addr_alloc_page(&paddr_alloc, 1);
//遍历二级页表的每一项
pte_t *dest_pte=(pte_t *)addr_alloc_page(&paddr_alloc,1);
uint32_t vaddr= (i << 22) | (j << 12); // 前10位,是pde在一级页表的索引,中间十位,是pte在二级页表的索引
int err=memory_create_map(dest_pde,vaddr,page,1,get_pte_perm(origin_pte));
if (err < 0) {
goto copy_uvm_failed;
}
kernel_memcpy(page,(void *)vaddr,MEM_PAGE_SIZE);
}
}
return (uint32_t )dest_pde;
copy_uvm_failed:
if (dest_pde) {
memory_destroy_uvm(dest_pde);
}
return -1;
};