手写操作系统 15.6 fork实现

本文详细解释了在手写操作系统中,如何通过特权级转换实现系统调用,如printf_msg和fork操作。作者介绍了sys_call函数的执行过程,以及如何在内核和用户态之间安全地传递参数,涉及TSS(任务状态段)的使用和页表的复制技术。
摘要由CSDN通过智能技术生成

李述铜老师的手写操作系统,复习笔记。

特权级的转换

应用程序如果想调用操作系统的接口,需要切换特权级,拿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;
};

  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值