关于操作系统:从零开始写-OS-内核-进程的实现

2次阅读

共计 5602 个字符,预计需要花费 15 分钟才能阅读完成。

系列目录

  • 序篇
  • 筹备工作
  • BIOS 启动到实模式
  • GDT 与保护模式
  • 虚拟内存初探
  • 加载并进入 kernel
  • 显示与打印
  • 全局描述符表 GDT
  • 中断解决
  • 虚拟内存欠缺
  • 实现堆和 malloc
  • 第一个内核线程
  • 多线程运行与切换
  • 锁与多线程同步
  • 进入用户态
  • 过程的实现
  • 零碎调用
  • 简略的文件系统
  • 加载可执行程序
  • 键盘驱动
  • 运行 shell

过程 Process

在后面几篇中,咱们搭建起了 thread 运行和调度的框架。本篇开始咱们将在 thread 之上,实现过程 process 的治理。

对于线程 thread 和 过程 process 的概念和区别应该是陈词滥调了,这里也不想赘述这些八股文。对于 scroll 这样一个小我的项目的实现来讲,thread 是重点,是骨架,因为 thread 才是工作运行的根本单位;而 process 只是更下层的对一个或多个 threads 的封装,它更多地是负责资源的治理,例如 Linux 零碎中每个 process 治理的内容包含:

  • 虚拟内存;
  • 文件描述符;
  • 信号机制;
  • ……

咱们这个我的项目比较简单,不会波及简单文件系统和信号等内容,所以 process 的最主要职责就是对内存的治理,本篇首先定义 process 的构造,而后次要围绕它的两个性能开展:

  • user stack 治理;
  • page 治理;

在当前的几个篇章中,将会进一步展现 OS 如何加载并运行一个用户可执行程序,这同时将随同着零碎调用 fork / exec 等性能的实现,这些都是以 process 为对象进行的操作。

process 构造

定义 process_struct,即 Linux 里所谓的 pcbprocess control block):

struct process_struct {
  uint32 id;
  char name[32];
  enum process_status status;
  // map {tid -> threads}
  hash_table_t threads;
  // allocate user space stack for threads
  bitmap_t user_thread_stack_indexes;
  // exit code
  int32 exit_code;
  // page directory
  page_directory_t page_dir;
};

typedef struct process_struct pcb_t;

这里只列出了最重要的一些字段,正文应该写的很分明。对于目前而言,这样一个简略的构造足以满足需要了。

user stack 调配

上一篇里曾经提到过,每个 process 下的多个 threads,它们在 user 空间上领有本人的 stack,所以 process 就要负责为它的 threads 调配这些 stack 的地位,其实非常简单,这些 stacks 就是在 3GB 的下方左近顺次排列:

例如咱们规定一个 stack top 的地位,而后每个 stack 规定是 64 KB,这样调配 stack 就非常简单,只须要一个 bitmap 就能够搞定:

#define USER_STACK_TOP   0xBFC00000  // 0xC0000000 - 4MB
#define USER_STACK_SIZE  65536       // 64KB

struct process_struct {
  // ...
  bitmap_t user_thread_stack_indexes;
  // ...
}

能够看到在 create_new_user_thread 函数里,有为 user thread 调配 stack 的过程:

// Find a free index from bitmap.
uint32 stack_index;
yieldlock_lock(&process->lock);
if (!bitmap_allocate_first_free(&process->user_thread_stack_indexes, &stack_index)) {spinlock_unlock(&process->lock);
  return nullptr;
}
yieldlock_unlock(&process->lock);

// Set user stack top.
thread->user_stack_index = stack_index;
uint32 thread_stack_top = USER_STACK_TOP - stack_index * USER_STACK_SIZE;

留神到这里上了锁,因为一个 process 下可能会有多个 threads 竞争。

page 治理

process 的另一个十分重要的工作就是治理该过程的虚拟内存。咱们晓得虚拟内存是以 process 为单位进行隔离的,每个 process 都会保留本人的 page directorypage tables。在 threads 切换时,如果 thread 所属的 process 产生了扭转,那么就须要从新加载 page directory,这在 scheduler 的 context switch 时体现:

void do_context_switch() {
  // ...
  if (old_thread->process != next_thread->process) {process_switch(next_thread->process);
  }
  // ...
}

void process_switch(pcb_t* process) {reload_page_directory(&process->page_dir);
}

复制 page table

显然每个 process 在创立时都须要创立它本人的 page directory,不过一般来说除了 kernel 初始化时的几个原始 kernel 过程,新的 process 都是从已有的过程 fork 而来,用户态 process 更是如此。

题外话,不晓得你是否想过为什么 process 非得从已有的 fork 进去,难道不能间接凭空创立,而后载入新程序运行吗?我想你应该理解 Linux 下 fork 的应用和编程范式,fork 的后果上面还要判断一下当初本人是在 parent 还是 child 过程,而且大多数状况下都是 fork + exec 联结应用,与其这么麻烦,为什么不一个零碎调用搞定呢,例如这样:

int create_process(char* program, int argc, char** argv)

它齐全能够代替 fork + exec 这一对组合。

这外面有 Unix 的历史起因,也有它的设计哲学的思考,网上能够搜下有很多探讨,有人喜爱有人拥护,是一个很难扯的清的问题。既然咱们是菜鸟,权且就仿照 Unix 那样,也用 fork 的形式创立过程。

残缺的 fork 实现将在前面的零碎调用一篇里具体开展,本篇只探讨 fork 过程中十分重要的一个步骤,即 page table 的复制。咱们晓得刚 fork 进去的 child 过程一开始和 parent 的虚拟内存是齐全一样的,这也是为什么 fork 完后就有了两个简直一样的过程运行,这里的起因就是 child 的 page table 是从 parent 复制而来,外面的内容是齐全一样的,这从节俭内存资源来说也是无利的。

然而如果 child 只是 read 内存倒还好,如果产生了 write 操作,那显然父子之间就不能够持续共享这一份内存了,必须各奔前程,这里就波及到了虚拟内存的 写时复制 copy-on-write)技术,这里也会实现之。

本节用到的代码次要是 clone_crt_page_dir 函数。

首先的是创立一个新的 page directory,大小是一个 page,这里为它调配了一个物理 frame 和一个虚构 page,留神这个 page 必须是 page aligned,而后手动将为它们建设映射关系。前面操作这个新的 page directory,就能够间接用虚拟地址拜访。

int32 new_pd_frame = allocate_phy_frame();
uint32 copied_page_dir = (uint32)kmalloc_aligned(PAGE_SIZE);
map_page_with_frame(copied_page_dir, new_pd_frame);

接下来为新的 page directory 建设 page tables 的映射。咱们以前提到过,所有的过程都是共享 kernel 空间的,所以 kernel 空间的 256 个页表是共享的:

因而所有过程的 page directory 中,pde[768] ~ pde[1023] 这 256 个表项都是一样的,只有简略复制过来就能够了。

pde_t* new_pd = (pde_t*)copied_page_dir;
pde_t* crt_pd = (pde_t*)PAGE_DIR_VIRTUAL;

for (uint32 i = 768; i < 1024; i++) {
  pde_t* new_pde = new_pd + i;
  if (i == 769) {
    new_pde->present = 1;
    new_pde->rw = 1;
    new_pde->user = 1;
    new_pde->frame = new_pd_frame;
  } else {*new_pde = *(crt_pd + i);
  }
}

然而留神有一个 pde 是非凡的,就是第 769 项。在虚拟内存初探一篇中具体解说过,第 769 个 pde,也就是 4GB 空间中的第 769 个 4MB 空间,咱们将它用来映射 1024 张 page tables 自身,所以第 769 项须要指向该过程的 page directory:

解决完 kernel 空间,接下来就是须要复制 user 空间的 page tables。这里的每张 page table 都须要复制一份进去,而后设置新 page directory 中的 pde 指向它。留神这里只复制了 page table,而没有持续往下复制 page table 所治理的 pages,这样父子过程所应用的虚拟内存实际上就完全一致:

int32 new_pt_frame = allocate_phy_frame();

// Copy page table and set ptes copy-on-write.
map_page_with_frame(copied_page_table, new_pt_frame);
memcpy((void*)copied_page_table,
       (void*)(PAGE_TABLES_VIRTUAL + i * PAGE_SIZE),
       PAGE_SIZE);

这里对 page table 的复制,和 page directory 一样,咱们都是手动调配了物理 frame 和虚构 page,并且建设映射,所有的内存操作都应用虚拟地址。

接下来是要害的一步,因为父子过程共享了用户空间的所有虚拟内存,然而在 write 时有须要将它们隔离,所以这里引入了 copy-on-write 机制,也就是说临时将父子的 page table 中的所有无效 pte 都标记为 read-only,谁如果试图进行 write 操作就会触发 page fault,在 page fault handler 中,将会复制这个 page,而后让 pte 指向这个新复制进去的 page,这样就实现了隔离:

// Mark copy-on-write: increase copy-on-write ref count.
for (int j = 0; j < 1024; j++) {pte_t* crt_pte = (pte_t*)(PAGE_TABLES_VIRTUAL + i * PAGE_SIZE) + j;
  pte_t* new_pte = (pte_t*)copied_page_table + j;
  if (!new_pte->present) {continue;}
  crt_pte->rw = 0;
  new_pte->rw = 0;
  int32 cow_refs = change_cow_frame_refcount(new_pte->frame, 1);
}

copy-on-write 异样解决

下面曾经说到了 copy-on-write 的原理,当触发了 copy-on-write 引起的 page fault 后,就须要在 page fault handler 里解决这个问题,相应的代码在这里。

留神这种类型的 page fault 产生的判断条件是:

if (pte->present && !pte->rw && is_write)

即 page 是存在映射的,但被标记为了 read-only,而且以后引发 page fault 的操作是一个 write 操作。

咱们应用了一个全局的 hash table,用来保留 frame 被 fork 过几次,即它以后被多少个 process 所共享。每次进行 copy-on-write 的解决,都会将它的援用计数减 1,如果依然有援用,则须要 copy;否则阐明这是最初一个 process 援用了,则它能够独享这个 frame 了,能够间接将它标记为 rw = true

int32 cow_refs = change_cow_frame_refcount(pte->frame, -1);
if (cow_refs > 0) {
  // Allocate a new frame for 'copy' on write.
  frame = allocate_phy_frame();
  void* copy_page = (void*)COPIED_PAGE_VADDR;
  map_page_with_frame_impl((uint32)copy_page, frame);
  memcpy(copy_page,
         (void*)(virtual_addr / PAGE_SIZE * PAGE_SIZE),
         PAGE_SIZE);
  pte->frame = frame;
  pte->rw = 1;

  release_pages((uint32)copy_page, 1, false);
} else {pte->rw = 1;}

总结

本篇只是 process 的开篇,次要定义了 process 的根本数据结构,实现了 process 对内存的治理性能,这也是在这个我的项目中 process 最重要的职责之一。前面几篇中咱们将开始真正地创立 process,并且将会从磁盘上加载用户可执行文件运行,也就是 fork + exec 零碎调用的经典组合。

正文完
 0