OSDev

для всех
Текущее время: 15 май 2024, 06:52

Часовой пояс: UTC + 3 часа




Начать новую тему Ответить на тему  [ Сообщений: 102 ]  На страницу Пред.  1 ... 3, 4, 5, 6, 7, 8, 9 ... 11  След.
Автор Сообщение
 Заголовок сообщения: Re: Планировщик
СообщениеДобавлено: 21 июл 2011, 21:42 

Зарегистрирован: 28 окт 2007, 18:33
Сообщения: 1418
Чесслово, ничего не понял. Какое отношение возможность переключения потоков имеет к реентерабельности? Что Вы вообще под этим словом понимаете?

При архитектуре, использовавшейся в RSX-11, VAX/VMS и с рядом изменений в Винде (и на которую опираюсь я), высокоприоритетный поток, выйдя из ожидания, немедленно вытесняет с процессора потоки с меньшим приоритетом; завладеть процессором ему может помешать только исполнение кода самого ядра (что совершенно правильно, если ядро сделано достаточно разумно, а в этом плане в сию категорию попадает и виндузовое ядро: все "медленные" работы там сваливаются на потоки ядра, которые ставятся на выполнение на общих основаниях с прикладными потоками, т.е. управляются планировщиком на основе их приоритетов). Насколько помню, у Винды стек ядра всего один (не считая стеков потоков ядра, но они в данном случае не в счёт, поскольку это именно что отдельные потоки со своим собственным контекстом, но существующие только в пространстве ядра, а посему нуждающиеся в стеке именно этого режима); у RSX-11 и VAX/VMS стек точно один. Тем не менее, это никак не мешает потокам получать управление вовремя в соответствии с их приоритетами, ну а RSX-11 и вовсе относится к ОС жёсткого реального времени, хотя в таковом качестве использовалась сравнительно редко: для применения в тогдашних контроллерах она слишком жирная (ей, видите ли, для работы нужно не меньше 32 Кбайт памяти, из коих примерно половину сожрёт сама система в минимальной конфигурации); кроме того, все её преимущества раскрываются лишь на достаточно мощной по тем временам конфигурации (хотя б 128 Кбайт ОЗУ, что, в свою очередь, требует наличия у процессора ММУ, что тогда было нечастым явлением среди мини-ЭВМ).

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

Пы.Сы. А про своё понимание реентерабельности и "горизонтально-вертикальную" реентерабельность, пожалуйста, поподробнее. Я даже представить не могу, что бы сие означало...


Вернуться к началу
 Профиль  
 
 Заголовок сообщения: Re: Планировщик
СообщениеДобавлено: 21 июл 2011, 22:02 

Зарегистрирован: 10 май 2007, 11:33
Сообщения: 1206
Отношение прямое. Это же элементарно. Вы нам навязываете подход, при котором системные вызовы должны выполняться либо "быстро" (как минимум без ожиданий), либо ожидания должны происходить на "границах" режима ядра (при входе/выходе), чтобы можно было использовать единственный системный стек, сохраняя адрес возврата и вершину прикладного стека в структуре потока. Получается, что полезная работа системного вызова должна происходить в нереентерабельном режиме. Отсюда и склонность к асинхронности... делать StartRead и Wait(сделать быстро, а потом ожидать)/StartReadAndWait (сделать быстро, а потом ожидать на "границе" при выходе из режима ядра) вместо Read, потому что по-другому никак. Т.е. по-другому совсем плохо... Read и все потоки ждут, пока чтение закончится, или в лучшем случае работают, но только пока исполняется их прикладной код.

Кстати размер стека ядра я выбирал с опорой на 32-разрядные Винды - 12 кб плюс стоп-фрейм (StopFrame/GuardPage) в одну страницу (в 64-разрядных версиях Виндов, если не ошибаюсь, размер стека ядра равен 24 кб).


Вернуться к началу
 Профиль  
 
 Заголовок сообщения: Re: Планировщик
СообщениеДобавлено: 22 июл 2011, 01:16 

Зарегистрирован: 28 окт 2007, 18:33
Сообщения: 1418
phantom-84 писал(а):
Отношение прямое. Это же элементарно. Вы нам навязываете подход, при котором системные вызовы должны выполняться либо "быстро" (как минимум без ожиданий), либо ожидания должны происходить на "границах" режима ядра (при входе/выходе), чтобы можно было использовать единственный системный стек, сохраняя адрес возврата и вершину прикладного стека в структуре потока. Получается, что полезная работа системного вызова должна происходить в нереентерабельном режиме. Отсюда и склонность к асинхронности... делать StartRead и Wait(сделать быстро, а потом ожидать)/StartReadAndWait (сделать быстро, а потом ожидать на "границе" при выходе из режима ядра) вместо Read, потому что по-другому никак. Т.е. по-другому совсем плохо... Read и все потоки ждут, пока чтение закончится, или в лучшем случае работают, но только пока исполняется их прикладной код.


Подход, что я "навязываю", является наиболее правильным, поскольку позволяет серьёзно упростить систему и ускорить её работу, параллельно уменьшив ещё и потребность в памяти. Иной подход порождает монстрообразные ядра, что мы видим на примере Линуха. Но дело не в этом.

Как я понял из Вашего объяснения (хотя желательно всё ж дождаться автора), под реентерабельностью в данном случае следует понимать возможность вызова системного сервиса до завершения полной обработки предыдущего вызова. Если это так, то Вы, похоже, не осознаёте, что никакой связи между числом стеков режима ядра и этой возможностью нет (хотя бы потому, что в природе существуют машины, вообще не имеющие стека -- те же ИБМовские мэйнфреймы, ну а ядро ОС/360 как раз было вытесняемым -- а заодно очень медленным, громоздким и ненадёжным). В каждый момент времени в любом случае обрабатывается один и только один вызов (на многопроцессорной системе, понятное дело, их может быть несколько, но по одному на процессор -- а у каждого процессора есть свой стек, так что картина в целом остаётся той же самой). Если обработку сервиса надо приостановить (у "меня" такое тоже может потребоваться, но, похоже, только для варианта ОС с поддержкой виртуальной памяти: какой-нибудь буфер режима пользователя может оказаться в файле подкачки, откуда его надо загрузить, прежде чем продолжить обработку), то ядро: 1) помечает поток как ожидающий завершения обработки сервиса и выводит его из числа готовых к выполнению; 2) сохраняет необходимую для продолжения обработки информацию в компактной структуре данных, выделяемой из динамической памяти системы; 3) запускает длительную операцию (например, подгрузку страницы) с запросом на вызов асинхронной процедуры ядра по завершению этой операции; 4) прекращает обработку сервиса и передаёт управление очередному готовому к выполнению потоку. После этого новый поток может вызвать какой-либо сервис, и всё повторится. Когда же закончится длительная операция, в порядке очерёдности будет вызвана заданная при запуске процедура, которая и продолжит обработку.

Можно возразить: а чем это лучше кучи стеков? Как минимум тем, что резко уменьшается расход памяти: вместо того, чтобы всегда выделять довольно значительный объём (несколько килобайт -- мало ли на какой глубине вложенности подпрограмм у ядра возникнет нужда "поспать") каждому потоку, достаточно выделять лишь маленький объём (несколько десятков байт, вряд ли на практике может потребоваться больше под собственно сохранение состояния), причём лишь тогда, когда он реально необходим. Как следствие, под память данных и стека ядра (если отбросить таблицы переадресации, но их объём не зависит от того, как устроено ядро) отводить нужно не мегабайты, а десятки килобайт. Кроме того, такой подход, скорее всего, упростит разработку и отладку ядра: ожидание никогда не может иметь место "неожиданно" для программиста, поскольку он сам явным образом его вызывает, а заодно сам "складывает" нужные для продолжения выполнения данные, за счёт чего лучше понимает, какие ресурсы остаются закреплёнными за ожидающим обработчиком сервиса, а значит, меньше вероятность, что он чего-то не заметит и тем самым допустит возникновение блокировки. Из потенциальных минусов можно отметить два. Первый -- асинхронный характер вызова для продолжения обработки. На самом деле, однако, никаких проблем это не доставляет: ядро -- это сплошь асинхронность (прерывания приходят асинхронно, а в многопроцессорной системе так вообще ядро параллельно самому себе работать может), так что этот момент в любом случае приходится учитывать и тем или иным способом разруливать. Здесь очень помогает механизм отложенной обработки (не знаю, как в Линухе, а в Винде он имеется, куда перекочевал из VAX/VMS, а туда -- из RSX-11; в более ранних известных мне системах его не было, но я не знаток столь глубоких древностей). При использовании этого механизма вызов асинхронной процедуры продолжения обработки системного сервиса произойдёт "синхронно": в тот момент, когда ядро закончило все ранее начатые отложенные обработчики, поскольку они выполняются строго последовательно в порядке их появления. Второй потенциально отрицательный момент -- дополнительное время на выделение-освобождение памяти и сохранение в ней данных. Минус этот сомнительный сразу по нескольким причинам: 1) Зачастую данные, связанные с обработкой запроса задачи, в любом случае удобнее хранить в некоторой "централизованной" структуре, а не "размазывать" по стеку; соответственно, копирование данных исключается. 2) Хотя выделение/освобождение памяти -- довольно медленная операция по сравнению с "откусыванием" куска стека, она выполняется крайне быстро по сравнению со временем, необходимым на выполнение операции, из-за которой приходится откладывать обработку сервиса (типичный пример -- как раз подгрузка страницы с диска), поэтому общие потери времени совершенно незначительны. 3) Более того, увеличение, пускай и незначительное, времени на выделение-освобождение памяти будет, скорей всего, с лихвой скомпенсировано сильным уменьшением расхода памяти: её больше останется для полезной работы. 4) Добавление времени на выделение-освобождение памяти по крайней мере частично компенсируется исключением необходимости переключать стек. Особенно существенным это может оказаться на процессоре с кэшем, где единый стек системы, имеющий малый объём, с очень приличной вероятностью "застрянет" в кэше навсегда, а вот довольно крупыне кэши потоков там точно не удержатся: кэш всё ж не резиновый, даже если он большой.

Цитата:
Кстати размер стека ядра я выбирал с опорой на 32-разрядные Винды - 12 кб плюс стоп-фрейм (StopFrame/GuardPage) в одну страницу (в 64-разрядных версиях Виндов, если не ошибаюсь, размер стека ядра равен 24 кб).


Вполне разумный размер при создании системы на ЯВУ -- конечно, если стек всего один :)

Пы.Сы. Похоже, Вы по-прежнему убеждены в практической бесполезности асинхронного ввода-вывода, но доказательств сему не привели. В частности, фактически без ответа остались мои алгоритмы обработки файла с информацией, используя асинхронный ввод-вывод. Хотелось бы дождаться по этому поводу чего-нибудь вразумительного, тем более что я настаиваю абсолютно категорически: асинхронный ввод-вывод необходим для любой хоть сколько-нибудь вменяемой операционной системы, поскольку он обеспечивает массу полезных возможностей, которые попросту невозможно реализовать при его отсутствии, и в то же время не имеет никаких неприятных "побочных эффектов" (ну, кроме необходимости включать мозги, когда его применяешь, но это как бы само собой разумеется), а вот отдельный синхронный ввод-вывод на уровне логики системы (а не прикладных библиотек) не нужен вообще, поскольку в реальности это всегда комбинация из асинхронного ввода-вывода и ожидания.


Вернуться к началу
 Профиль  
 
 Заголовок сообщения: Re: Планировщик
СообщениеДобавлено: 22 июл 2011, 01:55 

Зарегистрирован: 18 апр 2010, 15:59
Сообщения: 155
Поясняю.

Под вертикальной реентерабельностью я понимаю способность ядра поддерживать возможность корректного прерывания и возобновления исполняющегося потока инструкций более привелигированным (с точки зрения апаратуры) потоком инструкций. Пример: обрабатывается system call в режиме ядра, возникает исключение с приоритетом 5 (например 0x54) и прерывает эту обработку. Во время работы обработчика прерывания (0х54) возникает прерывание с более высоким приоритетом (например 0х86) и прерывает предыдущий обработчик (0х54) и т.д. После чего происходит последовательное возобновление всех прерванных потоков вплоть до первоначального system call-а. При этом, как вы уже заметили, все прерывания происходят незамедлительно сразу же после наступления определенного события и при этом формируется стек прерванных потоков инструкций.

Под горизонтальной реентерабельностью я понимаю способность ядра поддерживать возможность корректного прерывания и возобновления исполняющейся задачи (по терминологии Intel) более привелигированной задачей (с точки зрения операционной системы). Пример: обрабатывается system call в режиме ядра, при этом на каком-то этапе этой обработки освобождается какой-то логический ресурс (например мьютекс), которого ожидает другая, более приоритетная, задача. Обнаруживая это ядро незамедлительно производит переключение на эту более приоритетную задачу, не дожидаясь того момента, когда менее приоритетная задача завершит обработку system call-а. При этом, как вы уже заметили, все прерывания происходят незамедлительно сразу же после наступления освобождения какого-то логического ресурса, ожидаемого более приоритетной задачей и при этом переключения происходят по кольцу задач.

При этом могут происходить более сложные комбинированные сценарии. Например задача А сделала системный вызов, во время обработки которого произошло исключение (1 акт верт. реентерабельности), во время обработки которго произошло прерывание (2 акт верт. реентерабельности), во время обработки которого было принято решение о передаче сообщения о прерывании высокоприоритетной задачи обработчику находящейся в состоянии ожидания. Сразу же по окончанию обработчика прерывания операционная система производит переключение на эту более привелигированную задачу-обработчик. Когда же планировщик примет решение вернуть управление задаче А (при этом возможно, что после задачи-обработчика управление давалось еще доброй сотне других задач) происходит пкорректное возобновление обработки прерывания в контексте задачи А, потом возобновление обработки system call-а, потом возврат управления в user space.

Так что phantom-84 понял меня правильно.

В случае одного стека, операционная система не способна осуществить передачу управления в любой точке своего кода. И в только что описанном примере, вы будете вынуждены ожидать полного завершения обработки и обработчика исключения и system call-а прежде чем сможете передать управление задаче-обработчику прерывания, хотя задача А может быть пасьянсом косынкой, а задача-обработчик может контролировать какой-нибудь критически важный высокоточный таймер или датчик или звуковую карту. В итоге вы получите либо трудно-контролируемую погрешность в измерении времени, либо трагедию либо прерывный звук из колонок.


Вернуться к началу
 Профиль  
 
 Заголовок сообщения: Re: Планировщик
СообщениеДобавлено: 22 июл 2011, 02:11 

Зарегистрирован: 21 сен 2007, 17:24
Сообщения: 1088
Откуда: Балаково
Мне кажется, что пользовательский поток может работать со своим основным стеком и во время системного вызова. Ядро должно только проверить указатель ESP, чтобы был в границах региона выделенного под стек, и всё.


Вернуться к началу
 Профиль  
 
 Заголовок сообщения: Re: Планировщик
СообщениеДобавлено: 22 июл 2011, 05:45 

Зарегистрирован: 28 окт 2007, 18:33
Сообщения: 1418
ZarathustrA писал(а):
Поясняю.

Под вертикальной реентерабельностью я понимаю способность ядра поддерживать возможность корректного прерывания и возобновления исполняющегося потока инструкций более привелигированным (с точки зрения апаратуры) потоком инструкций. Пример: обрабатывается system call в режиме ядра, возникает исключение с приоритетом 5 (например 0x54) и прерывает эту обработку. Во время работы обработчика прерывания (0х54) возникает прерывание с более высоким приоритетом (например 0х86) и прерывает предыдущий обработчик (0х54) и т.д. После чего происходит последовательное возобновление всех прерванных потоков вплоть до первоначального system call-а. При этом, как вы уже заметили, все прерывания происходят незамедлительно сразу же после наступления определенного события и при этом формируется стек прерванных потоков инструкций.

Под горизонтальной реентерабельностью я понимаю способность ядра поддерживать возможность корректного прерывания и возобновления исполняющейся задачи (по терминологии Intel) более привелигированной задачей (с точки зрения операционной системы). Пример: обрабатывается system call в режиме ядра, при этом на каком-то этапе этой обработки освобождается какой-то логический ресурс (например мьютекс), которого ожидает другая, более приоритетная, задача. Обнаруживая это ядро незамедлительно производит переключение на эту более приоритетную задачу, не дожидаясь того момента, когда менее приоритетная задача завершит обработку system call-а. При этом, как вы уже заметили, все прерывания происходят незамедлительно сразу же после наступления освобождения какого-то логического ресурса, ожидаемого более приоритетной задачей и при этом переключения происходят по кольцу задач.


Сначала сделаю важное замечание. Вы абсолютно неверно используете термин "реентерабельность", из-за чего я и не мог Вас понять. Реентерабельность, или повторная входимость (re-enterable) -- это пригодность кода к повторному его вызову до того момента, как завершилось его предыдущее использование. Чтобы обеспечить это, должны соблюдаться две вещи: 1) код не должен модифицировать сам себя ни прямо, ни косвенно (через вызовы чего-то там ещё); 2) для каждого из параллельных вызовов должен быть полностью свой набор изменяемых данных (т.е. не допускаются статические переменные, только создаваемые динамически в стеке или в динамической памяти). Операционная система в целом в принципе не может быть реентерабельной, поскольку там встречаются участки кода, принципиально не допускающие параллельного выполнения (из-за чего и прибегают к запрету прерываний, а в многопроцессорных системах -- ещё и ко всякого рода средствам синхронизации), хотя большая часть кода системы, если он написан нормально, является реентерабельной (т.е. одна и та же процедура может быть вызвана несколько раз одновременно). К возможности же переключения потоков в произвольный момент времени или к возможности прерывания кода самого ядра реентерабельность прямого отношения не имеет, и уж тем более она не имеет вообще никакого отношения к числу стеков (поскольку конкретная процессорная архитектура может вообще не иметь стека).

Ваша "вертикальная реентерабельность" на самом деле является вытесняемостью ("знатоки" русского языка временами используют английскую кальку -- преемптивность) обработчиков прерываний. Подавляющее большинство операционных систем её имеют, да и реализуется она легко и просто. Замечу, что сами обработчики прерываний при этом могут быть нереентерабельными. Например, если произошло прерывание А, то до завершения его обработки это же прерывание повторно происходить не должно, иначе такому обработчику "сорвёт крышу", однако вполне могут произойти прерывания Б, В и Г, каждое из которых обслуживается своим собственным обработчиком. Простейший пример нереентерабельного обработчика, наверное, таков: он подсчитывает число прерываний (например, тиков таймера), в некоторой переменной, к которой по тем или иным причинам не имеет атомарного доступа (например, процессор не имеет команды инкремента содержимого ячейки памяти: надо сначала загрузить в регистр, увеличить и записать результат -- АРМ именно таков). Если значение уже было загружено в регистр и в этот момент произошло прерывание, снова вызвавшее этот обработчик, то его повторное исполнение загрузит ещё не модифицированное значение, увеличит, сохранит и вернёт управление первой копии обработчика, которая увеличит ранее загруженное немодифицированное значение, что в итоге даст неверный результат: счётчик будет увеличен на 1, а не на 2.

Ваша "Горизонтальная реентерабельность" -- это вытесняемость ядра прикладным кодом. К числу стеков у ядра она также прямого отношения не имеет, хотя, конечно, обеспечить её в системе, где для каждого потока режима пользователя есть стек режима ядра, действительно проще. Ещё проще, однако, реализовать её в настоящей микроядерной системе (существуют ли такие в природе, не считая всяких экспериментальных -- не знаю; всем известная QNX является микроядерной только в смысле пиара, реально же это система с монолитным ядром, из которого в режим пользователя вынесены драйверы и только драйверы -- весь остальной код системы исполняется в едином адресном пространстве, а значит, лишён единственного преимущества микроядерности -- надёжной защиты компонентов системы друг от друга): в ней весь системный код, за исключением маленького микроядра, исполняется в виде потоков пользовательского режима, а значит, управляется планировщиком и в любой момент может быть изменён.

Цитата:
В случае одного стека, операционная система не способна осуществить передачу управления в любой точке своего кода. И в только что описанном примере, вы будете вынуждены ожидать полного завершения обработки и обработчика исключения и system call-а прежде чем сможете передать управление задаче-обработчику прерывания, хотя задача А может быть пасьянсом косынкой, а задача-обработчик может контролировать какой-нибудь критически важный высокоточный таймер или датчик или звуковую карту. В итоге вы получите либо трудно-контролируемую погрешность в измерении времени, либо трагедию либо прерывный звук из колонок.


Как я уже сказал, и с одним стеком, и вообще без стека решить эту задачу можно, только надо думать при создании кода несколько больше, чем со многими стеками (там думать вообще не надо, по большому счёту). Но взгляните на проблему с другой стороны, а именно: нужна ли такая вытесняемость вообще? Я утверждаю категорически: нет, не нужна, и вот почему.

Действительно, есть очень критичные к времени обработки процессы, ставящие крайне жёсткие временные рамки. На практике, однако, такие события обрабатывают либо специализированными контроллерами, работающими обычно вообще без операционной системы, либо с помощью драйверов режима ядра. Дело в том, что, даже если само ядро будет полностью вытесняемым, за что Вы ратуете, всё равно время входа в обработчик прерывания будет на один-два порядка ниже, чем время активации спящего потока по той простой причине, что вход в обработчик прерывания осуществляется чисто аппаратным способом и не займёт больше времени, чем максимально допустимая длительность работы с запрещёнными прерываниями (а оно в правильно спроектированной системе крайне невелико -- например, в RSX-11 не превышало 100 мкс, и это на машине, делавшей порядка 250 тыс. операций "регистр-регистр" в секунду, т.е. когда даже на простую команду типа MOV R1, R2 уходило примерно 4 мкс), а активация потока требует выполнения приличной последовательности команд по полному сохранению текущего контекста, восстановлению контекста вызываемого потока и т.д. (посчитайте сами: сохранение и восстановление семи регистров у PDP-11 -- это 14 команд формата "регистр-память" с автоинкрементной-автодекрементной адресацией, которые выполнялись примерно вдвое медленнее, чем "регистр-регистр"; соответственно, только они в аналогичных условиях будут выполняться больше тех самых 100 мкс, а ведь надо выполнить целую кучу других действий, чтобы пробудить поток). Таким образом, обработка действительно очень жёстко ограниченных по времени событий кодом режима пользователя обычно попросту неприемлема.

Однако существуют и менее критичные по времени события (те же 100 мкс, что были пределом для техники начала 1970-х, сейчас -- чуть ли не плёвое дело даже для слабых микроконтроллеров). Как быть с ними? Пихать всё подряд в ядро, понятное дело, не хочется, и если для действительно критичного ко времени кода это будет разумным (а то и вообще единственно возможным) шагом, то для чего-нибудь не столь важного хотелось бы подобного избежать. Казалось бы, здесь вытесняемым ядрам будет самое место: сверхбыструю реакцию для потоков они не обеспечат, но просто быструю -- вполне. Однако и здесь более правильным путём станет создание просто быстрого ядра, которое за небольшое процессорное (подчеркну: не астрономическое, а именно процессорное) время способно разделаться с любым запросом прикладного кода. Наихудшее время реакции (т.е. передачи управления высокоприоритетному потоку) будет складываться из времени работы всех обработчиков прерываний (ведь худший случай -- это наличие всех потенциально возможных запросов прерываний одновременно), работающих по наиболее долгому сценарию (вероятно, для каждого -- завершение выполнявшейся операции ввода-вывода и запуск следующей за ней операции, находящейся в очереди) плюс время обработки самого длинного запроса со стороны потока. Это, на первый взгляд, многократно превосходит время, необходимое на вытеснение кода ядра и вызов потока, однако не всё так просто: 1) обработку прерываний, вообще говоря, откладывать нельзя, поскольку может возникнуть переполнение буферов или ещё что-то в этом роде, т.е. время на работу обработчиков можем смело вычитать: их всё равно придётся выплнять до вызова потока; 2) нельзя не выполнять и обработку завершения операций ввода-вывода, поскольку завершения может ожидать ещё более приоритетный поток, и окончательно разобраться, какому ж потоку передавать управление, можно лишь после того, как все физически завершившиеся операции были завершены и логически, а ожидающие их потоки выведены из ожидания; 3) затягивать начало операций ввода-вывода после окончания предыдущих в общем случае тоже нельзя: устройство может быть критичным к времени запуска новых операций (обмен данными по синхронному каналу связи, например). В итоге получается, что безболезненно можно прервать лишь обработку вызова системного сервиса со стороны другого потока. Однако, если эта обработка занимает мало времени, есть ли смысл её прерывать? У приснопоминаемой мною RSX-11, помнится, наихудшее время обработки системного вызова составляло что-то вроде 10 мс -- на той же самой машине с быстродействием порядка 250 тыс. оп/с. (во время оно приходилось видеть подробный расчёт этого и других времён для конкретной версии ОС на конкретном железе -- типа методического пособия, так сказать, но, поскольку сие было лет двадцать тому назад, точные цифры уже не помню). Легко посчитать, что 10 мс для того железа -- это порядка 2500 команд "регистр-регистр" (реально их было меньше 1000, поскольку основную массу составляли команды "регистр-память" и "память-память"). Если перенести на наши дни и предположить, что за счёт более плохой системы команд и более низкого качества кода (использование ЯВУ, а не ассемблера), а также усложнения функций самой системы потребное число команд на обработку аналогичного по сути системного вызова увеличилось в 10 раз (25000 операций "регистр-регистр"), то для 100-МГц несуперскалярного процессора, выполняющего каждый такт по команде, т.е. имеющем быстродействие 100 млн. операций "регистр-регистр" в секунду (таковы все АРМы до версии АРМв7, а также 80486) получим время в 250 мкс. Увеличим его ещё в 10 раз в расчёте на случай, что никаких данных в кэше нет и т.п. -- получим 2,5 мс, что раза в 4 лучше той самой RSX-11, которая, напомню, была системой жёсткого реального времени. Причём замечу, что мы получаем такой расклад на крайне слабом с современной точки зрения процессоре даже для мобильного устройства (например, в моём телефоне установлен Кортех-А9 частотой 1 ГГц, выполняющий две команды за такт, т.е. имеющий пиковое быстродействие 2 млрд. оп/с).

Возвращаясь к исходной теме. Этот расчёт я привёл в качестве примера того, какое время можно реально сэкономить для активации потока, если сделать вытесняемое ядро. Как видите, оно весьма и весьма невелико, что, мягко говоря, заставляет усомниться в ценности этого решения: да, активировать поток можно быстрее, но времени для действительно критичного обработчика всё равно не хватит, и его придётся делать в ядре, ну а не очень критичный может и подождать (т.е. его можно написать таким образом, что он не будет слишком чувствителен к задержкам, не выходящим за разумные пределы). Примером такового является тот же аудиоплеер: что мешает иметь, предположим, три буфера, каждый из которых вмещает по полсекунды звучания? В нормальном случае два буфера заполнены, третий заполняется, а когда он заполнен, поток уходит в ожидание. При опустошении одного из буферов поток пробуждается, однако, если это невозможно сделать сразу (система страдает обработкой чужого запроса), несколько лишних миллисекунд ожидания ничем не повредят: запас-то в целую секунду сделан. Зато отказ от вытесняемости ядра упрощает само ядро и снижает накладные расходы, т.е. больше времени (и памяти) остаётся на работу приложений, а если им делать всё равно нечего, то меньше расходуется энергии (сделав все дела, процессор просто засыпает и перестаёт жрать электричество вагонами). Для мобильных применений последнее более чем критично, да и для настольных экономия не помешает (платить-то меньше, пускай разница и не колоссальная).

Что же касается применения вытесняемых ядер на практике, то все эти случаи -- либо следствие особых требований, предъявляемых к системе, либо банально неграмотное проектирование (ну или смесь того и другого, конечно). Например, ядро ОС/360 половину времени работает при запрещённых прерываниях и посему ни разу не удовлетворяет требованиям реального времени, однако во всю вторую половину времени оно является вытесняемым. Последнее объясняется тем, что почти всё ядро выполнено в виде множества мелких транзитных модулей (размером от 8 байт до 1 Кбайта), чтобы довольно крупная система (суммарный объём кода ядра составляет несколько сотен килобайт) могла работать на машине с малым объёмом памяти (изначально -- от 64 Кбайт). Понятное дело, пока система подгружает с диска свои очередные кишки, процессору простаивать не резон, вот она сама себя и вытесняет. Она могла бы вытеснять ядро и для ускоренной передачи управления потоку (задаче в тамошней терминологии); это не делалось лишь потому, что было лишено реального смысла (изначально система рассчитывалась на пакетную обработку, где требование №1 -- фактическая полезная производительность, а время реакции вообще никакой роли не играет). RSX-11 имела невытесняемое ядро: оно целиком помещалось в памяти и работало очень быстро, в то же время задачи тоже могли либо целиком находиться в памяти, либо целиком быть выгруженными на диск (из-за особенностей системы команд и ММУ, ежели таковое имелось), а поэтому в принципе не возникало ситуаций, когда необходимо было бы приостановить работу кода ядра для подгрузки чего-либо с диска. VAX/VMS умела вытеснять своё ядро на время подгрузки из файла подкачки, а вот вытесняться, чтобы предоставить процессор потоку -- насколько помню, нет (поскольку работала тоже очень быстро, будучи, по большому счёту, расширенным вариантом RSX-11, перенесённым на вычислительную машину другой архитектуры). В Винде вытесняемости потоками как таковой нет (опять-таки, насколько помню), но там она и не нужна: длительная обработка возлагается на потоки режима ядра (что само по себе изврат, но лучше, чем полная вытесняемость ядра), ну а обычные запросы выполняются ядром достаточно быстро, и поэтому нет резона его вытеснять. В Линухе (и вообще во многих, если не во всех, унихах) вытесняемость ядра -- это, судя по всему, костыль, возникший из-за грубейших просчётов при создании системы и её АПИ (в первую очередь, отсутствия асинхронного ввода-вывода, из-за чего пришлось, насколько знаю, не только "усыплять" поток в системе во время обработки запросов ввода-вывода, но ещё и придумывать возможность прерывания уже выполняющихся длительных операций ввода-вывода, чтобы приложение могло получить какой-нибудь срочный сигнал -- ввод-вывод-то синхронный, поэтому послать уведомление человеческим путём невозможно).


Вернуться к началу
 Профиль  
 
 Заголовок сообщения: Re: Планировщик
СообщениеДобавлено: 22 июл 2011, 05:51 

Зарегистрирован: 28 окт 2007, 18:33
Сообщения: 1418
Himik писал(а):
Мне кажется, что пользовательский поток может работать со своим основным стеком и во время системного вызова. Ядро должно только проверить указатель ESP, чтобы был в границах региона выделенного под стек, и всё.


Если ядро выполняет вызов из этого потока, то поток по определению работать не может: он ожидает результата системного вызова. Правда, можно сделать костыль с прерываемыми системными вызовами, что резко усложняет и систему, и прикладную программу (ей же надо разбираться, как завершился вызов, надо ли его перезапускать, если надо, то как именно), но куда проще сделать асинхронный ввод-вывод и нормальную обработку асинхронных сигналов: это начисто исключает необходимость в каких-либо костылях, а в реализации очень просто (и всегда де-факто реализовано внутри самого ядра, поскольку ядро, отвечая за обработку прерываний, по самой природе является асинхронным, и усложнение требуется не для асинхронности, а наоборот). Ну а если ядро выполняет запрос потока А, то поток Б, понятное дело, может спокойно себе работать со своим стеком -- если машина многопроцессорная или если ядро полностью вытесняемое.


Вернуться к началу
 Профиль  
 
 Заголовок сообщения: Re: Планировщик
СообщениеДобавлено: 22 июл 2011, 10:35 

Зарегистрирован: 21 сен 2007, 17:24
Сообщения: 1088
Откуда: Балаково
SII писал(а):
Если ядро выполняет вызов из этого потока, то поток по определению работать не может: он ожидает результата системного вызова.

Вот именно. Так зачем ему второй стек?
Кстати, о потоках для обработчиков прерываний. Я сделал для этого специальный максимально облегчённый тип потоков. Эти потоки не имеют привязки к адресному пространству. Вызов такого потока приводит только к перезагрузке регисторов и формальному переключению номера активного потока. Скорость нормальная.


Вернуться к началу
 Профиль  
 
 Заголовок сообщения: Re: Планировщик
СообщениеДобавлено: 22 июл 2011, 13:59 

Зарегистрирован: 28 окт 2007, 18:33
Сообщения: 1418
А кто утверждает, что нужен второй стек? Лично я утверждаю как раз то, что у каждого потока должен быть всего один стек -- режима пользователя, и всё. Никаких вторых стеков, в т.ч. режима ядра (у ядра свой собственный стек на все случаи жизни).

Насчёт "максимально облегчённых потоков". А зачем они понадобились? Почему просто не обрабатывать прерывания в обычных обработчиках, вытесняющих друг друга в соответствии с аппаратными приоритетами? Ведь полноценно обработать прерывание для любого случая в таком потоке, как и в обычном обработчике, не получится: бывают случаи, когда нужен доступ к адресному пространству задачи, а значит, всё равно надо разделять выполнение в "произвольном контексте" и в "контексте задачи", имея в виду как минимум настройку ММУ.


Вернуться к началу
 Профиль  
 
 Заголовок сообщения: Re: Планировщик
СообщениеДобавлено: 22 июл 2011, 14:19 

Зарегистрирован: 21 сен 2007, 17:24
Сообщения: 1088
Откуда: Балаково
SII писал(а):
А кто утверждает, что нужен второй стек?
Насчёт "максимально облегчённых потоков". А зачем они понадобились?

Насчёт стека это пожалуй вопрос к знатокам.
Насчёт потоков, там производится первая стадия обработки. И не всегда ему требуется контекст пользовательского потока. Происходит множество прерываний, прежде чем сформируется готовый блок данных, пригодный для обработки прикладной задачей. Например, принятие скан-кода от клавиатуры происходит в виде длинной серии прерываний, и лишь просуммировав их, получается готовый скан-код, пригодный для отправки на терминал. Нет смысла дёргать терминал от _каждого_ прерывания клавиатуры. Многие устройства работают по аналогичной схеме.
С таймером немного другая ситуация. Первичный обработчик таймера хранит массив периодов для разых потоков, которым надо будет посылать сигнал по наступлению времени. Контекст целевого потока таймеру не нужен, он всего лишь посылает сигнал (возможно, даже несколько, разным потокам), да и трудно предсказать, какому потоку нужно будет сигнализировать в следующем периоде.


Последний раз редактировалось Himik 22 июл 2011, 14:37, всего редактировалось 3 раз(а).

Вернуться к началу
 Профиль  
 
Показать сообщения за:  Поле сортировки  
Начать новую тему Ответить на тему  [ Сообщений: 102 ]  На страницу Пред.  1 ... 3, 4, 5, 6, 7, 8, 9 ... 11  След.

Часовой пояс: UTC + 3 часа


Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 84


Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете добавлять вложения

Найти:
Перейти:  
cron
Создано на основе phpBB® Forum Software © phpBB Group
Русская поддержка phpBB