文章目录
一、fork函数
fork 函数从其执行位置分化为两个进程来执行剩下的代码, 调用 fork 的函数称为父函数, 分化出来的函数称为子函数.
头文件: #include <unistd.h>
声明: pid_t fork(void);
关于返回值, 函数调用成功后就分化为两个进程, 给父进程返回子进程的 pid(大于0), 给子进程返回 0, 函数调用失败后返回 -1.
例子:
cpp
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
pid_t ret = fork(); //以此往后分化为两个进程执行
//往下的代码父、子进程都能看到
//子进程会调用
if (ret == 0)
{
//循环输出pid和ppid
while (1)
{
cout << "I am child, my pid: " << getpid() << ", my ppid: " << getppid() << "." << endl;
sleep(1); //休眠1s, 头文件#include <unistd.h>
}
}
//父进程会调用
else if (ret > 0)
{
while (1)
{
cout << "I am father, my pid: " << getpid() << ", my ppid: " << getppid() << "." << endl;
sleep(1);
}
}
return 0;
}
补充:
pid 可以理解为进程的编号.
ppid 就是某个进程的父进程的 pid.
getpid(): 哪个进程调用就返回哪个进程的 pid.
getppid(): 哪个进程调用就返回哪个进程的 ppid.
运行结果就是死循环的往控制台输出父进程和子进程的 pid 和 ppid:
通过指令:
ps axj | head -1 && ps axj | grep ./process | grep -v grep
- ps axj: 查看目前运行的所有进程.
- head -1: 通过 | (管道)将目前运行的所有进程过滤, 只保留第一行.
- &&: 逻辑与(并且), 将前半部分的内容与后半部分的内容拼接输出.
- grep ./process: 通过 | (管道)将目前运行的所有进程过滤, 只保留 ./process 这个进程的信息.
- grep -v grep: grep也是一个进程, 会把自己也筛选出来, -v grep 表示不包含 grep.
运行结果:
可以看到和前面输出的一致, 那么此时的父进程的父进程的 pid 为 10202, 那么这个 pid 为 10202 的进程是谁呢?
可以看到它是 bash, bash就是命令行解释器, 也就是这个东西:
它是所有我们在此输入的指令的父进程.
二、进程地址空间
了解认识了 fork 函数之后, 存在一个疑问, 那就是在之前的认知里, 一个函数只有一个返回值, 那么 fork 是如何做到成功后返回两个值的呢? 通过一段代码再来观察一下:
cpp
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
pid_t ret = fork(); //以此往后分化为两个进程执行
//往下的代码父、子进程都能看到
//子进程会调用
if (ret == 0)
{
cout << "child ret: " << ret << ", ret address: " << &ret << endl;
}
//父进程会调用
else if (ret > 0)
{
cout << "father ret: " << ret << ", ret address: " << &ret << endl;
}
sleep(1);
return 0;
}
运行结果:
通过此现象可以看到, 相同地址的变量 ret 存储的值居然是不同的, 那么此时可以断定该地址并不是真实的物理地址(真实的内存中的地址), 因为如果是真实的物理地址不可能存在相同的地址存储不同的值的情况, 那么这个地址就是虚拟地址(线性地址).
通过图示了解进程地址空间(32位系统):
它其实是一个结构体, 称为 mm_struct, 类似如下:
cpp
struct mm_struct
{
int stack_begin;
int stack_end;
int heap_begin;
int heap_end;
...
};
拿32位系统举例, 32位系统最多支持4GB内存, 那如何为各个区域划分空间呢? 很简单, 用类似 begin, end 等表示每个区域自己的区间范围, 也就是从 1 - 2^32 次方内划分各个区域的空间范围, 比如
[a, b] 为栈的空间范围, [b, c] 为堆的空间范围, 以此就可以很好的划分出各个区域的范围区间了.
所以每一个进程都有一个进程地址空间, 那通过进程地址空间又是如何访问到真实的物理地址呢? 此时要借助于一个叫页表 的东西来辅助, 简单来说页表 中存储的是键值对, key 为进程地址空间中的虚拟地址, 而 val 则是操作系统为其分配的真实物理内存中的地址, 大致如下图所示:
每个进程都有自己的进程地址空间, 再看回往上同一个地址存在两个值的问题, 子进程继承父进程的进程地址空间中的大部分数据, 比如代码、数据等, 而如果两个进程都是只读的访问数据时, 一切正常, 如果此时有一个进程对数据进行了修改, 就会产生写时拷贝 , 所谓的写时拷贝 就是当某一个进程访问了共同的数据时, 不让你直接修改共同的数据, 而是将这个数据拷贝一份, 再让你指向这份拷贝的数据, 然后修改的就是这份拷贝后的数据, 大致如下图所示:
此时的父子进程的进程地址空间中的 ret 的虚拟地址虽然是相同的, 但是其通过页表映射后的物理地址是不相同的, 这就解释了为什么同一个地址存在两个值的问题.
补充:
为什么 fork 会有两个返回值呢? fork 函数的作用是分化两个进程, 一个父进程一个子进程来执行剩下的代码, 而在 fork 函数中可能就完成了进程的分化, 类似下图:
所以 fork 函数成功调用后才会有两个返回值.