Вопрос или проблема
Каковы основные стеки в Linux? Я имею в виду, например, когда происходит прерывание, какой стек будет использоваться для него, и в чем разница между стеками процессов пользователя и ядра?
Это очень специфично для платформы. Пока вы не привязаны к определенной платформе (даже разница между x86-32 и x86-64 является принципиальной), на это нельзя ответить. Но если ограничиться x86, согласно вашему последнему комментарию, я мог бы предложить некоторую информацию.
Существуют два основных стиля запроса службы (“syscall”) из пространства пользователя в пространство ядра: стиль прерываний и стиль sysenter. (Эти термины я придумал для данного описания.) Запросы в стиле прерываний обрабатываются процессором именно так же, как внешние прерывания. В защищенном режиме x86 это называется использованием int 0x80
(новый) или lcall 7,0
(самый старый вариант, совместимый с SysV) и реализовано с использованием так называемых шлюзов, настроенных как специальные дескрипторы сегментов. Переключение задач выполняется процессором. В самом дорогом варианте (шлюз задачи; единственный, который может использовать lcall 7,0
), во время этого переключения старые регистры задачи, включая общие регистры и указатель стека, сохраняются в TSS старой задачи, а новые регистры задачи, включая общие регистры и указатель стека, загружаются из TSS новой задачи. Другими словами, все “обычные” регистры сохраняются и загружаются (так что это очень долгий процесс). В более дешевой версии “шлюза вызова” (int 0x80
использует его) общие регистры не сохраняются/не восстанавливаются, только указатель стека и флаги; задача не переключается. (Существует отдельная проблема с состоянием FPU/SSE и пр., изменения которого откладываются – см. документацию для подробностей.)
Для обработки таких запросов службы ядро подготавливает отдельный стек для каждого потока (может также называться LWP – легковесный процесс), потому что поток может быть переключен во время любого блокируемого вызова функции. Такой стек обычно имеет небольшой размер (например, 4 КБ).
Поскольку переключение задач на x86 всегда изменяет указатель стека, нет шанса повторно использовать стек пользовательского пространства для ядра. С другой стороны, такое повторное использование не должно быть разрешено вовсе (за исключением небольшого количества данных текущего потока), поскольку страница процесса пользователя может быть небезопасной: другой активный поток может изменить или даже аннулировать ее. Поэтому просто запрещено использовать стек пользовательского пространства для выполнения в ядре, так что каждый поток должен иметь разные стеки для своего пользовательского и ядрового пространства; это остается верным для современных процессов в стиле sysenter. (С другой стороны, как уже было отмечено выше, каждый поток должен иметь стек для своего пространства ядра, отличающийся от другого потока.)
Обработка в стиле sysenter была разработана гораздо позже и реализована с помощью команд процессора SYSENTER и SYSCALL (единственной для x86-64). Они отличаются тем, что они не были разработаны с учетом старого (слишком жесткого) ограничения, что поддержка системных вызовов на аппаратном уровне должна сохранять все регистры. Вместо этого они были разработаны ближе к обычному ABI вызова функции (также известному как “конвенция вызова”), который позволяет функции произвольно изменять некоторые регистры (в большинстве конвенций вызова это называется “временные” или “временные” регистры), только несколько регистров изменяются, и забота о сохранении старых значений обеспечивается промежуточными функциями системных вызовов в пользовательском пространстве. Пара команд SYSENTER/SYSEXIT (для 32 и 64 бит) испортила старые значения RDX и RCX (странным образом – пользовательское пространство должно заранее заполнить их соответствующими значениями, а SYSEXIT использует их при возвращении), и новые RIP и RSP загружаются из соответствующих MSR, таким образом, стек немедленно переключается на стек ядра. В отличие от этого, SYSCALL/SYSRET (только для 64 бит) используют RCX и R11 для адреса возврата и флагов и не изменяют стек сами по себе. В современных реализациях ядро немедленно переключает стек на стек ядра при входе (и, соответственно, восстанавливает RSP при выходе). Практически это то же самое, что и результат: стек переключается перед тем, как в него помещается единственное значение ядром. Ядро может использовать части старого стека, но на самом деле избегает этого, потому что 1) нет гарантии, что стек пользовательского пространства достаточно велик, чтобы хранить все необходимые значения, и 2) по соображениям безопасности (см. выше). С этой точки зрения у нас снова есть стек ядра на поток. Наличие запасных регистров позволяет свободно манипулировать несколькими значениями во время обработки этого стека, но более того, использование SWAPGS позволяет минимизировать влияние (это так сейчас делает большинство ОС).
Помимо потоков пользовательского пространства, существует множество потоков только ядра (вы можете увидеть их в выводе ps
как названия внутри квадратных скобок). Каждый такой поток имеет свой собственный стек. Они реализуют 1) периодические процедуры, начинающиеся по какому-то событию или таймауту, 2) временные действия или 3) обрабатывают действия, запрашиваемые от реальных обработчиков прерываний. (Для случая 3 они названы “bh” в старых ядрах и “ksoftirqd” в более новых.) Большая часть этих потоков прикреплена к одному логическому процессору. Поскольку у них нет пользовательского пространства, у них нет стека пользовательского пространства.
Обработчики внешних прерываний были ограничены в Linux не более чем одним одновременно выполняемым обработчиком для каждого логического процессора; во время выполнения такого обработчика не разрешены IO-прерывания. (NMIs – ужасное исключение с подверженной ошибкам обработкой.) Они приходят с использованием прерывания переключения задач и имеют собственный стек для каждого логического процессора, по тем же причинам, что и описано выше.
ОБНОВЛЕНИЕ (октябрь 2024): это ограничение на обработку одного прерывания в конечном итоге снято с объединением так называемого “прерываемого ядра” в основное дерево. Конечно, обработка прерываний все еще происходит с большой осторожностью, чтобы не переполнить имеющиеся ресурсы.
Как уже отмечалось, большая часть этого текста слишком специфична для x86. Переключение задач с обязательной заменой указателя стека редко встречается на других архитектурах. Например, ARM (как 32, так и 64 бит) имеет один указатель стека на уровень привилегий, поддерживаемый аппаратно, поэтому, если внешнее прерывание приходит во время выполнения в пространстве ядра, указатель стека вообще не изменяется.
Некоторые детали в этом ответе могут устареть из-за высокой скорости разработки ядра. Рассматривайте это только как общую рекомендацию и проверяйте против конкретной версии, которую вы будете исследовать. Для более подробного описания обработки прерываний x86 и переключения задач, пожалуйста, обратитесь к “Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3A: System Programming Guide, Part 1” (бесплатно доступно на сайте Intel).
В Linux, при работе в пространстве пользователя, используется стек пользовательского пространства. Если процесс работает в пространстве ядра, он использует стек ядра, “принадлежащий” процессу. Прерывания обрабатываются в ядре.
Вероятно, лучшее место для получения таких подробностей о частных частях вашего любимого ядра – это покопаться на сайтах, посвященных программированию его, таких как, например, kernelnewbies или поискать на LWN в “страницах ядра” для Linux. Вы должны быть в состоянии найти аналогичные места для BSD или Solaris, и даже MacOS. Информации о Windows может быть труднее достать…
Такая информация не обсуждается в типичных учебниках по операционным системам, вам придется искать описания для разработчиков.
Ответ или решение
Вопрос о стеке в Linux, особенно в контексте обработки прерываний и различий между пользовательским и ядровым стеком, представляет собой важную тему для тех, кто работает в области системного программирования и администрирования Linux. Здесь мы рассмотрим основные аспекты, касающиеся стеков в Linux, с акцентом на архитектуру x86 и её особенности.
Основные Стековые Концепции в Linux
1. Стек Пользователя и Стек Ядра
В Linux процессы работают в двух режимах: пользовательском и ядровом. Каждый режим использует разные стеки для управления выполнения.
-
Стек Пользователя: Этот стек используется, когда процесс выполняется в пространстве пользователя. Он управляет локальными переменными, аргументами функций и возвратом из функций.
-
Стек Ядра: При переключении контекста на ядро, например, в процессе возникновения системного вызова или обработки прерываний, процесс использует стек ядра. Он выделяется для каждого потока (или легковесного процесса) и, как правило, имеет меньший размер, чем стек пользователя, часто около 4 КБ.
Это разделение стека необходимо по нескольким причинам:
- Безопасность: Стек пользователя может быть изменён другими потоками, и использование его в режиме ядра стало бы уязвимым местом.
- Изоляция: Для обеспечения стабильности при выполнении в ядре необходимо иметь отдельные области памяти для управления данными.
2. Обработка Прерываний
Обработка прерываний — это особый случай, который требует аккуратного управления стеком. В Linux происходит следующее:
-
В случае аппаратного прерывания, ядро выделяет отдельный стек для каждого логического процессора. Это важно, поскольку одновременно не может выполняться более одной обработчика прерываний на каждом процессоре.
-
Обработчики низкого уровня, такие как внешние прерывания, выполняются на выделенном стеке, чтобы избежать проблем с переполнением и обеспечить изоляцию при выполнении критических операций.
3. Системные Вызовы (Syscalls)
Системные вызовы в Linux имеют свои специфики, указывающие на два основных стиля:
-
Стили Прерываний: Реализуется через инструкции
int 0x80
, которые координируют работу с ядром, переключая стеки. -
Стили Sysenter/Syscall: Более современные и более быстрые, не требуют полного сохранения всех регистров. Вместо этого они работают на основе ABI (Application Binary Interface), что позволяет более эффективно обрабатывать системные вызовы.
Каждой пользовательской нити соответствует своя полоса стека для ядра, что позволяет избежать проблем, связанных с переключением контекста, и делает обработку более эффективной.
4. Архитектурные Отличия
Важно заметить, что эти концепции могут варьироваться в зависимости от архитектуры. Например, на архитектуре ARM в каждом режиме может использоваться один указатель стека на каждый уровень привилегий.
Заключение
Понимание различий между стеком ядра и стеком пользователя, а также механизма обработки прерываний и системных вызовов, крайне важно для разработчиков и системных администраторов, работающих в среде Linux. Это знание помогает обеспечить безопасность и стабильность работы системы. Для более глубокого понимания рекомендую обратиться к документации по конкретной версии ядра и специализированным ресурсам, таким как kernelnewbies.org и LWN.net.
Продолжайте исследовать, и вы сможете углубить свои знания в этой динамично развивающейся области системного программирования.