Kernel/ru: Difference between revisions
Pathoswithin (talk | contribs) |
No edit summary |
||
(10 intermediate revisions by 3 users not shown) | |||
Line 1: | Line 1: | ||
== Введение == | |||
[[File:Kernel_includes.png|thumbnail|Рис.1 Граф исходников ядра на момент 06.03.2019]] | |||
На Рис.1 представлен граф исходников ядра, как видно, самом центре стоит '''kernel32.inc''' — этот, по сути заголовочный файл просто include’ит в себя почти все подсистемы ядра. | |||
Чуть правее и ниже расположен '''kernel.asm''' — это самый главный файл ядра. Он include’ит вышеупомянутый '''kernel32.inc''' и еще некоторые файлы. | |||
Таким образом, при компиляции '''kernel.asm''', компилятор FASM, выполнив все директивы '''include''', получит один большой-большой ассемблерный исходник, который и будет откомпилирован. | |||
В начале всех файлов ядра располагаются строки вида '''$Revision: 5363 $''' . Дело в том, что для всех файлов кода ядра в SVN репозитории установлено свойство '''svn:keywords''' со значением '''Rev'''. Это означает, что при коммите, SVN будет подставлять в файлы с таким свойством номер последней ревизии, в которой они были изменены. К примеру, если в файле написано '''$Revision: 7365 $''', это значит что последний раз он был изменен в ревизии 7365. Но, зачем же все это нужно? — спросите вы. Нужно, потому, что в '''macros.inc''' из исходников ядра, подключающемся первым, определён макрос fasm'а '''$Revision''', который вычисляет максимум из всех мест, где он встретился (то есть во всех файлах, прямо или косвенно подключаемым к kernel.asm). Этот самый максимум и является номером самой свежей ревизии ядра (не драйверов). В конце '''kernel.asm''' он присваивается переменной '''__REV__''', ну а '''boot/bootstr.inc''' включает значение '''__REV__''' в начальную выводимую строку на синем экране загрузки. | |||
В файле proc32.inc содержатся макросы для определения и вызова процедур. Это дает возможность использовать в коде такого вида конструкции: | |||
proc enable_irq stdcall, irq_line:dword | |||
… | |||
endp | |||
Также, широко используются макросы для определения структур и работы с ними. Файл '''struct.inc''' содержит очень удобный макрос '''struct''', позволяющий писать например так: | |||
struct MUTEX | |||
wait_list LHEAD | |||
count dd ? | |||
ends | |||
Рассмотрим файл '''kglobals.inc'''. На первый взгляд вовсе не очевидно, что же делают определенные в нем макросы '''iglobal''' и '''uglobal'''. Их используют при объявления данных в некоторых участках кода ядра. Но зачем же они нужны? Задача этих макросов собрать данные, объявленные с помощью них, в конец компилируемого файла, чтобы добиться уменьшения размера файла (за счет отсутствия лишних выравниваний на границу align 4) и возможно лучшего сжатия, процедурой упаковки ядра. Данные, обозначенные макросом '''iglobal''' являются инициализированными и уже заранее содержат нужное значение. Также есть макрос '''uglobal''', который также собирает данные в конец файла, но помещает их за пределами получаемого бинарника - это не инициализированные данные и содержимое их не гарантируется, хотя обычно содержит ноль. Цель макросов '''iglobal''' и '''uglobal''' — не столько уменьшение размера файла, хотя и это тоже присутствует, сколько ускорение, процессору существенно лучше, когда код и данные разделены. Данные, размещённые в '''uglobal''', сами по себе действительно неинициализированы, но конкретно в случае ядра Колибри при загрузке выполняется код, который область '''uglobal''' явным образом обнуляет. | |||
В файле '''fdo.inc''' содержится набор макросов для повышения удобства отладки ядра. Кстати, fdo это сокращение от '''Formatted Debug Output''' (Форматированный отладочный вывод). Работает он подобно функции printf(), выводя на доску отладки форматированные сообщения. Доступны спецификаторы формата %s, %d, %u, %x . Использовать обычно нужно только макрос '''DEBUGF''', хотя доступны также и '''DEBUGS''', '''DEBUGD''', '''DEBUGH'''. Например так: | |||
DEBUGF 1, "%s - %d (%x)", eax, 123, ch | |||
Первый аргумент (здесь единичка) — уровень отладочного сообщения. Отладочное сообщение выведется, если его уровень >= '''__DEBUG_LEVEL__''' и отладка включена т.е '''__DEBUG__''' = 1 . Константы '''__DEBUG__''' и '''__DEBUG_LEVEL__''' объявляются в программе заранее. | |||
Темы на форуме про FDO: http://board.kolibrios.org/viewtopic.php?f=32&t=602&p=7841 и https://board.kolibrios.org/viewtopic.php?f=32&t=3454 | |||
== Управление процессами. == | == Управление процессами. == | ||
1. Понятие процесса как такового в Колибри очень зачаточное: процесс - объединение потоков с одним и тем же адресным пространством. У всех таких объединяемых потоков одно и то же имя | 1. Понятие процесса как такового в Колибри очень зачаточное: процесс - объединение потоков с одним и тем же адресным пространством. У всех таких объединяемых потоков одно и то же имя, один и тот же размер используемой памяти, одни и те же адреса загруженных библиотек.<br /> | ||
Потоки, впрочем, существуют и обладают следующими характеристиками ( | Потоки, впрочем, существуют и обладают следующими характеристиками (структура APPDATA в const.inc из исходников ядра): | ||
* идентификатор (TID), каждому создаваемому потоку назначается уникальный идентификатор; | * идентификатор (TID), каждому создаваемому потоку назначается уникальный 32 битный идентификатор; | ||
* состояние потока: активен (выполняется прямо сейчас либо ждёт переключения задач на него), заморожен, завершается, ждёт; | * состояние потока: активен (выполняется прямо сейчас либо ждёт переключения задач на него), заморожен, завершается, ждёт; | ||
* окно: каждый поток имеет ровно одно окно, которое может быть невидимым, но обязательно существует; | * окно: каждый поток имеет ровно одно окно, которое может быть невидимым, но обязательно существует. Оно определено в отдельным массивом, но два этих массива можно рассматривать как один; | ||
* использование процессора: число тактов за последнюю секунду, которое процессор потратил на выполнение именно этого потока; | * использование процессора: число тактов за последнюю секунду, которое процессор потратил на выполнение именно этого потока; | ||
* имя процесса ( | * имя процесса (11 символов от имени исполняемого файла); | ||
* маска событий, о которых система извещает поток; | * маска событий, о которых система извещает поток; | ||
* системный стек; | * системный стек; | ||
* список объектов ядра, ассоциированных с этим потоком; | * список объектов ядра, ассоциированных с этим потоком; | ||
* карта разрешённых портов ввода/вывода; | * карта разрешённых портов ввода/вывода(8 байт, APPDATA.io_map); | ||
* текущая папка для функций файловой системы; | * текущая папка для функций файловой системы(APPDATA.cur_dir); | ||
* буфер для сообщений IPC (присутствует, только если поток его явно определил);<br /> | * буфер для сообщений IPC (присутствует, только если поток его явно определил);<br /> | ||
2. Процесс | * указатель на курсор | ||
* APPDATA.tls_base | |||
2. Процесс идентифицируется структурой PROC из const.inc; информация о потоках внутри ОС собрана в статический массив на 255 входов (нумеруемых от 0 до 255, причём 0-й слот не может использоваться, так что всего в системе может быть не более 255 потоков).<br /> | |||
Некоторые системные функции принимают номер слота, некоторые - идентификатор.<br /> | Некоторые системные функции принимают номер слота, некоторые - идентификатор.<br /> | ||
3. Создание ''нового процесса'' отличается от создания потока (пожалуй, это единственное место в API, где такое отличие есть). Создание процесса: [core/taskman.inc, fs_execute] принимает на вход имя бинарного файла для загрузки, параметры командной строки для нового процесса и флаги, сейчас только то, запускается процесс как отлаживаемый или как обычный. | 3. Создание ''нового процесса'' отличается от создания потока (пожалуй, это единственное место в API, где такое отличие есть). Создание процесса: [core/taskman.inc, fs_execute] принимает на вход имя бинарного файла для загрузки, параметры командной строки для нового процесса и флаги, сейчас только то, запускается процесс как отлаживаемый или как обычный. | ||
* загружает бинарник (целиком в память ядра; если он упакован kpack'ом, то распаковывается в памяти); | * загружает бинарник (целиком в память ядра; если он упакован kpack'ом, то распаковывается в памяти); | ||
* проверяет заголовок исполняемого файла, вычисляются нужные параметры (есть | * проверяет заголовок исполняемого файла, вычисляются нужные параметры (есть три версии заголовка, немного отличающиеся); | ||
* захватывает мьютекс | * захватывает мьютекс application_table_mutex, управляющий доступом на запись к таблице потоков (вышеупомянутому массиву) | ||
* находит пустой слот для нового потока; если такого нет (255 потоков уже запущены) - выход с ошибкой; | * находит пустой слот для нового потока; если такого нет (255 потоков уже запущены) - выход с ошибкой; | ||
* очищает полученную структуру APPDATA | |||
* заполняет имя процесса; | * заполняет имя процесса; | ||
* вызов функции create_process, возвращая указатель на PROC | |||
* в PROC.hdr_emem записывается используемая память из заголовка исполняемого файла | |||
* Если указатель не нулевой, то добавляем полученную структуру PROC в общий список структур | |||
* записываем в APPDATA.process указатель на структуру PROC | |||
* структура APPDATA добавляется в список PROC.thr_list | |||
* создаёт новое адресное пространство (это отдельная история); | * создаёт новое адресное пространство (это отдельная история); | ||
* вызывает функцию set_app_params, заполняющую остальные поля структуры потока (подробнее - ниже); | * вызывает функцию set_app_params, заполняющую остальные поля структуры потока (подробнее - ниже); | ||
* освобождает мьютекс | * освобождает мьютекс application_table_mutex.<br /> | ||
Создание ''нового потока'': [core/taskman.inc, new_sys_threads] принимает на вход entry point нового процесса и указатель на user-mode стек. | Создание ''нового потока'': [core/taskman.inc, new_sys_threads] принимает на вход entry point нового процесса и указатель на user-mode стек. | ||
* захватывает application_table_status | * захватывает application_table_status | ||
* находит пустой слот для нового потока; если такого нет - выход с ошибкой; | * находит пустой слот для нового потока; если такого нет - выход с ошибкой; | ||
* очищает полученную структуру | |||
* копирует имя процесса и информацию об адресном пространстве вызывающего потока в структуру для нового; | * копирует имя процесса и информацию об адресном пространстве вызывающего потока в структуру для нового; | ||
* копирует APPDATA.tls_base если его нет, то выделяет 4096 байт для неё и записывает в APPDATA.tls_base нового потока | |||
* копирует APPDATA.process | |||
* добавляет этот процесс в PROC.thr_list | |||
* вызывает set_app_params | * вызывает set_app_params | ||
* освобождает application_table_status<br /> | * освобождает application_table_status<br /> | ||
Line 37: | Line 76: | ||
* выделяет очередной идентификатор (каждый следующий TID равен предыдущему + 1); | * выделяет очередной идентификатор (каждый следующий TID равен предыдущему + 1); | ||
* инициализирует user-mode контекст, значения eip и esp берутся из параметров вызова для sys_new_threads и из заголовка для fs_execute; | * инициализирует user-mode контекст, значения eip и esp берутся из параметров вызова для sys_new_threads и из заголовка для fs_execute; | ||
* инициализация структуры WDATA (смотреть const.inc) | |||
* в зависимости от флагов устанавливает регистр CS и приоритет | |||
* если новый процесс загружается как отлаживаемый, то помечает его состояние как замороженное, иначе - как работающее (начиная с этого места на новый поток возможны переключения задач).<br /> | * если новый процесс загружается как отлаживаемый, то помечает его состояние как замороженное, иначе - как работающее (начиная с этого места на новый поток возможны переключения задач).<br /> | ||
Завершение процесса: [core/sys32.inc, terminate] когда системный поток получает управление (главный цикл системы), одним из его действий является проход по списку процессов, поиск потоков в завершающемся состоянии и убийство таких процессов. Все нижеследующие действия происходят в контексте системного потока. | Завершение процесса: [core/sys32.inc, terminate] когда системный поток получает управление (главный цикл системы), одним из его действий является проход по списку процессов, поиск потоков в завершающемся состоянии и убийство таких процессов. Все нижеследующие действия происходят в контексте системного потока. | ||
Line 48: | Line 89: | ||
* если окно потока было на вершине оконного стека, активирует следующее окно | * если окно потока было на вершине оконного стека, активирует следующее окно | ||
* если поток рухнул (или был прибит) в процессе работы с жёстким диском, освобождает мьютекс занятости жёсткого диска; то же самое для CD и дискеты | * если поток рухнул (или был прибит) в процессе работы с жёстким диском, освобождает мьютекс занятости жёсткого диска; то же самое для CD и дискеты | ||
* освобождает выделенные потоком | * освобождает выделенные потоком порты | ||
* если текущий прибиваемый процесс - отладчик, помечает как завершающиеся все отлаживаемые им процессы | * если текущий прибиваемый процесс - отладчик, помечает как завершающиеся все отлаживаемые им процессы | ||
* перерисовывает экран | * перерисовывает экран | ||
* освобождает application_table_status<br /> | * освобождает application_table_status<br /> | ||
4. Взаимодействия процессов практически нет - есть только специальная системная функция для передачи данных от процесса-источника процессу-приёмнику, причём приёмник должен заранее подготовить буфер и ожидать этих данных, и некоторые возможности по отладке приложений.<br /> <br /> | 4. Взаимодействия процессов практически нет - есть только специальная системная функция для передачи данных от процесса-источника процессу-приёмнику, причём приёмник должен заранее подготовить буфер и ожидать этих данных, и некоторые возможности по отладке приложений.<br /> <br /> | ||
5. Соответственно синхронизации тоже практически нет - один процесс может только проверить, завершился ли другой.<br /> <br /> | 5. Соответственно синхронизации тоже практически нет - один процесс может только проверить, завершился ли другой, но для потоков одного ядра есть фьютексы.<br /> <br /> | ||
6. Состояния перечислены выше.<br /> | 6. Состояния перечислены выше.<br /> | ||
* Активный/ждущий -> замороженный: вызов (кем-то) функции заморозки 69.4<br /> | * Активный/ждущий -> замороженный: вызов (кем-то) функции заморозки 69.4<br /> | ||
Line 68: | Line 109: | ||
* Функция 69 (целиком) - отладка. | * Функция 69 (целиком) - отладка. | ||
* Функция 70.7 - запуск программы. | * Функция 70.7 - запуск программы. | ||
* Функция 77 - posix подсистема ядра | |||
* Функция -1 - завершение потока.<br /> <br /> | * Функция -1 - завершение потока.<br /> <br /> | ||
Line 76: | Line 118: | ||
Для получения данных мыши есть специальная сисфункция 37 (подфункции 0,1,2,3,7 - получить информацию о разных аспектах происходящего с мышью) и специальное событие - при любом дёргании мыши система извещает всех подряд, что с мышью что-то произошло (по умолчанию поток не реагирует на события мыши, а должен явно установить маску учитываемых событий, разрешающую событие мыши). Приложение может управлять формой курсора мыши (сисфункция 37, подфункции 4,5,6) для своего окна (когда курсор проходит над окном потока, он принимает заданную форму; физически хэндл курсора для окна хранится в структуре для потока, но логически это скорее атрибут окна, поэтому я его не указал в предыдущем посте). Приложение может управлять настройками движения мыши, может переместить курсор в нужную позицию, может симулировать нужное состояние клавиш мыши - всё это функцией 18.19 (есть ещё хронологически более старая функция 18.15: поместить курсор в центр экрана). Кроме того, приложение может определить некоторое количество кнопок (кнопки реализованы в ядре). Кнопка - прямоугольная область в окне (ядро обычно рисует их самостоятельно, но приложение может попросить ядро не делать этого), которой приписан (приложением) некоторый идентификатор; при нажатии на кнопку мышью ядро посылает потоку-владельцу окна событие о нажатии кнопки.<br /> <br /> | Для получения данных мыши есть специальная сисфункция 37 (подфункции 0,1,2,3,7 - получить информацию о разных аспектах происходящего с мышью) и специальное событие - при любом дёргании мыши система извещает всех подряд, что с мышью что-то произошло (по умолчанию поток не реагирует на события мыши, а должен явно установить маску учитываемых событий, разрешающую событие мыши). Приложение может управлять формой курсора мыши (сисфункция 37, подфункции 4,5,6) для своего окна (когда курсор проходит над окном потока, он принимает заданную форму; физически хэндл курсора для окна хранится в структуре для потока, но логически это скорее атрибут окна, поэтому я его не указал в предыдущем посте). Приложение может управлять настройками движения мыши, может переместить курсор в нужную позицию, может симулировать нужное состояние клавиш мыши - всё это функцией 18.19 (есть ещё хронологически более старая функция 18.15: поместить курсор в центр экрана). Кроме того, приложение может определить некоторое количество кнопок (кнопки реализованы в ядре). Кнопка - прямоугольная область в окне (ядро обычно рисует их самостоятельно, но приложение может попросить ядро не делать этого), которой приписан (приложением) некоторый идентификатор; при нажатии на кнопку мышью ядро посылает потоку-владельцу окна событие о нажатии кнопки.<br /> <br /> | ||
Внутренне в системе происходит следующее. Есть поддержка COM-мышей и обычных PS/2, и то, и другое вынесено в драйвера (commouse. | Внутренне в системе происходит следующее. Есть поддержка COM-мышей и обычных PS/2, и то, и другое вынесено в драйвера (commouse.sys, ps2mouse.sys соответственно). Ядро экспортирует для драйверов функцию SetMouseData (это имя для драйверов; реализована в [hid/mousedrv.inc, set_mouse_data]; драйвер мыши при поступлении очередного события вызывает эту функцию с нужными аргументами, сообщая ядру, что именно произошло с мышью. Ядро преобразует данные о движении мыши в перемещения курсора в соответствии с настройками (18.19), обновляет свои переменные (из которых впоследствии берёт информацию для 37) и устанавливает флаг активности мыши [mouse_active]; когда главный цикл системы получит управление, он проверит этот флаг и известит все приложения, что что-то произошло с мышью. Работа идёт по схеме (мышь) <-> (драйвер) <-> (ядро) <-> (приложения); PS/2-драйвер предоставляет определённые API приложению напрямую (версия драйвера и тип мыши), но их никто не использует.<br /> <br /> | ||
Работа с клавиатурой. Здесь полезны комментарии к функции 2 из документации. У приложения есть два режима получения данных о нажатых клавиш: ASCII и сканкоды, переключение - функция 66. Судьба нажатой клавиши зависит от следующих вещей:<br /> | Работа с клавиатурой. Здесь полезны комментарии к функции 2 из документации. У приложения есть два режима получения данных о нажатых клавиш: ASCII и сканкоды, переключение - функция 66. Судьба нажатой клавиши зависит от следующих вещей:<br /> | ||
Line 83: | Line 125: | ||
* было ли установлено соответствующее сочетание клавиш как горячая комбинация для захвата каким-то другим приложением.<br /> <br /> | * было ли установлено соответствующее сочетание клавиш как горячая комбинация для захвата каким-то другим приложением.<br /> <br /> | ||
Обработчик клавиатуры [hid/keyboard.inc, irq1] обновляет состояние клавиатуры (Alt/Shift/Ctrl/*Lock) для клавиш-модификаторов (и переключает огоньки на клаве при нажатии *Lock); проверяет, не нажато ли Ctrl+Alt+Del, и если да, то устанавливает соответствующий флаг, который будет проверен главным циклом системы, когда тот получит управление (что приведёт к запуску приложения /sys/cpu); сканирует список установленных горячих комбинаций и, если такая комбинация зарегистрирована, посылает событие клавиатуры зарегистрированному приложению (пример: @ | Обработчик клавиатуры [hid/keyboard.inc, irq1] обновляет состояние клавиатуры (Alt/Shift/Ctrl/*Lock) для клавиш-модификаторов (и переключает огоньки на клаве при нажатии *Lock); проверяет, не нажато ли Ctrl+Alt+Del, и если да, то устанавливает соответствующий флаг, который будет проверен главным циклом системы, когда тот получит управление (что приведёт к запуску приложения /sys/cpu); сканирует список установленных горячих комбинаций и, если такая комбинация зарегистрирована, посылает событие клавиатуры зарегистрированному приложению (пример: @taskbar регистрирует нажатие на клавишу Win для вызова меню и комбинации типа Alt+F4, Alt+Tab, Alt+Shift+Tab и Ctrl+Shift - полный список есть в hot_keys.txt из дистрибутива, API здесь - та же функция 66), а если нет, то кладёт её в буфер нажатых клавиш для активного окна (есть такой системный массив на 120 байт), что активирует событие клавиатуры для потока-владельца активного окна. Как именно кладёт, зависит от режима: в сканкодном просто кладёт сканкод, полученный от клавиатуры, а в ASCII-режиме клавиши-модификаторы и события об отпускании клавиш просто игнорирует, а нормальные клавиши транслирует в ASCII-коды с помощью таблиц преобразования. Таблицы для каждого языка свои, переключение языка заключается в установке правильной таблицы (это делает приложение @taskbar), API - 21.2, 26.2. Клавишам F1-F12 тоже соответствуют определённые коды, которые совпадают с нормальными клавишами (например, F1='2',F5='6'), и в результате приложения, работающие в этом режиме (например, sysxtree, eolite, mtdbg), не могут отличить кнопку F5 и цифру 6.<br /> <br /> | ||
На уровне системы: поддержка клавиатуры зашита в ядре (уже упоминавшийся обработчик irq1 из hid/keyboard.inc). Схема обработки: | На уровне системы: поддержка клавиатуры зашита в ядре (уже упоминавшийся обработчик irq1 из hid/keyboard.inc), но ядро так же экспортирует функции SetKeyboardData, RegKeyboard, DelKeyboard . Схема обработки: | ||
(клавиатура) <-> (ядро) <-> (приложения). | (клавиатура) <-> (ядро) <-> (приложения). | ||
Работа с видеокартой - это, собственно, GUI, на тему которого можно говорить отдельно довольно долго. Ограничиваясь работой с железом:<br /> | Работа с видеокартой - это, собственно, GUI, на тему которого можно говорить отдельно довольно долго. Ограничиваясь работой с железом:<br /> | ||
* вывод на экран осуществляет ядро; | * вывод на экран осуществляет ядро; | ||
* для видеокарт от ATI есть специальный драйвер, вспомогательный для ядра (если он есть и при загрузке сообщил, что хочет работать, то ядро будет иногда прибегать к его услугам), поддерживающий аппаратный курсор и вроде в последних версиях на каком-то уровне аппаратное ускорение (через API для приложений); | * для видеокарт от ATI и Intel есть специальный драйвер, вспомогательный для ядра (если он есть и при загрузке сообщил, что хочет работать, то ядро будет иногда прибегать к его услугам), поддерживающий аппаратный курсор и вроде в последних версиях на каком-то уровне аппаратное ускорение (через API для приложений); | ||
* поддерживаются стандартные видеорежимы EGA/CGA и VGA и видеорежимы, возвращаемые VESA BIOS. Установка видеорежима осуществляется средствами BIOS при загрузке ещё в реальном режиме процессора. Для режимов VESA2 работа идёт через framebuffer, и у приложений есть прямой доступ к нему как на чтение, так и на запись. Подробнее - описание функции 61;<br /> | * поддерживаются стандартные видеорежимы EGA/CGA и VGA и видеорежимы, возвращаемые VESA BIOS. Установка видеорежима осуществляется средствами BIOS при загрузке ещё в реальном режиме процессора. Для режимов VESA2 работа идёт через framebuffer, и у приложений есть прямой доступ к нему как на чтение, так и на запись. Подробнее - описание функции 61;<br /> | ||
Здесь схема работы в типичном случае выглядит так: | Здесь схема работы в типичном случае выглядит так: | ||
Line 100: | Line 142: | ||
В менее типичных случаях приложения могут обращаться напрямую к драйверу и/или framebuffer'у видеокарты.<br /> <br /> | В менее типичных случаях приложения могут обращаться напрямую к драйверу и/или framebuffer'у видеокарты.<br /> <br /> | ||
Поддержка аудио есть для SB-совместимых карт и для AC97-кодеков на определённом железе, здесь ядро уже не принимает прямого участия, а приложение общается напрямую с соответствующим драйвером (infinity. | Поддержка аудио есть для SB-совместимых карт, HDA-совместимых карт и для AC97-кодеков на определённом железе, здесь ядро уже не принимает прямого участия, а приложение общается напрямую с соответствующим драйвером (infinity.sys, в свою очередь опирающийся на драйвер sound.sys/sis.sys, какой именно, зависит от железа). Драйвер предоставляет соответствующие API.<br /> <br /> | ||
Системным динамиком приложение может попищать с помощью функции 55.55, но только если это разрешено в настройках (с точки зрения пользователя: есть иконка в "трее" - правой части @ | Системным динамиком приложение может попищать с помощью функции 55.55, но только если это разрешено в настройках (с точки зрения пользователя: есть иконка в "трее" - правой части @taskbar, включающей этот режим, по умолчанию писк запрещён; если включить, можно понаслаждаться писком при открытии меню и запуска программ по иконкам и через трей). Данные для функции 55.55 - это ноты в определённом формате (описанном в документации), ядро пересчитывает их в нужную последовательность частот с задержками и на основании результатов вычислений пишет нужные значения в третий канал таймера - порты 42h/43h (и 61h для включения/выключения динамика), код в [sound/playnote.inc, playNote].<br /> <br /> | ||
Системным таймером управляет исключительно ядро. При загрузке система программирует таймер на срабатывание 100 раз в секунду. Обработчик прерывания от таймера, [core/sched.inc, irq0], делает следующее:<br /> | Системным таймером управляет исключительно ядро. При загрузке система программирует таймер на срабатывание 100 раз в секунду. Обработчик прерывания от таймера, [core/sched.inc, irq0], делает следующее:<br /> | ||
Line 110: | Line 152: | ||
* служит планировщиком, переключаясь на следующую задачу; алгоритм выбора описан в предыдущем посте, а при переключении увеличивается счётчик тактов у текущего потока (от которого управление уходит) и заполняются системные структуры - kernel-mode стек, карта разрешения ввода/вывода, page table (cr3), отладочные регистры drN (если нужно) и устанавливает бит TS в cr0. (Регистры CPU хранятся в системном стеке, так что popa после переключения стека автоматически восстановит регистры задачи, которая стала текущей.) Последнее действие нужно для "ленивой выгрузки" контекста FPU/MMX/SSE: если этот контекст переключать сразу, это займёт какое-то время, при том, что новая задача, возможно, вообще не использует ничего, кроме CPU; поэтому эти регистры остаются на своих местах, но устанавливается флаг TaskSwitch, в результате чего при следующем обращении к регистрам (именно обращении! когда нужно действительно переключать весь контекст) процессор возбудит исключение, обработчик которого молча сохранит регистры ушедшего потока, загрузит регистры нового потока и перезапустит инструкцию, сделав вид, что ничего не случилось.<br /> <br /> | * служит планировщиком, переключаясь на следующую задачу; алгоритм выбора описан в предыдущем посте, а при переключении увеличивается счётчик тактов у текущего потока (от которого управление уходит) и заполняются системные структуры - kernel-mode стек, карта разрешения ввода/вывода, page table (cr3), отладочные регистры drN (если нужно) и устанавливает бит TS в cr0. (Регистры CPU хранятся в системном стеке, так что popa после переключения стека автоматически восстановит регистры задачи, которая стала текущей.) Последнее действие нужно для "ленивой выгрузки" контекста FPU/MMX/SSE: если этот контекст переключать сразу, это займёт какое-то время, при том, что новая задача, возможно, вообще не использует ничего, кроме CPU; поэтому эти регистры остаются на своих местах, но устанавливается флаг TaskSwitch, в результате чего при следующем обращении к регистрам (именно обращении! когда нужно действительно переключать весь контекст) процессор возбудит исключение, обработчик которого молча сохранит регистры ушедшего потока, загрузит регистры нового потока и перезапустит инструкцию, сделав вид, что ничего не случилось.<br /> <br /> | ||
Чтение данных с CD/DVD, равно как и работа с жёсткими дисками и дискетами, относится скорее к области файловой системы ("железная" часть, впрочем, как и всё остальное, зашита в ядро: blkdev/cd_drv.inc для работы с CD/DVD, blkdev/flp_drv.inc для работы с дискетами, blkdev/hd_drv.inc для работы с жёсткими дисками (собственными силами обрабатывает ATA, обращается к BIOS для поддержки BIOS-дисков через V86 из core/v86.inc).<br /> <br /> | Чтение данных с CD/DVD, равно как и работа с жёсткими дисками и дискетами, относится скорее к области файловой системы ("железная" часть, впрочем, как и всё остальное, зашита в ядро: blkdev/ahci.inc для работы с sata дисками, blkdev/cd_drv.inc для работы с CD/DVD, blkdev/flp_drv.inc для работы с дискетами, blkdev/hd_drv.inc для работы с жёсткими дисками (собственными силами обрабатывает ATA, обращается к BIOS для поддержки BIOS-дисков через V86 из core/v86.inc).<br /> <br /> | ||
== Управление вводом-выводом: железо с точки зрения приложений == | == Управление вводом-выводом: железо с точки зрения приложений == | ||
Line 138: | Line 180: | ||
Но что делать с нестандартными устройствами и "бездрайверным железом"? Колибри предоставляет пользовательскому приложению возможность отображения (''page mapping'') скрытых от него физических адресов MMIO на видимое ему "своё" адресное пространство, используя для этого подфункции 11-13 той же функции 62. Разработчик нового драйвера или инженер-электронщик, тестирующий новое устройство, должен заранее указать его PCI-адрес в системной константе mmio_pci_addr. По умолчанию эта константа в ядре не определена и пользовательский доступ к MMIO закрыт. <br /> | Но что делать с нестандартными устройствами и "бездрайверным железом"? Колибри предоставляет пользовательскому приложению возможность отображения (''page mapping'') скрытых от него физических адресов MMIO на видимое ему "своё" адресное пространство, используя для этого подфункции 11-13 той же функции 62. Разработчик нового драйвера или инженер-электронщик, тестирующий новое устройство, должен заранее указать его PCI-адрес в системной константе mmio_pci_addr. По умолчанию эта константа в ядре не определена и пользовательский доступ к MMIO закрыт. <br /> | ||
4. | 4. Обработка IRQ для приложений удалена из ядра.<br /> <br /> | ||
'''Вариант Б. Из соответствующего драйвера.'''<br /> <br /> | '''Вариант Б. Из соответствующего драйвера.'''<br /> <br /> | ||
Line 147: | Line 186: | ||
Драйвер выполняется в ring-0 и, как следствие, получает доступ ко всему, чему можно. В частности, с чтением/записью в порты никаких проблем не возникает. Кроме того, ядро предоставляет некоторые сервисы драйверам. В их числе: <br /> | Драйвер выполняется в ring-0 и, как следствие, получает доступ ко всему, чему можно. В частности, с чтением/записью в порты никаких проблем не возникает. Кроме того, ядро предоставляет некоторые сервисы драйверам. В их числе: <br /> | ||
* AttachIntHandler (и GetIntHandler) для перехвата IRQ | * AttachIntHandler (и GetIntHandler) для перехвата IRQ; при приходе такого IRQ ядро просто вызывает функцию драйвера, адрес которой драйвер указывает в AttachIntHandler | ||
* ReservePortArea - точный аналог (с точностью до сдвига регистров) функции 46; в принципе всё будет работать и без её вызова, но желательно всё-таки дать ядру знать об используемых портах | * ReservePortArea - точный аналог (с точностью до сдвига регистров) функции 46; в принципе всё будет работать и без её вызова, но желательно всё-таки дать ядру знать об используемых портах | ||
* PciApi - точный аналог (с точностью до сдвига регистров) функции 62 | * PciApi - точный аналог (с точностью до сдвига регистров) функции 62 |
Latest revision as of 19:37, 1 August 2022
Введение
На Рис.1 представлен граф исходников ядра, как видно, самом центре стоит kernel32.inc — этот, по сути заголовочный файл просто include’ит в себя почти все подсистемы ядра. Чуть правее и ниже расположен kernel.asm — это самый главный файл ядра. Он include’ит вышеупомянутый kernel32.inc и еще некоторые файлы. Таким образом, при компиляции kernel.asm, компилятор FASM, выполнив все директивы include, получит один большой-большой ассемблерный исходник, который и будет откомпилирован.
В начале всех файлов ядра располагаются строки вида $Revision: 5363 $ . Дело в том, что для всех файлов кода ядра в SVN репозитории установлено свойство svn:keywords со значением Rev. Это означает, что при коммите, SVN будет подставлять в файлы с таким свойством номер последней ревизии, в которой они были изменены. К примеру, если в файле написано $Revision: 7365 $, это значит что последний раз он был изменен в ревизии 7365. Но, зачем же все это нужно? — спросите вы. Нужно, потому, что в macros.inc из исходников ядра, подключающемся первым, определён макрос fasm'а $Revision, который вычисляет максимум из всех мест, где он встретился (то есть во всех файлах, прямо или косвенно подключаемым к kernel.asm). Этот самый максимум и является номером самой свежей ревизии ядра (не драйверов). В конце kernel.asm он присваивается переменной __REV__, ну а boot/bootstr.inc включает значение __REV__ в начальную выводимую строку на синем экране загрузки.
В файле proc32.inc содержатся макросы для определения и вызова процедур. Это дает возможность использовать в коде такого вида конструкции:
proc enable_irq stdcall, irq_line:dword … endp
Также, широко используются макросы для определения структур и работы с ними. Файл struct.inc содержит очень удобный макрос struct, позволяющий писать например так:
struct MUTEX wait_list LHEAD count dd ? ends
Рассмотрим файл kglobals.inc. На первый взгляд вовсе не очевидно, что же делают определенные в нем макросы iglobal и uglobal. Их используют при объявления данных в некоторых участках кода ядра. Но зачем же они нужны? Задача этих макросов собрать данные, объявленные с помощью них, в конец компилируемого файла, чтобы добиться уменьшения размера файла (за счет отсутствия лишних выравниваний на границу align 4) и возможно лучшего сжатия, процедурой упаковки ядра. Данные, обозначенные макросом iglobal являются инициализированными и уже заранее содержат нужное значение. Также есть макрос uglobal, который также собирает данные в конец файла, но помещает их за пределами получаемого бинарника - это не инициализированные данные и содержимое их не гарантируется, хотя обычно содержит ноль. Цель макросов iglobal и uglobal — не столько уменьшение размера файла, хотя и это тоже присутствует, сколько ускорение, процессору существенно лучше, когда код и данные разделены. Данные, размещённые в uglobal, сами по себе действительно неинициализированы, но конкретно в случае ядра Колибри при загрузке выполняется код, который область uglobal явным образом обнуляет.
В файле fdo.inc содержится набор макросов для повышения удобства отладки ядра. Кстати, fdo это сокращение от Formatted Debug Output (Форматированный отладочный вывод). Работает он подобно функции printf(), выводя на доску отладки форматированные сообщения. Доступны спецификаторы формата %s, %d, %u, %x . Использовать обычно нужно только макрос DEBUGF, хотя доступны также и DEBUGS, DEBUGD, DEBUGH. Например так:
DEBUGF 1, "%s - %d (%x)", eax, 123, ch
Первый аргумент (здесь единичка) — уровень отладочного сообщения. Отладочное сообщение выведется, если его уровень >= __DEBUG_LEVEL__ и отладка включена т.е __DEBUG__ = 1 . Константы __DEBUG__ и __DEBUG_LEVEL__ объявляются в программе заранее. Темы на форуме про FDO: http://board.kolibrios.org/viewtopic.php?f=32&t=602&p=7841 и https://board.kolibrios.org/viewtopic.php?f=32&t=3454
Управление процессами.
1. Понятие процесса как такового в Колибри очень зачаточное: процесс - объединение потоков с одним и тем же адресным пространством. У всех таких объединяемых потоков одно и то же имя, один и тот же размер используемой памяти, одни и те же адреса загруженных библиотек.
Потоки, впрочем, существуют и обладают следующими характеристиками (структура APPDATA в const.inc из исходников ядра):
- идентификатор (TID), каждому создаваемому потоку назначается уникальный 32 битный идентификатор;
- состояние потока: активен (выполняется прямо сейчас либо ждёт переключения задач на него), заморожен, завершается, ждёт;
- окно: каждый поток имеет ровно одно окно, которое может быть невидимым, но обязательно существует. Оно определено в отдельным массивом, но два этих массива можно рассматривать как один;
- использование процессора: число тактов за последнюю секунду, которое процессор потратил на выполнение именно этого потока;
- имя процесса (11 символов от имени исполняемого файла);
- маска событий, о которых система извещает поток;
- системный стек;
- список объектов ядра, ассоциированных с этим потоком;
- карта разрешённых портов ввода/вывода(8 байт, APPDATA.io_map);
- текущая папка для функций файловой системы(APPDATA.cur_dir);
- буфер для сообщений IPC (присутствует, только если поток его явно определил);
- указатель на курсор
- APPDATA.tls_base
2. Процесс идентифицируется структурой PROC из const.inc; информация о потоках внутри ОС собрана в статический массив на 255 входов (нумеруемых от 0 до 255, причём 0-й слот не может использоваться, так что всего в системе может быть не более 255 потоков).
Некоторые системные функции принимают номер слота, некоторые - идентификатор.
3. Создание нового процесса отличается от создания потока (пожалуй, это единственное место в API, где такое отличие есть). Создание процесса: [core/taskman.inc, fs_execute] принимает на вход имя бинарного файла для загрузки, параметры командной строки для нового процесса и флаги, сейчас только то, запускается процесс как отлаживаемый или как обычный.
- загружает бинарник (целиком в память ядра; если он упакован kpack'ом, то распаковывается в памяти);
- проверяет заголовок исполняемого файла, вычисляются нужные параметры (есть три версии заголовка, немного отличающиеся);
- захватывает мьютекс application_table_mutex, управляющий доступом на запись к таблице потоков (вышеупомянутому массиву)
- находит пустой слот для нового потока; если такого нет (255 потоков уже запущены) - выход с ошибкой;
- очищает полученную структуру APPDATA
- заполняет имя процесса;
- вызов функции create_process, возвращая указатель на PROC
- в PROC.hdr_emem записывается используемая память из заголовка исполняемого файла
- Если указатель не нулевой, то добавляем полученную структуру PROC в общий список структур
- записываем в APPDATA.process указатель на структуру PROC
- структура APPDATA добавляется в список PROC.thr_list
- создаёт новое адресное пространство (это отдельная история);
- вызывает функцию set_app_params, заполняющую остальные поля структуры потока (подробнее - ниже);
- освобождает мьютекс application_table_mutex.
Создание нового потока: [core/taskman.inc, new_sys_threads] принимает на вход entry point нового процесса и указатель на user-mode стек.
- захватывает application_table_status
- находит пустой слот для нового потока; если такого нет - выход с ошибкой;
- очищает полученную структуру
- копирует имя процесса и информацию об адресном пространстве вызывающего потока в структуру для нового;
- копирует APPDATA.tls_base если его нет, то выделяет 4096 байт для неё и записывает в APPDATA.tls_base нового потока
- копирует APPDATA.process
- добавляет этот процесс в PROC.thr_list
- вызывает set_app_params
- освобождает application_table_status
Функция set_app_params:
- выделяет в адресном пространстве ядра буфер под стек ядра и область для сохранения состояния FPU и SSE;
- инициализирует разные параметры потока значениями по умолчанию;
- копирует командную строку и путь к приложению в адресное пространство процесса по адресам, записанным в заголовке бинарника (или не копирует, если эти значения в заголовке нулевые, что означает, что программа в них не нуждается);
- выделяет очередной идентификатор (каждый следующий TID равен предыдущему + 1);
- инициализирует user-mode контекст, значения eip и esp берутся из параметров вызова для sys_new_threads и из заголовка для fs_execute;
- инициализация структуры WDATA (смотреть const.inc)
- в зависимости от флагов устанавливает регистр CS и приоритет
- если новый процесс загружается как отлаживаемый, то помечает его состояние как замороженное, иначе - как работающее (начиная с этого места на новый поток возможны переключения задач).
Завершение процесса: [core/sys32.inc, terminate] когда системный поток получает управление (главный цикл системы), одним из его действий является проход по списку процессов, поиск потоков в завершающемся состоянии и убийство таких процессов. Все нижеследующие действия происходят в контексте системного потока.
- захватывает application_table_status
- проходит по списку объектов ядра и вызывает деструкторы
- если этот поток - последний в своём процессе, уничтожает адресное пространство
- освобождает разные системные ресурсы, которые мог выделить этот поток и которых нет в списке объектов ядра (список горячих клавиш, список кнопок, определённых потоком)
- если поток отлаживается, посылает извещение отладчику
- освобождает память под kernel-mode стек и область сохранения FPU/SSE
- освобождает карту ввода/вывода (если она была изменена - есть стандартная карта ввода/вывода, которая создаётся при загрузке и разделяется между всеми потоками)
- если окно потока было на вершине оконного стека, активирует следующее окно
- если поток рухнул (или был прибит) в процессе работы с жёстким диском, освобождает мьютекс занятости жёсткого диска; то же самое для CD и дискеты
- освобождает выделенные потоком порты
- если текущий прибиваемый процесс - отладчик, помечает как завершающиеся все отлаживаемые им процессы
- перерисовывает экран
- освобождает 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 - запуск программы.
- Функция 77 - posix подсистема ядра
- Функция -1 - завершение потока.
Управление вводом-выводом: железо с точки зрения ядра
Устройства бывают разные. Бывают стандартные устройства, которые понимает система. Система самостоятельно работает с таймером, мышью, клавиатурой, видеокартой, аудио, системным динамиком, сетевыми картами, CD/DVD, жёсткими дисками, не давая приложениям доступа к этим устройствам напрямую.
Для получения данных мыши есть специальная сисфункция 37 (подфункции 0,1,2,3,7 - получить информацию о разных аспектах происходящего с мышью) и специальное событие - при любом дёргании мыши система извещает всех подряд, что с мышью что-то произошло (по умолчанию поток не реагирует на события мыши, а должен явно установить маску учитываемых событий, разрешающую событие мыши). Приложение может управлять формой курсора мыши (сисфункция 37, подфункции 4,5,6) для своего окна (когда курсор проходит над окном потока, он принимает заданную форму; физически хэндл курсора для окна хранится в структуре для потока, но логически это скорее атрибут окна, поэтому я его не указал в предыдущем посте). Приложение может управлять настройками движения мыши, может переместить курсор в нужную позицию, может симулировать нужное состояние клавиш мыши - всё это функцией 18.19 (есть ещё хронологически более старая функция 18.15: поместить курсор в центр экрана). Кроме того, приложение может определить некоторое количество кнопок (кнопки реализованы в ядре). Кнопка - прямоугольная область в окне (ядро обычно рисует их самостоятельно, но приложение может попросить ядро не делать этого), которой приписан (приложением) некоторый идентификатор; при нажатии на кнопку мышью ядро посылает потоку-владельцу окна событие о нажатии кнопки.
Внутренне в системе происходит следующее. Есть поддержка COM-мышей и обычных PS/2, и то, и другое вынесено в драйвера (commouse.sys, ps2mouse.sys соответственно). Ядро экспортирует для драйверов функцию 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); сканирует список установленных горячих комбинаций и, если такая комбинация зарегистрирована, посылает событие клавиатуры зарегистрированному приложению (пример: @taskbar регистрирует нажатие на клавишу Win для вызова меню и комбинации типа Alt+F4, Alt+Tab, Alt+Shift+Tab и Ctrl+Shift - полный список есть в hot_keys.txt из дистрибутива, API здесь - та же функция 66), а если нет, то кладёт её в буфер нажатых клавиш для активного окна (есть такой системный массив на 120 байт), что активирует событие клавиатуры для потока-владельца активного окна. Как именно кладёт, зависит от режима: в сканкодном просто кладёт сканкод, полученный от клавиатуры, а в ASCII-режиме клавиши-модификаторы и события об отпускании клавиш просто игнорирует, а нормальные клавиши транслирует в ASCII-коды с помощью таблиц преобразования. Таблицы для каждого языка свои, переключение языка заключается в установке правильной таблицы (это делает приложение @taskbar), API - 21.2, 26.2. Клавишам F1-F12 тоже соответствуют определённые коды, которые совпадают с нормальными клавишами (например, F1='2',F5='6'), и в результате приложения, работающие в этом режиме (например, sysxtree, eolite, mtdbg), не могут отличить кнопку F5 и цифру 6.
На уровне системы: поддержка клавиатуры зашита в ядре (уже упоминавшийся обработчик irq1 из hid/keyboard.inc), но ядро так же экспортирует функции SetKeyboardData, RegKeyboard, DelKeyboard . Схема обработки:
(клавиатура) <-> (ядро) <-> (приложения).
Работа с видеокартой - это, собственно, GUI, на тему которого можно говорить отдельно довольно долго. Ограничиваясь работой с железом:
- вывод на экран осуществляет ядро;
- для видеокарт от ATI и Intel есть специальный драйвер, вспомогательный для ядра (если он есть и при загрузке сообщил, что хочет работать, то ядро будет иногда прибегать к его услугам), поддерживающий аппаратный курсор и вроде в последних версиях на каком-то уровне аппаратное ускорение (через API для приложений);
- поддерживаются стандартные видеорежимы EGA/CGA и VGA и видеорежимы, возвращаемые VESA BIOS. Установка видеорежима осуществляется средствами BIOS при загрузке ещё в реальном режиме процессора. Для режимов VESA2 работа идёт через framebuffer, и у приложений есть прямой доступ к нему как на чтение, так и на запись. Подробнее - описание функции 61;
Здесь схема работы в типичном случае выглядит так:
(видеокарта) <-> (ядро) <-> (приложения), \ / (драйвер)
В менее типичных случаях приложения могут обращаться напрямую к драйверу и/или framebuffer'у видеокарты.
Поддержка аудио есть для SB-совместимых карт, HDA-совместимых карт и для AC97-кодеков на определённом железе, здесь ядро уже не принимает прямого участия, а приложение общается напрямую с соответствующим драйвером (infinity.sys, в свою очередь опирающийся на драйвер sound.sys/sis.sys, какой именно, зависит от железа). Драйвер предоставляет соответствующие API.
Системным динамиком приложение может попищать с помощью функции 55.55, но только если это разрешено в настройках (с точки зрения пользователя: есть иконка в "трее" - правой части @taskbar, включающей этот режим, по умолчанию писк запрещён; если включить, можно понаслаждаться писком при открытии меню и запуска программ по иконкам и через трей). Данные для функции 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, в результате чего при следующем обращении к регистрам (именно обращении! когда нужно действительно переключать весь контекст) процессор возбудит исключение, обработчик которого молча сохранит регистры ушедшего потока, загрузит регистры нового потока и перезапустит инструкцию, сделав вид, что ничего не случилось.
Чтение данных с CD/DVD, равно как и работа с жёсткими дисками и дискетами, относится скорее к области файловой системы ("железная" часть, впрочем, как и всё остальное, зашита в ядро: blkdev/ahci.inc для работы с sata дисками, blkdev/cd_drv.inc для работы с CD/DVD, blkdev/flp_drv.inc для работы с дискетами, blkdev/hd_drv.inc для работы с жёсткими дисками (собственными силами обрабатывает ATA, обращается к BIOS для поддержки BIOS-дисков через V86 из core/v86.inc).
Управление вводом-выводом: железо с точки зрения приложений
Но бывают устройства, о которых система ничего не знает. Тут появляются два варианта работы с ними.
Вариант 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. Доступ из приложения к любым конкретным адресам в физической памяти безусловно запрещен. К "бортовой" памяти устройств (называемой также блоками ввода-вывода с отображением на память, Memory-Mapped I/O или MMIO) прикладные программы должны обращаться через API-функции и драйверы устройств.
Но что делать с нестандартными устройствами и "бездрайверным железом"? Колибри предоставляет пользовательскому приложению возможность отображения (page mapping) скрытых от него физических адресов MMIO на видимое ему "своё" адресное пространство, используя для этого подфункции 11-13 той же функции 62. Разработчик нового драйвера или инженер-электронщик, тестирующий новое устройство, должен заранее указать его PCI-адрес в системной константе mmio_pci_addr. По умолчанию эта константа в ядре не определена и пользовательский доступ к MMIO закрыт.
4. Обработка IRQ для приложений удалена из ядра.
Вариант Б. Из соответствующего драйвера.
Подробно про то, что из себя представляет драйвер, как его можно загрузить и как приложение (и другие драйвера) может с ним общаться, рассказано здесь: viewtopic.php?f=3&t=707.
Драйвер выполняется в ring-0 и, как следствие, получает доступ ко всему, чему можно. В частности, с чтением/записью в порты никаких проблем не возникает. Кроме того, ядро предоставляет некоторые сервисы драйверам. В их числе:
- AttachIntHandler (и GetIntHandler) для перехвата IRQ; при приходе такого IRQ ядро просто вызывает функцию драйвера, адрес которой драйвер указывает в AttachIntHandler
- ReservePortArea - точный аналог (с точностью до сдвига регистров) функции 46; в принципе всё будет работать и без её вызова, но желательно всё-таки дать ядру знать об используемых портах
- PciApi - точный аналог (с точностью до сдвига регистров) функции 62
- Pci{Read/Write}{8/16/32} для чтения/записи PCI-регистров
- GetPgAddr для получения физического адреса по известному виртуальному
- и другие [core/exports.inc]
Файловая система
1. Собственной файловой системы у Колибри нет, стандартно используется FAT, есть чтение с NTFS и ext4.
2. Управляющих блоков для файлов нет. При всех операциях с файлами приложение задаёт полное имя файла (а ядро, соответственно, это полное имя каждый раз разбирает).
3. В Колибри файл - это всегда файл на диске (возможно, на рамдиске), другие сущности файловыми API не адресуются.
4. API файловой системы - функция 70 (с соответствующими подфункциями, описана в документации). Кроме того, есть некоторое количество устаревших функций, удаляемых по мере обновления приложений на (относительно) новую 70-ю.
5. Ограничений доступа нет (всё, что в принципе можно сделать, может сделать любое приложение). Соответственно и авторизации нет.
Управление памятью
Используемый тип управления: Страничная организация памяти, плоская модель (с fs=0 и особой трактовкой сегментного регистра gs). Нижние 2 Гб виртуальной памяти (диапазон адресов 0-0x7FFFFFFF включительно) отводятся приложению (и свои для каждого процесса), верхние 2 Гб - для системы (и разделяются между всеми процессами). Программы грузятся по нулевому адресу.
GDT описана в [data32.inc, gdts], LDT не используется. В регистр gs загружается селектор, описывающий сегмент на 8 Мб, описыващий область памяти, выделяемую для работы с графическими данными (упоминавшуюся при описании работы системы с видеокартой), выводимыми на экран - для vesa2-видеорежимов c LFB туда просто маппится этот LFB, для ega/vga и cga-режимов это специально выделяемая при загрузке память, а для vesa1.2-режимов селектор имеет нулевую базу.
Принцип отображения адресного пространства: Стандартным образом, через таблицы страниц. Файла подкачки нет. Для преобразования адресов выполнены следующие утверждения:
- преобразование нетождественно;
- преобразование нижних 2 Гб зависит от текущего процесса, преобразование верхних 2 Гб не меняется при переключении задач;
- преобразование меняется на время вызовов APM и V86;
- инициализирует системную таблицу страниц процедура init_mem из init.inc;
- начальный кусок системных адресов [OS_BASE, OS_BASE+a), где OS_BASE = 0x80000000 (const.inc), маппится на начало физической памяти [0,a) "почти тривиально" - вычитанием OS_BASE;
- длина системного куска a = HEAP_BASE+HEAP_MIN_SIZE-OS_BASE;
- первые 4 Мб (куда входит само ядро и часть системных таблиц) маппятся одной "длинной" страницей;
- размер остальных страниц - по 4Кб;
- Для 4К-страниц действует двухтабличное PDE-PTE преобразование линейных адресов в физические:
- старшие 10 бит линейного адреса указывают номер элемента в каталоге таблиц (PDE), расположенному в статической области ядра по линейному адресу sys_pgdir;
- этот элемент содержит физический адрес соответствующей таблицы страниц (PTE), элементы которого адресуются битами 21..12 линейного адреса;
- обращаться к таблицам страниц можно только из ring-0, по линейным адресам [0xFDC00000, 0xFE000000) (4 Мб, начиная с page_tabs из const.inc)
- в принципе, таблицы страниц PTE - это динамические структуры, создаваемые и удаляемые менеджером памяти по мере необходимости;
- однако существует важный диапазон линейных адресов [OS_BASE, OS_BASE+b), для которых таблицы страниц расположены в системной области в виде "плоского" статического массива. Длина этого диапазона b равна размеру доступной физической памяти, но не может превышать 2Гб. Размер массива = 4К x (b>>22 - 1); первая таблица располагается в физической памяти после (sys_pgmap + (b>>15) - OS_BASE).
- элемент таблицы страниц с установленным битом присутствия представляет страницу в памяти;
- элемент таблицы страниц со сброшенным битом присутствия, но установленным 1-м битом (маска 2) соответствует ситуации, когда страница должна быть выделена при первом обращении к ней (обработчик #PF при обнаружении исключения из-за обращения к такой странице выделяет страницу физической памяти, маппит по соответствующему линейному адресу и возвращает управление);
- таблицу страниц для процесса создаёт функция [core/taskman.inc|create_app_space] и удаляет [core/taskman.inc|destroy_app_space];
- схема работы create_app_space:
- на вход получает размер памяти, указанный в заголовке бинарника ([app_size]), и сам загруженный в память ядра бинарник (указатель [img_base] + размер [img_size]);
- захватывает мьютекс pg_data.pg_mutex, контролирующий запись в таблицы страниц;
- проверяет, достаточно ли свободной физической памяти для приложения; если недостаточно, возвращает ошибку;
- выделяет новую страницу [dir_addr] под PDE, маппит её по линейному адресу [tmp_task_pdir] (чтобы к ней можно было обращаться; место в системном адресном пространстве для этой цели было зарезервировано при загрузке), обнуляет user-mode указатели на PTE и копирует kernel-mode часть; заменяет вход PDE, соответствующий page_tabs, указателем на себя ([dir_addr]);
- устанавливает созданную PDE как текущую таблицу страниц (дальнейшие махинации с page_tabs пойдут внутрь [dir_addr], создаваемой таблицы);
- создаёт нужное (для описания всей памяти [app_size]) количество страниц PTE, заносит указатели на них в PDE;
- маппит в адресное пространство нового процесса загруженный бинарник;
- если при компиляции ядра константа GREEDY_KERNEL ненулевая, то помечает оставшиеся страницы в памяти приложения значением 2 (упомянутом ранее - физическую память выделит обработчик #PF, когда она будет нужна); а если нулевая - выделяет физическую память под все остальные страницы;
- размаппит страницу [dir_addr] из линейного адреса [tmp_task_pdir];
- возвращается; текущая таблица страниц соответствует адресному пространству нового процесса.
- схема работы destroy_app_space:
- захватывает мьютекс pg_data.pg_mutex;
- выясняет, является ли текущий поток последним в своём процессе (проходит по списку потоков в поисках потоков с тем же адресным пространством); если нет - освобождает мьютекс и выходит;
- маппит таблицу PDE по линейному адресу [tmp_task_pdir], чтобы с ней можно было работать;
- проходит по всем user-mode элементам (элементы PDE - указатели на PTE), каждую выделенную страницу с PTE маппит по линейному адресу [tmp_task_ptab] (место в адресном пространстве ядра, как и для [tmp_task_pdir], было зарезервировано при загрузке системы), вызывает вспомогательную процедуру destroy_page_table (которая в свою очередь проходит по элементам теперь уже PTE, освобождая все выделенные страницы) и освобождает саму страницу с PTE;
- освобождает страницу с таблицей PDE;
- размаппит страницы из [tmp_task_ptab] и [tmp_task_pdir];
- освобождает мьютекс pg_data.pg_mutex
При управлении памятью бывают разные задачи.
А. Управление физической памятью.
- Есть функция выделения одной физической страницы [core/memory.inc, alloc_page],
- функция выделения нескольких физических страниц [core/memory.inc, alloc_pages], выделяющая связный диапазон, причём кратный 8 страницам,
- функция освобождения ранее выделенной физической страницы [core/memory.inc, free_page].
Система хранит массив битов, который для каждой физической страницы описывает, выделена она или свободна, а также вспомогательные переменные: подсказку [page_start] - нижнюю границу при поиске свободной страницы (указатель внутри битового массива, относительно которого известно, что все предшествующие данные забиты единицами, а соответствующие страницы выделены), указатель [page_end] на конец массива, число свободных страниц [pg_data.pages_free].
Физические страницы выделяются по принципу first-fit, возвращается первый подходящий вариант (первая свободная страница либо первый свободный блок нужной длины).
Б. Управление адресным пространством ядра.
В core/heap.inc есть alloc_kernel_space и free_kernel_space, которые соответственно выделяют и освобождают непрерывный диапазон в адресном пространстве ядра.
В. Управление памятью ядра.
- Есть функция получения физического адреса по указанному линейному [core/memory.inc, get_pg_addr],
- функция маппинга указанной физической страницы по указанному линейному адресу [core/memory.inc, map_page] (стандартное добавление элемента в таблицу страниц; работает и с user-mode пространством), способная также размаппить страницу (нулевой элемент таблицы страниц соответствует свободной линейной странице),
- аналогичная функция для непрерывного блока адресов [core/memory.inc, commit_pages],
- обратная ей [core/memory.inc, unmap_pages],
- функция [core/memory.inc, release_pages], которая принимает линейный адрес и размер блока и одновременно размаппит из линейных адресов и освобождает физические страницы из этого блока.
- а также функция [core/memory.inc, map_io_mem], создающая проекцию заданного блока физических страниц в адресном пространстве ядра (вызывает alloc_kernel_space, а потом добавляет в таблицу страниц преобразование указанных физических адресов на только что выделенные линейные),
- общая функция выделения памяти ядра [core/heap.inc, kernel_alloc], которая одновременно выделяет место в адресном пространстве ядра, физическую память, устанавливает соответствие между ними и возвращает линейный адрес блока. Алгоритм работы: выделяет нужный диапазон линейных адресов (alloc_kernel_space); если запрошено A*8+B страниц, 0<=B<8, то выделяет непрерывный блок из A*8 страниц через alloc_pages и маппит по нужным линейным адресам, а потом B раз выделяет по одной физической странице (alloc_page) и тоже маппит по нужным адресам,
- обратная ей функция освобождения памяти ядра [core/heap.inc, kernel_free], основывающаяся на release_pages и free_kernel_space.
Г. Куча ядра для маленьких блоков памяти.
Файл core/malloc.inc предоставляет функции malloc и free, предназначенные для выделения маленьких блоков памяти (kernel_alloc/kernel_free из предыдущего пункта работают только с целыми страницами, что может быть много). Как сказано в комментариях (которым нет причин не верить), всё основано на коде Doug Lea ftp://gee.cs.oswego.edu/pub/misc/malloc.c . При загрузке [core/malloc.inc, init_malloc] под кучу выделяется 256 Кб через kernel_alloc, и malloc/free оперируют исключительно внутри этой области.
Д. Работа с памятью приложений.
Когда-то довольно давно адресное пространство приложения было обязано быть непрерывным диапазоном, который для приложения начинался с нулевого адреса; с тех пор в структуре, возвращаемой функцией 9 для потока, есть поля "адрес процесса в памяти" и "размер используемой памяти" (точнее, лимит = размер-1). В то время единственной возможностью по динамическому перераспределению памяти было изменение размера адресного пространства, и с того времени идёт сисфункция 64, [core/memory.inc, new_mem_resize]. Которая при уменьшении используемой памяти проходит по "лишнему" пространству и освобождает выделенные страницы (free_page), при увеличении сначала выделяет дополнительные страницы под таблицы PTE (если нужно), а потом проходит по "добавляемому" пространству и выделяет запрошенные страницы через alloc_page+map_page. Кроме того, в конце она вызывает вспомогательную функцию update_mem_size, которая проходит по списку потоков и для всех потоков текущего процесса обновляет поле с размером памяти в структуре потока.
Но ясно, что при таком подходе далеко не всегда можно освободить память, которая стала ненужной. Поэтому была написана куча для приложений, функции init_heap, user_alloc, user_free, user_realloc из core/heap.inc. Они работают с отдельными страницами и блоками страниц.
Использование кучи несовместимо с перераспределением памяти сисфункцией 64, так что для активации режима кучи нужно вызвать соответствующую сисфункцию, которая инициализирует кучу (init_heap) и после которой функция 64 будет всегда возвращать ошибку. Организация данных: есть некоторые поля в структуре потока, хранящие базу кучи и размер адресного пространства, отводимого под кучу; в куче бывают выделенные и свободные блоки (все блоки занимают целое число страниц), все блоки организованы в односвязный список следующим образом. Информация о физических страницах для линейных адресов хранится в таблице страниц, при этом "нормальные" элементы таблицы либо нулевые (страница не выделена, обращаться к ней нельзя), либо имеют установленный бит присутствия (страница выделена и находится в памяти), либо имеют установленный 1-й бит (был запрос на выделение страницы, но она будет выделена при первом обращении). Информация о блоке размещается там же, в элементе таблицы страниц, предшествующем собственно блоку, и в таком элементе младшие два бита нулевые, зато установлен либо 2-й бит (маска FREE_BLOCK=4), соответствующий свободному блоку, либо 3-й бит (маска USED_BLOCK=8); старшие 32-12 = 20 бит содержат длину блока, это и организует односвязный список всех блоков. Кстати, при таком хранении информации любые два блока разделены хотя бы одной свободной страницей. Выделение блока - алгоритм first-fit, в цикле по блокам находим первый свободный блок подходящего размера; если он оказался в точности запрошенного размера, то он просто переводится в статус занятого, иначе от него отделяется хвост, остающийся свободным блоком. В любом случае страницы нового блока помечаются значением 2 (отложенное выделение физической памяти). Функция освобождения блока освобождает все выделенные страницы из блока через free_page, помечает блок как свободный, после чего проходит по списку блоков, объединяя соседние свободные блоки в один свободный блок. Функция перераспределения блока при уменьшении размера блока освобождает лишние страницы и либо создаёт новый свободный блок, либо расширяет следующий свободный блок, а при увеличении размера блока смотрит, есть ли сразу после запрошенного блока свободный блок нужного размера (можно ли увеличить запрошенный блок на месте), если нет, то ищет свободный блок полного размера (тоже first-fit), перемаппит все физические страницы из старого блока в новый, помечает старый блок как свободный и запускает объединение свободных блоков; добавленные страницы в любом случае помечаются значением 2. Все три функции в конце работы вызывают update_mem_size.
Стандартной процедуры для работы с блоками меньше страниц нет. В различных библиотеках для ЯВУ есть различные реализации malloc/realloc/free на основе системных функций, но это уже зависит от конкретной программы. Некоторые программы вообще обходятся без маленьких блоков и неплохо себя чувствуют.
6. API для других подсистем ядра - ранее описанные функции. API для драйверов: AllocPage, AllocPages, FreePage, GetPgAddr, MapPage, MapIoMem, CommitPages, ReleasePages, AllocKernelSpace, FreeKernelSpace, KernelAlloc, KernelFree, UserAlloc, UserFree, Kmalloc, Kfree (это malloc/free из кучи малых блоков ядра).
API для приложений: сисфункции 64, 68.11, 68.12, 68.13, 68.20, 18.16, 18.17, 18.20.
История системы жестких дисков
Что было в Менуэт:
- Одновременная работа только с одним разделом жесткого диска. Ни о каком копировании файла с раздела на раздел без дополнительных телодвижений со стороны пользователя не могло быть и речи.
- Один единственный буфер кэширования на 1 Мб. При переключении на другой раздел жесткого диска (даже одного физического диска) происходило полное очищение кэша.
- Обмен данными с жёстким диском производился исключительно по одному сектору в PIO режиме.
Как это прирастало в Колибри:
1.Изначально дисковая система была идентична Менует.
2.Поскольку было большое желание работать хотя бы с двумя разделами, а лучше со всеми без дополнительных телодвижений была введена модернизация имен жестких дисков, позволившая обращаться к более чем одному жесткого диска без дополнительных телодвижений с программой SETUP. В процессе экспериментов было выявлено, что определение параметров раздела при каждом обращении к дисковой подсистеме тормозит скорость работы. По этой причине эта часть кода была вынесена в отдельную процедуру которая вызывается при загрузке системы и определяет все наличествующие разделы на жестком диске, а затем сохраняет их в специальном буфере (просмотреть его можно функцией 18.11). Из этого буфера во время работы эти данные извлекаются и используются по мере обращения к определенным разделам.
3.Было внедрено использование режимов DMA и UltraDMA (максимального который установил BIOS при загрузке компьютера).
3.1 В ходе разработке кода выяснилось, что чтение по 1 сектору как и запись скашивают весь прирост скорости в DMA режиме. По этой причине с целью минимизации изменения существующего кода был введен пред-буфер чтения DMA на 16 секторов жесткого диска (каждый физический сектор 512 байт). При запросе 1 сектора производится считывание не только 1 сектора а еще 15 последующих, поскольку считывание производится за один запрос к физическому устройству, то никаких дополнительных задержек это не вызывает. Поскольку обращения к жесткому диск по большей части последовательное (разумеется мы не рассматриваем очень сильную фрагментацию данных, это повод для запуска дефрагментатора), то прирост скорости по сравнению с по секторным считыванием колоссален. Если в предкэше нужного сектора не окажется то производится его считывание вместе с последующими 15, т. е. Кэш полностью обновляется. С одной стороны максимальный «штраф» может быть в 15 секторов, но в ходе поставленных опытов выяснилось что 16 секторов это оптимальная величина, считывание 32 сектора было медленней и «штраф» тоже был больше, с другой стороны при считывании 8 секторов наблюдался резкий провал производительности, который выравнивался только к 4, то это уже было заметное снижение производительности. По этим причинам 16 стало опорным значением.
3.2 Для ускорения записи был применен несколько иной подход. Поскольку кэшу же был и совершать дополнительные телодвижения задействуя процессор для сортировки нужной последовательности секторов, чтобы слить их единым блоком не лучший выход. По этой причине есть две процедуры записи для DMA — одна записывает по одному сектору, другая от 2 до 64 секторов за раз. Поскольку в кэше данные могут располагаться как последовательно так и вразнобой, то соответственно алгоритм кода если не находит последовательных секторов общим числом больше 1, т.е. 2 и более вплоть до 64, записывает по одному сектору. Как только найдено два последовательных сектора начинается подсчет последовательных секторов вплоть до 64 секторов. Когда последовательность оборвется или достигнет 64 — этот кусок сбрасывается за один заход DMA на жесткий диск. Затем процедуры повторяются вплоть до того как весь кэш (те из его секторов которые помечены для записи) не будет записан по месту назначения. Такой подход позволил поднять скорость записи без дополнительных затрат для центрального процессора.
4.Поскольку буфер был один на все устройства и разделы, то сначала возникла идея доработать код чтобы он не очищался если чтение или запись производятся на разные разделы одного физического устройства. Это было реализовано и некоторое время система функционировала в таком виде. Однако провал в скорости при копировании на разные физические устройства был очень сильным. По этой причине было введена система независимых кэшей — каждому физическому устройству по кэшу. Первоначально их было максимум 4 потом стало больше но об этом позже. Изначально планировалось динамическое выделение памяти при старте системы, но в ходе экспериментов выяснилось что при размере кэша боле 1 Мб перебор всех секторов в таком кэше занимает много времени и замедляет скорость доступа к жесткому диску. Проблему решил бы алгоритм «хеширования», но по некоторым (неважным в контексте этой статьи) причинам он так и не был реализован. По этому размер кэша был ограничен 1 Мб — это максимальное значение, минимальное значение каждого кэша это 128 Кб. Минимальное значение взято приблизительно и исключительно для работы на старых компьютерах с недостаточным количеством оперативной памяти (16, 12, 8 Мб). Разумеется при уменьшении размера буфера скорость работы с файловой подсистемой падает ,но это меньшее зло чем совсем не работать с ней.
Дополнительная мера для повышения скорости работы — это сделать так чтобы служебные данные раздела и данные директорий - которые бывают нужны часто, а обновляются редко не выбивались из кэша новыми данными. Для этого планировалось разделить кэш на две неравны части — одну меньшую часть для служебных данных, другую большую часть для данных считываемых файлов. Также по некоторым причинам это было реализовано лишь для ATAPI устройств, поскольку для них требовалось только читающая часть кода, то написание кода было немного проще. После внедрения этого механизма — MP3 плеер перестал заикаться при непосредственном проигрывании CD и DVD дисков (раньше заикался потому что у него нету собственного кэша для файла, а сейчас система обеспечивает своевременную подачу порции данных), хотя ATAPI устройства до сих пор работают в PIO режиме. Реализация пакетного DMA режима для ATAPI устройств более сложная задача чем реализация DMA для жестких дисков.
В ходе экспериментов с кэшами также выяснилось, что код работы с кэшем внедренный в Менуэт ,который лежал в основе системы кэширования содержал фатальную ошибку, из-за которой первые реализации кода в DMA режиме вызывали порчу данных на жестком диске. Область данных кэша содержавших указатели номеров содержащихся секторов была на 1 единицу больше чем сам размер кэша, пока кэш был стабилен по местоположению видимо затирались не особо важные данные, а когда он стал динамическим это вылезло большими проблемами. Впоследствии ситуация была разрешена и баг «как бы» сам исчез с внедрением отдельных кэшей для каждого физического устройства.
5.Были добавлены дополнительные буферы для видимых из БИОС, но не видимых пока в самой Колибри устройств. Для тех дисков которые уже обнаружены самой ОС кэш не дублируется, а используется совместно при обащении к HD и BD дискам.
6.В итоге было реализовано динамическое определение дисковых устройств, функции чтения/записи произвольного количества секторов, LBA48 и полноценный DMA со scatter-gather для IDE. Возможность использования AHCI на сегодня ограничена поддержкой APIC. Прерывания построены на старой модели, где их максимум 15 штук (1 используется для каскадирования, хотя реально никакого каскадирования нету, но это сделано для совместимости со старым железом). Новые контроллеры SATA любят садится на прерывания выше 20-го, либо не садятся вообще, а это PIO режим.