Linux 子进程 -- fork函数
子进程
什么是子进程?
子进程指的是由一个已经存在的进程(称为父进程或父进程)创建的进程.
如: OS (操作系统) 就可以当作是一个进程, 用来管理软硬件资源, 当我点击浏览器, 想让浏览器运行起来时, 实际上是由 OS 接收指令, 然后 OS 帮我们将浏览器运行起来, 浏览器就是一个子进程, 是由 OS 创建出来的进程, 所以 OS 就是浏览器这个进程的父进程.
( OS 并不是一个单独的进程, 是由多个进程和线程组成的复杂结构, 这里为了方便理解, 将整个 OS 当作是一个进程)
如何创建子进程
在 Linux 中, 操作系统为我们提供了一个系统调用函数: fork()
fork() 函数可以在程序中, 创建一个子进程.
#include <sys/types.h>
#include <unistd.h>
// 以上为所需的库函数
// 函数声明
pid_t fork(void);
可以看到这个函数没有参数, 有一个 pid_t 类型的返回值.
成功后, 子进程的PID将在父进程中返回, 0将在子进程中返回.
失败时, -1在父进程中返回, 不创建子进程, 并且适当设置了errno.
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t pid = fork();
if(pid == -1)
{
std::cout << "fork error" << std::endl;
}
if(pid == 0)
{
std::cout << pid << ": this is child" << std::endl;
}
else
{
std::cout << "创建的子进程的pid: " << pid << std::endl;
}
return 0;
}
可以看到程序运行之后, 打印出了两条语句.
创建出来的子进程的 pid 是 598281.
但是我们只是看到了有进程被创建了, 怎么证明他们之间是父子关系?
先来介绍两个函数:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); // 返回当前进程的 pid
pid_t getppid(void); // 返回当前进程的 父进程的 pid
然后将上面的代码修改一下
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t pid = fork();
if(pid == -1)
{
std::cout << "fork error" << std::endl;
}
if(pid == 0)
{
std::cout << "this is child ppid: " << getppid() << " pid: " << getpid() << std::endl;
}
else
{
std::cout << "this is parent pid: " << getpid() << std::endl;
}
return 0;
}
可以观察到子进程打印出来的父进程的pid, 和父进程的 pid 是相同的, 所以他们之间确实是父子关系.
fork 函数讲解 一
从上面的代码我们可以感知到, 子进程所执行的代码, 是 fork 函数之后的代码.
否者子进程也从头开始执行, 那么就形成了一个死循环, 子进程还回去创建自己的进程.
就会创建出无数的进程, 最后软件资源也就被分配完了, 电脑也可以关机重启了.
那么 fork 函数有什么作用呢?
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t pid = fork();
if(pid == -1)
{
std::cout << "fork error" << std::endl;
}
if(pid == 0)
{
// 执行子进程的专有代码
}
else
{
// 执行父进程的专有代码
}
return 0;
}
比如: 我在使用 QQ 聊天, 此时有人发了一个文件给我, 我点击接收文件, 然后 QQ 就创建了一个子进程去执行文件的下载, 这样在文件下载的时候, 我还是能聊天.
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t pid = fork();
if (pid == -1)
{
std::cout << "fork error" << std::endl;
}
if (pid == 0)
{
while (1)
{
std::cout << "我在聊天" << std::endl;
}
}
else
{
while (1)
{
std::cout << "我在下载东西" << std::endl;
}
}
return 0;
}
fork 函数讲解 二
那么 fork 函数究竟是怎么创建出子进程的?
fork 函数创建的子进程, 实际上是一个父进程的副本, 子进程中没有代码和数据, 子进程和父进程共享代码和数据
所以 fork 之后父子进程执行的代码是一样的.
那么问题来了, 在上面创建子进程的代码中, 我们通过 fork 的返回值 pid 来区分父子进程.
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t pid = fork();
if(pid == -1)
{
std::cout << "fork error" << std::endl;
}
if(pid == 0)
{
// 执行子进程的专有代码
}
else
{
// 执行父进程的专有代码
}
return 0;
}
我们又说父子进程之间是共享代码和数据的, 那么 pid 这个变量也是两方共享的, pid 是一个变量, 一个变量怎么可能同时存在多个值.
fork 函数讲解 三
(1) 两个值从哪来
首先我们要知道, 这两个值是从哪里来的?
之前说了, 父子进程都会执行 fork 函数之后的内容.
当 fork 函数执行到 return 时, fork 函数才算结束.
那么我们可以看到, 在 fork 函数执行 return 之前, 子进程已经被创建, 并且运行起来了.
之前所说的 子进程 执行 fork 函数之后的代码, 是不准确的,
在 fork 的内部还有一部分代码子进程是会执行的, 从放入调度队列之后.
那么也就是说, 父子进程都是执行 fork 的 return 函数, 那么 return 函数就被执行了两次, 那么也就出现了两个返回值.
(2) 一个变量怎么有多个值
我们已经知道了两个值是怎么来的.
那么下一个问题: 为什么 pid 会有两个值?
之前我们说过了, 父子进程共享数据, 那么父子进程访问同一个变量, 得到的值应该是相同的.
首先我们需要知道: 进程之间具有独立性, 进程有自己的堆栈, 数据 .......
在上面我们说了, 父子进程之间共享数据, 那么当父子进程运行之后, 可能就会修改数据
那么无论哪一方修改数据, 对另一个进程而言, 它的独立性都遭到了破坏. 这是不合理的.
所以, 为什么在创建子进程的时候, 父子进程共享代码和数据?
因为: 拷贝数据需要消耗资源, 而父子进程在运行时, 可能都不会发生修改数据的操作.
那么我就在父子进程修改数据之前, 让父子进程共享,
如果有进程发生了修改数据的操作, 此时我们在将父子进程的数据分离开来. (写时拷贝)
父子进程各自有一份独立的数据 ,各自维护
fork 函数的返回值, 赋值给 pid 时, 也就是发生了数据的修改, 那么此时就会发生写时拷贝, 将父子进程之间的资源分离开, 各自维护各自的数据.
所以当我们在父子进程使用同一个对象时, 获取的 值 不同也就可以理解了.
pid 看上去时一个变量, 实际上是在父子进程中的 两个不同的变量, 只是在父子进程中名称相同.