点我查看操作系统秘籍连载


Linux进程的创建

在Linux中,除了pid=1的init进程外,所有进程都有父进程,父子进程以树型结构的方式存在,父进程创建出来的多个子进程之间称为兄弟进程。

在Linux中,使用fork()系统调用来创建一个新进程,调用fork()的进程是父进程,新创建出来的进程是子进程。注意:只有操作系统有权限创建进程,其它程序要想创建进程都只能通过发送fork()系统调用请求内核帮忙创建。

也许,通过代码来演示创建子进程的过程是最直观的。这里使用shell伪代码来演示,以便没有任何编程基础的人也能看懂这个过程。但是注意,shell并没有提供fork命令,所以这里的fork是假设它存在的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pid=$(fork)  # 从此开始,有两个进程分支
# 将fork的返回值(假设返回值就是fork伪命令的输出结果)赋值给pid变量
# fork对父进程的返回值是子进程的PID属性,是大于0的数值,所以父进程的pid变量大于0
# fork对子进程的返回值是0,所以子进程的pid变量等于0

# 从这里开始,下面所有的代码都是父子进程共享的,所以父子进程都能执行这些代码
if [ $pid -eq 0 ];then
# pid变量值为0,说明这是子进程分支
# 只有子进程才会进入这段if代码
echo "Child"
exit # 子进程退出
fi

# 虽然这是父、子进程都有的代码段,但是子进程在if中执行了退出,所以只有父进程会执行下面的代码
echo "Parent"

这段伪代码的流程如图。

fork()创建子进程时会复制父进程,因此它会从父进程处继承非常非常多的东西,只有每个进程应该独立的东西才不会继承,例如进程的PID属性是每个进程唯一的,子进程肯定不会去继承父进程,代表父进程的PPID属性的值也是不同的。而像内存中的数据(即虚拟内存),基本上会复制,比如父子进程的堆、栈空间在创建出来时是完全相同的。

因为创建进程需要复制非常多的东西,所以如果全都复制的话,效率会非常低也非常消耗内存资源。所以,在真正创建进程的时候采用的是一种称为写时复制(copy-on-write,COW)的技术来降低这种复制开销。内核在创建子进程的时候,内核会将父子进程的虚拟地址空间的数据页指向同一物理内存的数据页,并且将其标记为只读,这样就不会复制这些数据,当父或子进程要读取这些数据的时候,可以直接从物理内存中读取,而当父或子进程需要修改这些数据的时候,内核才将这个数据页拷贝到父或子进程的地址空间,这样修改的那一页数据只会影响各进程自身,而不会影响其它进程。这就是写时复制技术。

另外需要注意的是,刚创建子进程的时候,虽然父子的虚拟内存是一致的,并在有需要的时候复制相关数据页,但父子进程的代码段是共享的(内核会将代码段设置为只读),也就是说,父子进程拥有完全相同的代码,所以父子进程都能执行从发起fork()系统调用开始后面所有的代码。

Shell下执行的所有命令,都是通过fork+exec+wait让命令运行起来的,这里暂时先不管wait。仍然以shell伪代码来简单演示:

1
2
3
4
5
6
7
8
9
10
pid=$(fork)  # 从此开始,有两个进程分支

if [ $pid -eq 0 ];then
echo "Child Process" # 子进程会执行这行
exec cat a.log # 加载cat程序替换子进程,从此开始子进程称为cat进程
exit # 这行代码是多余的,子进程在exec后会销毁原有的所有代码段
fi

# 只有父进程会执行下面的代码
echo "Parent Process"