13

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

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

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

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

4

7 回答 7

7

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

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

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

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

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

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

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

于 2011-03-20T14:20:27.773 回答
5

您需要 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 或 pthreads。

于 2009-07-04T23:46:41.083 回答
2

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

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

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

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

于 2009-07-09T15:02:01.603 回答
2

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

#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");
    }
}
于 2010-04-21T23:23:16.570 回答
0

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

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

于 2009-07-04T23:43:54.257 回答
0

请注意,clone系统调用接受堆栈位置的参数。它实际上就像fork. 它只是接受该论点的 glibc 包装器。

于 2009-07-06T21:18:01.800 回答
0

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

于 2012-02-13T07:34:17.757 回答