Распределение стека для зеленых потоков С++

Я занимаюсь некоторыми исследованиями в зеленых потоках С++, в основном boost::coroutine2 и подобных функциях POSIX, таких как makecontext()/swapcontext(), и планируем реализовать библиотеку потоков зеленого потока С++ поверх boost::coroutine2. Оба требуют, чтобы пользовательский код выделял стек для каждой новой функции/сопрограммы.

Моя целевая платформа - x64/Linux. Я хочу, чтобы моя библиотека зеленых потоков была пригодна для общего использования, поэтому стеки должны расширяться по мере необходимости (разумный верхний предел хорош, например, 10 МБ), было бы здорово, если бы стеки могли сокращаться, когда слишком много памяти не использовалось (не требуется). Я не вычислил соответствующий алгоритм для распределения стеков.

После некоторого поиска в Google я сам определил несколько вариантов:

  • использовать сплит-стек, реализованный компилятором (gcc -fsplit-stack), но сплит-стек имеет служебные издержки. Go уже отошел от расщепленного стека из-за причин производительности.
  • выделите большой кусок памяти с помощью mmap(), надеясь, что ядро ​​достаточно умное, чтобы оставить физическую память нераспределенной и распределить только при доступе стеков. В этом случае мы находимся во власти ядра.
  • зарезервируйте большое пространство памяти с помощью mmap(PROT_NONE) и настройте обработчик сигнала SIGSEGV. В обработчике сигнала, когда SIGSEGV вызван доступом к стеку (доступная память находится в большом объеме памяти), выделите нужную память с помощью mmap(PROT_READ | PROT_WRITE). Вот проблема для этого подхода: mmap() не является асинхронным, не может быть вызван внутри обработчика сигнала. Он по-прежнему может быть реализован, но очень сложно: создать другой поток во время запуска программы для распределения памяти и использовать pipe() + read()/write() для передачи информации о распределении памяти из обработчика сигнала в поток.

Еще несколько вопросов о опции 3:

  • Я не уверен, что накладные расходы на производительность этого подхода, насколько хорошо/плохо работает ядро ​​/CPU, когда пространство памяти чрезвычайно фрагментировано из-за тысяч вызовов mmap()?
  • Правильно ли этот подход, если доступ к нераспределенной памяти в пространстве ядра? например когда read() вызывается?

Есть ли другие (лучшие) варианты для распределения стека для зеленых потоков? Как зеленые стеки потоков распределены в других реализациях, например. Go/Java?

Ответ 1

Способ, которым glibc выделяет стеки для обычных программ на C, - это mmap область со следующим флагом mmap, разработанным именно для этой цели:

   MAP_GROWSDOWN
          Used for stacks.  Indicates to the kernel virtual memory  system
          that the mapping should extend downward in memory.

Для совместимости вы, вероятно, должны использовать MAP_STACK тоже. Тогда вам не нужно писать обработчик SIGSEGV самостоятельно, и стек растет автоматически. Оценки могут быть установлены, как описано здесь Что такое "ulimit -s unlimited" делать?

Если вам нужен размер ограниченного стека, который обычно используется для обработчиков сигналов, если они хотят вызвать sigaltstack(2), просто выполните обычный вызов mmap.

Ядро Linux всегда отображает физические страницы, которые возвращают виртуальные страницы, ломает страницу при первой странице доступа (возможно, не в ядрах реального времени, но, конечно, во всех других конфигурациях). Вы можете использовать интерфейс /proc/<pid>/pagemap (или этот инструмент, который я написал https://github.com/dwks/pagemap), чтобы проверить это, если вы заинтересованы.

Ответ 2

Почему mmap? Когда вы назначаете новый (или malloc), память остается нетронутой и определенно не отображается.

const int STACK_SIZE = 10 * 1024*1024;
char*p = new char[STACK_SIZE*numThreads];

p теперь имеет достаточно памяти для тех потоков, которые вы хотите. Когда вам понадобится память, начните доступ к p + STACK_SIZE * i