Оценка размера стека
В многопоточном встроенном программном обеспечении (написанном на C или C++) потоку должно быть выделено достаточно стекового пространства, чтобы он мог завершить свои операции без переполнения. Правильный размер стека имеет решающее значение в некоторых встроенных средах реального времени, потому что (по крайней мере, в некоторых системах, с которыми я работал) операционная система НЕ обнаружит это за вас.
Обычно размер стека для нового потока (отличного от основного потока) назначается во время создания потока (т. Е. В аргументе pthread_create() или тому подобного). Часто эти размеры стеков жестко запрограммированы на значения, которые, как известно, хороши на момент написания или тестирования кода.
Однако будущие изменения в коде часто нарушают предположения, на которых основаны жестко запрограммированные размеры стека, и в один роковой день ваш поток входит в одну из более глубоких ветвей своего графа вызовов и переполняет стек - разрушая всю систему или молча портит память.
Я лично видел эту проблему в случае, когда код, выполняемый в потоке, объявляет экземпляры структуры в стеке. Когда структура увеличивается для хранения дополнительных данных, размер стека соответственно увеличивается, что может привести к переполнению стека. Я полагаю, что это может быть огромной проблемой для установленных кодовых баз, где полный эффект добавления полей в структуру не может быть известен немедленно (слишком много потоков / функций, чтобы найти все места, где используется эта структура).
Так как обычный ответ на вопросы о размере стека - "они не переносимы", давайте предположим, что компилятор, операционная система и процессор - все известные величины для этого исследования. Предположим также, что рекурсия не используется, поэтому мы не имеем дело со сценарием "бесконечной рекурсии".
Каковы некоторые надежные способы оценить необходимый размер стека для потока? Я бы предпочел методы, которые автономны (статический анализ) и автоматические, но все идеи приветствуются.
10 ответов
Продолжительность-оценка
Онлайн-метод состоит в том, чтобы нарисовать весь стек определенным значением, например, 0xAAAA (или 0xAA, независимо от вашей ширины). Затем вы можете проверить, насколько большой размер стека максимально вырос в прошлом, проверив, какая часть рисунка осталась нетронутой.
Посмотрите на эту ссылку для объяснения с иллюстрацией.
Преимущество в том, что все просто. Недостатком является то, что вы не можете быть уверены, что размер вашего стека в конечном итоге не превысит объем используемого стека во время тестирования.
Статическая оценка
Есть некоторые статические проверки, и я думаю, что существует даже взломанная версия gcc, которая пытается это сделать. Единственное, что я могу вам сказать, это то, что в общем случае статическую проверку очень сложно выполнить.
Также взгляните на этот вопрос.
Вы можете использовать инструмент статического анализа, такой как StackAnalyzer, если ваша цель соответствует требованиям.
Если вы хотите потратить значительные средства, вы можете использовать коммерческий инструмент статического анализа, такой как Klocwork. Хотя Klocwork в первую очередь нацелен на обнаружение дефектов программного обеспечения и уязвимостей безопасности. Однако он также имеет инструмент под названием "kwstackru", который можно использовать для обнаружения переполнения стека в задаче или потоке. Я использую для встроенного проекта, над которым я работаю, и у меня были положительные результаты. Я не думаю, что какой-либо инструмент, подобный этому, идеален, но я считаю, что эти коммерческие инструменты очень хороши. Большинство инструментов, с которыми мне приходилось сталкиваться, борются с указателями функций. Я также знаю, что многие поставщики компиляторов, такие как Green Hills, теперь встраивают аналогичную функциональность прямо в свои компиляторы. Это, вероятно, лучшее решение, потому что компилятор хорошо знает все детали, необходимые для принятия точных решений о размере стека.
Если у вас есть время, я уверен, что вы можете использовать язык сценариев для создания собственного инструмента анализа переполнения стека. Сценарий должен будет определить точку входа в задачу или поток, сгенерировать полное дерево вызовов функций, а затем вычислить объем стекового пространства, которое использует каждая функция. Я подозреваю, что, вероятно, есть бесплатные инструменты, которые могут генерировать полное дерево вызовов функций, чтобы облегчить его. Если вы знаете специфику вашей платформы, генерирующей пространство стека, то каждая функция может быть очень простой. Например, первой инструкцией по сборке функции PowerPC часто является слово сохранения с инструкцией обновления, которая корректирует указатель стека на величину, необходимую для функции. Вы можете взять размер в байтах прямо из первой инструкции, что делает определение общего используемого пространства стека относительно простым.
Все эти типы анализа дадут вам приблизительную верхнюю границу использования стека, что именно вам и нужно знать. Конечно, эксперты (например, те, с которыми я работаю) могут жаловаться, что вы выделяете слишком много стекового пространства, но они - динозавры, которым не важно хорошее качество программного обеспечения:)
Еще одна возможность, хотя она не рассчитывает использование стека, заключается в использовании модуля управления памятью (MMU) вашего процессора (если он есть) для обнаружения переполнения стека. Я сделал это на VxWorks 5.4 с помощью PowerPC. Идея проста, просто поместите страницу памяти с защитой от записи в самый верх вашего стека. В случае переполнения произойдет освобождение процессора, и вы быстро получите предупреждение о проблеме переполнения стека. Конечно, это не говорит вам о том, насколько вам нужно увеличить размер стека, но если вы хорошо справляетесь с отладкой файлов исключений / ядра, вы можете, по крайней мере, выяснить последовательность вызовов, которая переполнила стек. Затем вы можете использовать эту информацию, чтобы соответствующим образом увеличить размер вашего стека.
-djhaus
Не бесплатно, но Coverity выполняет статический анализ стека.
Статическая (автономная) проверка стека не так сложна, как кажется. Я реализовал его для нашей встроенной IDE ( RapidiTTy) - в настоящее время он работает для ARM7 (NXP LPC2xxx), Cortex-M3 (STM32 и NXP LPC17xx), x86 и нашего собственного внутреннего ядра FPGA, совместимого с MIPS ISA.
По сути, мы используем простой анализ исполняемого кода, чтобы определить использование стека каждой функции. Наиболее значительное распределение стека выполняется в начале каждой функции; просто убедитесь, что он меняется с различными уровнями оптимизации и, если применимо, с наборами команд ARM/Thumb и т. д. Помните также, что задачи обычно имеют свои собственные стеки, а ISR часто (но не всегда) разделяют отдельную область стека!
Если у вас есть использование каждой функции, довольно просто создать дерево вызовов из анализа и рассчитать максимальное использование для каждой функции. Наша IDE генерирует для вас планировщики (эффективные тонкие RTOS), поэтому мы точно знаем, какие функции обозначены как "задачи", а какие - ISR, поэтому мы можем определить наихудшее использование для каждой области стека.
Конечно, эти цифры почти всегда превышают фактический максимум. Подумайте о функции, как sprintf
это может занимать много места в стеке, но сильно варьируется в зависимости от строки формата и параметров, которые вы предоставляете. В этих ситуациях вы также можете использовать динамический анализ - запустите в стеке известное значение при запуске, затем на некоторое время запустите в отладчике, сделайте паузу и посмотрите, сколько из каждого стека все еще заполнено вашим значением (тестирование стиля с высоким водяным знаком),
Ни один из подходов не идеален, но объединение обоих даст вам довольно хорошую картину того, на что будет похоже реальное использование.
Как обсуждалось в ответе на этот вопрос, распространенным методом является инициализация стека с известным значением, а затем некоторое время запускать код и смотреть, где останавливается шаблон.
Это не автономный метод, но в проекте, над которым я работаю, есть команда отладки, которая считывает верхнюю отметку во всех стеках задач в приложении. Это выводит таблицу использования стека для каждой задачи и количества доступного запаса. Проверка этих данных после 24-часового запуска с большим количеством взаимодействия с пользователем дает нам некоторую уверенность в том, что определенные распределения стека являются "безопасными".
Это работает с использованием хорошо зарекомендовавшей себя методики заполнения стеков известным шаблоном и предположения, что единственный способ, которым это можно переписать, - это обычное использование стека, хотя, если оно записывается любым другим способом, переполнение стека является меньше всего твоих забот!
Мы пытались решить эту проблему на встроенной системе на моей работе. Он сошел с ума, слишком много кода (как нашей, так и сторонних платформ), чтобы получить какой-либо надежный ответ. К счастью, наше устройство было основано на Linux, поэтому мы вернулись к стандартному поведению: каждому потоку было выделено 2 МБ, и администратор виртуальной памяти оптимизировал использование.
Нашей единственной проблемой с этим решением был один из сторонних инструментов, выполнивших mlock
на все пространство памяти (в идеале для повышения производительности). Это привело к тому, что все 2 МБ стека для каждого потока его потоков (75-150 из них) были выгружены. Мы потеряли половину нашего пространства памяти до тех пор, пока не выяснили это и не закомментировали ошибочную строку.
Sidenote: Менеджер виртуальной памяти Linux (vmm) распределяет оперативную память в 4K-блоках. Когда новый поток запрашивает 2 МБ адресного пространства для своего стека, vmm назначает поддельные страницы памяти всем, кроме самой верхней страницы. Когда стек превращается в поддельную страницу, ядро обнаруживает сбой страницы и заменяет поддельную страницу реальной страницей (которая потребляет еще 4 КБ фактической ОЗУ). Таким образом, стек потока может вырасти до любого необходимого размера (при условии, что он меньше 2 МБ), и vmm обеспечит использование только минимального объема памяти.
Помимо некоторых уже сделанных предложений, я хотел бы отметить, что часто во встраиваемых системах вам приходится жестко контролировать использование стека, потому что вы должны поддерживать размер стека в разумном размере.
В некотором смысле, использование стекового пространства немного похоже на распределение памяти, но без (простого) способа определить, было ли ваше выделение выполнено успешно, поэтому неконтролируемое использование стека приведет к вечной борьбе, чтобы выяснить, почему ваша система снова выходит из строя. Так, например, если ваша система выделяет память для локальных переменных из стека, выделите эту память с помощью malloc() или, если вы не можете использовать malloc(), напишите свой собственный обработчик памяти (что является достаточно простой задачей).
Нет нет:
void func(myMassiveStruct_t par)
{
myMassiveStruct_t tmpVar;
}
Да-да:
void func (myMassiveStruct_t *par)
{
myMassiveStruct_t *tmpVar;
tmpVar = (myMassiveStruct_t*) malloc (sizeof(myMassicveStruct_t));
}
Кажется довольно очевидным, но часто нет - особенно когда вы не можете использовать malloc().
Конечно, у вас все еще будут проблемы, так что это только то, что поможет, но не решит вашу проблему. Это, однако, поможет вам оценить размер стека в будущем, поскольку, как только вы найдете подходящий размер для стеков, и если затем, после некоторых модификаций кода, снова исчерпаете пространство стека, вы сможете обнаружить ряд ошибок или другие проблемы (слишком глубокие стеки вызовов для одного).
Не уверен на 100%, но я думаю, что это тоже можно сделать. Если у вас открыт порт jtag, вы можете подключиться к Trace32 и проверить максимальное использование стека. Хотя для этого вам придется дать начальный довольно большой произвольный размер стека.