十九. fork的原理及实现

十九. fork的原理及实现

fork的原理


先通过下面这段代码简单的介绍一下fork这个函数,了解一下它的功能


#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;
    }
}

下图是这段代码的运行结果


看到这个结果是不是很奇怪,为什么if的分支执行到了,else的分支也执行到了。这明显不符合程序执行最基本的原理。这个放到后面再来解释,先来了解一下fork这个函数


pid_t fork();


上面是fork函数的原型,它有三个返回值
- 该进程为父进程时,返回子进程的pid
- 该进程为子进程时,返回0
- fork执行失败,返回-1

那么问题来了,fork它是如何知道一个进程是父进程还是子进程的。

这个就涉及到fork本身的功能了,它的作用是克隆进程,也就是将原先的一个进程再克隆出一个来,克隆出的这个进程就是原进程的子进程,这个子进程和其他的进程没有什么区别,同样拥有自己的独立的地址空间。不同的是子进程是在fork返回之后才开始执行的,就像一把叉子一样,执行fork之后,父子进程就分道扬镳了,所以fork这个名字就很形象,叉子的意思。

这幅图就非常形象



接下来同过ps命令查看一下是否真的出现了两个一样的进程


透过这些现象,来看一下fork的本质。

fork在执行之后,会创建出一个新的进程,这个新的进程内部的数据是原进程所有数据的一份拷贝。因此fork就相当于把某个进程的全部资源复制了一遍,然后让cs:eip指向新进程的指令部分。

fork给父进程返回子进程pid,给其拷贝出来的子进程返回0,这也是他的特点之一,一次调用,两次返回。两次返回看上去有点神秘,实质是在子进程的栈中构造好数据后,子进程从栈中获取到的返回值。

接下来就看看fork的实现


fork的实现


fork的实现分为以下两步
1. 复制进程资源
2. 执行该进程

复制进程的资源包括以下几步
1. 进程pcb
2. 程序体,即代码段数据段等
3. 用户栈
4. 内核栈
5. 虚拟内存池
6. 页表

进行进程的话就比较简单了,只需要将其加入到就绪队列即可,接下来就等待cpu的调度了。

将父进程的pcb、虚拟地址位图拷贝给子进程


static int32_t copy_pcb_vaddrbitmap_stack0(task_struct *child_thread, 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->ticks = child_thread->priority; // 为新进程把时间片充满
      child_thread->parent_pid = parent_thread->pid;
      child_thread->general_tag.prev = child_thread->general_tag.next = NULL;
      child_thread->all_list_tag.prev = child_thread->all_list_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);
      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;

      ASSERT(strlen(child_thread->name) < 11); // pcb.name的长度是16,为避免下面strcat越界
      strcat(child_thread->name, "_fork");
      return 0;
}



复制子进程的进程体(代码和数据)及用户栈


static void copy_body_stack3(task_struct *child_thread, 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;
      uint32_t idx_byte = 0;
      uint32_t idx_bit = 0;
      uint32_t prog_vaddr = 0;

      /* 在父进程的用户空间中查找已有数据的页 */
      while (idx_byte < btmp_bytes_len)
      {
            if (vaddr_btmp[idx_byte])
            {
                  idx_bit = 0;
                  while (idx_bit < 8)
                  {
                        if ((BITMAP_MASK << idx_bit) & vaddr_btmp[idx_byte])
                        {
                              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);
                        }
                        idx_bit++;
                  }
            }
            idx_byte++;
      }
}



为子进程构建thread_stack和修改返回值


static int32_t build_child_stack(task_struct *child_thread)
{
      /* a 使子进程pid返回值为0 */
      /* 获取子进程0级栈栈顶 */
      intr_stack *intr_0_stack = (intr_stack *)((uint32_t)child_thread + PG_SIZE - sizeof(intr_stack));
      /* 修改子进程的返回值为0 */
      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;

      /* ebp在thread_stack中的地址便是当时的esp(0级栈的栈顶),
   即esp为"(uint32_t*)intr_0_stack - 5" */
      uint32_t *ebp_ptr_in_thread_stack = (uint32_t *)intr_0_stack - 5;

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

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



拷贝父进程本身所占资源给子进程


static int32_t copy_process(task_struct *child_thread, 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;
}



/* fork子进程,内核线程不可直接调用 */
pid_t sys_fork(void)
{
      task_struct *parent_thread = running_thread();
      task_struct *child_thread = get_kernel_pages(1); // 为子进程创建pcb(task_struct结构)
      if (child_thread == NULL)
      {
            return -1;
      }
      ASSERT(INTR_OFF == intr_get_status() && parent_thread->pgdir != NULL);

      if (copy_process(child_thread, parent_thread) == -1)
      {
            return -1;
      }

      /* 添加到就绪线程队列和所有线程队列,子进程由调试器安排运行 */
      ASSERT(!elem_find(&thread_ready_list, &child_thread->general_tag));
      list_append(&thread_ready_list, &child_thread->general_tag);
      ASSERT(!elem_find(&thread_all_list, &child_thread->all_list_tag));
      list_append(&thread_all_list, &child_thread->all_list_tag);

      return child_thread->pid; // 父进程返回子进程的pid
}



fork的应用


fork的应用场景非常多,这里只讨论在这里kernel中的应用。

在之后的内容中,将会实现shell,那么这个shell由谁来调用呢。比如说内建的shell命令,他是写死在程序中的,本质上就是一个函数。肯定要有一个东西来调用它

在这个kernel的设计中,会有一个init进程,通过这个init进程fork出一个子进程,这个子进程就专门来处理我们的shell。

下一节就会实现shell了。终于从内核层到了用户层,可以直观的看出效果了。

发布于 2018-05-15

文章被以下专栏收录