进程fork的原理

创建新进程

需要创建新进程的场景:

  • 系统初始化
  • 执行创建新进程的系统调用
  • 用户请求创建
  • 初始化批处理任务

Unix 系统调用:fork/exec 先用fork() 复制出一个不同PID的子进程,然后用exec()重写当前新创建的子进程。之所以要安排两步,是为了在fork之后允许子进程处理父进程的文件描述符,这就可以完成对标准I/O文件和标准错误文件的重定向

具体来说

fork() 创建一个继承的子进程

  • 复制父进程所有变量和内存。只读的内存区是共享的
  • 复制父进程除区分PID的寄存器以外所有的寄存器
  • 子进程返回0,父进程返回子进程PID,这个返回值就是childPID 对于新创建的子进程来说,它的子进程PID就是0

注意:fork()循环两次会产生四个进程!而不是三个!

对于内核线程共享地址空间,对于用户进程是直接复制

exec() 重写子进程

  • pid = = 0 的条件下执行,即要先验证是否为新创建的子进程
  • 第一个参数为将要执行的文件名/bin/** ,第二个参数为指向变量数组的指针,第三个参数为指向环境变量数组的指针用于将终端类型和根目录等信息传给程序
  • 执行会把整个地址空间内容改变

对于父进程,满足pid > 0 它会执行waitpid系统调用,等待直至子进程终止。

可以看出,这两步操作中,第一步的复制是开销昂贵且没有的必要的。于是有了改进:vfork() 加快对exec 的调用。

现在的系统支持写时复制(COW)技术,即把复制延迟到某一进程想要对共享的内存进行改写时才进行。

init特殊进程

因为用到了fork() 很容易想到鸡生蛋蛋生鸡的问题,第一个进程到底是怎么产生的呢?

用户态进程创建之前,首先要创建一个内核态init进程,然而这个init进程是系统中的第二个进程即1号进程,它产生于0号进程idle 进程。

这个内核态的kernel_init 执行init函数转成用户态的第一个进程,读入一个说明终端数量的文件,然后为每个终端创建一个新的进程,就可以运行各种用户态的进程了。

上述过程可描述为:0号进程->1号内核进程->1号用户进程(init进程)->getty进程->shell进程

参见:Linux下1号进程的前世(kernel_init)今生(init进程)

代码例子

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
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>

#define SIZE 5

int nums[SIZE] = {0,1,2,3,4};

int main()
{
int i;
pid_t pid;

pid = fork();

if( pid == 0){
for(i = 0; i < SIZE; i++){
nums[i] *= -i ;
printf("CHILD:%d", nums[i]);
}
}
else if (pid > 0){
wait(NULL);
for(i = 0; i < SIZE; i++){
printf("PARENT:%d",nums[i]);
}
}
return 0;
}

上述代码最终的输出结果为:

CHILD:0 CHILD:-1 CHILD:-4 CHILD:-9 CHILD:-16 PARENT:0 PARENT:1 PARENT:2 PARENT:3 PARENT:4

很明显可以看出子进程是在一块新的内存区域对数据进行改写,所以父进程的数组内容不会受到影响