fork的原理
先通过下面这段代码简单的介绍一下fork这个函数,了解一下它的功能
1 |
|
下图是这段代码的运行结果
看到这个结果是不是很奇怪,为什么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的实现分为以下两步
- 复制进程资源
- 执行该进程
复制进程的资源包括以下几步
- 进程pcb
- 程序体,即代码段数据段等
- 用户栈
- 内核栈
- 虚拟内存池
- 页表
进行进程的话就比较简单了,只需要将其加入到就绪队列即可,接下来就等待cpu的调度了。
将父进程的pcb、虚拟地址位图拷贝给子进程
1 | static int32_t copy_pcb_vaddrbitmap_stack0(task_struct *child_thread, task_struct *parent_thread) |
复制子进程的进程体(代码和数据)及用户栈
1 | static void copy_body_stack3(task_struct *child_thread, task_struct *parent_thread, void *buf_page) |
为子进程构建thread_stack和修改返回值
1 | static int32_t build_child_stack(task_struct *child_thread) |
拷贝父进程本身所占资源给子进程
1 | static int32_t copy_process(task_struct *child_thread, task_struct *parent_thread) |
1 | /* fork子进程,内核线程不可直接调用 */ |
fork的应用
fork的应用场景非常多,这里只讨论在这里kernel中的应用。
在之后的内容中,将会实现shell,那么这个shell由谁来调用呢。比如说内建的shell命令,他是写死在程序中的,本质上就是一个函数。肯定要有一个东西来调用它
在这个kernel的设计中,会有一个init进程,通过这个init进程fork出一个子进程,这个子进程就专门来处理我们的shell。
下一节就会实现shell了。终于从内核层到了用户层,可以直观的看出效果了。