手写简易操作系统(二十七)--实现fork

前情提要

前面我们已经实现了文件系统,后面我们将实现linux中非常重要的一个调用,fork

fork是什么?叉子?对,fork就是叉子,没有比这个更完美的答案了。

一、什么是fork

fork函数原型是pid_t fork(void),返回值是数字,该数字有可能是子进程的pid,有可能是0,也有可能是−1。为了让父进程获知自己的孩子是谁,fork会给父进程返回子进程的pid。子进程可以通过系统调用getppid获知自己的父亲是谁,并且没有pid为0的进程,因此fork给子进程返回0,以从返回值上和父进程区分开来。如果fork失败了,返回的数字便是−1,自然也没有子进程产生。

让我们看一下下面的例子

#include "unistd.h"
#include "stdio.h"

int main() {
    int pid = fork();
    if (pid == -1) {
        return -1;
    }
    if (pid) {
        printf("I am father, my pid is %d\n",getpid());
        return 0;
    } else {
        printf("I am child, my pid is %d\n",getpid());
        return 0;
    }
}

image-20240408150733582

在fork函数调用之后,后面会根据pid的结果跳转到不同的分支,注意,此时已经有两个进程在执行相同的代码了,只是两个进程中fork的返回值不同。

如果进程的pid值非0,那么说明此时执行的是父进程。

如果进程的pid值为0,那么说明此时执行的是子进程。

fork之后,由之前的一个进程变成了两个进程,fork的作用就是克隆进程。进程拥有独立的地址空间,因此两个进程执行的是独立且相同的代码,也就是两套代码,而且它们各自的指令中都包括fork调用,只是子进程是在fork函数返回之后才开始执行的,因此执行的是fork之后的代码(其实可以让子进程的执行流回到fork之前重新调用fork,但意义不大且非常麻烦),所以在fork之后,父子进程像是“分道扬镳”了。

程序是指在磁盘上存储的文件,是静态的,进程是指程序被加载到内存后,在内存中运行中的程序映像,简而言之,进程就是运行的程序。父子进程的进程体(代码段数据段等)是一模一样的,相当于执行了同一程序的两个实例进程。

二、fork的实现

fork 的实现分为两步,先复制资源,再跳过去执行

fork 需要复制的资源有

(1)进程的pcb,即task_struct,这是让任务有“存在感”的身份证。
(2)程序体,即代码段数据段等,这是进程的实体。
(3)用户栈,不用说了,编译器会把局部变量在栈中创建,并且函数调用也离不了栈。
(4)内核栈,进入内核态时,一方面要用它来保存上下文环境,另一方面的作用同用户栈一样。
(5)虚拟地址池,每个进程拥有独立的内存空间,其虚拟地址是用虚拟地址池来管理的。
(6)页表,让进程拥有独立的内存空间。

克隆出来的进程该如何执行呢?这个简单,只要将新进程加入到就绪队列中就可以啦,当然提前要把相关的栈准备好才行。

#include "fork.h"
#include "process.h"
#include "memory.h"
#include "interrupt.h"
#include "assert.h"
#include "thread.h"    
#include "string.h"
#include "file.h"
#include "mlfq.h"

extern void intr_exit(void);

/**
 * @description: 将父进程的pcb、虚拟地址位图拷贝给子进程
 * @param {task_struct*} child_thread 子进程
 * @param {task_struct*} parent_thread 父进程
 * @return {*}
 */
static int32_t copy_pcb_vaddrbitmap_stack0(struct task_struct* child_thread, struct task_struct* parent_thread) {
    // a 复制pcb所在的整个页,里面包含进程pcb信息及特级0极的栈,里面包含了返回地址, 然后再单独修改个别部分
    memcpy(child_thread, parent_thread, PG_SIZE);
    child_thread->pid = fork_pid();
    child_thread->elapsed_ticks = 0;
    child_thread->status = TASK_READY;
    child_thread->parent_pid = parent_thread->pid;
    child_thread->general_tag.prev = child_thread->general_tag.next = NULL;
    child_thread->all_tag.prev = child_thread->all_tag.next = NULL;
    block_desc_init(child_thread->u_block_desc);
    // b 复制父进程的虚拟地址池的位图
    uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8, PG_SIZE);
    // c 获得一个内核地址页面
    void* vaddr_btmp = get_kernel_pages(bitmap_pg_cnt);
    if (vaddr_btmp == NULL) return -1;
    /* 此时child_thread->userprog_vaddr.vaddr_bitmap.bits还是指向父进程虚拟地址的位图地址
     * 下面将child_thread->userprog_vaddr.vaddr_bitmap.bits指向自己的位图vaddr_btmp */
    memcpy(vaddr_btmp, child_thread->userprog_vaddr.vaddr_bitmap.bits, bitmap_pg_cnt * PG_SIZE);
    child_thread->userprog_vaddr.vaddr_bitmap.bits = vaddr_btmp;
    // pcb.name的长度是16,为避免下面strcat越界
    ASSERT(strlen(child_thread->name) < 11);
    strcat(child_thread->name, "_fork");
    return 0;
}

/**
 * @description: 复制子进程的进程体(代码和数据)及用户栈
 * @param {task_struct*} child_thread 子进程
 * @param {task_struct*} parent_thread 父进程
 * @param {void*} buf_page 
 * @return {*}
 */
static void copy_body_stack3(struct task_struct* child_thread, struct task_struct* parent_thread, void* buf_page) {
    uint8_t* vaddr_btmp = parent_thread->userprog_vaddr.vaddr_bitmap.bits;
    uint32_t btmp_bytes_len = parent_thread->userprog_vaddr.vaddr_bitmap.btmp_bytes_len;
    uint32_t vaddr_start = parent_thread->userprog_vaddr.vaddr_start;
    
    /* 在父进程的用户空间中查找已有数据的页 */
    for (uint32_t idx_byte = 0; idx_byte < btmp_bytes_len; idx_byte++) {
        if (vaddr_btmp[idx_byte]) {
            for (uint32_t idx_bit = 0; idx_bit < 8; idx_bit++) {
                if ((BITMAP_MASK << idx_bit) & vaddr_btmp[idx_byte]) {
                    uint32_t prog_vaddr = (idx_byte * 8 + idx_bit) * PG_SIZE + vaddr_start;
                    // 下面的操作是将父进程用户空间中的数据通过内核空间做中转,最终复制到子进程的用户空间
                    // a 将父进程在用户空间中的数据复制到内核缓冲区buf_page,目的是下面切换到子进程的页表后,还能访问到父进程的数据
                    memcpy(buf_page, (void*)prog_vaddr, PG_SIZE);
                    // b 将页表切换到子进程,目的是避免下面申请内存的函数将pte及pde安装在父进程的页表中
                    page_dir_activate(child_thread);
                    // c 申请虚拟地址prog_vaddr
                    get_a_page_without_opvaddrbitmap(PF_USER, prog_vaddr);
                    // d 从内核缓冲区中将父进程数据复制到子进程的用户空间
                    memcpy((void*)prog_vaddr, buf_page, PG_SIZE);
                    // e 恢复父进程页表
                    page_dir_activate(parent_thread);
                }
            }
        }
    }
}

/**
 * @description: 为子进程构建thread_stack和修改返回值
 * @param {task_struct*} child_thread
 * @return {*}
 */
static int32_t build_child_stack(struct task_struct* child_thread) {
    // a 使子进程pid返回值为0
    struct intr_stack* intr_0_stack = (struct intr_stack*)((uint32_t)child_thread + PG_SIZE - sizeof(struct intr_stack));
    intr_0_stack->eax = 0;

    // b 为switch_to 构建 struct thread_stack,将其构建在紧临intr_stack之下的空间
    uint32_t* ret_addr_in_thread_stack = (uint32_t*)intr_0_stack - 1;
    uint32_t* esi_ptr_in_thread_stack = (uint32_t*)intr_0_stack - 2;
    uint32_t* edi_ptr_in_thread_stack = (uint32_t*)intr_0_stack - 3;
    uint32_t* ebx_ptr_in_thread_stack = (uint32_t*)intr_0_stack - 4;
    uint32_t* ebp_ptr_in_thread_stack = (uint32_t*)intr_0_stack - 5;

    // c switch_to的返回地址更新为intr_exit,直接从中断返回
    *ret_addr_in_thread_stack = (uint32_t)intr_exit;

    // d 这几个值会在出栈时被覆盖
    *ebp_ptr_in_thread_stack = 0;
    *ebx_ptr_in_thread_stack = 0;
    *edi_ptr_in_thread_stack = 0;
    *esi_ptr_in_thread_stack = 0;

    // e 把构建的thread_stack的栈顶做为switch_to恢复数据时的栈顶
    child_thread->self_kstack = ebp_ptr_in_thread_stack;

    return 0;
}

/**
 * @description: 更新inode打开数
 * @param {task_struct*} thread
 * @return {*}
 */
static void update_inode_open_cnts(struct task_struct* thread) {
    int32_t local_fd = 3, global_fd = 0;
    while (local_fd < MAX_FILES_OPEN_PER_PROC) {
        global_fd = thread->fd_table[local_fd];
        ASSERT(global_fd < MAX_FILE_OPEN);
        if (global_fd != -1) {
            file_table[global_fd].fd_inode->i_open_cnts++;
        }
        local_fd++;
    }
}

/**
 * @description: 拷贝父进程本身所占资源给子进程
 * @param {task_struct*} child_thread
 * @param {task_struct*} parent_thread
 * @return {*}
 */
static int32_t copy_process(struct task_struct* child_thread, struct task_struct* parent_thread) {
    // 内核缓冲区,作为父进程用户空间的数据复制到子进程用户空间的中转
    void* buf_page = get_kernel_pages(1);
    if (buf_page == NULL) return -1;

    // a 复制父进程的pcb、虚拟地址位图、内核栈到子进程
    if (copy_pcb_vaddrbitmap_stack0(child_thread, parent_thread) == -1) return -1;

    // b 为子进程创建页表,此页表仅包括内核空间
    child_thread->pgdir = create_page_dir();
    if (child_thread->pgdir == NULL) return -1;

    // c 复制父进程进程体及用户栈给子进程
    copy_body_stack3(child_thread, parent_thread, buf_page);

    // d 构建子进程thread_stack和修改返回值pid
    build_child_stack(child_thread);

    // e 更新文件inode的打开数
    update_inode_open_cnts(child_thread);

    // 释放内核缓冲区
    mfree_page(PF_KERNEL, buf_page, 1);
    return 0;
}

/**
 * @description: fork子进程,内核线程不可直接调用
 * @return {*}
 */
uint32_t sys_fork(void) {
    // 获取父进程pcb
    struct task_struct* parent_thread = running_thread();
    // 为子进程创建pcb(task_struct结构)
    struct task_struct* child_thread = get_kernel_pages(1);
    // 确保关中断以及当前调用的线程不是内核线程
    ASSERT(INTR_OFF == intr_get_status() && parent_thread->pgdir != NULL);
    // 拷贝父进程本身所占资源给子进程
    if (copy_process(child_thread, parent_thread) == -1) return -1;
    // 添加到就绪线程队列和所有线程队列,子进程由调试器安排运行
    mlfq_push_wspt(child_thread);
    // 父进程返回子进程的pid
    return child_thread->pid;    
}

实现了sys_fork,只需要把这个加到中断调用中就好。这里不在赘述

三、实现init进程

在UNIX系统中,init进程是系统的第一个用户级进程,其作用是初始化操作系统并管理系统的运行。具体来说,init进程主要负责以下几个方面的工作:

  1. 系统初始化: 当计算机启动时,init进程是第一个被内核启动的用户级进程。它负责初始化系统的各个方面,包括启动必要的系统服务、加载必要的驱动程序等。
  2. 进程管理: init进程负责创建和管理系统中的其他进程。它根据系统配置文件(如 /etc/inittab/etc/init.d)启动和终止系统服务和守护进程,确保系统中的进程按照预期运行。
  3. 系统运行级别管理: 在UNIX系统中,运行级别(runlevel)是指系统的工作状态,例如单用户模式、多用户模式等。init进程负责根据系统配置文件切换不同的运行级别,以满足用户或系统管理员的需求。
  4. 处理系统关机和重启: 当用户执行关机或重启命令时,init进程负责协调系统的关机和重启过程。它会发送信号给系统中的其他进程,通知它们进行清理和关闭工作,然后执行关机或重启操作。
/**
 * @description: init进程
 * @return {*}
 */
static void init_th(void) {
    uint32_t ret_pid = fork();
    if (ret_pid) {
        printf("i am father, my pid is %d, child pid is %d\n", getpid(), ret_pid);
    }
    else {
        printf("i am child, my pid is %d, ret pid is %d\n", getpid(), ret_pid);
    }
    while (1);
}

需要在线程初始化代码中创建init进程

/**
 * @description: 初始化线程环境
 * @return {*}
 */
void thread_init(void) {
    put_str("thread_init start\n");

    // pid锁初始化
    lock_init(&pid_lock);
    // 多级队列初始化
    mlfq_init();
    // 创建第一个用户进程init
    process_execute(init_th, "init");
    // 创建主线程
    make_main_thread();
    // 创建idle线程
    idle_thread = thread_start("idle", idle, NULL);

    put_str("thread_init done\n");
}

四、仿真

image-20240408161232174

结束语

本节我们实现了fork调用,以及init进程,下节我们将简单的实现一个shell

老规矩,本节的代码地址:https://github.com/lyajpunov/os

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LyaJpunov

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值