Linux系统编程——从内存空间看fork创建进程后到底发生了什么

在 C 程序执行中,当调用 fork() 函数时,操作系统会创建一个新进程(即子进程)。从 存储空间分配 的角度来看,fork() 的执行涉及多个内存区域的处理。父进程和子进程的内存空间在 fork() 之后是非常相似的,但它们有一些重要的差异,尤其是在操作系统如何管理父子进程的资源方面。

1. fork() 的行为概述

fork() 是一个系统调用,它的作用是在调用它的进程(父进程)中创建一个几乎完全相同的新进程(子进程)。在 fork() 调用之后,父进程和子进程会继续执行相同的代码,返回值不同:

  • 父进程:fork() 返回子进程的进程 ID(PID)。

  • 子进程:fork() 返回 0。

fork() 调用后,父子进程的 代码数据(如全局变量、堆、栈等)会被复制到子进程中,但它们是独立的,可以分别修改。

2. fork() 之后的内存空间分配

1. 代码区(Text Segment)

父进程和子进程共享 代码区。因为它们执行的是相同的程序,所以它们的程序代码是相同的。在内存中,代码区只会有一份副本,并且是只读的,父子进程都可以访问。

  • 父子进程共享fork() 后,父进程和子进程共享同一块代码区,不需要复制。

2. 数据区(Data Segment)

数据区存储的是全局变量和静态变量。fork() 后,父进程和子进程的全局变量和静态变量值相同,但它们有各自独立的内存空间。

  • 父子进程分配:父进程和子进程拥有各自独立的 数据区,因此它们的全局变量和静态变量相互独立。

3. BSS 区域(BSS Segment)

BSS 区域存储的是未初始化的全局变量和静态变量。与数据区类似,fork() 后父子进程各自拥有独立的 BSS 区域。

  • 父子进程分配:父进程和子进程的 BSS 区域互不影响,初始化时它们的值都是 0。

4. 堆区(Heap)

堆区用于动态分配内存(如通过 malloc()calloc() 等函数分配的内存)。fork() 会复制父进程的堆区,但它们各自拥有独立的堆空间。

  • 父子进程分配fork() 后,父子进程的堆区是分开的。虽然堆区的内容(数据)是复制的,但是它们有各自独立的堆内存,修改一个进程的堆数据不会影响另一个进程。

5. 栈区(Stack)

栈区用于存储局部变量和函数调用时的返回地址。fork() 会复制父进程的栈区,子进程拥有独立的栈。

  • 父子进程分配fork() 后,父子进程的栈区是分开的。每个进程都在栈区中有自己的局部变量和函数调用信息。


3. fork() 的内存复制方式:写时复制(Copy-on-Write)

虽然 fork() 在父进程和子进程之间创建了几乎相同的内存空间,但操作系统并没有直接复制整个内存空间。相反,它使用了 写时复制(Copy-on-Write, COW) 技术来优化内存分配。

  • 写时复制:当父子进程的内存区域发生变化时(例如,修改全局变量、堆数据、栈变量),操作系统才会将对应的内存区域复制到子进程中。换句话说,在 读操作 时,父子进程共享内存,而 写操作 会触发内存的复制。

这种技术可以显著减少内存开销,因为直到进程真的需要修改内存时,才会进行复制,避免了不必要的内存复制。

例如:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int global_var = 10;  // 全局变量,存储在数据区

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork failed");
        return 1;
    }

    if (pid == 0) {
        // 子进程
        global_var = 20;  // 修改全局变量
        printf("Child: global_var = %d\n", global_var);
    } else {
        // 父进程
        printf("Parent: global_var = %d\n", global_var);
    }

    return 0;
}
结果:
  • 父进程和子进程都从 fork() 处开始执行,但它们有 独立的内存

  • 父进程中的 global_var 依然是 10,子进程中修改 global_var 为 20 不会影响父进程。

  • 由于采用 写时复制,只有当子进程写入 global_var 时,操作系统才会为子进程复制一份 global_var 的数据。


4. fork() 后的进程间关系

  1. 父进程和子进程共享打开的文件描述符: 父进程和子进程会继承父进程的文件描述符(例如通过 fopen() 打开的文件)。这意味着它们可以共享打开的文件,且文件的读写操作是互相影响的。

  2. 父子进程的进程 ID

    • 父进程:fork() 返回子进程的 PID。

    • 子进程:fork() 返回 0。

    • 父进程和子进程通过 getpid()getppid() 获取各自的 PID 和父进程的 PID。

  3. 独立的地址空间: 父进程和子进程在 fork() 后拥有 独立的地址空间,这意味着修改一个进程的内存不会影响另一个进程。


5. fork() 后需要注意的几个问题

  • 父进程和子进程的执行顺序fork() 创建的子进程和父进程是 并发执行 的,父进程和子进程的执行顺序是由操作系统调度器决定的,通常不确定哪个先执行。可以通过 wait() 等函数来控制父子进程的同步。

  • 进程资源的独立性:虽然子进程是父进程的副本,父子进程拥有 独立的资源(如内存、文件描述符等)。因此修改子进程的资源(如局部变量、堆内存等)不会影响父进程。

  • 僵尸进程(Zombie Process):子进程终止后,其进程信息(如 PID)仍然保留在操作系统中,直到父进程通过 wait() 等函数回收子进程的退出状态。如果父进程不调用 wait(),子进程的状态会一直存在,成为僵尸进程。


总结

fork() 调用后,父进程和子进程拥有几乎相同的内存空间,但它们的内存是 独立的,采用 写时复制(COW) 技术来减少内存复制的开销。每个进程都拥有自己的堆区、栈区等数据区域,且它们通过文件描述符共享文件。理解 fork() 的内存管理和进程间的关系,对编写高效的多进程程序非常重要。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hardStudy_h

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值