Атрибуты устройств, или ioctl must die

    В процессе работы над ОС Фантом, которая вообще не Юникс никаким местом, мне, тем не менее, захотелось сделать в нём Unix-compatible подсистему. Не то, чтобы прямо POSIX, но что-то достаточно близкое. Отчасти из любопытства, отчасти для удобства, отчасти как ещё один migration path. (Ну и вообще было интересно, насколько трудно написать простенький Юникс «из головы».) В качестве цели номер 1 была поставлена задача запустить quake 1 for Unix, которая и была достигнута.

    В процессе, естественно, появились 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 );
    


    Правила:

    1. Никаких дефайнов с номерами, только имена.
    2. Никаких бинарных данных, только строки


    Это не очень эффективно, но хочется предположить, что установка/чтение свойств — процесс редкий, и потому упираться в его скорость смысла немного, да и само переключение контекста при вызове стоит немало.

    Можно несколько оптимизировать интерфейс, например, так:

    // 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-летним стажем, кажутся просто очевидными. Будет время — опубликую.

    Ссылки на реализацию (кожа и кости, но всё же):



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

    Похожие публикации

    Средняя зарплата в IT

    110 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 8 813 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

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

      0
      Хм. Осталось сделать в модулях ядра Linux соответствующие «стандартные» ioctl-ы, и можно продвигать обратную совместимость с ОС Фантом. :)
        0
        Верно. Но есть более полезный (и более простой организационно) заход для этого упражнения. Напишу позже.
        +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 узнает, которому из устройств обслуживаемых драйвером предназначена команда?
          0
          он же на fd меппится. что open позволяет открыть, то и предмет. захочет драйвер дать открыть не устройство, а себя вообще — будут проперти драйвера.
            0
            Это же универсальный интерфейс для выполнения драйвером обслуживающим файл произвольных действий с передачей произвольных данных.


            Увы, да. Можно и это поддержать, сделав какой-нибудь ioread( fd, ioctl_id, void *, size_t ), но, кажется, перебор.
              0
              Кстати, разумный механизм — это через ioctl только переключать стейт, а ввод-вывод всё равно делать через read/write.

              set_property( fd, "iomode", "palette" ); 
              write( fd, &palette, sizeof(palette) );
              
                +4
                Ой, стейтфул, ой, глобальные переменные, только в профиль, ой, а что если второй поток рядом сделает другой set_property?
                  0
                  Это тоже правда, но в этом смысле весь позикс — одна сплошная дыра. Впрочем, не давайте fd другому потоку, и всё. Ну или мьютекс возьмите — многопоточное программирование предполагает управление параллельным доступом через примитивы синхронизации, так или иначе.
                    +1
                    Ну так если у вас есть шанс спроектировать новый API, то почему бы не воспользоваться этим шансом и не сделать сразу хорошо?

                    Чем меньше точек синхронизации торчит в пользовательский код, тем лучше. Чем меньше глобального стейта, в конце концов, тем лучше.
                0
                Мне кажется, что современный тренд в том, чтобы через ioctl настраивать прямую связь юзерспейса с железом — IOMMU, аппаратные очереди в адресном пространстве процесса, а дальше они сами разберутся, без ядра.
                  0
                  Ну это наверное ближе к духу QNX, чем Linux.
                    0
                    Это ближе к духу OpenOnload, HSA, OpenCL и т.п., всё оно работает на линуксе.
                      0
                      Ну да, я как раз хотел сказать что такие штуки в основном используются для общения с GPU. Это в общем нарушает принципы UNIX, и делается в основном ради увеличения производительности в ущерб стабильности.
                        0
                        Это в общем нарушает принципы UNIX

                        Какие именно и каким образом?

                        делается в основном ради увеличения производительности в ущерб стабильности

                        Ради увеличения производительности — да, в ущерб стабильности — нет.
                          0
                          Какие именно и каким образом?

                          Унифицированный доступ к устройствам. Если взять например framebuffer device, то любой драйвер поддерживает стандартный API + какие-то свои расширения. А значит моя программа может открыть /dev/fb0, замапить его себе в память и не задумываясь больше ни о чем работать-работать-работать.
                          Если же мне нужны вычисления на GPU — мне надо недостаточно открыть /dev/dri/card0. Мне надо взять условный libopencl для моей конкретной видеокарты, потому что именно он знает как правильно замапить память видеокарты и как посылать ей комманды которые специфичны именно для неё.

                          Ради увеличения производительности — да, в ущерб стабильности — нет.

                          Ну лично мне неюутно от мысли что любое приложение в юзерспейсе может слать любые команды моей видеокарте. Ведь в данном случае ядро не может контролировать что делается с видеокартой.
                            0
                            Если же мне нужны вычисления на GPU — мне надо недостаточно открыть /dev/dri/card0. Мне надо взять условный libopencl для моей конкретной видеокарты, потому что именно он знает как правильно замапить память видеокарты и как посылать ей комманды которые специфичны именно для неё.

                            Но вы же не пытаетесь выводить во фреймбуфер текстовым редактором или командой sed? Условный libopencl — это такой же инструмент для вычислений на gpu. При желании из него можно сделать классическую программу-фильтр, принимающую на вход текст кернела и входные данные и выводящую результаты.

                            Ну лично мне неюутно от мысли что любое приложение в юзерспейсе может слать любые команды моей видеокарте. Ведь в данном случае ядро не может контролировать что делается с видеокартой.

                            Частично может — с помощью правильно сконфигурированного IOMMU оно может защитить память не принадлежащую процессу от доступа со стороны видеокарты от имени и по поручению этого процесса. В остальном приложение взаимодействует, в основном, с фирмварью, которую можно считать специализированным «ядром» для видеокарты.
                              0
                              Но вы же не пытаетесь выводить во фреймбуфер текстовым редактором или командой sed?

                              Я могу как минимум попытаться. Например я похожим образом несколько раз проверял, работает ли фрейбуфер в моем устройстве или определял какой файл фрейбуффера какому экрану соотвествует. Я даже могу сделать скриншот с помощью команды cat.

                              Частично может — с помощью правильно сконфигурированного IOMMU оно может защитить память не принадлежащую процессу от доступа со стороны видеокарты от имени и по поручению этого процесса.

                              Да, IOMMU (там где оно есть) может защитить память других процессов. Но если приложение решит поиграться с клоками видеокарты, например? Или с управлением скоростью куллеров? Фирмварь видеокарты же не сможет определить от кого приходят команды.

                              Я просто веду к тому, что драйвера стали вылазить за пределы ядра. Пропал общий интерфейс. Сначала это произошло с принтерами и сканерами, из-за чего мы имеем огромный CUPS и несколько алтернативных паков «драйверов» к нему. Теперь то же самое происходит с GPU.

                              Кстати, вот ещё пример почему это плохо. В ядре есть CryptoAPI, который позволяет добавлять свои модули, например для аппаратного ускорения шифрования. Допустим, я хочу использовать OpenCL что бы ускорить AES. Если бы весь интерфейс к видеокарте жил в ядре — проблем бы небыло. Я пишу код для GPU, мой модуль аттачится к DRM, выставляет наружу интерфейс для CryptoAPI и все счастливы. Ядро (и все драйвера) может шифровать с аппаратным ускорением, юзерспейс может шифровать с аппаратным ускорением, все счастливы.
                              При текущем же дизайне мы максимум что можем сделать — это модуль для openssl. Все ядерные клиенты остаются в пролете.
                                +1
                                Я даже могу сделать скриншот с помощью команды cat.

                                Точно так же вы можете запустить какой-нибудь стандартный 2х2=4 на gpu.

                                Но если приложение решит поиграться с клоками видеокарты, например?

                                Не решит, не даст ему никто.

                                Фирмварь видеокарты же не сможет определить от кого приходят команды.

                                Сможет. Она же участвует в создании пользовательских очередей. Там же весь дизайн нацелен на то, чтобы протащить нужный контекст до нужного места. Без этого ни один здравомыслящий архитектор не предложил бы такую технологию.

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

                                У линуксового CryptoAPI не файловый интерфейс. См. http://lwn.net/Articles/410763/, особенно комментарии про socat и т.п.

                                Ядро (и все драйвера) может шифровать с аппаратным ускорением, юзерспейс может шифровать с аппаратным ускорением, все счастливы.

                                И тут не всё гладко. Некоторым драйверам (PPTP например) требуется синхронный интерфейс, потому что они хотят заниматься криптографией из SoftIRQ. Не всякий криптопровайдер на это способен.

                                Я просто веду к тому, что драйвера стали вылазить за пределы ядра. Пропал общий интерфейс.

                                Не всё укладывается в общий файловый интерфейс, и не всё что удаётся уложить — красиво и эффективно.
                                  +2
                                  Кажется, любой юниксосрач можно завершить этой мудрой мыслью:

                                  Не всё укладывается в общий файловый интерфейс, и не всё что удаётся уложить — красиво и эффективно.


                                  :)
                    0
                    в принципе, да — но, всё же, смысл существования ОС — в виртуализации железа. смысл opengl — обобщить и унифицировать интерфейсы и capabilities железа. обойти его — и ОС превращается в толстый и дорогой MS DOS.
                    0
                    Зачем, если технически это одно и то же: пробросить из/в юзерспейс какие-то буфера, их размеры и команду. А там уж — в зав-ти от вида команды — будь то rw или ioctl трактуем содержимое буфера.
                    +1
                    Ну тогда надо будет делать пару ioread/iowrite что нифига не удобно, потому что есть немало вызовов где делается и чтение и запись. Например, педерать параметры команды и получить обратно какие-то результаты. Делать ioread и затем iowrite не выход, потому что появляется состояние и все связанные с ним проблемы. Поэтому опять приходим к нужности единого вызова ioctl. Короче, Томпсон, Ритчи и Ко были не дураки.
                      +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
                      );
                      
                      

                        +1
                        Ну да, это прямой аналог ioctl из мира Windows. Трудно придумать более универсальный и гибкий способ общаться с ядром.
                      0
                      Не перебор. Это критически важный функционал. Любой драйвер, цепляющийся к сети и регулирующий прохождение трафика, предполагает, что в него во время настройки будет загружена куча правил. И эти правила не имеют имён. В лучшем случае они имеют генерируемые на лету числовые идентификаторы, в худшем — каждое правило идентифицируется лишь собственным содержанием.
                    +1
                    В дефайнах и прочих енамах хорошо то, что опечатку поймает компилятор. Тут опечатку поймает в лучшем случае рантайм, в худшем — никто. Что, если у вас опечатка в имени свойства в каком-нибудь редком control flow path, например, в коде обработки ошибок?
                      –7
                      Это правда.

                      Но посмотрите на жизнь с другой стороны.

                      Языка Си больше нет.

                      Вся современная разработка — переносимые языки с динамическим рантаймом, и никаких дефайнов там нет. Они всё равно мепятся в ioctl через ж. автогеном, и всё равно в рантайме.
                        +4
                        И компиляторов нет?

                        Пишу на C++ (поэтому изначально хотел предложить enum class) и на Haskell. Разработка вроде даже современная, правда, не очень лоу-левел, не на уровне ядра, от ядра мне максимум прямой вызов mmap нужен. Так что зря вы так, не вся.
                          0
                          Надеюсь, Вы знаете, на кой хрен Вы до сих пор с С++. Я вижу только две причины: исторические наслоения и те 10% кода, которые правда надо оптимизировать.
                            +1
                            О, языкосрачик А какой язык вы предлагаете?
                              +2
                              Решение требует задачи. Нет смысла выбирать ЯП абстрактно.

                              Однако, как 20 лет назад был универсальный ответ «не знаешь, на чём писать — пиши на Си (++)», так сегодня дефолтным ЯП является Ява, если ты юниксоид и С#, если ты хочешь жить и умереть в виндах. Инфраструктура поддержки этих двух ЯП несравнима ни с чем (включая си).

                              Если тебе нужно скриптик или страничку — похрен. Хоть на коболе.
                                0
                                На современных плюсах писать как-то выразительнее, быстрее и приятнее, чем на Яве.
                                  –1
                                  Вы о себе или об индустрии? Строка кода на С++ в 4-5 раз дороже строки на Яве.
                                    +1
                                    О себе, конечно. Вы ж спрашивали, на кой хрен я до сих пор с С++.
                                    0
                                    Можно пример-два? Не срача ради. ПМСМ, чем дальше, тем меньше разницы.
                                      +1
                                      Да хотя бы auto.
                                        0
                                        Ага, как раз про ауто думал сегодня, читая вот эту статью:
                                        https://habrahabr.ru/company/luxoft/blog/278313/
                            +3
                            > Вся современная разработка — переносимые языки с динамическим рантаймом

                            Что? Это только какая-то часть современной разработки. Возьмите, например, любую современную AAA игру, она написана на C++.
                              0
                              Она написана на GPU, который занимается графикой, и на динамическом языке, на котором пишут AI. Си там — тонкая прослойка между ними, которая написана на Си только в силу стереотипности мышления разрабов. Технически в этом давно нет смысла.

                              Скучно это. Я в 90-х наблюдал истерику разработчиков на ассемблере, которым говорили, что асм умер и всё пишут на си. Потом истерику сишников с переходом на яву. Сегодня истерика явистов с переходом на ФП языки.

                              (И да, если вы ещё истерите по поводу фразы «си умер в пользу явы», вы проспали поколение — уже пора рассказывать, что ява — это легаси.)
                                0
                                Сегодня истерика явистов с переходом на ФП языки.

                                Хде истерика-то? На самом деле, явистам пофиг, часть ФП языков приходит в JVM экосистему, то есть является ещё одним инструментов явистов, которым это интересно, а большая часть существующих проектов никогда с явы не слезет, а значит тем явистам, которым не интересно на ближайшие лет 10-15 работы на явы не сильно убавиться. Да, и большинство явистов признает недостатки явы (просто она довольно удобна для некоторых задач энтерпрайза), так что каких-то истерик не ждите. Ну и потихонку ФП идет в java, я говорю о ряде библиотек, которые позволяют вносить ФП конструкции в джаву.
                                  0
                                  Всё верно. Но иногда люди эмоциональны. :)
                              +1
                              Черт, а я уже столько лет пишу на несуществующем языке.
                              Надо завтра черкануть в мэйлинг лист линуксового ядра, чтобы тоже прекращали использовать несуществующий язык и переходили на РНР.
                                +1
                                Я всего лет 7 назад помогал в исправлении ошибки в коде на языке Кобол. Кобол. Знаете такой? Джермейн, IBM360, середина прошлого века, слышали? Работает! Используют!

                                Так и Си.

                                Некоторым не везёт. Некоторые балдеют. Я вот пишу на Си уже тридцать лет, нехреново умею это делать и определённое удовольствие от этого испытываю.

                                Но не нужно путать неизбежность (хрен куда Линукс с Си спрыгнет, это ясно), собственные сексуальные предпочтения (писать на си — разновидность онанизма) и ЦЕЛЕСООБРАЗНОСТЬ.

                                Писать на Си в 99% случаев — НЕЦЕЛЕСООБРАЗНО. Потому что 3.14здец как дорого и 3.14здец как ненадёжно. Это я говорю вам как человек, который этим языком занимается 30 (тридцать) лет и как создатель нескольких крупнейших софтверных систем в России, типа Я.Маркета и инфраструктуры портала Яндекса, Юлмарта и ещё с сотни проектов помельче.

                                И как человек, написавший ОС почти в одно рыло.

                                Так вот — почти ПОЛОВИНА кода ОС посвящена заморочкам, которые из Си делают что-то жизнеспособное.

                                Всякие референс каунты (ручной ad hoc garbage collection), очереди, списки, эмуляции виртуальных методов, мемберофы, проверки и затычки.

                                Другое дело, что в некоторых проектах деваться тупо некуда — gcc поддержан на таком количестве платформ, что его ещё долго не получится забыть. Ну и есть реально тесные платформы типа avr, при паре килобайт памяти не разгуляешься.

                                Но это всё — уже история. Как Кобол. Лямбды уверенно вытесняют указатели на функцию.
                                  0
                                  Всякие референс каунты (ручной ad hoc garbage collection)
                                  По-вашему, ОС с ядром на языке со сборкой мусора будет в чём-то лучше конкурентов? Оберон вон как-то не взлетел.
                            0
                            Это не очень эффективно, но хочется предположить, что установка/чтение свойств — процесс редкий, и потому упираться в его скорость смысла немного, да и само переключение контекста при вызове стоит немало.

                            В принципе, если properties метод — просто обертки над ioctl кодами, то это не проблема — в случае необходимости можно реализовать вызов напрямую.
                              +3
                              Никаких дефайнов с номерами, только имена.

                              Никаких имен. Только GUID, только хардкор.

                              Применение строковых имен чревато их конфликтами, перепутыванием прописных и строчных букв, потерей пробелов, знаков препинания и т.д. Кроме того, обработка C-строк менее эффективна, чем обработка данных фиксированной длины, таких как GUID.
                              Никаких бинарных данных, только строки

                              Ну и это ни к чему. Появляется парсинг строк, диагностика синтаксических и прочих ошибок, уязвимости парсера. Эта современная мода на текстовое представление всего и вся — она годится только для малых объемов данных и в тех случаях, где производительность некритична. Даже XML-файл прочитать на несколько десятков мегабайт может не каждая библиотека.
                                0
                                Ну я бы не сказал, что ioctl прям вот всегда процесс редкий. Если какой-нибудь сервер принимает кучу коротких входящих соединений, то там соотношение Ioctl и fread вполне может быть близко к 1/1.
                                  0
                                  Properties получаются альтернативой sysfs (где тоже строковые ключи и значения), а не ioctl.
                                    0
                                    реализую похожую модель, но в приложении к другому объекту.
                                    с другой стороны, возможно, правильная схема именно fs:

                                    /dev/tty0
                                    /dev/tty0.speed
                                    /dev/tty0.stop_bits

                                    +2
                                    Ага, ага. Понятно во что оно мигрирует:
                                    — Сделать «невидимые» атрибуты, например начинающиеся с _ или $ в стиле MS
                                    — Сделать систему безопасности, каждый атрибут должен иметь список на чтение-запись-просмотр
                                    — Сделать иерархическую систему атрибутов, объединив похожие атрибуты в каталоги атрибутов по типу Registry
                                    — Невидимость атрибутов можно реализовать через систему безопасности атрибутов и каталоги атрибутов конечно же
                                    — Сделать «метаатрибуты», в которых будут хранится свойства атрибутов, в частности их тип, по образцу SNMP OID

                                    P.S. В принципе тут уже упомянули sysfs. Надо только добавить, что заниматься этим должен обязательно systemd.

                                    [sarcasm mode off]
                                      0
                                      ls /proc

                                      это, в общем, тот же путь, да.
                                        0
                                        /proc — помойка, в той части, которая к PID не привязана. Раскидывание свойств по устройствам — это sysfs.
                                          0
                                          Любой нейминг без централизованного управления приходит в состояние помойки. Вопрос времени. :)
                                            0
                                            В целом — согласен. Есть разница в том, что sysfs — это ,, проекция'' реальных структур ядра в файловую систему. Т.е. основа в виде каталогов всегда будет осмысленной. А в /proc каждый кидал что и как хотел.
                                              0
                                              Да, это помогает. Должно, по крайней мере. Семантика диктует структуру.
                                      0

                                      AVDictionary из FFmpeg вспомнились и сразу проблемы (которые, в принципе, все выше перечислены):


                                      • опечатки, как в именах, так и в значениях
                                      • конфликт имён и типов значений
                                      • отсутствие проверки со стороны компилятора и
                                      • как следствие — валится в рантайме в самый неподходящий момент

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

                                        0
                                        проблема только с именами, и то — частичная. рантайм вернёт ошибку. Всё остальное есть у ioctl в полный рост. никто и никак не проверит бинарный номер и формат структуры. а так же выравнивание полей.

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                      Самое читаемое