共计 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 里所谓的 pcb
(process 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 directory
和 page 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
零碎调用的经典组合。