我正在尝试分析使用pthread和堆栈的代码:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define NTHREADS 8
#define ARRAY_SIZE 500000
#define MEGEXTRA 1000000
pthread_attr_t attr;
void *Hello(void *threadid)
{
double A[ARRAY_SIZE];
int i;
long tid;
size_t mystacksize;
tid = (long)threadid;
sleep(3);
for (i=0; i<ARRAY_SIZE; i++)
{
A[i] = i * 1.0;
}
printf("%ld: Hello World! %f\n", tid, A[ARRAY_SIZE-1]);
pthread_attr_getstacksize (&attr, &mystacksize);
printf("%ld: Thread stack size = %li bytes \n", tid, mystacksize);
pthread_exit(NULL);
}
int main(int argc, char *argv[])
{
pthread_t threads[NTHREADS];
size_t stacksize;
int rc;
long t;
pthread_attr_init(&attr);
stacksize = ARRAY_SIZE*sizeof(double) + MEGEXTRA;
pthread_attr_setstacksize (&attr, stacksize);
pthread_attr_getstacksize (&attr, &stacksize);
printf("Thread stack size = %li bytes (hint, hint)\n",stacksize);
for(t=0;t<NTHREADS;t++){
rc = pthread_create(&threads[t], &attr, Hello, (void *)t);
if (rc){
printf("ERROR; return code from pthread_create() is %d\n", rc);
exit(-1);
}
}
printf("Created %ld threads.\n", t);
pthread_exit(NULL);
}
我不知道这部分
#define MEGEXTRA 1000000
(...)
stacksize = ARRAY_SIZE*sizeof(double) + MEGEXTRA;
pthread_attr_setstacksize (&attr, stacksize);
pthread_attr_getstacksize (&attr, &stacksize);
为什么我需要将此MEGEXTRA值添加到stacksize。我的意思是,为什么不添加此值,程序就会出现段错误。
答案 0 :(得分:0)
无论何时创建pthread
,pthread库都必须为其分配一些堆栈空间。不必为堆栈空间分配物理内存,而是为堆栈分配虚拟地址空间。分配给线程的默认堆栈大小取决于实现,但是如果您要在堆栈上分配一个大数组(实际上,在所有C实现中都放置了 automatic 存储类变量) ,您需要调整分配的空间以确保足够大。
考虑:假设实现(在pthreads库中)已决定为每个线程默认分配2MB的堆栈空间。然后,在创建3个线程之后,您的虚拟内存映射可能看起来像这样(确切的地址和其他详细信息当然会有所不同):
8060000-8080000 Thread 3 stack
8030000-8050000 Thread 2 stack
8000000-8020000 Thread 1 stack
7000000-8000000 Main thread stack
[...] Other program regions (program code, heap, initialized data, library code/data, etc)
需要注意的几件事。烟囱向下生长。堆栈指针从分配区域的顶部开始,当您通过调用子例程或为局部变量分配空间将内容推入堆栈时,堆栈指针会减少。内核通常会不立即为您的堆栈分配实际的物理页面。这将很浪费,因为您可能永远不会使用它们(并且可能必须从RAM中驱逐其他东西才能这样做)。而是为该区域中的每个页面分配了页面映射条目,但将其标记为空。然后,当您尝试写入每个页面时,您的程序将出现页面错误。内核通过为您分配一个物理页面,将其映射到正确的虚拟地址并更新页面映射条目(然后无需您意识到任何这些即可自动恢复程序)来处理故障。
还请注意,堆栈区域不是立即连续的。这样一来,内核就可以区分出虚拟地址空间耗尽的时间。 那是导致您的场景中出现分段违规的原因:您炸毁了堆栈的底部,并进入了没有分配页面映射条目的空间。
因此,当您使用pthread_attr_setstacksize
时,您是在告诉库和内核您确切知道要堆栈的大小并相应地配置内存映射。但是,由于您仅提供了足够的空间来精确地包含数组,所以您没有为调用线程函数的堆栈框架或其他局部变量({{1} },tid
,i
),或用于任何填充或其他本地堆栈使用。
因此,这段代码的原始作者实际上是在说:“我需要确保我的大数组在每个线程中都有空间,然后为局部变量(调用堆栈帧)另外抛出mystacksize
个字节以及其他任何开销。”再次注意,这只是分配虚拟地址空间,因此这样做并不浪费(虚拟地址空间通常不是64位体系结构上的宝贵资源)。在程序的实际运行中,您可能仅使用该额外空间的另外一两个页面。
注意的另一件事:堆栈大小计算的第一部分(MEGEXTRA
)等于400万。以十六进制表示,即0x3D0900,不是页面大小的倍数(通常为4K或0x1000)。使用该数字的结果是不确定的。内核可能会将其扩展到下一个页面大小边界(0x3d10000),或者可能截断到前一个边界(0x3d0000),或者(根据linux手册页)它可能返回错误。
posix规范(http://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_attr_setstacksize.html)表示
stacksize属性应定义为创建的线程堆栈分配的最小堆栈大小(以字节为单位)。
,而且对于未对齐页面的大小没有任何说明,可以这么说,将大小扩展到下一页边界是唯一的正确行为。但是glibc似乎没有进行这种调整,并且Linux内核实现似乎会截断所提供的大小。
无论如何,最好不要将这些内容切得太近。最多很难预测现实程序中实际的实际堆栈使用情况。