代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。
fork函数原型是pid_t fork(void),返回值是数字,该数字有可能是子进程的pid,有可能是0,也有可能是-1,1个函数有3种返回值,Linux中没有获取子进程pid的方法,因此,为了让父进程获知自己的孩子是谁, fork会给父进程返回子进程的pid。 子进程可以通过系统调用getppid获知自己的父亲是谁,并且没有pid为0的进程,因此fork给子进程返回0,以从返回值上和父进程区分开来。 如果fork失败了,返回的数字便是-1,自然也没有子进程产生。
fork函数本质是复制进程
#include<unistd.h>
#include<stdio.h>
int main(){
int pid = fork();
if (pid == -1){
return 1;
}
printf("my pid is %d\n",getpid());
sleep(5);
return 0;
}编译
gcc forkDemo.c -o forkDemo
执行,如图,可以看到产生了两个进程


fork 利用老进程克隆出一个新进程并使新进程执行,新进程之所以能够执行,本质上是它具备程序体,这其中包括代码和数据等资源。 因此 fork 就是把某个进程的全部资源复制了一份,然后让处理器的 cs:eip寄存器指向新进程的指令部分。
故实现fork也要分两步,先复制进程资源,然后再跳过去执行。
进程的资源如下:
- 进程的pcb,即task_struct。
- 程序体,即代码段数据段等,这是进程的实体。
- 用户栈,编译器会把局部变量在栈中创建,并且函数调用也离不了栈。
- 内核栈,进入内核态时,一方面要用它来保存上下文环境,另一方面的作用同用户栈一样。
- 虚拟地址池,每个进程拥有独立的内存空间,其虚拟地址是用虚拟地址池来管理的。
- 页表,让进程拥有独立的内存空间。
将新进程加入到就绪队列中,克隆出来的进程就能执行
修改thread.h的task_struct中,parent_pid表示父进程的pid,也就是自己的父进程是谁。
/* 进程或线程的pcb,程序控制块 */
struct task_struct {
uint32_t* self_kstack; // 各内核线程都用自己的内核栈
pid_t pid;
enum task_status status;
char name[16];
uint8_t priority;
uint8_t ticks; // 每次在处理器上执行的时间嘀嗒数
/* 此任务自上cpu运行后至今占用了多少cpu嘀嗒数,
* 也就是此任务执行了多久*/
uint32_t elapsed_ticks;
/* general_tag的作用是用于线程在一般的队列中的结点 */
struct list_elem general_tag;
/* all_list_tag的作用是用于线程队列thread_all_list中的结点 */
struct list_elem all_list_tag;
uint32_t* pgdir; // 进程自己页表的虚拟地址
struct virtual_addr userprog_vaddr; // 用户进程的虚拟地址
struct mem_block_desc u_block_desc[DESC_CNT]; // 用户进程内存块描述符
int32_t fd_table[MAX_FILES_OPEN_PER_PROC]; // 已打开文件数组
uint32_t cwd_inode_nr; // 进程所在的工作目录的inode编号
int16_t parent_pid; // 父进程pid
uint32_t stack_magic; // 用这串数字做栈的边界标记,用于检测栈的溢出
};
修改thread.c 中的 init_thread 函数中增加一句”pthread->parent_pid=-1″,使任务的父进程默认为-1, -1 表示 没有父进程。
/* 初始化线程基本信息 */
void init_thread(struct task_struct* pthread, char* name, int prio) {
memset(pthread, 0, sizeof(*pthread));
pthread->pid = allocate_pid();
strcpy(pthread->name, name);
if (pthread == main_thread) {
/* 由于把main函数也封装成一个线程,并且它一直是运行的,故将其直接设为TASK_RUNNING */
pthread->status = TASK_RUNNING;
} else {
pthread->status = TASK_READY;
}
/* self_kstack是线程自己在内核态下使用的栈顶地址 */
pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE);
pthread->priority = prio;
pthread->ticks = prio;
pthread->elapsed_ticks = 0;
pthread->pgdir = NULL;
/* 标准输入输出先空出来 */
pthread->fd_table[0] = 0;
pthread->fd_table[1] = 1;
pthread->fd_table[2] = 2;
/* 其余的全置为-1 */
uint8_t fd_idx = 3;
while (fd_idx < MAX_FILES_OPEN_PER_PROC) {
pthread->fd_table[fd_idx] = -1;
fd_idx++;
}
pthread->cwd_inode_nr = 0; // 以根目录做为默认工作路径
pthread->parent_pid = -1; // -1表示没有父进程
pthread->stack_magic = 0x19870916; // 自定义的魔数
}
修改thread.c,增加了个分配 pid的函数
/* 分配 pid */
static pid_t allocate_pid(void) {
static pid_t next_pid = 0; // next_pid 会不断自增
lock_acquire(&pid_lock);
next_pid++;
lock_release(&pid_lock);
return next_pid;
}
/* fork进程时为其分配pid,因为allocate_pid已经是静态的,别的文件无法调用.
不想改变函数定义了,故定义fork_pid函数来封装一下。*/
pid_t fork_pid(void) {
return allocate_pid();
}这么做的原因是allocate_pid是个静态函数,不能被外部调用,同时又不想破坏其原有类型,所以用fork_pid封装它。
对应thread.h添加声明
pid_t fork_pid(void);
接着在kernel/memory.c添加一个函数
/* 安装1页大小的vaddr,专门针对fork时虚拟地址位图无须操作的情况 */
void* get_a_page_without_opvaddrbitmap(enum pool_flags pf, uint32_t vaddr) {
struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
lock_acquire(&mem_pool->lock);
void* page_phyaddr = palloc(mem_pool);
if (page_phyaddr == NULL) {
lock_release(&mem_pool->lock);
return NULL;
}
page_table_add((void*)vaddr, page_phyaddr);
lock_release(&mem_pool->lock);
return (void*)vaddr;
}对应kernel/memory.c添加声明
void* get_a_page_without_opvaddrbitmap(enum pool_flags pf, uint32_t vaddr);
函数的功能同get_a_page类似,只是少了虚拟地址池位图的操作,它接受2个参数,内存池标识pf、虚拟地址vaddr,功能是为vaddr分配一物理页,但无需从虚拟地址内存池中设置位图。
接下来就是真正的逻辑了
新建userprog/fork.c
#include "fork.h"
#include "process.h"
#include "memory.h"
#include "interrupt.h"
#include "debug.h"
#include "thread.h"
#include "string.h"
#include "file.h"
extern void intr_exit(void);
/* 将父进程的pcb、虚拟地址位图拷贝给子进程 */
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->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(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;
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(struct task_struct *child_thread) {
/* a 使子进程pid返回值为0 */
/* 获取子进程0级栈栈顶 */
struct intr_stack *intr_0_stack = (struct intr_stack *) ((uint32_t) child_thread + PG_SIZE -
sizeof(struct 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;
/*** 这三行不是必要的,只是为了梳理thread_stack中的关系 ***/
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;
/**********************************************************/
/* 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更加清晰,其实也不需要,
* 因为在进入intr_exit后一系列的pop会把寄存器中的数据覆盖 */
*ebp_ptr_in_thread_stack = *ebx_ptr_in_thread_stack = \
*edi_ptr_in_thread_stack = *esi_ptr_in_thread_stack = 0;
/*********************************************************/
/* 把构建的thread_stack的栈顶做为switch_to恢复数据时的栈顶 */
child_thread->self_kstack = ebp_ptr_in_thread_stack;
return 0;
}
/* 更新inode打开数 */
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++;
}
}
/* 拷贝父进程本身所占资源给子进程 */
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;
}
/* fork子进程,内核线程不可直接调用 */
pid_t sys_fork(void) {
struct task_struct *parent_thread = running_thread();
struct 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
}
copy_peb_vaddrbitmap_stack0函数接受2个参数,子进程child_thread、父进程parent_thread,功能是 将父进程的pcb、虚拟地址位图拷贝给子进程。
函数开头通过memcpy把父进程的pcb及其内核栈一同复制给子进程。 下面开始单独修改pcb中的属性值。 child_thread->pid = fork_pid()通过fork_pid函数为子进程分配新的pid,此函数仅是 allocate_pid 的封装, 目的是可供外部调用。
接下来的主要是置子进程的status为TASK_READY,目的是让调试器schedule安排其上CPU。 还有将子进程时间片ticks置为child_thread->priority,为其加满时间片,以及将parent_pid置为parent_thread->pid 等。
用child_thread->userprog_vaddr.vaddr_bitmap.bits来管理进程的虚拟地址空间,此时它还是指向父进程虚拟地址位图所在的内核页框,每个进程都是单独的4GB虚拟地址空间,子进程不能和父进程共用同一个虚拟地址位图。因此下面准备复制父进程虚拟地址池的位图给子进程。
通过”block_desc_init(child_thread->u_block_desc) “初始化进程自己的内存块描述符,如果没这句代码的话,此处继承的是父进程的块描述符,子进程分配内存时会导致缺页异常。
先在uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 – USER_VADDR_START) / PG_SIZE / 8, PG_SIZE)计算虚拟地址位图需要的页框数bitmap_pg_cnt,然后在void *vaddr_btmp = get_kernel_pages(bitmap_pg_cnt)申请bitmap_pg_cnt一个内核页框来存储位图。
在memcpy(vaddr_btmp, child_thread->userprog_vaddr.vaddr_bitmap.bits, bitmap_pg_cnt * PG_SIZE)和child_thread->userprog_vaddr.vaddr_bitmap.bits = vaddr_btmp完成了虚拟地址位图的复制。
函数内的最后两行是将子进程的函数名加上了后缀”_fork”,按理说子进程与父进程是同名的,因此这是咱们调试用的,以后能在进程列表中看到区别,调试过后会把这两行代码删掉。
下面是函数copy_body_stack3,它接受3个参数,子进程child_thread、父进程parent_thread、页缓冲 区buf_page,bufpage必须是内核页,我们要用它作为所有进程的数据共享缓冲区。 函数功能是复制子进程的进程体及用户栈。 此函数的主要功能就是拷贝进程的代码和数据资源,也就是复制一份进程体。 按理说进程拥有4GB的虚拟地址空间,其中低3GB的是用户空间,我们只要把用户空间中有效的部分,也就是有数据的部分拷贝出来就行了。
用户使用的内存是用虚拟内存池来管理的,也就是pcb中的userprog_vaddr。 这包括用户进程体占用的内存、堆中申请的内存和和用户栈内存。 我们之前已经了解过进程的内存布局,其中低3GB的虚拟地址空间中,低地址处是进程的数据段、代码段,其余部分是堆和栈共同的空间,堆从低地址往高地址发展,栈从USER_STACK3_VADDR,即0xc0000000-0x1000处往低地址发展。 它们的分布不连续,因此我们要遍历虚拟地址位图中的每一位,这样才能找出进程正在使用的内存。
下面开始寻找父进程占用的内存。
我们的目的是将父进程用户空间中的数据复制到子进程的用户空间。 但用户进程的低3GB空间是独立的,各用户进程不能互相访问彼此的空间,但高1GB是内核空间,内核空间是所有用户进程共享的,因此要想把数据从一个进程拷贝到另一个进程,必须要借助内核空间作为数据中转,即先将父进程用户空间中的数据复制到内核的buf_page中,然后再将buf_page复制到子进程的用户空间中。
为节省缓冲区空间,这里我们采用的方法是:在父进程虚拟地址空间中每找到一页占用的内存,就在子进程的虚拟地址空间中分配一页内存,然后将buf_page中父进程的数据复制到为子进程新分配的虚拟地址空间页,也就是一页一页的对拷,因此我们的buf_page只要1页大小就够了。 但是不同进程之所有拥有单独的虚拟地址空间,原因是它们各自有单独的页目录表,我们在分配内存的时候,会在页表中产生新的pte,如果申请的内存跨4MB的页表大小的话,还要在页目录表中创建pde,既然我们是为子进程分配内存,那么我们要确保这些pte和pde是创建在子进程的页目录表中。 所以在将buf_page的数据拷贝到子进程之前,一定要将页表替换为子进程的页表。
while (idx_byte < btmp_bytes_len)在父进程虚拟地址位图字节长度btmp_bytes_len的范围内逐字节查看位图,if (vaddr_btmp[idx_byte])如果该字节不为0,也就是某位为1,即某个位有效,已分配,下面while (idx_bit < 8)开始逐位查看该字节。通过if ((BITMAP_MASK << idx_bit) & vaddr_btmp[idx_byte])的 if判断,如果某位的值为1,就在prog_vaddr = (idx_byte * 8 + idx_bit) * PG_SIZE + vaddr_start将该位转换为虚拟地址prog_vaddr,接下来通过memcpy将prog_vaddr处的1页复制到buf_page,注意此时只是完成了父进程的数据拷贝到内核空间。
下面在为子进程分配内存之前,先调用”page_dir_activate(child_thread)”激活子进程的页表,然后再调用”get_a_page_without_opvaddrbitmap(PF_USER, prog_vaddr)”为子进程分配1页,接着再调用”memcpy((void*)progvaddr, buf_page, PG_SIZE);”完成内核空间到子进程空间的复制,最后再调用”page_dir_activate(parent_thread)” 将父进程的页表恢复。然后进入下一循环,继续寻找父进程占用的虚拟空间。由于用户栈位于低3GB虚拟空间中的最高处,所以循环到最后时会完成用户栈的复制。
build_child_stack函数接受1个参数,子进程child_thread。 功能是为子进程构建thread_stack和修改返回值。 父进程在执行fork系统调用时会进入内核态,中断入口程序会保存父进程的上下文,这其中包括进程在用户态下的 CS:EIP 的值,因此父进程从 fork 系统调用返回后,可以继续 fork 之后的代码执行。子进程也是从fork后的代码处继续运行的,在这之前我们已经通过函数copy_peb_vaddrbitmap_stack0将父进程的内核栈复制到了子进程的内核栈中,那里保存了返回地址,也就是fork之后的地址,为了让子进程也能继续fork之后的代码运行,我们必须让它同父进程一样,从中断退出,也就是要经过intr_exit.,子进程是由调试器schedule调度执行的,它要用到switch_to函数,而switch_to函数要从栈thread_stack 中恢复上下文,因此我们要想办法构建出合适的thread_stack。
intr_stack栈是在kernel. S中的中断入口程序intr%1entry中保存任务上下文的地方。 函数开头先获得子进程的intr_stack栈的地址,目的是在下一行将eax的值置为0,原因是fork会 为子进程返回0值,根据abi约定,eax寄存器中是函数返回值,因此intr_0_stack->eax = 0将intr_stack栈中的eax置为0。
下面我们要为switch_to函数构建一个thread_stack,这里把它的栈底放在intr_stack栈顶的下面,也就是intr_0_stack的地址减4字节的地方,即uint32_t *ret_addr_in_thread_stack = (uint32_t *) intr_0_stack – 1,此地址是 thread_stack栈中eip的位置。
接下来分别为thread_stack中的esi、edi、ebx、ebp安排位置,这里最重要的是uint32_t *ebp_ptr_in_thread_stack = (uint32_t *) intr_0_stack – 5的指针ebp_ptr_in_thread_stack,它是thread_stack的栈顶,我们必须把它的值存放在peb中偏移为0的地方,即task_struct中的self_kstack处,将来switch_to要用它作为栈顶,并且执行一系列的pop来恢复上下文。下面3个寄存器指针只是为了使thread_stack栈显得更加具有可读性,实际运行中不需要它们的具体值。 *ret_addr_in_thread_stack = (uint32_t) intr_exit将地址 ret_addr_in_thread_stack 处的值赋值为 intr_exit 的地址,也就是 thread_stack 中的 eip 是intr_exit,这就保证了子进程被调度时,可以直接从中断返回,也就是实现了从fork之后的代码处继续执行的目的。
最后在child_thread->self_kstack = ebp_ptr_in_thread_stack把ebp_ptr_in_thread_stack的值,也就是thread_stack的栈顶记录在pcb的self_kstack处,这样 switch_to 便获得了刚刚构建的 thread_stack 栈顶,从而使程序迈向 intr_exit。
update_inode_open_ents函数接受1个参数,线程thread,功能是fork之后,更新线程thread 的inode打开数。遍历fd_table中除前3个标准文件描述符之外的所有文件描述符,从中获得全局文件表file_table的下标global_fd,通过它在file_table中找到对应的文件结构,使相应文件 结构中fd_inode的i_open_cnts加1。
copy_process函数接受2个参数,子进程child_thread和父进程parent_thread,功能是拷贝父进程本身所占资源给子进程。函数开头申请了1页的内核空间作为内核缓冲区,即buf_page,然后调用函数copy_peb_vaddrbitmap_stack0把父进程子的pcb、虚拟地址位图及内核栈复制给子进程,接着调用create_page_dir函数为子进程创建页表,该函数定义在process.c中。 然后调用函数copy_body_stack3复制父进程进程体及用户栈给子进程,接着调用函数build_child_stack为子进程构建 thread_stack,随后调用 update_inode_open_cnts 更新 inode的打开数,最后释放 buf_page。
sys_fork函数即fork的内核实现部分,fork本身是无参数的,因此sys_fork也无参数。 功能是克隆当前进程,即父进程。 函数先调用get_kernel_pages(1)获得1页内核空间作为子进程的pcb。 接下来调用copy_process复制父进程的信息到子进程,然后将其加入到就绪队列和全部队列,最后返回子进程的pid。
接下来把实现fork系统调用
在syscall.h中的enum SYSCALL_NR结构中添加SYS_FORK,和函数声明
/* 用来存放子功能号 */
enum SYSCALL_NR {
SYS_GETPID,
SYS_WRITE,
SYS_MALLOC,
SYS_FREE,
SYS_FORK
};int16_t fork(void);
在syscall.c中添加fork(),原型是pid_t fork(void)。
#include "thread.h"
/* 派生子进程,返回子进程pid */
pid_t fork(void){
return _syscall0(SYS_FORK);
}
在syscall-init.c中的函数syscall_init中,添加代码”syscall_table[SYS_FORK] = sys_fork”
#include "fork.h"
/* 初始化系统调用 */
void syscall_init(void) {
put_str("syscall_init start\n");
syscall_table[SYS_GETPID] = sys_getpid;
syscall_table[SYS_WRITE] = sys_write;
syscall_table[SYS_MALLOC] = sys_malloc;
syscall_table[SYS_FREE] = sys_free;
syscall_table[SYS_FORK] = sys_fork;
put_str("syscall_init done\n");
}
在 Linux 中,init 是用户级进程,它是第一个启动的程序,因此它的 pid是1,后续的所有进程都是它的孩子,故init是所有进程的父进程,所以它还负责所有子进程的资源回收。
修改main.c
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall-init.h"
#include "syscall.h"
#include "stdio.h"
#include "memory.h"
#include "dir.h"
#include "fs.h"
void init(void);
int main(void) {
put_str("I am kernel\n");
init_all();
/******** 测试代码 ********/
/******** 测试代码 ********/
while(1);
return 0;
}
/* init进程 */
void init(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函数先调用fork,派生出子进程,然后在父子进程中分别打印自己的pid以及fork 的返回值 ret_pid。
init是用户级进程,因此要调用process_execute创建进程,pid是从1开始分配的,init的pid是1,因此得早早地创建init进程,抢夺1号pid。
修改thread/thread.c
extern void init(void);
/* 初始化线程环境 */
void thread_init(void) {
put_str("thread_init start\n");
list_init(&thread_ready_list);
list_init(&thread_all_list);
lock_init(&pid_lock);
/* 先创建第一个用户进程:init */
process_execute(init, "init"); // 放在第一个初始化,这是第一个进程,init进程的pid为1
/* 将当前main函数创建为线程 */
make_main_thread();
/* 创建idle线程 */
idle_thread = thread_start("idle", 10, idle, NULL);
put_str("thread_init done\n");
}
执行结果:

参考
郑钢著操作系统真象还原
田宇著一个64位操作系统的设计与实现
丁渊著ORANGE’S:一个操作系统的实现


