Как стать автором
Обновить

Низкоуровневое программирование под 8086 для любопытных, часть 2

Уровень сложностиСредний
Время на прочтение15 мин
Количество просмотров9.1K
Всего голосов 45: ↑45 и ↓0+57
Комментарии30

Комментарии 30

ой, давно это было, могу где-то и соврать, и так начнем накидывать :)

  1. прерывание по таймеру можно перепрограмировать на более маленькие промежутки времени (я например делал 0.01 сек), но это чревато проблеммами с дисками, раньше рекомендовалось на время изменение частоты отключать все операции с дисками

  2. память имеет кучу и стек, при чем стек имеет ограниченый размер (и расположение в конце памяти), что может помешать (переполнение стека) запускать много нитей

  3. изменять стек вроде нелья было, да и вообще адресация строго в рамках сегмента (65к), конечно есть оператор извращения типа проекции файла в память, или глобальный mem (точно уже не помню все нюансы), но прямое изменение любой уже выделеной памяти вне своего сегмента данных для кода была проблеммой

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

  5. не забываем, что в системе могут быть чужие резидентные программы которые то-же могут пытатся использовать прерывания.

Короче тема интересная, но если реально пытатся ее реализовать, то нужно ориентироватся на совсем низкий уровень (на уровень микросхем или на уровне переопределения 1Ch а не 8h, хотя 1Ch вроде на время выполнения блокирует все прерывания)

Хотя опять-же давно это было мог все перепутать.

добавлю ссылку в тему таймеров https://frolov-lib.ru/books/bsp.old/v02/ch5.htm

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

С чего бы?

Если мне не изменяет моя старческая память, то можно использовать прерывание IRQ 8 от RTC, которое сидит на INT 70h. Правда это было ещё в далёкие времена 80286 и DOS, не знаю как с этим обстоят дела в DOSBox и поддерживает ли он такую вот древность.

Я думаю что без проблем поддерживает. Более того, все эти рудименты поддерживаются и во всех х86 современных компах.

Статья очень хорошая. Но у меня есть пара вопросов.

В первом листинге я сначала удивился, зачем вы загружаете значение из AddedThreadCounter сначала в BL, а оттуда в AL, можно же было сразу. Только потом я увидел, что BL используется как параметр в GetSavedSPOffset, сильно ниже по листингу. Неочевидно. И ещё по этому листингу получается, что самый первый элемент в области ThreadStacks не используется, теоретически он под main зарезервирован?

Возможно, я не очень понял часть вопроса на счёт ThreadStacks... Отвечу на то, как понял :)

Стек начинается со старших адресов. По сути, стек первой нити начинается... да, с младшего адреса блока памяти, предназначенного для второй нити :) Но мы не пугаемся, потому что указатель стека - регистр SP - уменьшается перед записью в стек нового значения.

Операции со стеком по сути не сложные, но часто вызывают скрежет шестерёнок мозга.

Да, на счёт BL/AL не особо наглядно. Дело в том. что прямой инкремент значения в памяти - это чтение-изменение-запись. Но мы уже и так прочитали переменную в регистр, поэтому инкремент переменной в памяти я решил не делать и заменил на инкремент регистра. В итоге потерялась наглядность.

Спасибо за ваш комментарий, от меня лучи добра и плюсики!

Если развивать систему многопоточности из статьи, то замечу следующее.

В своё время, когда встраивали многопоточность в Windows 95 - упустили интересную возможность: возбуждение исключения в другом потоке. В чём суть: допустим, главный поток отвечает лишь за пользовательский ввод и другой интерфейс, и программа запускает дополнительный поток, который нагружает длительными вычислениями. А потом пользователь решает закрыть программу, когда дополнительный поток ещё не завершил работу. В Windows 95 было, по сути дела, всего два способа прервать дополнительный поток: либо вызвать TerminateThread, либо заставить дополнительный поток периодически проверять какой-то флаг, и если этот флаг изменил значение - завершиться. Оба эти способа - плохие. Если вызвать TerminateThread, то поток убивается практически сразу, не вызвав никаких завершающих функций, может быть посреди важного вызова, который испортит файлы или системные данные с последующим "синим экраном". А если использовать флаг, то из-за его проверок замедлятся вычисления, усложнится алгоритм расчётов, и может появиться заметная задержка между попыткой пользователя закрыть программу и реальным её завершением.

Что можно было сделать.
Когда планировщик переключает потоки, то он в специальной структуре сохраняет контекст потока, включая адрес, с которого следует продолжить вычисления. Так вот, в этой структуре второй поток мог бы явно установить адрес обработчика исключения на случай, если понадобится досрочно его завершить. И когда главному потоку понадобится прервать дополнительный поток, то он бы вызвал какую-нибудь функцию RaiseTermination( thread ), что заставило бы планировщик при следующем переключении на дополнительный поток возобновить работу не с последнего адреса, а с адреса обработчика исключений. А это бы позволило нормально вызвать все деструкторы и выйти из функции потока.

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

Программно бы процедура потока на псевдокоде могла выглядеть примерно так:

int threadProc(void*)
{
    // Сохраняем в контексте потока адрес обработчика уничтожения потока
    SetTerminationHandler(termination);

    try
    {
        // Долгие вычисления
        // ...
        LockTermination(); // Увеличение счётчика запрета уничтожения 
        // Вызов системных функций - в это время поток не может быть уничтожен
        // ...
        UnlockTermination(); // Уменьшение счётчика запрета уничтожения 
        // ...
    }
    on termination // Специальный обработчик исключения уничтожения потока
    {
        // Сохраняем данные, освобождаем ресурсы
        // ...
    }
    return 0;
}

Спасибо за такое развёрнутое дополнение. Оно стоит отдельной статьи. Я пока решил показать минимально работающий пример. Конечно, там нет многих важных для реальных систем вещей. Вам от меня плюсики.

Кстати, может быть Вам будет интересно, действительно существует порт FreeRTOS под относительно живую x86 архитектуру: https://www.freertos.org/Documentation/02-Kernel/03-Supported-devices/04-Demos/x86/RTOS_Intel_Quark_Galileo_GCC

Advantech похоже выпускает платы с этим SoC, видимо гдето в промышленности используются

Нас ждёт погружение в один из способов организации мультипоточности на базе единственного ядра процессора

Если "мультипоточность" или "многопоточность" обозначает то, о чем пишет автор, то что такое тогда "многозадачность"?

Зачем брать давно известные термины с общепринятыми значениями и менять их значения?

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

Скорее речь о прерываниях и их обработке в однозадачном однопоточном режиме.

Подобным образом, например организовано в BIOS IBM PC чтение клавиатуры.

По прерыванию от клавиатуры int9h читается сканкод и меняется регистр состояния клавиатуры.

Любая другая программа в свою очередь может этот сканкод прочитать и интерпретировать по своим алгоритмам.

При этом нет ни процессов ни потоков в общепринятом понимании.

Прерывания как аппаратные так и программные выполняются в рамках одного потока и процесса.

Человек пытается запилить аналог DESQview и использует терминологию того времени, с поправками на маркетинговые материалы Quarterdeck Office Systems.

Человек пытается запилить аналог DESQview

Терминология того времени называет многозадачность под 8086 в стиле Desqview "кооперативной многозадачностью".

Так-то похоже на "ореховку" Швейка....

Наверное все же нет. Прерывание выполняется/является в отдельном процессе, со своим стеком, контекстом итп

Это приблуды ядра. А по факту прерывание приходит в текущем контексте. Железное переключение контекста возможно только при вызове прерывания/исключения через task gate, что изначально выпилено в 64-битном режиме.

Прерывание выполняется/является в отдельном процессе, со своим стеком, контекстом итп

"Процесс" - в DOS?

"Мама дорогая..."

Стеки и контексты приложение обеспечивает в DOS. Такое как упомянутый Desqview.

Содержимое стека и регистров по IRQ копируется в память, а по IRET - восстанавливается.

Вот и все "отдельные процессы".

Ес-но, а что ещё вы хотели от реального режима =)

От реального режима - ничего.

А вот от написателя статьи хочется некоторого понимания того, о чем он пишет.

"Началось с того, что я вырезал из английского журнала "Country Life" картинку, изображающую птичку, сидящую на ореховом дереве. Я назвал ее "ореховкой", точно так же, как не поколебался бы назвать птицу, сидящую на рябине, "рябиновкой"."

(С) Гашек Я.;"Похождения бравого солдата Швейка".

В моём примере всё намного проще. Первое, после настройки нового вектора таймера, прерывание вызывает обработчик в том же самом "процессе", где работает вся программа примера. Нити (threads) тоже работают в том же самом процессе, но им выделены отдельные стеки. Процессор 8086 никакими "взрослыми" средствами изоляции процессов не обладает. По сути, пример в статье показывает многозадачность (или многонитевость) примерно на том уровне, как это сделано в RTOS, работающей на одноядерном микроконтроллере.

автор показал как сделать вытесняющую многозадачность с переключением контекста, что не так? Современная FreeRTOS например делала бы тоже самое, если её на эту архитектуру портировать

Я бы с удовольствием прочитал курс лекций по старому (386 и старше) с нормальными примерами и пояснениями. Пищал бы от полного восторга.

Так читайте Питера Нортона, он весьма детально пишет

  1. На мой взгляд, надо сразу написать, что мы прошли путь от 16-битного через 32-битный к 64-битному процессору интел. 16-битный работал вот так...
    (был еще 8-битный - наблюдаем рудименты в виде 1-байтных регистров, но нам он не достался... :)))))
    16-битный режим назывался реальным режимом, а 32-битный - защищенным.
    Как называется 64-битный, я не в курсе.

  2. Цикл статей про это очень полезен народу, который пишет проги для встроенных микропроцессоров. И весьма полезен для понимания "мультипрограммирования" - аналог термина многозадачность.

  3. Термин многопоточность, как и поток в отношении параллельных процессов мне абсолютно не нравится уже не одно десятилетие. Термин нить - слишком бытовой.
    Поэтому предлагаю называть ни/потоки тредами. В конце-концов мы же называем файлы файлами, а фреймворки - фреймворками. Тред - и сразу понятно, что это. Ибо есть ведь еще и fiber'ы...
    А потомки оставим для stream. Кстати, stream - это же стремнина. Вполне себе русское слово.

mov al, bl ; al = bl

inc al ; al++

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

Понимаю вашу точку зрения. Теперь уж выкидывать их бессмысленно, но полезные комментарии там тоже есть. Спасибо за замечание.

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

Одновременно с єтим, переход на архитектуру 386 меняет ситуацию, и одновременно с єтим делает не менее бесполезным заявленным код, только уже по другой причине - в архитектуре которая предоставляет подход, к решению фундаментальных проблем многозадачности, даже на ее уровне кооперативной, нужно применять другие машинерии, опирающиеся на архитектуру того же самого 386

Хорошая статья, напомнила старый анекдот:
- Папа, а что такое многозадачность?
- Сынок, сейчас дискетку отформатирую и покажу...

Зачем все это вспоминать? Таким занимались в конце 80 и начале 90, когда не было современных средств разработки и других процессоров.Сам писал на ассемблере драйвера под разные устройства. Сейчас все это в прошлом и давно забыто.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации