fork 基本用法
作用:通过系统调用创建一个与原来进程几乎完全相同的进程。系统为新的进程分配资源,将原来的进程的所有数据都复制到新的新进程中,除了某些细节有所不同,在某种意义上相当于克隆了一个自己。
我们来看一段代码,先简单了解一下fork函数的功能:
#include <iostream>
#include <unistd.h>
using namespace std;
int main(){
int num = 0;
pid_t id = fork();
cout << getpid() <<",父进程为" << getppid() << endl;
}
以上还用到了两个函数:
getpid()
:获取进程IDgetppid()
:获取进程父进程ID
运行结果为:
显然,cout << getpid() <<",父进程为" << getppid() << endl;
这条代码执行了两次,这是因为ID为7541的进程也就是我们上述代码生成的进程中由于fork
分叉出了7542的子进程。该子进程中拥有父进程中所有数据,他们执行的代码完全相同。
fork 特点
1. 返回值
fork
函数如果执行成功,就会生成一个新的进程,该进程的父进程为原进程。但是我们该如何区分这两个进程呢?这就要说到我们fork
函数的返回值,该函数很特殊,它执行一次,返回值却有两个。在原进程也就是新生进程的父进程中,该函数返回值为新生进程ID,在新生进程中,该函数的返回值为0。所以我们可以通过区分fork
函数的返回值区分当前正在处理的进程是父进程还是子进程。
除此以外,fork
函数的返回值有三种值,具体如下表:
返回值 | 含义 |
---|---|
-1 | 失败 |
0 | 目前处于子进程逻辑控制流 |
其他(子进程PID) | 目前处于父进程逻辑控制流 |
执行下面的代码:
#include <iostream>
#include <unistd.h>
using namespace std;
int main(){
int num = 0;
pid_t id = fork();
if(id == 0){ // 子进程
cout <<"这是刚创建的子进程:" << getpid() <<",父进>程为" << getppid() << endl;
}else{ // 父进程
cout << "当前进程为父进程:" << getpid() << ",创建>的子进程为:"<< id << endl;
}
}
代码执行结果如下:
2. 拥有相同且独立的虚拟空间
我们首先介绍一下程序的虚拟地址空间,每一个进程都会对应一个虚拟地址空间,在2位系统中的寻址能力为
2
32
2^{32}
232,也就是4G空间大小,在这种系统中将会默认可以提供4G的空间给程序,其中0-3G
的空间存放该进程的数据、代码等信息,这一部分中各进程的用户空间是彼此独立的,对实际物理地址的映射是不同的。3-4G
是内核空间,主要存放的是机器指令,操作系统内核的各个模块,这一部分是公用的,不同程序对实际物理地址的映射相同。
各变量所处的位置:
变量 | 位置 | 段名字 |
---|---|---|
经过初始化的全局变量和静态变量 | .data | 数据段 |
未经初始化的全局变量和静态变量 | .bss | BSS段 |
函数内部声明的局部变量 | stack | 栈区 |
const修饰的全局变量 | .text | 代码段 |
const修饰的局部变量 | stack | 栈区 |
字符串常量 | .text | 代码段 |
该部分测试代码如下:
#include <iostream>
#include <unistd.h>
using namespace std;
int main(){
int num = 0;
pid_t id = fork();
if(id == 0){ // 子进程
for(int i=0; i<=10; ++i) cout << "子进程:" << &num << ":" << ++num << endl;
}else{ // 父进程
for(int i=0; i<=10; ++i) cout << "父进程:" << &num << ":" << --num << endl;
}
}
由于每个进程都有各自的虚拟空间,所以fork之后得到的新进程和原进程中各有一份相同的数据,甚至连地址都相同,但是这个相同的地址指的只是虚拟空间地址不是真实地址,上述代码运行结果如下,两个进程中的n
不受对方程序改变的影响:
3. 并发执行
fork
创建出的子进程与父进程的执行顺序是“同时”进行的,在这里我们由两个概念,并发与并行。
概念 | 状态 | 硬件 | 特点 |
---|---|---|---|
并发 | 两个或者多个进程在同时存在 | 单核 | 进程指令同时或者交错执行 |
并行 | 两个或者多个进程在同时执行 | 多核 | 一种特殊的并发 |
并行是很好理解的,我们每一个内核可以执行一个进程,并行就是多个处理器同时处理多个进程,它们的时间是连续的,没有资源的冲突。并发是指在一段时间内两个甚至多各程序同时执行,但是在某个时刻,该处理器只能处理一个进程,但是如果我们在一段时间中看待,这两个程序同时都在被处理。
我们用一段代码进行测试:
#include <iostream>
#include <unistd.h>
using namespace std;
int main(){
int num = 0;
pid_t id = fork();
if(id == 0){ // 子进程
for(;;) cout <<"这是刚创建的子进程:" << getpid() <<",父进程>为" << getppid() << endl;
}else{ // 父进程
for(;;) cout << "当前进程为父进程:" << getpid() << ",创建的>子进程为:"<< id << endl;
}
}
4. 共享文件
我们知道fork后的子进程和父进程实际上的物理存储空间是独立的,两者的交互其实都是依托文件进行的,我们执行代码如下:
#include <iostream>
#include <unistd.h>
#include <fstream>
using namespace std;
int main(){
// fork后可以使用文件进行交互
ofstream ofs("./test");
int num = 0;
pid_t id = fork();
if(id == 0){ // 子进程
for(int i=0; i<=10; ++i) ofs << "子进程:" << &num << ":" << ++num << endl;
}else{ // 父进程
for(int i=0; i<=10; ++i) ofs << "父进程:" << &num << ":" << --num << endl;
}
ofs.close();
}
代码执行结果如下:
父进程和子进程的输出都存放在了文件中,两者是可以通过文件的方式进行交互的。
父子进程共享内容
在刚fork之后
- 父子进程共享:data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式;
- 父子进程不同点: 进程ID、fork返回值、父进程ID 、进程运行时间 、闹钟(定时器) 、未决信号集。
除此以外,我们从用户区进程和内核区进程两个方面再来讨论一下这个问题:
- 用户区进程:
父子进程的代码是完全相同的,同时还会复制父进程的数据,但是为了防止无谓的拷贝浪费内存,这种复制采用的往往是读时共享,写时拷贝的方式,也就是只有在任一进程对数据进行写操作时,复制才会发生(先缺页中断,操作系统再给子进程分配内存并复制父进程的数据)。 - 内核区进程:
内核区实际上基本不共享,共享的只有文件描述符和mmap
映射区(可以用其建立共享内存).