【问题标题】:How to mmap the stack for the clone() system call on linux?如何在 Linux 上为 clone() 系统调用映射堆栈?
【发布时间】:2010-11-08 03:56:25
【问题描述】:

Linux 上的 clone() 系统调用采用指向堆栈的参数,以供新创建的线程使用。最明显的方法是简单地 malloc 一些空间并传递它,但是你必须确保你已经 malloc 的堆栈空间与该线程将使用的一样多(很难预测)。

我记得在使用 pthreads 时我不必这样做,所以我很好奇它做了什么。我遇到了this site,它解释说,“Linux pthreads 实现使用的最佳解决方案是使用 mmap 分配内存,标志指定在使用时分配的内存区域。这样,分配内存栈,如果系统无法分配额外的内存,则会发生分段冲突。”

我听说过使用 mmap 的唯一上下文是将文件映射到内存中,实际上读取 mmap 手册页需要一个文件描述符。这如何用于分配动态长度堆栈以提供给 clone()?那个网站疯了吗? ;)

在任何一种情况下,内核是否都不需要知道如何为新堆栈找到一堆空闲内存,因为这是用户启动新进程时它必须一直做的事情?如果内核已经可以解决这个问题,为什么还需要首先指定堆栈指针?

【问题讨论】:

标签: linux multithreading clone mmap stack-memory


【解决方案1】:

堆栈的增长空间不是,也永远不可能是无限的。与其他所有内容一样,它们位于进程的虚拟地址空间中,并且它们可以增长的数量始终受到与相邻映射内存区域的距离的限制。

当人们谈论堆栈动态增长时,他们的意思可能是以下两件事之一:

  • 堆栈的页面可能是写时复制零页面,在执行第一次写入之前不会生成私有副本。
  • 堆栈区域的较低部分可能尚未保留(因此不计入进程的提交费用,即内核已占为进程保留的物理内存/交换量),直到命中保护页,在这种情况下,内核会提交更多内容并移动保护页,或者如果没有剩余内存可以提交,则终止进程。

试图依赖MAP_GROWSDOWN 标志是不可靠且危险的,因为它无法保护您免受mmap 在您的堆栈附近创建一个新映射,然后该映射将被破坏。 (见http://lwn.net/Articles/294001/)对于主线程,内核自动保留堆栈大小ulimit值的地址空间(不是内存)并防止@ 987654330@ 从分配它。 (但要小心!一些损坏的供应商补丁内核禁用了这种行为,导致随机内存损坏!)对于其他线程,您只需 必须 mmap 线程可能需要堆栈的整个地址空间范围创建它。没有其他办法。您可以使其大部分最初不可写/不可读,并在出现故障时对其进行更改,但随后您需要信号处理程序,并且此解决方案在 POSIX 线程实现中是不可接受的,因为它会干扰应用程序的信号处理程序。 (请注意,作为扩展,内核可以提供特殊的MAP_ 标志来传递不同的信号,而不是SIGSEGV 对映射的非法访问,然后线程实现可以捕获并采取行动在这个信号上。但是Linux目前没有这个功能。)

最后,请注意clone 系统调用不接受堆栈指针参数,因为它不需要它。系统调用必须从汇编代码中执行,因为用户空间包装器需要更改“子”线程中的堆栈指针以指向所需的堆栈,并避免向父堆栈写入任何内容。

实际上,clone 确实接受了堆栈指针参数,因为在返回用户空间后等待更改“子”中的堆栈指针是不安全的。除非所有信号都被阻塞,否则信号处理程序可能会立即在错误的堆栈上运行,并且在某些架构上,堆栈指针必须是有效的,并且始终指向可以安全写入的区域。

不仅不能从 C 中修改堆栈指针,而且您也无法避免编译器在系统调用之后但堆栈指针更改之前破坏父堆栈的可能性。

【讨论】:

【解决方案2】:

您需要为 mmap 设置 MAP_ANONYMOUS 标志。以及 MAP_GROWSDOWN,因为您想将其用作堆栈。

类似:

void *stack = mmap(NULL,initial_stacksize,PROT_WRITE|PROT_READ,MAP_PRIVATE|MAP_GROWSDOWN|MAP_ANONYMOUS,-1,0);

有关更多信息,请参阅 mmap 手册页。请记住,克隆是一个低级概念,除非您真的需要它提供的功能,否则您不打算使用它。它提供了很多控制——比如设置它自己的堆栈——以防万一你想做一些欺骗(比如让堆栈在所有相关进程中都可以访问)。除非您有充分的理由使用克隆,否则请坚持使用 fork 或 pthread。

【讨论】:

  • 这如何让您获得动态增长的堆栈?您还不必指定长度吗?还是像 pthreads 这样的实现传递了一个巨大的长度并依赖于写入时复制?
  • 是的,他们依赖于写时复制。我不确定 pthread 堆栈大小现在有多大,默认情况下它曾经是 2Mb - 您可以使用 ulimit -s 命令更改它。
  • 好的,使用 pthread_attr_getstacksize 进行测试表明现在默认堆栈大小为 10485760 字节,并且
  • 我认为您的评论在“和”之后被截断了。
  • MAP_GROWSDOWN 很危险,千万不要使用。见lwn.net/Articles/294001
【解决方案3】:

约瑟夫,回答你的最后一个问题:

当用户创建一个“正常”的新进程时,这是由 fork() 完成的。在这种情况下,内核根本不必担心创建新堆栈,因为新进程完全复制了旧进程,一直到堆栈。

如果用户使用 exec() 替换当前正在运行的进程,那么内核确实需要创建一个新堆栈 - 但在这种情况下,这很容易,因为它是从一张白纸开始的。 exec() 清除进程的内存空间并重新初始化它,因此内核会说“在 exec() 之后,堆栈总是存在于 HERE”。

但是,如果我们使用 clone(),那么我们可以说新进程将与旧进程 (CLONE_VM) 共享一个内存空间。在这种情况下,内核不能像在调用进程中那样离开堆栈(就像 fork() 那样),因为这样我们的两个进程就会相互踩踏对方的堆栈。内核也不能只将其放在默认位置(如 exec()),因为该位置已被此内存空间占用。唯一的解决方案是让调用进程为它找到一个位置,这就是它的作用。

【讨论】:

    【解决方案4】:

    这是代码,它映射一个堆栈区域并指示克隆系统调用将此区域用作堆栈。

    #include <sys/mman.h>
    #include <stdio.h>
    #include <string.h>
    #include <sched.h>
    
    int execute_clone(void *arg)
    {
        printf("\nclone function Executed....Sleeping\n");
        fflush(stdout);
        return 0;
    }
    
    int main()
    {
        void *ptr;
        int rc;
        void *start =(void *) 0x0000010000000000;
        size_t len = 0x0000000000200000;
    
        ptr = mmap(start, len, PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE|MAP_FIXED|MAP_GROWSDOWN, 0, 0);
        if(ptr == (void *)-1) 
        {
            perror("\nmmap failed");
        }
    
        rc = clone(&execute_clone, ptr + len, CLONE_VM, NULL);
    
        if(rc <= 0) 
        {
            perror("\nClone() failed");
        }
    }
    

    【讨论】:

      【解决方案5】:

      mmap 不仅仅是将文件映射到内存中。事实上,一些 malloc 实现将使用 mmap 进行大分配。如果您阅读了精美的手册页,您会注意到 MAP_ANONYMOUS 标志,并且您会发现根本不需要提供文件描述符。

      至于为什么内核不能仅仅“找到一堆空闲内存”,如果你想让别人为你做这项工作,要么使用 fork 代替,要么使用 pthreads。

      【讨论】:

      • 我的意思是它应该能够“找到一堆空闲内存”,因为它显然已经可以“找到一堆空闲内存”。 Fork 创建了一个不同的新流程,我知道我可以通过使用库来抽象出任何细节。但我要给内核开发人员荣誉,并假设事情以这种方式工作是有充分理由的,我想知道为什么。
      • fork(实际上是执行,因为 fork 只是复制所有内容)是“给我找一堆空闲内存”功能。 clone 是“我想控制我的进程创建的细节”功能。 pthread_create 是“为我创建一个线程,使用默认值”功能。这些是你的选择。新线程需要自己的堆栈,并且您不能使用传统的分配堆栈的方法(从(用户)地址空间的顶部/底部开始,然后向下/向上增长到以另一种方式增长的堆),因为地址空间只有一个顶部/底部。
      • 内核在底层做内存管理。您可以告诉它使用 100Mb 作为堆栈。在您真正开始使用它之前,它不会使用 100Mb 的单个字节(毕竟它只是虚拟空间),它会在访问的物理内存页面中出错。您将只使用所需的堆栈内存,并且它将在 mmap 的大小内“增长”。坏事当然是你需要设置一个不能增长的固定大小的堆栈。身体上。一些操作系统允许你为 mmap 指定标志,允许它自动增长。但上次我看了,那是几年前的事了,linux 没有。
      • 约瑟夫,noselasd 在这里是正确的。将虚拟内存映射到物理内存(和交换)与内存是否打算用于堆栈或堆或其他东西无关。内核的那部分不需要知道这种区别。
      • @joseph,如果是有限的,则导致虚拟内存空间。有例如共享库,它们被映射到虚拟内存空间。有可执行代码本身,有数据空间(全局变量,分配的内存)——一个可以通过 sbrk 系统调用扩展的有点特殊的映射。还有一些映射文件,也许应用程序也想映射到内存中。这些 mmap 不能重叠,并且它们需要具有不同的保护(读/写/执行)。当然您可以指定所有可用内存,但这会与共享库和动态内存所需的空间发生冲突
      【解决方案6】:

      注意clone 系统调用接受堆栈位置的参数。它实际上就像fork 一样工作。它只是接受该参数的 glibc 包装器。

      【讨论】:

      • 你确定吗?我可以在网上找到的每个签名都包含一个子堆栈。如果系统调用不需要它,为什么要 glibc?
      • 否则glibc怎么回给你?
      【解决方案7】:

      我认为栈会向下增长直到不能增长,例如当它增长到之前已经分配的内存时,可能会通知一个故障。可以看出一个默认是最小可用栈大小,如果有堆栈满时向下是冗余空间,可以向下增长,否则系统可能会通知故障。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-08-23
        • 2019-03-02
        • 1970-01-01
        • 2023-03-23
        • 2013-04-22
        • 1970-01-01
        • 2010-10-03
        • 2015-05-07
        相关资源
        最近更新 更多