线程共享地址空间,但不共享堆栈:矛盾吗?

时间:2019-02-10 08:41:56

标签: c multithreading process

我知道线程共享地址空间,但是不共享它们的堆栈。这不是矛盾的吗? 为什么实际上不共享堆栈却说它们共享地址空间-堆栈是地址空间的一部分,不是吗?

我假设它的线程共享堆,数据和代码段,而不是堆栈段。对我来说,所有这些都被认为是进程地址空间。

有人可以澄清一下吗?谢谢!

4 个答案:

答案 0 :(得分:6)

是的,线程具有相同的地址空间,但不共享堆栈。一个线程在内存中看到的任何内容都可以在同一地址看到,而另一个线程可以看到相同的地址,但是每个线程的堆栈位于地址空间的不同位置中,因此它们彼此独立地调用其他函数,而不会互相干扰。

以以下程序为例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

void *foo(void *arg)
{
    int *n = arg;
    printf("in thread, arg=%p, value=%d, &n=%p\n", arg, *n, (void *)&n);
    return NULL;
}

int main()
{
    int x = 4;
    printf("in main, x=%d, &x=%p\n", x, (void *)&x);
    pthread_t tid;
    pthread_create(&tid, NULL, foo, &x);
    sleep(3);
    pthread_join(tid, NULL);

    return 0;
}

main函数将驻留在主线程堆栈中的局部变量的地址传递给另一个线程。线程能够取消引用该指针并读取变量的值。

在我的系统上,它输出以下内容:

in main, x=4, &x=0x7fff2142985c
in thread, arg=0x7fff2142985c, value=4, &n=0x7f6abaa90f08

在这里您可以看到主线程和子线程在主函数中为x看到相同的地址和值。您还可以看到,位于子线程堆栈中的n中变量foo的地址与xmain的地址很远(约637GB)。

这表明两个线程可以读取具有相同地址的相同内存,并且每个线程都有自己的堆栈。

答案 1 :(得分:1)

可能具有单独堆栈地址空间的线程,但这取决于两个因素:如何实现线程以及操作系统施加哪些限制:

  1. 如果仅在用户空间中实现它们而没有任何内核帮助(例如旧Unix OS中的第一个线程库),则它们将共享堆栈地址空间。区别在于堆栈在每个线程中的起始位置。

  2. 如果操作系统实现用于构建线程的特殊syscall(例如,基于Mach3的内核cthread),或者围绕特殊的fork syscall(例如Linux的克隆)或围绕非posix syscall(例如Windows)构建,则它们可以共享大多数通用地址空间,但堆栈段具有不同的匿名内存。

请注意,在第一种情况(用户空间线程)中,线程共享所有内容,即使是相同的PID,线程之间也没有真正的分隔。如果一个线程被阻塞或杀死进程,则所有线程都被阻塞或杀死(没有真正分开的执行)。当然。在这种情况下,堆栈地址空间由同一PID中的所有线程共享。

在其他情况下(在OS的支持下),隔离程度取决于两件事:线程库和内核设施。如果该库尽管具有使用共享资源的不同组合(例如Linux克隆)创建进程的机制,但仍未使用它,则这些线程肯定会共享堆栈。如果该库是高级库并支持这种奇特功能,则它可能会分隔堆栈。

但是在不同的地址空间中分离堆栈会带来一个大问题:您不能在线程之间共享堆栈中的变量。乍一看,这似乎不是什么大问题,甚至您可能会认为这是一个优势。但事实并非如此,实际上,在多个线程之间共享堆栈中的变量是一个非常常见的用例(例如,在科学代码中)。以下是OpenMP中的并行化for(源https://www.openmp.org/wp-content/uploads/openmp-examples-4.5.0.pdf):

void simple(int n, float *a, float *b)
{
  int i;
#pragma omp parallel for
  for (i=1; i<n; i++) /*i is private by default*/
    b[i] = (a[i] + a[i-1]) / 2.0;
}

如您所见,ba向量在调用期间作为指针传递。您不保证它们位于“共享地址空间”中。如果此OpenMP库与线程在不同地址空间中具有堆栈的线程库链接,则此OpenMP并行化将失败。当线程库破坏了OpenMP库的入门示例之一时,这确实是一个糟糕的开始。

因此,出于兼容性考虑,尽管可以在大多数现代操作系统中实现,但最常见的是永远不要在不同的地址空间中分离堆栈。

答案 2 :(得分:0)

旧教科书经常这样描述它们,但是现代操作系统上的线程堆栈并不是进程地址空间的某些“特殊”组件。就像其他任何映射一样,它们是mmap创建的内存映射。

原始线程(处理中的第一个线程)可能会以特殊方式获取其堆栈,但其余线程已由用户空间线程库正常分配(通常使用mmap调用)。通常,堆栈可以由用户空间来操纵,有时甚至可以完全被另一个内存分配所取代。

大多数操作系统甚至都不检查,该线程实际上使用了它声称使用的堆栈-here is a description是最近实施的安全缓解技术,该技术实现了这种检查以防御漏洞。

答案 3 :(得分:0)

堆栈位于相同的地址空间,但是不应让每个线程都接触属于其他线程的堆栈。为什么?

好吧,仅仅是因为线程不可能共享堆栈,因为它不可避免地导致并发修改堆栈,数据损坏和崩溃。

请考虑以下内容:线程1将某些东西压入堆栈,然后线程2进行相同的操作。现在,线程1弹出堆栈,并获取线程2推送的数据。我们有未定义的行为。

关于堆,每个内存分配都是同步的-这意味着在任何给定时间只能有一个线程可以分配内存,这可以防止并发问题,但这也是内存分配成为巨大瓶颈的原因之一(非常慢!)。

除了每个线程都唯一的线程上下文(包括堆栈指针)之外,还可以使用称为线程本地存储(TLS)的每个线程来存储用户数据。