В процессе, естественно, появились open/close/r/w/ioctl, и появилось ощущение, что последний неприлично, постыдно устарел. В качестве упражнения для размятия мозга я реализовал (в дополнение к обычному ioctl) некоторый альтернативный API, который бы позволил управлять свойствами устройств более гибким и удобным с точки зрения пользователя способом. Этот API, конечно, имеет свои очевидны минусы, и, в целом, эта статья — RFC, aka request For Comments.
Итак, API на уровне пользователя:
// returns name of property with sequential number nProperty, or error
errno_t listproperties( int fd, int nProperty, char *buf, int buflen );
errno_t getproperty( int fd, const char *pName, char *buf, int buflen );
errno_t setproperty( int fd, const char *pName, const char *pValue );
Правила:
- Никаких дефайнов с номерами, только имена.
- Никаких бинарных данных, только строки
Это не очень эффективно, но хочется предположить, что установка/чтение свойств — процесс редкий, и потому упираться в его скорость смысла немного, да и само переключение контекста при вызове стоит немало.
Можно несколько оптимизировать интерфейс, например, так:
// returns name of property with sequential number nProperty, or error
errno_t listproperties( int fd, int nProperty, char *buf, int buflen );
// returns property id by name
errno_t property_by_name( int fd, int *nProperty, const char *name );
errno_t getproperty( int fd, int nProperty, char *buf, int buflen );
errno_t setproperty( int fd, int nProperty, const char *pValue );
// fast lane
errno_t getproperty_i32( int fd, int nProperty, int32_t *data );
errno_t setproperty_i32( int fd, int nProperty, int32_t data );
Эта схема для единичного свойства не медленнее, чем ioctl.
Чем она хороша: можно сделать общую команду (напр mode), которая управляет параметрами любого драйвера, не зная о нём ничего — mode /dev/myCrazyDriver выдаст список свойств, а mode /dev/myCrazyDriver name val установит свойство name в значение val.
Реализация внутри драйвера (для которой, конечно, в ядре есть соответствующая незамысловатая инфраструктура) тоже несложна:
static property_t proplist[] =
{
{ pt_int32, "leds", 0, &leds, 0, (void *)set_leds, 0, 0 },
};
Эта строка описывает свойство, которое имеет тип int32, лежит в переменной leds, и если оно изменилось, то надобно вызвать функцию set_leds.
В реальности кроме pt_int32 родились только pt_mstring — malloc'ed strings, что тоже довольно удобно.
Вообще, надо сказать, темпы развития API классического Юникса меня несколько удивляют — есть ощущение, что никто им всерьёз не занимается, хотя, кажется, определённая систематизация ему явно не повредит.
У меня есть ещё несколько дополнений к традиционному POSIX-у, которые мне, Юниксоиду с 30-летним стажем, кажутся просто очевидными. Будет время — опубликую.
Ссылки на реализацию (кожа и кости, но всё же):
Кажется очевидным, но упомяну, что механизм, в принципе, можно применять для любых сущностей, вовсе не только для устройств.
Комментарии (58)
jcmvbkbc
25.04.2016 19:02+2хочется предположить, что установка/чтение свойств — процесс редкий
Установка/чтение свойств — может быть и редкий процесс. Но ioctl — это же не только установка/чтение свойств. Это же универсальный интерфейс для выполнения драйвером обслуживающим файл произвольных действий с передачей произвольных данных.
можно сделать общую команду (напр mode), которая управляет параметрами любого драйвера, не зная о нём ничего
Устройства, а не драйвера?
static property_t proplist[] = { { pt_int32, "leds", 0, &leds, 0, (void *)set_leds, 0, 0 }, }; ... static void set_leds(void) ...
А как set_leds узнает, которому из устройств обслуживаемых драйвером предназначена команда?dzavalishin
25.04.2016 19:16он же на fd меппится. что open позволяет открыть, то и предмет. захочет драйвер дать открыть не устройство, а себя вообще — будут проперти драйвера.
dzavalishin
25.04.2016 19:19Это же универсальный интерфейс для выполнения драйвером обслуживающим файл произвольных действий с передачей произвольных данных.
Увы, да. Можно и это поддержать, сделав какой-нибудь ioread( fd, ioctl_id, void *, size_t ), но, кажется, перебор.dzavalishin
25.04.2016 19:23Кстати, разумный механизм — это через ioctl только переключать стейт, а ввод-вывод всё равно делать через read/write.
set_property( fd, "iomode", "palette" ); write( fd, &palette, sizeof(palette) );
0xd34df00d
25.04.2016 19:26+4Ой, стейтфул, ой, глобальные переменные, только в профиль, ой, а что если второй поток рядом сделает другой set_property?
dzavalishin
26.04.2016 14:05Это тоже правда, но в этом смысле весь позикс — одна сплошная дыра. Впрочем, не давайте fd другому потоку, и всё. Ну или мьютекс возьмите — многопоточное программирование предполагает управление параллельным доступом через примитивы синхронизации, так или иначе.
0xd34df00d
26.04.2016 14:51+1Ну так если у вас есть шанс спроектировать новый API, то почему бы не воспользоваться этим шансом и не сделать сразу хорошо?
Чем меньше точек синхронизации торчит в пользовательский код, тем лучше. Чем меньше глобального стейта, в конце концов, тем лучше.
jcmvbkbc
25.04.2016 19:34Мне кажется, что современный тренд в том, чтобы через ioctl настраивать прямую связь юзерспейса с железом — IOMMU, аппаратные очереди в адресном пространстве процесса, а дальше они сами разберутся, без ядра.
lorc
25.04.2016 19:38Ну это наверное ближе к духу QNX, чем Linux.
jcmvbkbc
25.04.2016 19:42Это ближе к духу OpenOnload, HSA, OpenCL и т.п., всё оно работает на линуксе.
lorc
25.04.2016 19:53Ну да, я как раз хотел сказать что такие штуки в основном используются для общения с GPU. Это в общем нарушает принципы UNIX, и делается в основном ради увеличения производительности в ущерб стабильности.
jcmvbkbc
25.04.2016 19:57Это в общем нарушает принципы UNIX
Какие именно и каким образом?
делается в основном ради увеличения производительности в ущерб стабильности
Ради увеличения производительности — да, в ущерб стабильности — нет.lorc
25.04.2016 20:09Какие именно и каким образом?
Унифицированный доступ к устройствам. Если взять например framebuffer device, то любой драйвер поддерживает стандартный API + какие-то свои расширения. А значит моя программа может открыть /dev/fb0, замапить его себе в память и не задумываясь больше ни о чем работать-работать-работать.
Если же мне нужны вычисления на GPU — мне надо недостаточно открыть /dev/dri/card0. Мне надо взять условный libopencl для моей конкретной видеокарты, потому что именно он знает как правильно замапить память видеокарты и как посылать ей комманды которые специфичны именно для неё.
Ради увеличения производительности — да, в ущерб стабильности — нет.
Ну лично мне неюутно от мысли что любое приложение в юзерспейсе может слать любые команды моей видеокарте. Ведь в данном случае ядро не может контролировать что делается с видеокартой.jcmvbkbc
25.04.2016 20:37Если же мне нужны вычисления на GPU — мне надо недостаточно открыть /dev/dri/card0. Мне надо взять условный libopencl для моей конкретной видеокарты, потому что именно он знает как правильно замапить память видеокарты и как посылать ей комманды которые специфичны именно для неё.
Но вы же не пытаетесь выводить во фреймбуфер текстовым редактором или командой sed? Условный libopencl — это такой же инструмент для вычислений на gpu. При желании из него можно сделать классическую программу-фильтр, принимающую на вход текст кернела и входные данные и выводящую результаты.
Ну лично мне неюутно от мысли что любое приложение в юзерспейсе может слать любые команды моей видеокарте. Ведь в данном случае ядро не может контролировать что делается с видеокартой.
Частично может — с помощью правильно сконфигурированного IOMMU оно может защитить память не принадлежащую процессу от доступа со стороны видеокарты от имени и по поручению этого процесса. В остальном приложение взаимодействует, в основном, с фирмварью, которую можно считать специализированным «ядром» для видеокарты.lorc
25.04.2016 20:59Но вы же не пытаетесь выводить во фреймбуфер текстовым редактором или командой sed?
Я могу как минимум попытаться. Например я похожим образом несколько раз проверял, работает ли фрейбуфер в моем устройстве или определял какой файл фрейбуффера какому экрану соотвествует. Я даже могу сделать скриншот с помощью команды cat.
Частично может — с помощью правильно сконфигурированного IOMMU оно может защитить память не принадлежащую процессу от доступа со стороны видеокарты от имени и по поручению этого процесса.
Да, IOMMU (там где оно есть) может защитить память других процессов. Но если приложение решит поиграться с клоками видеокарты, например? Или с управлением скоростью куллеров? Фирмварь видеокарты же не сможет определить от кого приходят команды.
Я просто веду к тому, что драйвера стали вылазить за пределы ядра. Пропал общий интерфейс. Сначала это произошло с принтерами и сканерами, из-за чего мы имеем огромный CUPS и несколько алтернативных паков «драйверов» к нему. Теперь то же самое происходит с GPU.
Кстати, вот ещё пример почему это плохо. В ядре есть CryptoAPI, который позволяет добавлять свои модули, например для аппаратного ускорения шифрования. Допустим, я хочу использовать OpenCL что бы ускорить AES. Если бы весь интерфейс к видеокарте жил в ядре — проблем бы небыло. Я пишу код для GPU, мой модуль аттачится к DRM, выставляет наружу интерфейс для CryptoAPI и все счастливы. Ядро (и все драйвера) может шифровать с аппаратным ускорением, юзерспейс может шифровать с аппаратным ускорением, все счастливы.
При текущем же дизайне мы максимум что можем сделать — это модуль для openssl. Все ядерные клиенты остаются в пролете.jcmvbkbc
25.04.2016 21:39+1Я даже могу сделать скриншот с помощью команды cat.
Точно так же вы можете запустить какой-нибудь стандартный 2х2=4 на gpu.
Но если приложение решит поиграться с клоками видеокарты, например?
Не решит, не даст ему никто.
Фирмварь видеокарты же не сможет определить от кого приходят команды.
Сможет. Она же участвует в создании пользовательских очередей. Там же весь дизайн нацелен на то, чтобы протащить нужный контекст до нужного места. Без этого ни один здравомыслящий архитектор не предложил бы такую технологию.
Кстати, вот ещё пример почему это плохо. В ядре есть CryptoAPI, который позволяет добавлять свои модули, например для аппаратного ускорения шифрования.
У линуксового CryptoAPI не файловый интерфейс. См. http://lwn.net/Articles/410763/, особенно комментарии про socat и т.п.
Ядро (и все драйвера) может шифровать с аппаратным ускорением, юзерспейс может шифровать с аппаратным ускорением, все счастливы.
И тут не всё гладко. Некоторым драйверам (PPTP например) требуется синхронный интерфейс, потому что они хотят заниматься криптографией из SoftIRQ. Не всякий криптопровайдер на это способен.
Я просто веду к тому, что драйвера стали вылазить за пределы ядра. Пропал общий интерфейс.
Не всё укладывается в общий файловый интерфейс, и не всё что удаётся уложить — красиво и эффективно.dzavalishin
26.04.2016 17:17+2Кажется, любой юниксосрач можно завершить этой мудрой мыслью:
Не всё укладывается в общий файловый интерфейс, и не всё что удаётся уложить — красиво и эффективно.
:)
dzavalishin
26.04.2016 14:07в принципе, да — но, всё же, смысл существования ОС — в виртуализации железа. смысл opengl — обобщить и унифицировать интерфейсы и capabilities железа. обойти его — и ОС превращается в толстый и дорогой MS DOS.
MacIn
25.04.2016 19:40Зачем, если технически это одно и то же: пробросить из/в юзерспейс какие-то буфера, их размеры и команду. А там уж — в зав-ти от вида команды — будь то rw или ioctl трактуем содержимое буфера.
lorc
25.04.2016 19:36+1Ну тогда надо будет делать пару ioread/iowrite что нифига не удобно, потому что есть немало вызовов где делается и чтение и запись. Например, педерать параметры команды и получить обратно какие-то результаты. Делать ioread и затем iowrite не выход, потому что появляется состояние и все связанные с ним проблемы. Поэтому опять приходим к нужности единого вызова ioctl. Короче, Томпсон, Ритчи и Ко были не дураки.
MacIn
25.04.2016 19:42+2Или тот же самый DeviceIoControl:
BOOL WINAPI DeviceIoControl( _In_ HANDLE hDevice, _In_ DWORD dwIoControlCode, _In_opt_ LPVOID lpInBuffer, _In_ DWORD nInBufferSize, _Out_opt_ LPVOID lpOutBuffer, _In_ DWORD nOutBufferSize, _Out_opt_ LPDWORD lpBytesReturned, _Inout_opt_ LPOVERLAPPED lpOverlapped );
lorc
25.04.2016 19:53+1Ну да, это прямой аналог ioctl из мира Windows. Трудно придумать более универсальный и гибкий способ общаться с ядром.
Shamov
26.04.2016 11:24Не перебор. Это критически важный функционал. Любой драйвер, цепляющийся к сети и регулирующий прохождение трафика, предполагает, что в него во время настройки будет загружена куча правил. И эти правила не имеют имён. В лучшем случае они имеют генерируемые на лету числовые идентификаторы, в худшем — каждое правило идентифицируется лишь собственным содержанием.
0xd34df00d
25.04.2016 19:09+1В дефайнах и прочих енамах хорошо то, что опечатку поймает компилятор. Тут опечатку поймает в лучшем случае рантайм, в худшем — никто. Что, если у вас опечатка в имени свойства в каком-нибудь редком control flow path, например, в коде обработки ошибок?
dzavalishin
25.04.2016 19:21-7Это правда.
Но посмотрите на жизнь с другой стороны.
Языка Си больше нет.
Вся современная разработка — переносимые языки с динамическим рантаймом, и никаких дефайнов там нет. Они всё равно мепятся в ioctl через ж. автогеном, и всё равно в рантайме.0xd34df00d
25.04.2016 19:25+4И компиляторов нет?
Пишу на C++ (поэтому изначально хотел предложить enum class) и на Haskell. Разработка вроде даже современная, правда, не очень лоу-левел, не на уровне ядра, от ядра мне максимум прямой вызов mmap нужен. Так что зря вы так, не вся.dzavalishin
26.04.2016 14:20Надеюсь, Вы знаете, на кой хрен Вы до сих пор с С++. Я вижу только две причины: исторические наслоения и те 10% кода, которые правда надо оптимизировать.
0xd34df00d
26.04.2016 14:52+1О, языкосрачикА какой язык вы предлагаете?dzavalishin
26.04.2016 17:12+2Решение требует задачи. Нет смысла выбирать ЯП абстрактно.
Однако, как 20 лет назад был универсальный ответ «не знаешь, на чём писать — пиши на Си (++)», так сегодня дефолтным ЯП является Ява, если ты юниксоид и С#, если ты хочешь жить и умереть в виндах. Инфраструктура поддержки этих двух ЯП несравнима ни с чем (включая си).
Если тебе нужно скриптик или страничку — похрен. Хоть на коболе.0xd34df00d
26.04.2016 17:20На современных плюсах писать как-то выразительнее, быстрее и приятнее, чем на Яве.
dzavalishin
26.04.2016 18:09-1Вы о себе или об индустрии? Строка кода на С++ в 4-5 раз дороже строки на Яве.
MacIn
26.04.2016 18:32Можно пример-два? Не срача ради. ПМСМ, чем дальше, тем меньше разницы.
0xd34df00d
26.04.2016 18:34+1Да хотя бы auto.
MacIn
26.04.2016 18:38Ага, как раз про ауто думал сегодня, читая вот эту статью:
https://habrahabr.ru/company/luxoft/blog/278313/
stepik777
25.04.2016 21:51+3> Вся современная разработка — переносимые языки с динамическим рантаймом
Что? Это только какая-то часть современной разработки. Возьмите, например, любую современную AAA игру, она написана на C++.dzavalishin
26.04.2016 14:16Она написана на GPU, который занимается графикой, и на динамическом языке, на котором пишут AI. Си там — тонкая прослойка между ними, которая написана на Си только в силу стереотипности мышления разрабов. Технически в этом давно нет смысла.
Скучно это. Я в 90-х наблюдал истерику разработчиков на ассемблере, которым говорили, что асм умер и всё пишут на си. Потом истерику сишников с переходом на яву. Сегодня истерика явистов с переходом на ФП языки.
(И да, если вы ещё истерите по поводу фразы «си умер в пользу явы», вы проспали поколение — уже пора рассказывать, что ява — это легаси.)terryP
27.04.2016 12:50Сегодня истерика явистов с переходом на ФП языки.
Хде истерика-то? На самом деле, явистам пофиг, часть ФП языков приходит в JVM экосистему, то есть является ещё одним инструментов явистов, которым это интересно, а большая часть существующих проектов никогда с явы не слезет, а значит тем явистам, которым не интересно на ближайшие лет 10-15 работы на явы не сильно убавиться. Да, и большинство явистов признает недостатки явы (просто она довольно удобна для некоторых задач энтерпрайза), так что каких-то истерик не ждите. Ну и потихонку ФП идет в java, я говорю о ряде библиотек, которые позволяют вносить ФП конструкции в джаву.
AlexSky
26.04.2016 01:22+1Черт, а я уже столько лет пишу на несуществующем языке.
Надо завтра черкануть в мэйлинг лист линуксового ядра, чтобы тоже прекращали использовать несуществующий язык и переходили на РНР.dzavalishin
26.04.2016 14:45+1Я всего лет 7 назад помогал в исправлении ошибки в коде на языке Кобол. Кобол. Знаете такой? Джермейн, IBM360, середина прошлого века, слышали? Работает! Используют!
Так и Си.
Некоторым не везёт. Некоторые балдеют. Я вот пишу на Си уже тридцать лет, нехреново умею это делать и определённое удовольствие от этого испытываю.
Но не нужно путать неизбежность (хрен куда Линукс с Си спрыгнет, это ясно), собственные сексуальные предпочтения (писать на си — разновидность онанизма) и ЦЕЛЕСООБРАЗНОСТЬ.
Писать на Си в 99% случаев — НЕЦЕЛЕСООБРАЗНО. Потому что 3.14здец как дорого и 3.14здец как ненадёжно. Это я говорю вам как человек, который этим языком занимается 30 (тридцать) лет и как создатель нескольких крупнейших софтверных систем в России, типа Я.Маркета и инфраструктуры портала Яндекса, Юлмарта и ещё с сотни проектов помельче.
И как человек, написавший ОС почти в одно рыло.
Так вот — почти ПОЛОВИНА кода ОС посвящена заморочкам, которые из Си делают что-то жизнеспособное.
Всякие референс каунты (ручной ad hoc garbage collection), очереди, списки, эмуляции виртуальных методов, мемберофы, проверки и затычки.
Другое дело, что в некоторых проектах деваться тупо некуда — gcc поддержан на таком количестве платформ, что его ещё долго не получится забыть. Ну и есть реально тесные платформы типа avr, при паре килобайт памяти не разгуляешься.
Но это всё — уже история. Как Кобол. Лямбды уверенно вытесняют указатели на функцию.prefrontalCortex
26.04.2016 22:45Всякие референс каунты (ручной ad hoc garbage collection)
По-вашему, ОС с ядром на языке со сборкой мусора будет в чём-то лучше конкурентов? Оберон вон как-то не взлетел.
MacIn
25.04.2016 19:27Это не очень эффективно, но хочется предположить, что установка/чтение свойств — процесс редкий, и потому упираться в его скорость смысла немного, да и само переключение контекста при вызове стоит немало.
В принципе, если properties метод — просто обертки над ioctl кодами, то это не проблема — в случае необходимости можно реализовать вызов напрямую.
MichaelBorisov
25.04.2016 22:55+3Никаких дефайнов с номерами, только имена.
Никаких имен. Только GUID, только хардкор.
Применение строковых имен чревато их конфликтами, перепутыванием прописных и строчных букв, потерей пробелов, знаков препинания и т.д. Кроме того, обработка C-строк менее эффективна, чем обработка данных фиксированной длины, таких как GUID.
Никаких бинарных данных, только строки
Ну и это ни к чему. Появляется парсинг строк, диагностика синтаксических и прочих ошибок, уязвимости парсера. Эта современная мода на текстовое представление всего и вся — она годится только для малых объемов данных и в тех случаях, где производительность некритична. Даже XML-файл прочитать на несколько десятков мегабайт может не каждая библиотека.
vanxant
25.04.2016 23:05Ну я бы не сказал, что ioctl прям вот всегда процесс редкий. Если какой-нибудь сервер принимает кучу коротких входящих соединений, то там соотношение Ioctl и fread вполне может быть близко к 1/1.
lumag
26.04.2016 02:33Properties получаются альтернативой sysfs (где тоже строковые ключи и значения), а не ioctl.
dzavalishin
26.04.2016 15:00реализую похожую модель, но в приложении к другому объекту.
с другой стороны, возможно, правильная схема именно fs:
/dev/tty0
/dev/tty0.speed
/dev/tty0.stop_bits
alecv
26.04.2016 11:44+2Ага, ага. Понятно во что оно мигрирует:
— Сделать «невидимые» атрибуты, например начинающиеся с _ или $ в стиле MS
— Сделать систему безопасности, каждый атрибут должен иметь список на чтение-запись-просмотр
— Сделать иерархическую систему атрибутов, объединив похожие атрибуты в каталоги атрибутов по типу Registry
— Невидимость атрибутов можно реализовать через систему безопасности атрибутов и каталоги атрибутов конечно же
— Сделать «метаатрибуты», в которых будут хранится свойства атрибутов, в частности их тип, по образцу SNMP OID
P.S. В принципе тут уже упомянули sysfs. Надо только добавить, что заниматься этим должен обязательно systemd.
[sarcasm mode off]dzavalishin
26.04.2016 14:10ls /proc
это, в общем, тот же путь, да.lumag
26.04.2016 14:39/proc — помойка, в той части, которая к PID не привязана. Раскидывание свойств по устройствам — это sysfs.
dzavalishin
26.04.2016 14:47Любой нейминг без централизованного управления приходит в состояние помойки. Вопрос времени. :)
lumag
26.04.2016 15:31В целом — согласен. Есть разница в том, что sysfs — это ,, проекция'' реальных структур ядра в файловую систему. Т.е. основа в виде каталогов всегда будет осмысленной. А в /proc каждый кидал что и как хотел.
monah_tuk
27.04.2016 17:12AVDictionary
из FFmpeg вспомнились и сразу проблемы (которые, в принципе, все выше перечислены):
- опечатки, как в именах, так и в значениях
- конфликт имён и типов значений
- отсутствие проверки со стороны компилятора и
- как следствие — валится в рантайме в самый неподходящий момент
Кстати, интерфейс словаря можно и тут добавить — что бы за вызов сразу пачку параметров изменить/получить. В FFmpeg после вызова в словаре остаются записи, которые примениться не смогли.
dzavalishin
27.04.2016 23:15проблема только с именами, и то — частичная. рантайм вернёт ошибку. Всё остальное есть у ioctl в полный рост. никто и никак не проверит бинарный номер и формат структуры. а так же выравнивание полей.
RealFSA
Хм. Осталось сделать в модулях ядра Linux соответствующие «стандартные» ioctl-ы, и можно продвигать обратную совместимость с ОС Фантом. :)
dzavalishin
Верно. Но есть более полезный (и более простой организационно) заход для этого упражнения. Напишу позже.