介绍创建进程之前,先简单地介绍一下 Linux 下的进程内存布局。
Stack - 所有函数的 local variables, arguments 和 return address 的存放内存区域
Heap - 动态申请的内存区域
bss - 所有未被初始化的 global variables 和 static variables 的存放内存区域
data - 所有已被初始化的 global variables 和 static variables 的存放内存区域
在 Linux 系统下可以通过调用 fork() 来创建一个新的进程。调用进程为父进程 (parent process) ,而诞生的新进程为子进程 (child process)。
fork() 比较特别,因为它会返回两次,也就是说会有两个返回值。我们可以通过这两个返回值来区分父、子进程。在父进程中,fock() 将会返回子进程的 process ID,而在子进程中成功返回0,失败则返回-1 (失败原因可参考手册)。子进程可以调用 getpid()
获取进程ID。
子进程诞生后将会获得来自父进程的数据副本,其中包括 bss, data segment 和 stack segment。值得注意的是,CentOS 8 无法保证调用 fork() 之后父、子进程的执行顺序。
我们可以从输出结果得知两个进程各自的数据都是独立的。
#include <unistd.h>
#include <iostream>
auto global_value = 11; // stored in data segment
int main()
{
auto local_value = 66; // stored in stack segment
double* dPtr = new double(3.14);
switch (fork())
{
case - 1:
std::cout << "failed to fork.\n";
_exit(-1);
case 0:
*dPtr = 5.12;
std::cout << "child process ID: " << getpid() << "\n";
std::cout << "global value: " << (++global_value) << "\n";
std::cout << "local value: " << (++local_value) << "\n";
std::cout << "double pointer value: " << (*dPtr) << "\n";
std::cout << "----------------------------------\n";
_exit(0);
}
std::cout << "parent process ID: " << getpid() << "\n";
std::cout << "global value: " << global_value << "\n";
std::cout << "local value: " << local_value << "\n";
std::cout << "double pointer value: " << (*dPtr) << "\n";
std::cout << "----------------------------------\n";
exit(0);
}
输出结果:
[me@localhost Documents]$ ./exe
parent process ID: 3961
global value: 11
local value: 66
double pointer value: 3.14
----------------------------------
child process ID: 3962
global value: 12
local value: 67
double pointer value: 5.12
----------------------------------
[me@localhost Documents]$ ./exe
parent process ID: 3988
global value: 11
local value: 66
double pointer value: 3.14
----------------------------------
child process ID: 3989
global value: 12
local value: 67
double pointer value: 5.12
----------------------------------
执行 fork() 时,子进程将会获得来自父进程的文件描述符副本。和拷贝数据一样,尽管文件描述符也是副本,但本质上这些文件描述符都是指向内核维护 open file table。所以这些文件描述对应的文件的偏移量以及文件状态标志都是相同的。
#include <unistd.h>
#include <fcntl.h>
#include <iostream>
int main()
{
auto fd = open("/home/usr1/Documents/reading.txt", O_NONBLOCK);
switch (fork())
{
case - 1:
std::cout << "failed to fork.\n";
_exit(-1);
case 0:
auto flags = fcntl(fd, F_GETFL);
if (O_NONBLOCK & flags)
std::cout << "O_NONBLOCK flag is on\n";
_exit(0);
}
auto flags = fcntl(fd, F_GETFL);
if (O_NONBLOCK & flags)
std::cout << "O_NONBLOCK flag is on\n";
close(fd);
return 0;
}
输出结果:
O_NONBLOCK flag is on
O_NONBLOCK flag is on
由于子进程可能在诞生后就立刻执行 exec()
族函数。这意味子进程从父进程那里拷贝而来的数据全部都会被冲洗掉,那么拷贝的功夫就全部白费了。出于效率的考虑,COW 被投入使用。原理很简单,调用 fork() 后父、子进程共享 read only memory images。如果没有任一进程对这块内存映像进行修改,那么它们拥有的内存影响都属于同一份。如果有任何一个进程想要对数据进行修改,那么内核才会为该进程拷贝新的一份内存映像便于该进程独立使用。
参考:
[^1] 6.4 Virtual Memory Management, The Linux Programming Interface.
[^2] 24.2.1 File Sharing Between Parent and Child, The Linux Programming Interface.
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。