从书中Unix环境下的高级编程我读到以下有关类 Unix 系统中线程的内容
进程内的所有线程共享相同的地址空间、文件描述符、堆栈和进程相关属性。由于它们可以访问相同的内存,因此线程之间需要同步对共享数据的访问以避免不一致。
作者在stacks
这里想表达什么意思呢?我从事 Java 编程并且知道每个线程都有自己的堆栈。所以我很困惑共享 stacks
概念在这里。
答案1
在 Unix 或 Linux 进程的上下文中,短语“堆栈”可以表示两件事。
首先,“堆栈”可以表示控制流的调用顺序的后进先出记录。当一个进程执行时,main()
首先被调用。main()
可能会打电话printf()
。编译器生成的代码将格式字符串的地址以及任何其他参数写入printf()
某些内存位置。然后代码写入控制流printf()
完成后应返回的地址。然后代码调用跳转或分支到 的开头printf()
。每个线程都有这些函数激活记录堆栈之一。请注意,许多 CPU 都有用于设置和维护堆栈的硬件指令,但其他 CPU(IBM 360 或其他名称)实际上使用可能分散在地址空间中的链表。
其次,“堆栈”可以指 CPU 将参数写入函数的内存位置,以及被调用函数应返回的地址。 “堆栈”是指进程地址空间的连续部分。
Unix 或 Linux 或 *BSD 进程中的内存是一条很长的线,从大约 0x400000 开始,到大约 0x7fffffffffff 结束(在 x86_64 CPU 上)。堆栈地址空间从最大的数字地址开始。每次调用函数时,函数激活记录堆栈都会“向下增长”:进程代码将函数参数和返回地址放入激活记录堆栈中,并递减堆栈指针,这是一个特殊的 CPU 寄存器,用于跟踪进程当前变量的值位于堆栈的地址空间中。
每个线程都会获得一块“堆栈”(堆栈地址空间)供自己使用,作为函数激活记录堆栈。在 0x7ffffffffff 和较低地址之间的某个位置,每个线程都有一个保留用于函数调用的内存区域。通常这只是通过约定而不是硬件强制执行的,因此如果您的线程在嵌套函数之后调用函数,则该线程堆栈的底部可以覆盖另一个线程堆栈的顶部。
因此,每个线程都有一块“堆栈”内存区域,这就是“共享堆栈”术语的由来。这是进程地址空间是单个线性内存块以及术语“堆栈”的两种使用的结果。我很确定一些较旧的 JVM(非常古老)实际上只有一个线程。 Java 代码中的任何线程实际上都是由单个真实线程完成的。较新的 JVM(调用真实线程来执行 Java 线程的 JVM)将具有与我上面描述的相同的“共享堆栈”。 Linux 和 Plan 9 有一个进程启动系统调用(Linux 中为clone(),Plan 9 中为 rfork()),它可以设置共享部分地址空间的进程,也许还有不同的堆栈地址空间,但是这种风格的线程从未真正流行起来。
答案2
作者在这里所说的堆栈是什么意思?我从事 Java 编程并且知道每个线程都有自己的堆栈。所以我对这里的共享堆栈概念感到困惑。
复数的使用有点奇怪,而且确实看起来有误导性,也许重点是多线程程序的多个堆栈共享相同的地址空间。
正如 Bruce Ediger 所描述的,“堆栈”是指连续地址空间的单个区域,其中数据按后进先出的方式放置。然而,每个本机线程在一个进程中有这样一个连续的区域——它们之间没有一个被划分的区域。创建线程确实需要分配额外的堆栈相同大小(这是一个固定数字,例如 Linux 上的默认值是 8 MB),这就是为什么过多的多线程应用程序会消耗过多的内存。
Java 使用本机线程来实现 Java 线程,但它为每个 Java 线程本身管理一个“堆栈”,独立于内核。这意味着留出一些全局内存用于此目的;请参阅此处最佳答案的第三部分:
https://stackoverflow.com/questions/5483047/why-is-creating-a-thread-said-to-be-expense
当然,这是指特定的实现(openjdk),但想必它们都必须这样做(本地分配堆以供内部使用作为“线程堆栈”)。
由于每个本机线程使用的单个堆栈由内核管理,因此我不同意 Bruce 的暗示,即这是属于整个进程的某个整体堆栈的一部分 - 同样,单个(也称为非)线程进程仅具有一个堆栈,但多线程进程中的主线程才不是与其他线程共享堆栈空间。
“共享堆栈”是指所有数据都以单个地址开始存储的堆栈——这就是嵌套函数调用共享同一堆栈的含义。然而,使用下面由 Bruce 提到的示例程序的调整版本,来自 Linux 版本pthread_create 手册页手册页:
./a.out one two three
In main() stack starts near: 0x7fff17b80f98
Thread 1: top of stack near 0x7f11ac6d3e78; argv_string=one
Thread 2: top of stack near 0x7f11abed2e78; argv_string=two
Thread 3: top of stack near 0x7f11ab6d1e78; argv_string=three
该程序获取线程函数中局部变量的地址;我添加的调整是在main()
创建线程之前执行相同的操作。运行在最新的64位linux系统上;请注意,线程的起始地址相距正好 8 MB,而且它们距离主线程堆栈的顶部确实很远。将此与嵌套函数调用进行对比,如下所示:
#include <stdio.h>
void eg (int n) {
char *p;
printf("#%d first variable at %p\n", n, &p);
if (n < 3) eg(n+1);
}
int main(int argc, const char *argv[]) {
char *p;
printf("main() first variable at %p\n", &p);
eg(1);
return 0;
}
运行示例:
main() first variable at 0x7fffef0aaf68
#1 first variable at 0x7fffef0aaf38
#2 first variable at 0x7fffef0aaf08
#3 first variable at 0x7fffef0aaed8
它们仅相距 48 个字节——换句话说,它们被一个接一个地连续放置(以及其他少量的实际数据),它们之间没有未使用的空间。如果我们在多线程程序中使用它,没有嵌套递归,但eg()
在每个线程中调用一次:
Thread 1: top of stack near 0x7f4bd5061e78; argv_string=one
Thread 2: top of stack near 0x7f4bd4860e78; argv_string=two
Thread 3: top of stack near 0x7f4bd405fe78; argv_string=three
#3 first variable at 0x7f4bd405fe48
#1 first variable at 0x7f4bd5061e48
#2 first variable at 0x7f4bd4860e48
每个变量都位于三个独立堆栈的顶部附近。
所有这些都在称为“堆栈空间”的高地址区域中,但在多线程程序中,它被分为多个堆栈;它不被视为一个大的 LIFO 结构。