ВАЖНО!!! Ядро - концепция работы

Kernel architecture questions
  • Управление процессами.
    1. Понятие процесса как такового в Колибри очень зачаточное: процесс - объединение потоков с одним и тем же адресным пространством. У всех таких объединяемых потоков одно и то же имя и один и тот же размер используемой памяти. Потоки, впрочем, существуют и обладают следующими характеристиками (memmap.inc из исходников ядра):
    - идентификатор (TID), каждому создаваемому потоку назначается уникальный идентификатор;
    - состояние потока: активен (выполняется прямо сейчас либо ждёт переключения задач на него), заморожен, завершается, ждёт
    - окно: каждый поток имеет ровно одно окно, которое может быть невидимым, но обязательно существует;
    - использование процессора: число тактов за последнюю секунду, которое процессор убил на выполнение именно этого потока;
    - имя процесса (имя исполняемого файла);
    - маска событий, о которых система извещает поток;
    - системный стек;
    - список объектов ядра, ассоциированных с этим потоком;
    - карта разрешённых портов ввода/вывода;
    - текущая папка для функций файловой системы;
    - буфер для сообщений IPC (присутствует, только если поток его явно определил)
    2. Процесс не идентифицируется никак; информация о потоках внутри ОС собрана в статический массив на 255 входов (нумеруемых от 0 до 255, причём 0-й слот не может использоваться, так что всего в системе может быть не более 255 потоков) (технически не в один массив, а в два разных, но сути дела это не меняет). Некоторые системные функции принимают номер слота, некоторые - идентификатор.
    3. Создание нового процесса отличается от создания потока (пожалуй, это единственное место в API, где такое отличие есть). Создание процесса: [core/taskman.inc, fs_execute] принимает на вход имя бинарного файла для загрузки, параметры командной строки для нового процесса и флаги, сейчас только то, запускается процесс как отлаживаемый или как обычный.
    - загружает бинарник (целиком в память ядра; если он упакован kpack'ом, то распаковывается в памяти);
    - проверяет заголовок исполняемого файла, вычисляются нужные параметры (есть две версии заголовка, мало отличающиеся);
    - захватывает мьютекс application_table_status, управляющий доступом на запись к таблице потоков (вышеупомянутым двум массивам)
    - находит пустой слот для нового потока; если такого нет (255 потоков уже запущены) - выход с ошибкой;
    - заполняет имя процесса;
    - создаёт новое адресное пространство (это отдельная история);
    - вызывает функцию set_app_params, заполняющую остальные поля структуры потока (подробнее - ниже);
    - освобождает мьютекс application_table_status.
    Создание потока: [core/taskman.inc, new_sys_threads] принимает на вход entry point нового процесса и указатель на user-mode стек.
    - захватывает application_table_status
    - находит пустой слот для нового потока; если такого нет - выход с ошибкой;
    - копирует имя процесса и информацию об адресном пространстве вызывающего потока в структуру для нового;
    - вызывает set_app_params
    - освобождает application_table_status
    Функция set_app_params:
    - выделяет в адресном пространстве ядра буфер под стек ядра и область для сохранения состояния FPU и SSE;
    - инициализирует разные параметры потока значениями по умолчанию;
    - копирует командную строку и путь к приложению в адресное пространство процесса по адресам, записанным в заголовке бинарника (или не копирует, если эти значения в заголовке нулевые, что означает, что программа в них не нуждается);
    - выделяет очередной идентификатор (каждый следующий TID равен предыдущему + 1);
    - инициализирует user-mode контекст, значения eip и esp берутся из параметров вызова для sys_new_threads и из заголовка для fs_execute;
    - если новый процесс загружается как отлаживаемый, то помечает его состояние как замороженное, иначе - как работающее (начиная с этого места на новый поток возможны переключения задач).
    Завершение процесса: [core/sys32.inc, terminate] когда системный поток получает управление (главный цикл системы), одним из его действий является проход по списку процессов, поиск потоков в завершающемся состоянии и убийство таких процессов. Все нижеследующие действия происходят в контексте системного потока.
    - захватывает application_table_status
    - проходит по списку объектов ядра и вызывает деструкторы
    - если этот поток - последний в своём процессе, уничтожает адресное пространство
    - освобождает разные системные ресурсы, которые мог выделить этот поток и которых нет в списке объектов ядра
    (список горячих клавиш, список кнопок, определённых потоком)
    - если поток отлаживается, посылает извещение отладчику
    - освобождает память под kernel-mode стек и область сохранения FPU/SSE
    - освобождает карту ввода/вывода (если она была изменена - есть стандартная карта ввода/вывода, которая создаётся при загрузке и разделяется между всеми потоками)
    - если окно потока было на вершине оконного стека, активирует следующее окно
    - если поток рухнул (или был прибит) в процессе работы с жёстким диском, освобождает мьютекс занятости жёсткого диска; то же самое для CD и дискеты
    - освобождает выделенные потоком IRQ и порты
    - если текущий прибиваемый процесс - отладчик, помечает как завершающиеся все отлаживаемые им процессы
    - перерисовывает экран
    - освобождает application_table_status
    4. Взаимодействия процессов практически нет - есть только специальная системная функция для передачи данных от процесса-источника процессу-приёмнику, причём приёмник должен заранее подготовить буфер и ожидать этих данных, и некоторые возможности по отладке приложений.
    5. Соответственно синхронизации тоже практически нет - один процесс может только проверить, завершился ли другой.
    6. Состояния перечислены выше.
    Активный/ждущий -> замороженный: вызов (кем-то) функции заморозки 69.4
    Активный -> завершающийся: либо сам поток вызывает функцию завершения -1, либо какой-то поток решает его прибить функциями 18.2 или 18.18.
    Активный -> ждущий: вызов (потоком) функции ожидания события 10.
    Замороженный -> активный/ждущий: вызов (кем-то) функции разморозки 69.5; поток возвращается в состояние до заморозки
    Замороженный -> завершающийся: 18.2, 18.18
    Ждущий -> активный: прибытие события (разрешённого маской событий потока)
    7. Планировщик циклически выделяет процессорное время всем активным потокам. Без всяких дополнительных ухищрений.
    8. Функция 9 - информация о потоке (по номеру слота). Функции 18.2 и 18.18 - прибить поток (первая принимает номер слота, вторая - идентификатор). Функция 18.21 - получить номер слота по идентификатору. Функция 51 -создать поток. Функция 69 (целиком) - отладка. Функция 70.7 - запуск программы. Функция -1 - завершение потока.
  • Управление вводом-выводом: железо с точки зрения ядра
    Устройства бывают разные. Бывают стандартные устройства, которые понимает система. Система самостоятельно работает с таймером, мышью, клавиатурой, видеокартой, аудио, системным динамиком, сетевыми картами, CD/DVD, жёсткими дисками, не давая приложениям доступа к этим устройствам напрямую.

    Для получения данных мыши есть специальная сисфункция 37 (подфункции 0,1,2,7 - получить информацию о разных аспектах происходящего с мышью) и специальное событие - при любом дёргании мыши система извещает всех подряд, что с мышью что-то произошло (по умолчанию поток не реагирует на события мыши, а должен явно установить маску учитываемых событий, разрешающую событие мыши). Приложение может управлять формой курсора мыши (сисфункция 37, подфункции 4,5,6) для своего окна (когда курсор проходит над окном потока, он принимает заданную форму; физически хэндл курсора для окна хранится в структуре для потока, но логически это скорее атрибут окна, поэтому я его не указал в предыдущем посте). Приложение может управлять настройками движения мыши, может переместить курсор в нужную позицию, может симулировать нужное состояние клавиш мыши - всё это функцией 18.19 (есть ещё хронологически более старая функция 18.15: поместить курсор в центр экрана). Кроме того, приложение может определить некоторое количество кнопок (кнопки реализованы в ядре). Кнопка - прямоугольная область в окне (ядро обычно рисует их самостоятельно, но приложение может попросить ядро не делать этого), которой приписан (приложением) некоторый идентификатор; при нажатии на кнопку мышью ядро посылает потоку-владельцу окна событие о нажатии кнопки.

    Внутренне в системе происходит следующее. Есть поддержка COM-мышей и обычных PS/2, и то, и другое вынесено в драйвера (commouse.obj, ps2mouse.obj соответственно). Ядро экспортирует для драйверов функцию SetMouseData (это имя для драйверов; реализована в [hid/mousedrv.inc, set_mouse_data]; драйвер мыши при поступлении очередного события вызывает эту функцию с нужными аргументами, сообщая ядру, что именно произошло с мышью. Ядро преобразует данные о движении мыши в перемещения курсора в соответствии с настройками (18.19), обновляет свои переменные (из которых впоследствии берёт информацию для 37) и устанавливает флаг активности мыши [mouse_active]; когда главный цикл системы получит управление, он проверит этот флаг и известит все приложения, что что-то произошло с мышью. Работа идёт по схеме (мышь) <-> (драйвер) <-> (ядро) <-> (приложения); PS/2-драйвер предоставляет определённые API приложению напрямую (версия драйвера и тип мыши), но их никто не использует.

    Работа с клавиатурой. Здесь полезны комментарии к функции 2 из документации. У приложения есть два режима получения данных о нажатых клавиш: ASCII и сканкоды, переключение - функция 66. Судьба нажатой клавиши зависит от следующих вещей:
    - является ли эта клавиша модификатором (Alt/Shift/Ctrl/*Lock) или нет;
    - в каком режиме находится активное окно (ASCII/сканкоды);
    - было ли установлено соответствующее сочетание клавиш как горячая комбинация для захвата каким-то другим приложением.
    Обработчик клавиатуры [hid/keyboard.inc, irq1] обновляет состояние клавиатуры (Alt/Shift/Ctrl/*Lock) для клавиш-модификаторов (и переключает огоньки на клаве при нажатии *Lock); проверяет, не нажато ли Ctrl+Alt+Del, и если да, то устанавливает соответствующий флаг, который будет проверен главным циклом системы, когда тот получит управление (что приведёт к запуску приложения /sys/cpu); сканирует список установленных горячих комбинаций и, если такая комбинация зарегистрирована, посылает событие клавиатуры зарегистрированному приложению (пример: @panel регистрирует нажатие на клавишу Win для вызова меню и комбинации типа Alt+F4, Alt+Tab, Alt+Shift+Tab и Ctrl+Shift - полный список есть в hot_keys.txt из дистрибутива, API здесь - та же функция 66), а если нет, то кладёт её в буфер нажатых клавиш для активного окна (есть такой системный массив на 120 байт), что активирует событие клавиатуры для потока-владельца активного окна. Как именно кладёт, зависит от режима: в сканкодном просто кладёт сканкод, полученный от клавиатуры, а в ASCII-режиме клавиши-модификаторы и события об отпускании клавиш просто игнорирует, а нормальные клавиши транслирует в ASCII-коды с помощью таблиц преобразования. Таблицы для каждого языка свои, переключение языка заключается в установке правильной таблицы (это делает приложение @panel), API - 21.2, 26.2. Клавишам F1-F12 тоже соответствуют определённые коды, которые совпадают с нормальными клавишами (например, F1='2',F5='6'), и в результате приложения, работающие в этом режиме (например, sysxtree, eolite, mtdbg), не могут отличить кнопку F5 и цифру 6.

    На уровне системы: поддержка клавиатуры зашита в ядре (уже упоминавшийся обработчик irq1 из hid/keyboard.inc). Схема обработки: (клавиатура) <-> (ядро) <-> (приложения).

    Работа с видеокартой - это, собственно, GUI, на тему которого можно говорить отдельно довольно долго. Ограничиваясь работой с железом:
    - вывод на экран осуществляет ядро;
    - для видеокарт от ATI есть специальный драйвер, вспомогательный для ядра (если он есть и при загрузке сообщил, что хочет работать, то ядро будет иногда прибегать к его услугам), поддерживающий аппаратный курсор и вроде в последних версиях на каком-то уровне аппаратное ускорение (через API для приложений);
    - поддерживаются стандартные видеорежимы EGA/CGA и VGA и видеорежимы, возвращаемые VESA BIOS. Установка видеорежима осуществляется средствами BIOS при загрузке ещё в реальном режиме процессора. Для режимов VESA2 работа идёт через framebuffer, и у приложений есть прямой доступ к нему как на чтение, так и на запись. Подробнее - описание функции 61;
    - для решения "проблемы 60 Гц" (при установке разрешения через BIOS ставится стандартная частота развёртки 60 Гц, которая на не LCD-мониторах режет глаз) есть трюк с VRR, Virtual Refresh Rate, - манипуляция регистрами CRT, в результате которых повышается частота развёртки за счёт снижения разрешения; манипуляции осуществляет "драйвер" vmode.mdr, предоставляющий приложениям API через 21.13 (используется приложениями vrr и vrr_m; если в загрузочном экране включена опция "использовать VRR", то vrr_m - первое загружаемое приложение, оно даёт нужные команды драйверу и продолжает процесс, загружая launcher; vrr - отдельное большое приложение). "Драйвер" в кавычках, потому что реально его сложно назвать драйвером - просто бинарный файл, который нужно загрузить по фиксированному адресу и явно передавать туда управление из функции 21.13);
    - здесь схема работы в типичном случае выглядит так:

    Code: Select all

    (видеокарта) <-> (ядро) <-> (приложения),
             \          /
              (драйвер)
    
    в менее типичных случаях приложения могут обращаться напрямую к драйверу и/или framebuffer'у видеокарты.

    Поддержка аудио есть для SB-совместимых карт и для AC97-кодеков на определённом железе, здесь ядро уже не принимает прямого участия, а приложение общается напрямую с соответствующим драйвером (infinity.obj, в свою очередь опирающийся на драйвер sound.obj/intel_hda.obj, какой именно, зависит от железа). Драйвер предоставляет соответствующие API.

    Системным динамиком приложение может попищать с помощью функции 55.55, но только если это разрешено в настройках (есть соответствующий пункт в меню рабочего стола -> "Настройка устройств"), если включено, можно понаслаждаться приложением MidAmp. Данные для функции 55.55 - это ноты в определённом формате (описанном в документации), ядро пересчитывает их в нужную последовательность частот с задержками и на основании результатов вычислений пишет нужные значения в третий канал таймера - порты 42h/43h (и 61h для включения/выключения динамика), код в [sound/playnote.inc, playNote].

    Системным таймером управляет исключительно ядро. При загрузке система программирует таймер на срабатывание 100 раз в секунду. Обработчик прерывания от таймера, [core/sched.inc, irq0], делает следующее:
    - увеличивает текущее время (число сотых долей секунды, прошедших с загрузки системы, может быть получено в приложении функцией 26.9, много где используется внутри ядра)
    - вызывает процедуру обработки текущей ноты для писка, описанного в предыдущем абзаце;
    - каждую 100-ю итерацию (каждую секунду) обнуляет счётчик "тактов в предыдущую секунду" (поле в структуре потока) у всех потоков;
    - служит планировщиком, переключаясь на следующую задачу; алгоритм выбора описан в предыдущем посте, а при переключении увеличивается счётчик тактов у текущего потока (от которого управление уходит) и заполняются системные структуры - kernel-mode стек, карта разрешения ввода/вывода, page table (cr3), отладочные регистры drN (если нужно) и устанавливает бит TS в cr0. (Регистры CPU хранятся в системном стеке, так что popa после переключения стека автоматически восстановит регистры задачи, которая стала текущей.) Последнее действие нужно для "ленивой выгрузки" контекста FPU/MMX/SSE: если этот контекст переключать сразу, это займёт какое-то время, при том, что новая задача, возможно, вообще не использует ничего, кроме CPU; поэтому эти регистры остаются на своих местах, но устанавливается флаг TaskSwitch, в результате чего при следующем обращении к регистрам (именно обращении! когда нужно действительно переключать весь контекст) процессор возбудит исключение, обработчик которого молча сохранит регистры ушедшего потока, загрузит регистры нового потока и перезапустит инструкцию, сделав вид, что ничего не случилось.

    Работа с сетевыми картами зашита в ядро (папка network, работа с железом сидит в network/eth_drv/drivers). API для работы с сетью - сисфункции 52 и 53, но работа с сетью - это отдельный разговор.

    CD/DVD бывают с данными и музыкальные. Музыкальные CD при определённых условиях может проигрывать сам привод, для поддержки этого есть сисфункция 24 (подфункции 1,2,3), поддержка зашита в ядро [blkdev/cdrom.inc, sys_cd_audio]. Чтение данных с CD/DVD, равно как и работа с жёсткими дисками и дискетами, относится скорее к области файловой системы ("железная" часть, впрочем, как и всё остальное, зашита в ядро: blkdev/cd_drv.inc для работы с CD/DVD, blkdev/flp_drv.inc для работы с дискетами, blkdev/hd_drv.inc для работы с жёсткими дисками (собственными силами обрабатывает ATA, обращается к BIOS для поддержки BIOS-дисков через V86 из core/v86.inc).
    Last edited by diamond on Wed Nov 26, 2008 12:55 pm, edited 1 time in total.
  • Управление вводом-выводом: железо с точки зрения приложений
    Но бывают устройства, о которых система ничего не знает. Тут появляются два варианта работы с ними.
    A. Напрямую из приложений.
    Ядро предоставляет API, позволяющие приложениям самостоятельно обрабатывать железо напрямую. Для общения с железом нужно:
    1. иметь возможность доступа к соответствующим портам ввода/вывода
    и/или
    2. иметь возможность доступа к пространству PCI
    и/или
    3. иметь возможность доступа к нужной области физической памяти
    и/или
    4. обрабатывать IRQ от устройства.

    1. Приложение может попытаться зарезервировать диапазон портов, нужный для общения с устройством (сисфункция 46). Ядро хранит список уже зарезервированных портов (статический массив на 255 входов по адресу RESERVED_PORTS, каждый вход содержит TID владельца, начало и конец зарезервированной области) и не даст зарезервировать диапазон, перекрывающийся с чем-то уже занятым. Если же всё в порядке, то [kernel.asm, rpal2] ядро разрешает в карте разрешения ввода/вывода процессора для запрашивающего потока обращения к запрошенным портам (кстати, при создании потока его карта инициализируется общей разделяемой между всеми потоками, которая запрещена для записи; при попытке первой записи процессор бросает #PF, его обработчик отслеживает эту ситуацию, выделяет новую страницу для карты с разрешённой записью, записывает её в структуру потока и возвращает управление), а также добавляет диапазон в общий список зарезервированных. После этого приложение получает возможность из ring-3 обращаться напрямую к нужным портам. При загрузке ядро резервирует для себя некоторые системные порты. Функция 46 позволяет потоку также освободить ранее выделенные им же порты.
    2. Здесь ядро берёт на себя все операции, а приложениям выделяет сисфункцию 62. Которая в принципе может быть запрещена (функция 21.12), но по умолчанию разрешена.
    3. Такого приложение сделать не может.
    4. Для этого есть набор из трёх функций 42, 44, 45. Когда приходит IRQ, поведение системы зависит от того, какое именно IRQ пришло.
    - IRQ0, IRQ1, IRQ6, IRQ13, IRQ14, IRQ15 - специальные номера, их ядро обрабатывает самостоятельно (обработка IRQ0=таймера и IRQ1=клавиатуры описана в предыдущем посте, IRQ6 - дискета, IRQ14 и IRQ15 - жёсткие диски)
    - остальные IRQ разбиваются на две категории в зависимости от определения константы USE_COM_IRQ при компиляции ядра: если она ненулевая, то первая состоит из IRQ3 и IRQ4, иначе первая пуста; вторая состоит из всех остальных. IRQ из первой группы может перехватить приложение, IRQ из второй группы может перехватить драйвер (о драйверах - ниже). Перехват приложением заклю