Фишка в том, чтобы правильно приложить свои силы. В случае операционной системы, важен не язык, на котором она написана, важен не режим процессора (современные операционные системы портабельны), важны две вещи:
1. система должна быть хорошо и правильно спроектирована;
2. система должна поддерживать какой-либо стандарт.
Если кто-нибудь будет отрицать, что проектирование не важно — в полемику вступать не буду — спорить нам не о чем — мы живём в разных мирах.
Что касается стандарта, то среди разработчиков ОС большая конкуренция и «подсадить» на свою систему какое-либо значительное количество пользователей, а тем более разработчиков — нереально. Ну разве что у кого папа, дядя или брат — министр промышленности. Использование стандарта позволяет использовать уже существующее программное обеспечение и даёт хоть какой-то шанс найти последователей и соратников.
Так вот, вторая ссылка — это спецификация системных вызовов микроядра L4. Существуют как минимум две реализации этой спецификации — Pistachio и OKL4. К чему я клоню? Реальная тема для тех, кто идеально знает и любит ассемблер, ориентируется в защищённом режиме х86 и желает написать операционную систему — напишите свою (самую оптимальную, быструю, красивую, минимальную) реализацию микроядра по спецификации, которую я привёл выше.
Выходит, первая проблема в том, что BSD partition была помечена как неиспользуемая, несмотря на то, что использовалась, а вторая проблема была в самом grub — паритет между BSD и GNU, не проиграл никто.
Поздравляю, теперь Вы можете загружать и тестировать микроядро L4 на Вашей системе.
Мне кажется, вместо уродских findNextDevice гораздо умнее будет findDevices(struct condition { type, busId, deviceId, vendorId }) и так далее, причем универсальная функция для любой шины а не только USB. Часто же надо например найти все жесткие диски, или что то вроде этого.
Насчёт универсальной функции, то она, конечно, не помешает. Но мне кажется, что это уже более высокоуровневый протокол. Т.е. некий Hardware Abstraction Layer (HAL) работающий поверх USB и других шин.
Фуникция findDevices — логичное решение, однако, не будет ли проблемы пересечений deviceId и vendorId для PCI и USB устройств? Если такой проблемы нет, то вообще прекрасно. Или Вы предлагаете определять тип шины параметрами type и busId?
Насёт FindFirst и FindNext — согласен, что это перебор устройств не лучший вариант с точки зрения производительности, но оптимальный с точки зрения затрат на память. Можно реализовать оба подхода и дать программисту выбор — использовать findDevices или findFirst/findNext.
Чтение шины мне кажется стоит отложить до момента первого обращения к findDevices (что зря процессор нагружать).
Согласен, это было бы оптимально.
Вместо callback о присоединении устройства стоило бы сделать единую систему событий, которую userland и kernel mode программы могут слушать (с фильтром естественно), а драйвера в нее писать. Можно, как в Линуксе, на файловых дескрипторах, можно как то по своему, лишь бы меньше процессорного времени и памяти расходовать.
Интересно. Прежде всего интересен формат сообщений. Детали реализации можно отдать на усмотрение разработчикам. Например, я бы реализовал такой протокол на основе L4 IPC.
Задать скорость и т.д. должен драйвер устройства, откуда обычная программа знает какие параметры выставлять? Драйвер должен сам ставить оптимальные значения.
Не спорю. Тут только один вопрос/пожелание — хочется по максимуму (насколько возможно) абстрагироваться от шины. Т.е. чтобы код, который читает/пишет в порты и читает/пишет данные, как можно меньше зависел от шины. Ради этого, я верю, можно даже поступиться быстродействием. Скажем, я бы смирился с ~10% потерей быстродействия ради универсальности. В случае же, когда из «железки» нужно выжать максимум производительности, то под неё пишется кастомный драйвер специально под определённую модель. Сожалею, сейчас у меня не хватает сил, чтобы вербализовать свою мысль.
Вместо readdevicecontrol etc лучше сделать функции: getDeviceOptionList() (какие опции поддерживает устройство), setManyOptions, getManyOptions.
Вот бы на конкретном примере какого-либо устройства это рассмотреть. То есть, скажем, USB HDD — в случае обычного IDE устройства, подключенного к IDE контроллеру на PCI шине, в простейшем случае программа устанавливает в регистрах контроллера номер головки, цилиндра и сектора, количество секторов для чтения, посылает команду, а затем, получив прерывание, читает данные из устройства, анализируя статус. В случае DMA/UDMA режима, операция несколько отличается — контроллер захватывает шину и сам пишет в память. А как это будет работать, если IDE устройство подключено через USB шину? Вот если бы кто-нибудь это расписал, мои посты были бы более внятные.
Ну и неблокируемое чтение тоже должно быть. Можно внутри ядра все операции сделать неблокируемыми, а юзерские процессы блокировать. Опять же, пригодилась бы единая, с минимальным оверхедом система работы с любыми асинхронными функциями, можно даже какое то подобие очереди событий сделать:
Написал большой ответ на этот абзац, а потом... Ваша идея натолкнула на интересную мысль, которая лежала на поверхности: не заводить отдельно потоки для управления и для данных, а передавать данные и контрольную информацию вместе. Тогда получается очень интересная и несложная схема — заголовок с командами/статусом, а вслед за ним передаваемые/принятые данные.
Что касается блокировок — совершенно согласен с Вашей идеей, но предлагаю детали реализации оставить разработчикам. Мне удалось решит проблему, развязав синхронные IPC в асинхронные через «контекст запроса» и конечный автомат, который переключается по этому контексту. Кстати, отдалённо напоминает Ваше описание работы многопоточных серверов.
Звучит заманчиво, но мне интересно, что Главстарт получает взамен?
Какой процент от проекта и как считаются эти проценты?
А ежели стартапер готов отдать какой-то процент от бизнеса, которого ещё нет, но ни при каких условиях не отдаст инвестору исходный код, что скажет инвестор?
Пожалуй, уточню последний вопрос: Готовы ли вы работать со стартап-проектами, которые ни при каких условиях не отдадут исходный код?
Спасибо. Заглянул в В.Кулаков «Программирование на аппаратном уровне». Действительно, есть даже пример работы с принтером и мышью через USB.
Жаль, примеры на ассемблере. Не хочу развязывать очередные войны, но последние годы стараюсь поменьше иметь дело с ассемблером. Есть на то причины — довелось писать не только для x86, но и для ARM, а также всяких экзотических архитектур. Поэтому хочется писать переносимые решения, а не под конкретную архитектуру. Вторая причина охлаждения любви к ассемблеру, это два разных синтаксиса на одной архитектуре — AT&T Syntax и Intel Syntax — это окончательно сломало мою голову и теперь только читаю на ассемблере, но почти не пишу. Только в тех местах, где без ассемблера действительно нельзя обойтись, благо, таких мест ничтожно мало.
Эх, боюсь как бы меня за многословность не сочли балаболом. Но давайте порассуждаем, как бы мог выглядеть сервис, обеспечивающий прозрачную работу с USB устройствами. Не будем углубляться в подробности реализации, а просто абстрактно опишем.
Некая задача, назовём её драйвер, инициализирует USB контроллер, проверяет все подключенные устройства, строит дерево устройств и производит начальную инициализацию найденных устройств, точнее не самих устройств, конфигурацию USB транспорта этих устройств. Также задача предоставляет некий интерфейс другим задачам, через который они могут получить список уже подключенных устройств. Причём, здесь логично использовать перечисление, скажем нечто вроде FindUsbHost, FindNextUsbDevice, где выходной параметр функций будет использован как аргумент для следующей итерации или служить признаком остановки перечисления. (Привет от MS-DOS и его FindFirst FindNext :) )
Далее, нам понадобится hot-plug/unplug. Для этого в интерфейс драйвера добавляем новый вызов — регистрация внешней задачи, принимающей сообщения от драйвера USB, о том, что появилось новое устройство или удалено существующее — эдакая регистрация callback'а. Т.е. когда мы втыкаем флэшку или любое другое устройство, внешняя задача получает сообщение о новом устройстве. Мы специально не рассматриваем детали реализации программы, которая следит за новыми устройствами — она может автоматически монтировать файловую систему, находящуюся на устройстве, начать импорт фотографий или установить связь через появившийся USB-модем или просто добавить новую запись в /dev — это нас не касается.
Следующее, что понадобится добавить в интерфейс драйвера, функции управления USB устройством — задать режим работы, скорость и прочие параметры USB.
Наконец, нам понадобятся четыре вызова для работы с самим устройством — ReadData, WriteData, ReadDeviceControl, WriteDeviceControl. С точки зрения драйвера USB, не имеет значения, какое устройство подключено — символьное, блочное или какое-либо ещё.
ReadDeviceControl и WriteDeviceControl — логично представить как блок памяти, представляющий собой некий диапазон портов ввода/вывода. Т.е. если на обычном PCI/ISA устройстве порты ввода/вывода отображаются в физическое адресное пространство, адресуемое микропроцессором, то в случае USB любая запись на удалённое устройство происходит сначала в локальный буфер, который после передачи удалённой стороне запишется в порты ввода/вывода удалённого устройства. Тут возникает вопрос, как быть, если надо записать лишь в один регистр? Я не знаю. Возможно, предварительно прочитать ReadDeviceControl, поменять регистр, а затем вызвать WriteDeviceControl. Я невнимательно читал документацию, возможно есть способ записать лишь один регистр. Хотя бы потому, что существуют регистры только для чтения. А для некоторых регистров вообще важен сам факт записи и даже если записать в него то же значение, которое там и было, то такая запись может изменить логику работы устройства.
Наконец, возникает вопрос в поведении ReadData. По идее, она может быть блокирующейся функцией, которая возвращает значение только в случае наличия данных на удалённом устройстве. В этом случае работу с портами придётся организовывать в отдельном потоке, что в данном случае не очень удобно. Можно поступить иначе — использовать неблокируемый вызов ReadData, который в случае отсутствия данных вернёт 0. В таком случае, чтобы избежать опроса устройства в цикле, необходимо предусмотреть возможность драйвером информировать приложение о готовности данных для чтения. Можно даже попробовать сочетать оба подхода (блокированный/неблокированный Read) и для каждого устройства использовать подходящий метод.
Извините за очередной поток сознания — пытался разобраться «как это работает» в процессе написания поста. Буду признателен всем, кто может дополнить, поправить или указать на ошибки и нестыковки.
Драйвер Floppy взят из FreeDOS, а туда он попал из Linux. Так что кое-что таки использую.
Вообще, формат сообщений между сервисами, реализующими протоколы, и драйверами устройств, почти устоялся. Во всяком случае это можно сказать о символьных и блочных устройствах. О вот что касается сетевых карт, то протокол ещё не устоялся, поэтому переносить много драйверов пока лениво — при каждом апдейте протокола придётся править каждый драйвер.
Что касается графических режимов видеокарт, то тут ещё не понятно каким образом лучше всего писать данные в видеопамять и какие примитивы использовать. Есть идея использовать нечто вроде виртуального холста, рисовать на нём в модуле, представляющем абстракцию для видеоустройства, а затем сообщениями передавать этот «холст» в драйвер видеокарты. Но всё это представляю очень туманно, а браться сейчас за графические режимы — значит оставить без внимания остальные модули.
Наконец, USB. Нет особого смысла брать готовую реализацию USB из open source проекта, пока не разработан внутренний протокол обмена с USB. Попробую пояснить — к примеру, у меня есть USB floppy привод, оставшийся от старого ноутбука. Здравый смысл подсказывает, что USB обеспечивает лишь интерфейс для обмена командами и данными с этим Floppy приводом, а формат команд соответствует обычному (не USB) приводу. И только когда появится понимание того, каким образом команды и данные транслируются удалённому устройству, можно разработать протокол (API/формат сообщений — называйте как хотите) для обмена. И уже после этого можно брать код из open source проекта и делать ему обвязку этого протокола.
И да, я прошу прощения за множество опечаток в моих постах — сильно сильно спешу, меня переполняют эмоции, хочется выразить свои мысли и чувства, а в результате получается дислексия.
За USB я пока не брался, поэтому и спрашивал помощи. Заняться всерьёз USB стеком собираюсь после перехода на VM Ware, а для этого шага сначала придётся запустить драйвер сетевой карты Intel 8254x — именно такая поддерживается той версией VM Ware, которая стоит у меня.
Что касается TCP/IP то именно в этом модуле ничего революционного (пока) нет. Например, Supervisor использует оригинальный алгоритм распределения виртуальной памяти задачам. Файловая система использует хитрый алгоритм диспетчеризации системных вызовов. А вот TCP/IP пока отлаживается. Могу рассказать историю его написания.
В далёком 2000 году мой друг «вытянул» меня в Сеул, где мы писали Bluetooth стек. Я занимался драйвером виртуального COM порта для Windows NT, чтобы поверх него запустить RFCOMM протокол, а друг реализовал базовые протоколы Bluetooth. На понадобился код для работы с COM портом и как любой ленивый программист, друг нашёл какую-то свободную реализацию. Но эта реализация не поддерживала одновременную приём и передачу данных — она работал в одом потоке, поэтому друг написал свою реализацию, которая использовала один поток для передачи данных, а другой для приёма. Это предыстория.
Через пару лет мне захотелось поработать с TCP/IP напрямую, не используя сокетов. Поскольку в любой современной системе получить доступ к регистрам сетевой карты из юзерспейса невозможно,
то тут и вспомнился код, который очень аккуратно и оптимально работал с COM портом. Был взят RS232 кабелm и с его помощью были соединены девелоперский компьютер Windows 2000 и какой-то RedHat линукс. На линуксе был поднят PPP сервер — история началась.
Всё происходило следующим образом — все пакеты, приходящие от PPP сервера анализировались, затем находился соответсвующий RFC и по нему писался код, который парсил входящий протокол. Кода какой-то протокол нёс в себе вложенный проткол, то вся повторялось сначала — сначала анализировались принятые данные, затем брался соответствующий RFC и писалась реализация. В некоторые моменты для продвижения вперёд был недостаточно анализировать входящие пакеты, но нужно было что-то отвечать удалённой стороне — так постепенно родился Xameleon TCP/IP стек.
Здесь можно сделать лирическое отступление и кинуть камень в огород Microsoft. (Хотя с тех пор я повзраслел и в Holy War больше не участвую). Майкрософт часто обвиняют в отступлении от стандартов. Мне пришлось испытать это на своей шкуре. Так вышло, что вместо Linux PPP, в один момент пришлось использовать Microsoft RAS сервер — т.е. всю отладку перенести на одну машину. Какого же был удивление, что Microsoft RAS сервер вместо привычных PPP пакетов, gthbjlbxtcrb посылал в COM порт строку CLIENT. Это была засада. Проблема решилась ответом CLIENTSERVER, после чего сценарий установки соединения совпадал с традиционным PPP.
В результате, прикладная программа, содержащая в себе TCP/IP стек, через шнур RS-232, который соединял порты COM1 и COM2, могла ходить в интернет. Максимум, что она умела, посылать запрос GET на близлежащий WEB сервер и показывать его ответ на консоли. Потом код TCP/IP стека несколько лет «пылился» на флешке.
Через несколько лет, когда файловая система Хамелеона стала более-менее стабильной, было решено достать «те самые» наработки, о которых я написал выше. Для начала нашлась спецификация на сетевую карту DEC21140 (потому-что её поддерживает Virtual PC). По спецификации написан код, который просто отображал заголовки все принятых Ethernet пакетов на консоли Хамелеона. Далее идёт длинная история о переносе TCP/IP стека на Xameleon, проектировании протокола обмена с сетевыми устройствами (между TCP/IP стеком и драйвером сетевухи), доработке драйвера сетевой карты, дизайн сокетов и разработка socket libc, реализующей POSIX совместимый протокол работы с сокетами.
При переносе своего TCP/IP стека под Xameleon, я вынес каждый слой в отдельный поток — это было, наверное, правильное решение. Оно позволяло сделать стек модульным и легко расширяемым. Но когда появились реальные приложения, работающие поверх стека, любая ошибка в стеке становилась трудно отлавливаемой, а поскольку всё работает асинхронно, то при любой нагрузке стек мог заблокироваться. Поэтому TCP/IP был полностью переработан — всё сетевые протоколы были вынесены в один поток. В это же время была реализована идея, подсмотренная при изучении исходников какого-то TCP/IP стека для встраиваемых устройств — размер буфера для передачи был выбрать чуть более, чем размер Ethernet MTU. Данные, полученные стеком для передачи (send, sendto) пишутся в этот буфер со смещением, а каждый сетевой протокол (TCP/UDP, IP, Ethernet) лишь дописывает свои заголовки к данным. Таким образом удалось избавиться от множества memcpy и несколько повысить быстродействие. (К слову говоря, это лишь один из способов и не самый главный). Все буфера имеют reference count, поскольку UDP и TCP разным образом работают с данными, то время жизни данных в передающих буферах для этих пакетов разное — UDP буфера освобождаются сразу после подтверждения передачи данных сетевой картой, а TCP буфера освобождаются после подтверждения удалённой стороной. Соответственно, использование reference count позволяет упростить работу с буферами — буфер освобождается когда RefCount = 0.
В общем, я могу бесконечно долго рассказывать об этом, но пожалейте мои пальцы. Выйдет новая сборка — опишу исправленные баги и добавленные вкусности, а пока лишь «поток сознания». Извините.
Уважаемый автор, так уж случилось, что я тоже пишу операционную систему. Пишу много лет и продвинулся весьма далеко. Вероятно, те, кто интересуется темой создания операционной системы, уже знакомы с Хамелеоном.
В общем, обращаюсь ко всем, кто пишет операционные системы. Если в какой-то момент у вас угаснет интерес к теме, то я готов принять некоторый исходный код. Интересуют прежде всего драйвера сетевых карт, код USB стека и драйверы, поддерживающие графический видео-режим.
Моя система микроядерная, поэтому лицензия не имеет значения — линковки с моим кодом не будет, ваш код может распространятся в виде отдельного модуля и исполняться как сервис, реализующий определённый протокол, и взаимодействующий с другими модулями посредством IPC. Не претендую на какие либо права — ваш код остаётся вашим. В случае любого конфликта в любой момент можете отозвать свой код из проекта. Язык реализации может быть почти любой, включая C/C++/Pascal, но модули ядра должны быть в формате Elf. Также есть возможность запускать исполняемые файлы формата PE (Portable Executable), но не на этапе начальной загрузки.
Более того, если кто-нибудь не чувствует силы завершить начатое до конца, а интересно продолжить, готов предоставить спецификации протоколов взаимодействия модулей. В идеале, можно помочь друг-другу развиваться вместе. Кроме того могу предложить участие в разработке новых протоколов.
В настоящее время я переписываю доморощенный tcp/ip стек, в надежде сделать его более стабильным и быстродействующим. Имею несколько идей для описания их на Хабре, но надеюсь каждую статью приурочить к новой сборке.
Если кого заинтересовало моё предложение, то выглядит моя система приблизительно так:
Тут гораздо выгоднее обрабатывать определённые объёмы объектов одной нитью, и делить такую обработку через пул нитей и очереди задач.
Вот вот. Но это не даёт автоматической гарантии повышения производительности. Всё зависит от реализации обработчика.
<основная функция> ---> <сложный обработчик> ---> <ещё один уровень обработчика>
Если обработчик, в свою очередь, в вызывает ещё один сложный обработчик, то для повышения производительности совмещаем точку входа от основной функции и из вложенного обработчика.
+---<-----------<------------<-----------<---------<-----------+
| ^
<основная функция> ---> <сложный обработчик> ---> <ещё один уровень обработчика> --+
Таким образом мы имеем одну точку входа в «сложном обработчике», куда попадают как запросы от верхнего уровня, так и ответы от вложенных обработчиков. Введём понятие «контекст», который будет передаваться по цепочке и на основе которого <сложный обработчик> производит диспетчеризацию между вызовами и ответами.
Что в результате? Пока <основная функция> вошла в <сложный обработчик> а тот, в свою очередь, обратился к нижележащему обработчику, следующий пользовательский поток может вызвать <сложный обработчик> и (о чудо!) взять данные из кэша (если они там оказались). Т.е. блокировки (в общепринятом смысле) как бы и не случилось вообще. Если же данных в кэше не оказалось, то пойдёт ещё одно сообщение во вложенный обработчик.
Прошу обратить внимание, что я не утверждаю, что мой способ универсальный и подойдёт для любого класса задач, но есть определённые задачи, где такая синхронизация даст выигрыш в производительности.
Например, если <сложный обработчик> активно работает со списками, то при использовании обрабатывающего потока мы избавляемся от массы блокировок, которые неизбежны для защиты данных при традиционном использовании многопоточности. При этом, развязка через единую точку входа в обработчик, позволяет безболезненно и максимально быстро обрабатывать синхронные запросы для случаев, когда данные для нового запроса «закэшированы» обработчиком на предыдущих итерациях.
Таким образом мы отказались от традиционных элементов блокировки и перенесли их на уровень синхронных IPC, что на самом деле не так и уж плохо, поскольку в данном случае количество блокировок определяется лишь точками входа в обработчики и никак не зависит от количества операций с данными, которые необходимо защищать от совместного доступа.
Предположим, анализ производительности вашего приложения выявил, что существенная часть процессорного времени тратится в некой вычислительной функции, и более того, эта функция многократно вызывается с одними и теми же параметрами — выполняя одинаковые вычисления вновь и вновь. Напрашивается простая оптимизация — кэш из одной записи, в котором бы хранились исходные данные и результат последнего вычисления.
Как бы решить эту проблему в системном приложении, работающем на микроядре L4?
Можно вывести вычислительную функцию в отдельный поток:
void CalculatingThread(void)
{
L4_ThreadId_t tid;
L4_Msg_t msg;
L4_MsgTag_t tag;
BOOL result;
int n;
while( true )
{
tag = L4_Wait( &tid ); // ждём сообщение
L4_Store( tag, &msg );
n = (int) L4_Get( &msg, 0 ); // берём первый аргумент сообщения
result = IsPrime( n ); // вызываем потоко-небезопасную функцию
L4_Clear(&msg);
L4_Append( &msg, (L4_Word_t) n ); // формируем ответ
L4_Load( &msg );
L4_Send( tid ); // посылаем ответ
}
}
Обращаемся к этому потоку через L4_Call — синхронный вызов IPC.
На первый взгляд — громоздко, но это ничуть не хуже, чем поведение TryEnterCriticalSection в случае, если критическая секция уже занята — на многопроцессорной системе сначала закрутятся спин-локи, в надежде подождать ресурс, а потом пойдёт переключение контекста.
Кстати, мой код делает не совсем то, что Вы описали — он «замыкает» не только IsPrime, но и slow_IsPrime. С одной стороны это не оптимально, зато гарантирует потоко-безопасность slow_IsPrime. В случае же, если значение уже в кэше, два IPC в одном адресном пространстве будут ничуть не тяжелее, чем работа с критическими секциями. Хотя, конечно, сравнивать не совсем корректно — платформы разные.
Я ни чуть не преуменьшаю ценность Вашей статьи — она познавательна и имеет практическую ценность, но у меня есть своё представление о lock-free системах, которые построены на несколько других принципах — реализуют асинхронные сообщения на основе синхронных сообщений и конечного автомата.
Ээээх. Где вы были раньше? Компания, в которой я работаю, неделю назад зарегистрировалась за 14 995 рублей. Разница, конечно, для компании небольшая, но всё равно неприятно.
Вероятно, многие устройства не поддерживаются в виду того, что используется пассивный режим USB? В таком случае без специального хаба невозможно подключить клавиатуру и другие пассивные устройства.
www.kolibrios.org/ — операционная система на ассемблере.
l4ka.org/l4ka/l4-x2-r7.pdf — очень вкусный документ.
Фишка в том, чтобы правильно приложить свои силы. В случае операционной системы, важен не язык, на котором она написана, важен не режим процессора (современные операционные системы портабельны), важны две вещи:
1. система должна быть хорошо и правильно спроектирована;
2. система должна поддерживать какой-либо стандарт.
Если кто-нибудь будет отрицать, что проектирование не важно — в полемику вступать не буду — спорить нам не о чем — мы живём в разных мирах.
Что касается стандарта, то среди разработчиков ОС большая конкуренция и «подсадить» на свою систему какое-либо значительное количество пользователей, а тем более разработчиков — нереально. Ну разве что у кого папа, дядя или брат — министр промышленности. Использование стандарта позволяет использовать уже существующее программное обеспечение и даёт хоть какой-то шанс найти последователей и соратников.
Так вот, вторая ссылка — это спецификация системных вызовов микроядра L4. Существуют как минимум две реализации этой спецификации — Pistachio и OKL4. К чему я клоню? Реальная тема для тех, кто идеально знает и любит ассемблер, ориентируется в защищённом режиме х86 и желает написать операционную систему — напишите свою (самую оптимальную, быструю, красивую, минимальную) реализацию микроядра по спецификации, которую я привёл выше.
Поздравляю, теперь Вы можете загружать и тестировать микроядро L4 на Вашей системе.
Насчёт универсальной функции, то она, конечно, не помешает. Но мне кажется, что это уже более высокоуровневый протокол. Т.е. некий Hardware Abstraction Layer (HAL) работающий поверх USB и других шин.
Фуникция findDevices — логичное решение, однако, не будет ли проблемы пересечений deviceId и vendorId для PCI и USB устройств? Если такой проблемы нет, то вообще прекрасно. Или Вы предлагаете определять тип шины параметрами type и busId?
Насёт FindFirst и FindNext — согласен, что это перебор устройств не лучший вариант с точки зрения производительности, но оптимальный с точки зрения затрат на память. Можно реализовать оба подхода и дать программисту выбор — использовать findDevices или findFirst/findNext.
Согласен, это было бы оптимально.
Интересно. Прежде всего интересен формат сообщений. Детали реализации можно отдать на усмотрение разработчикам. Например, я бы реализовал такой протокол на основе L4 IPC.
Не спорю. Тут только один вопрос/пожелание — хочется по максимуму (насколько возможно) абстрагироваться от шины. Т.е. чтобы код, который читает/пишет в порты и читает/пишет данные, как можно меньше зависел от шины. Ради этого, я верю, можно даже поступиться быстродействием. Скажем, я бы смирился с ~10% потерей быстродействия ради универсальности. В случае же, когда из «железки» нужно выжать максимум производительности, то под неё пишется кастомный драйвер специально под определённую модель. Сожалею, сейчас у меня не хватает сил, чтобы вербализовать свою мысль.
Вот бы на конкретном примере какого-либо устройства это рассмотреть. То есть, скажем, USB HDD — в случае обычного IDE устройства, подключенного к IDE контроллеру на PCI шине, в простейшем случае программа устанавливает в регистрах контроллера номер головки, цилиндра и сектора, количество секторов для чтения, посылает команду, а затем, получив прерывание, читает данные из устройства, анализируя статус. В случае DMA/UDMA режима, операция несколько отличается — контроллер захватывает шину и сам пишет в память. А как это будет работать, если IDE устройство подключено через USB шину? Вот если бы кто-нибудь это расписал, мои посты были бы более внятные.
Написал большой ответ на этот абзац, а потом...Ваша идея натолкнула на интересную мысль, которая лежала на поверхности: не заводить отдельно потоки для управления и для данных, а передавать данные и контрольную информацию вместе. Тогда получается очень интересная и несложная схема — заголовок с командами/статусом, а вслед за ним передаваемые/принятые данные.Что касается блокировок — совершенно согласен с Вашей идеей, но предлагаю детали реализации оставить разработчикам. Мне удалось решит проблему, развязав синхронные IPC в асинхронные через «контекст запроса» и конечный автомат, который переключается по этому контексту. Кстати, отдалённо напоминает Ваше описание работы многопоточных серверов.
Какой процент от проекта и как считаются эти проценты?
А ежели стартапер готов отдать какой-то процент от бизнеса, которого ещё нет, но ни при каких условиях не отдаст инвестору исходный код, что скажет инвестор?
Пожалуй, уточню последний вопрос: Готовы ли вы работать со стартап-проектами, которые ни при каких условиях не отдадут исходный код?
Жаль, примеры на ассемблере. Не хочу развязывать очередные войны, но последние годы стараюсь поменьше иметь дело с ассемблером. Есть на то причины — довелось писать не только для x86, но и для ARM, а также всяких экзотических архитектур. Поэтому хочется писать переносимые решения, а не под конкретную архитектуру. Вторая причина охлаждения любви к ассемблеру, это два разных синтаксиса на одной архитектуре — AT&T Syntax и Intel Syntax — это окончательно сломало мою голову и теперь только читаю на ассемблере, но почти не пишу. Только в тех местах, где без ассемблера действительно нельзя обойтись, благо, таких мест ничтожно мало.
Эх, боюсь как бы меня за многословность не сочли балаболом. Но давайте порассуждаем, как бы мог выглядеть сервис, обеспечивающий прозрачную работу с USB устройствами. Не будем углубляться в подробности реализации, а просто абстрактно опишем.
Некая задача, назовём её драйвер, инициализирует USB контроллер, проверяет все подключенные устройства, строит дерево устройств и производит начальную инициализацию найденных устройств, точнее не самих устройств, конфигурацию USB транспорта этих устройств. Также задача предоставляет некий интерфейс другим задачам, через который они могут получить список уже подключенных устройств. Причём, здесь логично использовать перечисление, скажем нечто вроде FindUsbHost, FindNextUsbDevice, где выходной параметр функций будет использован как аргумент для следующей итерации или служить признаком остановки перечисления. (Привет от MS-DOS и его FindFirst FindNext :) )
Далее, нам понадобится hot-plug/unplug. Для этого в интерфейс драйвера добавляем новый вызов — регистрация внешней задачи, принимающей сообщения от драйвера USB, о том, что появилось новое устройство или удалено существующее — эдакая регистрация callback'а. Т.е. когда мы втыкаем флэшку или любое другое устройство, внешняя задача получает сообщение о новом устройстве. Мы специально не рассматриваем детали реализации программы, которая следит за новыми устройствами — она может автоматически монтировать файловую систему, находящуюся на устройстве, начать импорт фотографий или установить связь через появившийся USB-модем или просто добавить новую запись в /dev — это нас не касается.
Следующее, что понадобится добавить в интерфейс драйвера, функции управления USB устройством — задать режим работы, скорость и прочие параметры USB.
Наконец, нам понадобятся четыре вызова для работы с самим устройством — ReadData, WriteData, ReadDeviceControl, WriteDeviceControl. С точки зрения драйвера USB, не имеет значения, какое устройство подключено — символьное, блочное или какое-либо ещё.
ReadDeviceControl и WriteDeviceControl — логично представить как блок памяти, представляющий собой некий диапазон портов ввода/вывода. Т.е. если на обычном PCI/ISA устройстве порты ввода/вывода отображаются в физическое адресное пространство, адресуемое микропроцессором, то в случае USB любая запись на удалённое устройство происходит сначала в локальный буфер, который после передачи удалённой стороне запишется в порты ввода/вывода удалённого устройства. Тут возникает вопрос, как быть, если надо записать лишь в один регистр? Я не знаю. Возможно, предварительно прочитать ReadDeviceControl, поменять регистр, а затем вызвать WriteDeviceControl. Я невнимательно читал документацию, возможно есть способ записать лишь один регистр. Хотя бы потому, что существуют регистры только для чтения. А для некоторых регистров вообще важен сам факт записи и даже если записать в него то же значение, которое там и было, то такая запись может изменить логику работы устройства.
Наконец, возникает вопрос в поведении ReadData. По идее, она может быть блокирующейся функцией, которая возвращает значение только в случае наличия данных на удалённом устройстве. В этом случае работу с портами придётся организовывать в отдельном потоке, что в данном случае не очень удобно. Можно поступить иначе — использовать неблокируемый вызов ReadData, который в случае отсутствия данных вернёт 0. В таком случае, чтобы избежать опроса устройства в цикле, необходимо предусмотреть возможность драйвером информировать приложение о готовности данных для чтения. Можно даже попробовать сочетать оба подхода (блокированный/неблокированный Read) и для каждого устройства использовать подходящий метод.
Извините за очередной поток сознания — пытался разобраться «как это работает» в процессе написания поста. Буду признателен всем, кто может дополнить, поправить или указать на ошибки и нестыковки.
Вообще, формат сообщений между сервисами, реализующими протоколы, и драйверами устройств, почти устоялся. Во всяком случае это можно сказать о символьных и блочных устройствах. О вот что касается сетевых карт, то протокол ещё не устоялся, поэтому переносить много драйверов пока лениво — при каждом апдейте протокола придётся править каждый драйвер.
Что касается графических режимов видеокарт, то тут ещё не понятно каким образом лучше всего писать данные в видеопамять и какие примитивы использовать. Есть идея использовать нечто вроде виртуального холста, рисовать на нём в модуле, представляющем абстракцию для видеоустройства, а затем сообщениями передавать этот «холст» в драйвер видеокарты. Но всё это представляю очень туманно, а браться сейчас за графические режимы — значит оставить без внимания остальные модули.
Наконец, USB. Нет особого смысла брать готовую реализацию USB из open source проекта, пока не разработан внутренний протокол обмена с USB. Попробую пояснить — к примеру, у меня есть USB floppy привод, оставшийся от старого ноутбука. Здравый смысл подсказывает, что USB обеспечивает лишь интерфейс для обмена командами и данными с этим Floppy приводом, а формат команд соответствует обычному (не USB) приводу. И только когда появится понимание того, каким образом команды и данные транслируются удалённому устройству, можно разработать протокол (API/формат сообщений — называйте как хотите) для обмена. И уже после этого можно брать код из open source проекта и делать ему обвязку этого протокола.
И да, я прошу прощения за множество опечаток в моих постах — сильно сильно спешу, меня переполняют эмоции, хочется выразить свои мысли и чувства, а в результате получается дислексия.
Что касается TCP/IP то именно в этом модуле ничего революционного (пока) нет. Например, Supervisor использует оригинальный алгоритм распределения виртуальной памяти задачам. Файловая система использует хитрый алгоритм диспетчеризации системных вызовов. А вот TCP/IP пока отлаживается. Могу рассказать историю его написания.
В далёком 2000 году мой друг «вытянул» меня в Сеул, где мы писали Bluetooth стек. Я занимался драйвером виртуального COM порта для Windows NT, чтобы поверх него запустить RFCOMM протокол, а друг реализовал базовые протоколы Bluetooth. На понадобился код для работы с COM портом и как любой ленивый программист, друг нашёл какую-то свободную реализацию. Но эта реализация не поддерживала одновременную приём и передачу данных — она работал в одом потоке, поэтому друг написал свою реализацию, которая использовала один поток для передачи данных, а другой для приёма. Это предыстория.
Через пару лет мне захотелось поработать с TCP/IP напрямую, не используя сокетов. Поскольку в любой современной системе получить доступ к регистрам сетевой карты из юзерспейса невозможно,
то тут и вспомнился код, который очень аккуратно и оптимально работал с COM портом. Был взят RS232 кабелm и с его помощью были соединены девелоперский компьютер Windows 2000 и какой-то RedHat линукс. На линуксе был поднят PPP сервер — история началась.
Всё происходило следующим образом — все пакеты, приходящие от PPP сервера анализировались, затем находился соответсвующий RFC и по нему писался код, который парсил входящий протокол. Кода какой-то протокол нёс в себе вложенный проткол, то вся повторялось сначала — сначала анализировались принятые данные, затем брался соответствующий RFC и писалась реализация. В некоторые моменты для продвижения вперёд был недостаточно анализировать входящие пакеты, но нужно было что-то отвечать удалённой стороне — так постепенно родился Xameleon TCP/IP стек.
Здесь можно сделать лирическое отступление и кинуть камень в огород Microsoft. (Хотя с тех пор я повзраслел и в Holy War больше не участвую). Майкрософт часто обвиняют в отступлении от стандартов. Мне пришлось испытать это на своей шкуре. Так вышло, что вместо Linux PPP, в один момент пришлось использовать Microsoft RAS сервер — т.е. всю отладку перенести на одну машину. Какого же был удивление, что Microsoft RAS сервер вместо привычных PPP пакетов, gthbjlbxtcrb посылал в COM порт строку CLIENT. Это была засада. Проблема решилась ответом CLIENTSERVER, после чего сценарий установки соединения совпадал с традиционным PPP.
В результате, прикладная программа, содержащая в себе TCP/IP стек, через шнур RS-232, который соединял порты COM1 и COM2, могла ходить в интернет. Максимум, что она умела, посылать запрос GET на близлежащий WEB сервер и показывать его ответ на консоли. Потом код TCP/IP стека несколько лет «пылился» на флешке.
Через несколько лет, когда файловая система Хамелеона стала более-менее стабильной, было решено достать «те самые» наработки, о которых я написал выше. Для начала нашлась спецификация на сетевую карту DEC21140 (потому-что её поддерживает Virtual PC). По спецификации написан код, который просто отображал заголовки все принятых Ethernet пакетов на консоли Хамелеона. Далее идёт длинная история о переносе TCP/IP стека на Xameleon, проектировании протокола обмена с сетевыми устройствами (между TCP/IP стеком и драйвером сетевухи), доработке драйвера сетевой карты, дизайн сокетов и разработка socket libc, реализующей POSIX совместимый протокол работы с сокетами.
При переносе своего TCP/IP стека под Xameleon, я вынес каждый слой в отдельный поток — это было, наверное, правильное решение. Оно позволяло сделать стек модульным и легко расширяемым. Но когда появились реальные приложения, работающие поверх стека, любая ошибка в стеке становилась трудно отлавливаемой, а поскольку всё работает асинхронно, то при любой нагрузке стек мог заблокироваться. Поэтому TCP/IP был полностью переработан — всё сетевые протоколы были вынесены в один поток. В это же время была реализована идея, подсмотренная при изучении исходников какого-то TCP/IP стека для встраиваемых устройств — размер буфера для передачи был выбрать чуть более, чем размер Ethernet MTU. Данные, полученные стеком для передачи (send, sendto) пишутся в этот буфер со смещением, а каждый сетевой протокол (TCP/UDP, IP, Ethernet) лишь дописывает свои заголовки к данным. Таким образом удалось избавиться от множества memcpy и несколько повысить быстродействие. (К слову говоря, это лишь один из способов и не самый главный). Все буфера имеют reference count, поскольку UDP и TCP разным образом работают с данными, то время жизни данных в передающих буферах для этих пакетов разное — UDP буфера освобождаются сразу после подтверждения передачи данных сетевой картой, а TCP буфера освобождаются после подтверждения удалённой стороной. Соответственно, использование reference count позволяет упростить работу с буферами — буфер освобождается когда RefCount = 0.
В общем, я могу бесконечно долго рассказывать об этом, но пожалейте мои пальцы. Выйдет новая сборка — опишу исправленные баги и добавленные вкусности, а пока лишь «поток сознания». Извините.
В общем, обращаюсь ко всем, кто пишет операционные системы. Если в какой-то момент у вас угаснет интерес к теме, то я готов принять некоторый исходный код. Интересуют прежде всего драйвера сетевых карт, код USB стека и драйверы, поддерживающие графический видео-режим.
Моя система микроядерная, поэтому лицензия не имеет значения — линковки с моим кодом не будет, ваш код может распространятся в виде отдельного модуля и исполняться как сервис, реализующий определённый протокол, и взаимодействующий с другими модулями посредством IPC. Не претендую на какие либо права — ваш код остаётся вашим. В случае любого конфликта в любой момент можете отозвать свой код из проекта. Язык реализации может быть почти любой, включая C/C++/Pascal, но модули ядра должны быть в формате Elf. Также есть возможность запускать исполняемые файлы формата PE (Portable Executable), но не на этапе начальной загрузки.
Более того, если кто-нибудь не чувствует силы завершить начатое до конца, а интересно продолжить, готов предоставить спецификации протоколов взаимодействия модулей. В идеале, можно помочь друг-другу развиваться вместе. Кроме того могу предложить участие в разработке новых протоколов.
В настоящее время я переписываю доморощенный tcp/ip стек, в надежде сделать его более стабильным и быстродействующим. Имею несколько идей для описания их на Хабре, но надеюсь каждую статью приурочить к новой сборке.
Если кого заинтересовало моё предложение, то выглядит моя система приблизительно так:
Вот вот. Но это не даёт автоматической гарантии повышения производительности. Всё зависит от реализации обработчика.
Если обработчик, в свою очередь, в вызывает ещё один сложный обработчик, то для повышения производительности совмещаем точку входа от основной функции и из вложенного обработчика.
Таким образом мы имеем одну точку входа в «сложном обработчике», куда попадают как запросы от верхнего уровня, так и ответы от вложенных обработчиков. Введём понятие «контекст», который будет передаваться по цепочке и на основе которого <сложный обработчик> производит диспетчеризацию между вызовами и ответами.
Что в результате? Пока <основная функция> вошла в <сложный обработчик> а тот, в свою очередь, обратился к нижележащему обработчику, следующий пользовательский поток может вызвать <сложный обработчик> и (о чудо!) взять данные из кэша (если они там оказались). Т.е. блокировки (в общепринятом смысле) как бы и не случилось вообще. Если же данных в кэше не оказалось, то пойдёт ещё одно сообщение во вложенный обработчик.
Прошу обратить внимание, что я не утверждаю, что мой способ универсальный и подойдёт для любого класса задач, но есть определённые задачи, где такая синхронизация даст выигрыш в производительности.
Например, если <сложный обработчик> активно работает со списками, то при использовании обрабатывающего потока мы избавляемся от массы блокировок, которые неизбежны для защиты данных при традиционном использовании многопоточности. При этом, развязка через единую точку входа в обработчик, позволяет безболезненно и максимально быстро обрабатывать синхронные запросы для случаев, когда данные для нового запроса «закэшированы» обработчиком на предыдущих итерациях.
Таким образом мы отказались от традиционных элементов блокировки и перенесли их на уровень синхронных IPC, что на самом деле не так и уж плохо, поскольку в данном случае количество блокировок определяется лишь точками входа в обработчики и никак не зависит от количества операций с данными, которые необходимо защищать от совместного доступа.
Простите.
Как бы решить эту проблему в системном приложении, работающем на микроядре L4?
Можно вывести вычислительную функцию в отдельный поток:
Обращаемся к этому потоку через L4_Call — синхронный вызов IPC.
На первый взгляд — громоздко, но это ничуть не хуже, чем поведение TryEnterCriticalSection в случае, если критическая секция уже занята — на многопроцессорной системе сначала закрутятся спин-локи, в надежде подождать ресурс, а потом пойдёт переключение контекста.
Кстати, мой код делает не совсем то, что Вы описали — он «замыкает» не только IsPrime, но и slow_IsPrime. С одной стороны это не оптимально, зато гарантирует потоко-безопасность slow_IsPrime. В случае же, если значение уже в кэше, два IPC в одном адресном пространстве будут ничуть не тяжелее, чем работа с критическими секциями. Хотя, конечно, сравнивать не совсем корректно — платформы разные.
Я ни чуть не преуменьшаю ценность Вашей статьи — она познавательна и имеет практическую ценность, но у меня есть своё представление о lock-free системах, которые построены на несколько других принципах — реализуют асинхронные сообщения на основе синхронных сообщений и конечного автомата.
4-clause license (original «BSD License»)
3-clause license («New BSD License» or «Modified BSD License»)
2-clause license («Simplified BSD License» or «FreeBSD License»)