Почему мне нужно установить больший размер стека, чем это должно быть на самом деле? - PullRequest
0 голосов
/ 22 января 2019

Я пытаюсь проанализировать этот код, который использует pthreads и стеки:

#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 в размер стека.Я имею в виду, почему без добавления этого значения программа segfaults.

1 Ответ

0 голосов
/ 23 января 2019

Всякий раз, когда вы создаете pthread, библиотека pthread должна выделить для него некоторое пространство стека. Это не обязательно выделяет физическую память для стекового пространства, оно выделяет виртуальное адресное пространство для стека. Размер стека по умолчанию, выделенный для потока, зависит от реализации, но если вы собираетесь выделить большой массив в стеке (именно в этом случае автоматические переменные класса хранения помещаются практически во всех реализациях C), вам нужно отрегулировать выделенное пространство, чтобы оно было достаточно большим.

Рассмотрим: допустим, реализация (в библиотеке pthreads) решила выделить 2 МБ стекового пространства по умолчанию для каждого потока. Затем, после создания 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)

Несколько вещей на заметку. Стеки растут вниз. Указатель стека начинается прямо в верхней части выделенной области, и когда вы помещаете вещи в стек, вызывая подпрограмму или выделяя пространство для локальных переменных, указатель стека уменьшается. Обычно ядро ​​ не выделяет реальные физические страницы для вашего стека немедленно. Это было бы расточительно, так как вы могли бы никогда не использовать их (и что-то еще, вероятно, должно быть выселено из ОЗУ, чтобы сделать это). Вместо этого записей карты страниц для каждой страницы в регионе выделяются, но помечаются пустыми. Затем, когда вы попытаетесь написать на каждой странице, ваша программа получит ошибку страницы. Ядро обрабатывает ошибку, выделяя вам физическую страницу, сопоставляя ее с правильным виртуальным адресом и обновляя запись карты страницы (затем автоматически возобновляя вашу программу без необходимости знать об этом).

Обратите внимание, что области стека не являются смежными непосредственно. Это сделано для того, чтобы ядро ​​могло различить, когда вы исчерпали виртуальное адресное пространство, зайдя слишком далеко. То, что приводит к нарушению сегментации в вашем сценарии: вы взорвали дно стека и вышли в пространство, для которого не выделены записи карты страницы.

Итак, когда вы используете pthread_attr_setstacksize, вы сообщаете библиотеке и ядру, что точно знаете, как велик стек, и настраиваете карту памяти соответственно. Но так как вы предоставили достаточно места только для точно , содержащего массив, вы не оставили места для стекового фрейма, используемого для вызова вашей функции потока, или для ее других локальных переменных (tid, i, mystacksize), или для любого заполнения или другого использования локального стека.

Итак, первоначальный автор этого кода по существу говорил: «Мне нужно убедиться, что в каждом потоке есть место для моего большого массива, а затем добавить дополнительные MEGEXTRA байтов для локальных переменных, кадра вызывающего стека и любых другие накладные расходы. " Опять же, обратите внимание, что только 1030 * выделяется виртуальное адресное пространство , поэтому это не расточительно (виртуальное адресное пространство обычно не является ценным ресурсом в 64-разрядной архитектуре). При фактическом запуске программы вы, скорее всего, только используете одну или две дополнительные страницы этого дополнительного пространства.

Еще одна вещь, которую стоит отметить: первая часть расчета размера стека (ARRAY_SIZE*sizeof(double)) равна 4 миллионам. В шестнадцатеричном формате, то есть 0x3D0900, что , а не , кратное размеру страницы (обычно 4K или 0x1000). Результат использования этой цифры является неопределенным. Ядро может расширить это до следующей границы размера страницы (0x3d10000), или оно может усечь до предыдущей границы (0x3d0000), или (согласно man-странице linux) может вернуть ошибку.

Спецификация posix (http://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_attr_setstacksize.html) говорит

Атрибут stacksize должен определять минимальный размер стека (в байтах), выделяемый для стека созданных потоков.

и ничего не говорит о размере, не выровненном по страницам, поэтому, возможно, расширение размера до границы следующей страницы является единственным правильным поведением.Но glibc, похоже, не выполняет такую ​​настройку, и реализация ядра Linux, по-видимому, усекает предоставленный размер.

В любом случае, хорошая идея - не сокращать эти вещи слишком близко.В лучшем случае сложно предсказать фактическое точное использование стека в реальной программе.

...