关于fork()后指针的问题

15

这是一个有点技术性的问题,如果你了解C语言和UNIX操作系统,或者这可能是一个非常新手的问题,也许你可以帮我一下!

今天在我们的操作系统课程中分析一些代码时出现了一个问题。我们正在学习在UNIX中“fork”进程的含义,我们已经知道它会创建与当前进程并行的进程副本,并且它们拥有独立的数据段。

但是我想,如果在执行fork()之前创建变量和指向该变量的指针,因为指针存储变量的内存地址,所以可以尝试通过使用该指针从子进程修改该变量的值。

我们在课堂上尝试了类似于这样的代码:

#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>

int main (){
    int value = 0;
    int * pointer = &value;
    int status;
    
    pid_t pid;
    
    printf("Parent: Initial value is %d\n",value);
    
    pid = fork();
    
    switch(pid){
    case -1: //Error (maybe?)
        printf("Fork error, WTF?\n");
        exit(-1);
        
    case 0: //Child process
        printf("\tChild: I'll try to change the value\n\tChild: The pointer value is %p\n",pointer);
        (*pointer) = 1;
        printf("\tChild: I've set the value to %d\n",(*pointer));
        
        exit(EXIT_SUCCESS);
        break;
    }
    
    while(pid != wait(&status)); //Wait for the child process
    
    printf("Parent: the pointer value is %p\nParent: The value is %d\n",pointer,value);
    
    return 0;
}

如果你运行它,你将会得到类似于这样的结果:

父进程: 初始值为0

子进程: 我会尝试改变这个值

子进程: 指针值为0x7fff733b0c6c

子进程: 我已经把这个值设为1了

父进程: 指针值为0x7fff733b0c6c

父进程: 值仍然是0

很明显,子进程并没有对父进程产生任何影响。实话说,我本来期望会因为访问了未授权的内存地址而出现"段错误",但真正发生了什么呢?

请记住,我不是在寻找进程间通信的方法,那不是重点。我想知道的是代码做了什么。在子进程内部,改变是可见的,所以它确实做了些什么。

我的主要假设是指针不是绝对的内存地址,它们是相对于进程堆栈的。但是我还没有找到答案(课上没有人知道,搜索引擎只能找到一些关于进程通信的问题),所以我想从你这里得到答案,希望有人能知道。

感谢您抽出时间阅读!


你可以将printf("Parent: the pointer value is %p\nThe value is %d\n",pointer,value);放在switch()的默认情况下。 - Haris
2
http://en.wikipedia.org/wiki/Virtual_memory - Mat
老实说,我原本预料到会出现“分段错误”(segmentation fault)的错误,因为访问了一个不允许的内存地址。但这毫无意义。好好思考一下。子代码的执行是完全合法的。唯一的问题是——它是否对父代码产生影响。 - David Schwartz
在每个进程中,由于每个进程都认为它拥有所有的内存,并且由于动态地址转换,指针在每个进程的地址空间中是(有效的)相同位置。当在子进程中改变值时,您实际上没有改变父进程中的值。它们是两个不同的(有效的)地址空间。 - user3629249
3个回答

33

关键在于虚拟地址空间的概念。

现代处理器(比如80386后的任何处理器)都有一个内存管理单元,它将每个进程的虚拟地址空间映射到由内核控制的物理内存页面上。

当内核设置一个进程时,它创建该进程的一组页表条目,用于定义物理内存页面到虚拟地址空间的映射。程序就是在这个虚拟地址空间中运行。

从概念上讲,当您进行fork时,内核将现有进程页面复制到一组新的物理页面,并设置新进程的页表,使得对于新进程而言,它似乎运行在与原始进程相同的虚拟内存布局中,而实际上却在完全不同的物理内存中寻址。

细节更加微妙,因为没有人想浪费时间复制数百 MB 的数据,除非必要。 当进程调用fork()时,内核设置第二组页表条目(新进程),但将它们指向与原进程相同的物理页面,然后在两组页面中都设置标志,使mmu认为它们是只读的......

只要任何一个进程写入一个页面,内存管理单元就会生成一个页故障(由于PTE条目将只读标志设置),然后页面故障处理程序就会从物理内存中分配一个新的页面,复制数据,更新页表条目并将页面设置回读/写。 通过这种方式,页面只在任何一个进程试图更改写时才被实际复制到写入副本的第一次,而且这种手法对任何一个进程都完全不可感知。

敬礼,丹。


6
逻辑上讲,使用fork()函数创建的子进程会获得独立的父进程状态副本。如果子进程中的指针指向父进程的内存,这种情况是行不通的。
具体实现方式因UNIX内核而异。Linux通过写时复制页面来实现子进程的内存,相对于其他可能的实现方式,使用fork()函数更加便宜。在这种情况下,子进程的指针实际上指向父进程的内存,直到子进程或父进程尝试修改该内存时,才会为子进程创建一个副本。所有这些都依赖于底层虚拟内存系统。其他UNIX和类UNIX系统可以采用不同的方法来实现。

我目前对虚拟内存不是很了解,所以我不知道那个。谢谢! - javierbg

3
这个孩子修改了一个指针,因为它是父进程的副本,所以在它的地址空间内是完全合法的。但这没有对父进程产生影响,因为内存不是逻辑共享的。每个进程在分叉后都可以独立运行,UNIX有许多创建共享内存的方式(其中一个进程可以修改内存并使另一个进程看到修改),但fork不是其中之一。这是一件好事,否则父和子之间的同步几乎是不可能的。

但是为什么这是合法的呢?难道不是父级拥有那个内存地址吗?值是相同的。 - javierbg
2
子进程并不知道或关心父进程拥有什么。是的,父进程拥有那个内存地址(在其地址空间内)。子进程也拥有该内存地址(在其地址空间内)。父进程和子进程开始时是副本,但随后可以分道扬镳。在fork之后,允许内存空间发生分歧,但它们在fork之前是相同的。 - David Schwartz
那么,我认为指针不会存储物理内存的绝对内存地址,这样理解对吗? - javierbg
@javierbg 这几乎是不可能的,有很多理由。请考虑 int a[10000];,这个数组可能尚未分配任何物理内存,因为它可能永远不会被填充。或者,请考虑在一个拥有 4GB 内存的 64 位系统上,int *j = malloc(8000000000); -- 物理内存不足以指向该地址。 - David Schwartz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接