进程创建就是一个进程向内核申请一个新进程来运行部分代码,原进程就称为父进程,新进程就称为子进程。创建成功后,子进程的代码与父拥有相同的代码,并且都运行到同一位置,接下来分别完成各自的代码。父进程调用 fork 函数来创建子进程,父进程调用 fork 后,内核就分配新的内存空间和内核数据结构给子进程,并且将父进程部分数据结构拷贝给子进程,再将子进程加入到系统进程列表中,最后 fork 返回,调度器开始调度。
1.fork 函数
#include <unistd.h> // 头文件
pid_t fork(void);
// 返回值 子进程返回 0,父进程返回子进程 id, 出错返回 -1
// pid_t 是一个类似整型的类型
// 子进程拷贝了父进程的代码,所以会有两个返回值
// 调用失败的原因可能是系统中进程过多,或者用户的进程数超过了限制
下面是一个例子,子进程只执行 fork 之后的代码:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main()
6 {
7 pid_t pid;
8 printf("before: pid is %d\n", getpid());
9 pid = fork();
10 if(pid < 0){
11 printf("fork error.\n");
12 }
13 printf("after: pid is %d\n", getpid());
14 sleep(1);
15 return 0;
16 }
子进程只执行 fork 之后的代码
2.写时拷贝
父进程创建子进程后,父子代码数据共享,这里采用的拷贝的方式是写时拷贝,就是先以浅拷贝的方式将父进程的所有代码与数据拷贝给子进程,当任意一方需要写入的数据时,采用深拷贝的方式将要修改的部分拷贝给子进程。父进程的代码与数据完整拷贝给子进程是需要较长时间的,而创建子进程很快,所以只是将父进程的内存地址保存给子进程,实质上就是与父进程看到同一份资源,当需要修改时,重新开辟内存,将要修改的部分拷贝给子进程。
3.vfork 函数
fork 函数创建子进程时,父子进程拥有各自独立的地址空间,数据不会互相影响,父子进程谁先被调度也是不确定的,要想保证子进程优先被调度,有 vfork 函数,vfork 函数也是用来创建子进程的,保证子进程先执行,只有子进程退出后,父进程才能继续执行,但是它创建的子进程和父进程共享地址空间,数据会互相影响。
以下为举例说明 fork 和 vfork:
(1)fork
27 int main() // fork
28 {
29 int num = 0;
30 pid_t pid = fork();
31 if(pid < 0){
32 printf("fork error.\n");
33 }else if(pid == 0){
34 // child
35 num = 1;
36 printf("i am child, pid is %d, num is %d\n", getpid(), num);
37 }else{
38 // father
39 printf("i am father, pid is %d, num is %d\n", getpid(), num);
40 sleep(1);
41 }
42 return 0;
43 }
子进程修改 num 数值,并未影响父进程的 num 的值
(2)vfork
6 int main() // vfork
7 {
8 int num = 0;
9 pid_t pid = vfork();
10 if(pid < 0){
11 printf("fork error.\n");
12 }else if(pid == 0){
13 // child
14 num = 1;
15 printf("i am child, pid is %d, num is %d\n", getpid(), num);
16 exit(0);
17 }else{
18 // father
19 printf("i am father, pid is %d, num is %d\n", getpid(), num);
20 sleep(1);
21 }
22 return 0;
23 }
子进程修改了 num 数值,父进程的 num 也改变了,父子进程公用内存空间