【问题标题】:About pointers after fork()关于 fork() 之后的指针
【发布时间】:2014-12-19 12:08:54
【问题描述】:

这是一个技术问题,如果你了解 C 和 UNIX,也许你可以帮助我(或者这可能是一个真正的新手问题!)

今天在分析我们的操作系统课程中的一些代码时出现了一个问题。我们正在学习在 UNIX 中“分叉”一个进程意味着什么,我们已经知道它会创建一个与当前进程并行的副本,并且它们具有单独的数据部分。

但后来我想,如果在执行 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()的默认情况下
  • "坦率地说,我期待一些“分段错误”错误,因为访问了不允许的内存地址。" 这没有任何意义。想想看。孩子执行的代码是完全合法的。唯一的问题是——它是否会影响父母?
  • 在每个进程中,由于每个进程都认为自己拥有所有内存,并且由于动态地址转换,指针是每个进程地址空间中(有效)的相同位置。在子进程中更改值时,您实际上并没有更改父进程中的值。它们是两个不同的(有效的)地址空间。

标签: c unix pointers process fork


【解决方案1】:

这里的关键是虚拟地址空间的概念。

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

当内核建立一个进程时,它会为该进程创建一组页表条目,这些条目定义了物理内存页面到虚拟地址空间的映射,程序在这个虚拟地址空间中执行。

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

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

一旦任何一个进程写入一个页面,内存管理单元就会产生一个页面错误(由于 PTE 条目设置了只读标志),然后页面错误处理程序从物理内存中分配一个新页面,复制数据结束,更新页表条目并将页面设置回读/写。 通过这种方式,页面仅在任何一个进程第一次尝试对写入页面上的副本进行更改时才被实际复制,并且任何一个进程都完全没有注意到手的轻微变化。

问候,丹。

【讨论】:

    【解决方案2】:

    从逻辑上讲,fork()ed 进程拥有自己的、或多或少的父进程整个状态的独立副本。如果子项中的指针引用属于父项的内存,那将无法正常工作。

    特定的类 UNIX 内核如何使其工作的细节可能会有所不同。 Linux 通过写时复制页面实现子进程的内存,这使得fork()ing 相对于其他可能的实现而言相对便宜。在这种情况下,子进程的指针确实指向父进程的内存,直到子进程或父进程尝试修改该内存,此时复制一份供子进程使用.这一切都依赖于底层的虚拟内存系统。其他 UNIX 和类 UNIX 系统可以并且已经完成了不同的操作。

    【讨论】:

    • 我目前对虚拟内存了解不多,所以不知道。谢谢!
    【解决方案3】:

    子修改了一个在其地址空间中完全合法的指针,因为它是其父的副本。由于内存不是逻辑共享的,因此对父级没有影响。在分叉之后,每个进程都将分道扬镳。

    UNIX 有多种创建共享内存的方法(其中一个进程可以修改内存并让另一个进程看到该修改),但fork 不是其中之一。这是一件好事,因为否则,父母和孩子之间的同步几乎是不可能的。

    【讨论】:

    • 但是为什么合法呢?那个内存地址不是父级拥有的吗?值是一样的
    • 孩子不知道也不关心父母拥有什么。是的,父级拥有该内存地址(在其地址空间中)。孩子还拥有该内存地址(在其地址空间中)。父母和孩子一开始是副本,但随后可以分道扬镳。分叉后允许内存空间发散,但在分叉后它们是相同的。
    • 所以,我假设指针不会将绝对内存地址存储到物理内存。对吗?
    • @javierbg 出于十几个原因,这是不可能的。考虑int a[10000];。甚至可能还没有为该阵列分配任何物理内存,因为它可能永远不会被填充。或者,考虑一下,在具有 4GB 内存的 64 位系统上,int *j = malloc(8000000000);——没有足够的物理内存可以指向。
    猜你喜欢
    • 1970-01-01
    • 2021-04-30
    • 2013-11-16
    • 1970-01-01
    • 2016-01-06
    • 2017-05-13
    • 2012-05-09
    • 1970-01-01
    相关资源
    最近更新 更多