十九. fork的原理及实现

fork的原理

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#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这个函数

1
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、虚拟地址位图拷贝给子进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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和修改返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 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了。终于从内核层到了用户层,可以直观的看出效果了。